# Limenka Knots v29.3 Comprehensive Security Audit **Target:** Limenka Knots `v29.3.knots20260508` (commit `56215eb9a5`, branch `blockfilter_v0`) **Scope:** Full codebase (~2,857 files, ~1,455 C++ source files, all vendored deps, build system, contrib, tests) **Date:** 2026-08-04 **Languages:** C++17, Python (tests), CMake (build), M4/autotools (secp256k1), Qt (GUI) --- ## Audit Summary | Severity | Count | Key Areas | |----------|-------|-----------| | **Critical** | 4 | Chainsplit vectors, dead policy code, shell injection | | **High** | 10 | Wallet crypto, RDTS deployment, wrong validation result, signet flags, TRUC guard, coin-age double, serialization | | **Medium** | 45 | DoS, crypto side-channels, mempool persistence, index corruption, race conditions, fee estimation, input validation | | **Low** | 52 | TOCTOU, buffer management, hardening gaps, prevector, clipboard, encoding edge cases | | **Info** | 25 | Documentation, defensive hardening, by-design choices | **Total:** 136 findings --- ## Critical (action required before deployment) ### C1. RDTS Flag Day Chainsplit - Consent vs Non-Consent Builds `src/kernel/chainparams.cpp:126-141`, `src/versionbits.cpp:99-103` RDTS activates by flag day at height 965664 on mainnet - miner signaling is irrelevant. The 55% threshold only provides early activation before the deadline. Nodes with `RDTS_CONSENT=UNSUPPORTED_UNSAFE_NO_ENFORCEMENT` disable EVERYTHING: BIP9 signaling (`nStartTime = NEVER_ACTIVE`) AND the flag day (`max_activation_height = INT_MAX`). The result is two classes of node: - **Consenting nodes**: enforce RDTS at height 965664 (fail non-RDTS-signaling blocks after LOCKED_IN) - **Non-consenting nodes**: never activate RDTS (accept non-RDTS-signaling blocks forever) At the flag day, consenting nodes will reject blocks produced by non-consenting miners (or non-Knots miners who never signal). **This is a permanent chainsplit between consenting and non-consenting builds of the same source code.** ```cpp // versionbits.cpp:99-102 - Forces LOCKED_IN regardless of miner signaling } else if (max_activation_height < std::numeric_limits::max() && pindexPrev->nHeight + 1 >= max_activation_height - nPeriod) { stateNext = ThresholdState::LOCKED_IN; // unconditional } // Non-consenting path disables both BIP9 AND the flag day: max_activation_height = std::numeric_limits::max(); // line 138 threshold = 0; // line 140 ``` **Fix:** Remove the `UNSUPPORTED_UNSAFE_NO_ENFORCEMENT` compile flag. Any node that rejects RDTS must also reject non-RDTS-signaling blocks at the flag day, staying on the same chain as consenting nodes. Minimum: if `g_rdts_consent == UNSUPPORTED_UNSAFE_NO_ENFORCEMENT` and `g_enable_rdts == false`, still set `max_activation_height = 965664` so the flag day forces consensus (but not policy) enforcement, keeping the chain unified. ### C2. Dead P2SPKH Witness Standardness Code - Mempool Policy Bypass `src/policy/policy.cpp:455-465` The P2SPKH witness standardness check is nested inside `if (witnessversion == 1 ...)` (line 427) and `if ((control_block[0] & TAPROOT_LEAF_MASK) == TAPROOT_LEAF_TAPSCRIPT)` (line 445). At that point `witnessversion` is always 1, never 3. **All P2SPKH witness standardness checks are dead code.** See original finding #1. ### C3. `runCommand` Shell Injection via `::system()` `src/common/system.cpp:49-61` ```cpp void runCommand(const std::string& strCommand) { if (strCommand.empty()) return; int nErr = ::system(strCommand.c_str()); ``` `::system()` passes the string to `/bin/sh -c`. There is NO input sanitization. Any caller that passes user-controlled data (e.g., `-walletnotify`, `-blocknotify`, `-alertnotify`) is vulnerable to shell metacharacter injection. **Fix:** Replace with `fork()` + `execvp()` using argument arrays. Remove `HAVE_SYSTEM` path entirely. ### C4. Software Expiry Hard-Rejects Blocks - NTP-Based DoS `src/validation.cpp:4638-4649` ```cpp if (IsThisSoftwareExpired(block.nTime)) { // ... check 144-block grace period ... return state.Invalid(BlockValidationResult::BLOCK_TIME_FUTURE, "node-expired", "..."); } ``` After the software expiry timestamp (~2 years after copyright year), the node **hard-rejects all new blocks**. An NTP attacker can advance the system clock past expiry, permanently splitting the node from the network. The 144-block (~1 day) grace period provides minimal buffer. **Fix:** Expiry should produce a warning, never a hard rejection. Or require `-softwareexpiry=0` to disable. --- ## High ### H1. P2SPKH ALWAYS_ACTIVE UASF - Chain Split Vector `src/kernel/chainparams.cpp:143-146`, `src/policy/policy.h:163` P2SPKH deploys with `ALWAYS_ACTIVE` on all chains. Knots enforces at consensus; Core sees as always-success. See original finding #2. ### H2. RDTS Flag Day Bypasses Miner Signaling on Mainnet `src/kernel/chainparams.cpp:131-132` Mainnet RDTS activates regardless of miner signaling. The 55% threshold (1109/2016) provides an early-activation path if miners signal before the deadline, but at `max_activation_height = 965664` the flag day overrides all signaling (versionbits.cpp:99-102 forces LOCKED_IN). The `active_duration = INT_MAX` means this is permanent on mainnet, not temporary. On testnet/signet, `max_activation_height = INT_MAX` so the flag day is disabled and only miner signaling with 55% threshold applies. **Risk:** The EXPIRED state machine path exists in `versionbits.cpp:121-123` but is unreachable with current params. If a future deployment uses a finite `active_duration`, the EXPIRED transition is minimally tested in production. ### H3. MAX_OUTPUT_SCRIPT_SIZE = 34 - Consensus-Level Output Size Cap `src/consensus/consensus.h:37` See original finding #4. ### H4. Wallet KDF is EVP_BytesToKey - Not PBKDF2/Argon2 `src/wallet/crypter.cpp:15-39` See original finding #5. ### H5. No AEAD on Wallet Ciphertext (legacy method 0) `src/wallet/crypter.cpp:76-92` AES-256-CBC with no MAC. See original finding #8. ### H6. Incorrect TxValidationResult in CheckOutputSizes `src/consensus/tx_verify.cpp:167-168` ```cpp return state.Invalid(TxValidationResult::TX_PREMATURE_SPEND, "bad-txns-vout-script-toolarge"); ``` Uses `TX_PREMATURE_SPEND` for a script size violation. This should be `TX_CONSENSUS`. Any code path filtering on `state.GetResult() == TX_CONSENSUS` would miss oversized-output transactions. In the mempool path through `PreChecks`, the wrong result type propagates, causing incorrect ban scoring and missed rejection in `MaybeReject`. **Fix:** Change to `TxValidationResult::TX_CONSENSUS`. ### H7. Signet Block Verification Uses Weakened Script Flags `src/signet.cpp:29` ```cpp static constexpr unsigned int BLOCK_SCRIPT_VERIFY_FLAGS = SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS | SCRIPT_VERIFY_DERSIG | SCRIPT_VERIFY_NULLDUMMY; ``` Omits `SCRIPT_VERIFY_MINIMALDATA`, `SCRIPT_VERIFY_CLEANSTACK`, `SCRIPT_VERIFY_STRICTENC`, `SCRIPT_VERIFY_LOW_S`, `SCRIPT_VERIFY_NULLFAIL`, and `SCRIPT_VERIFY_TAPROOT`. Signet block solutions can use non-standard encodings that would be rejected on mainnet. ### H8. Post-Deserialization Size Checks in P2P Message Handlers `src/net_processing.cpp:3879,3967,4058,4949` ADDR, INV, GETDATA, NOTFOUND handlers deserialize entire vectors before checking against `MAX_*_SZ`. See original finding #9. ### H9. ProcessMessage Exception Handler Swallows All Failures `src/net_processing.cpp:5080-5083` All deserialization exceptions caught silently without disconnecting the peer. See original finding #10. ### H10. No ZMQ Authentication + Wallet Transaction Leakage `src/zmq/zmqpublishnotifier.cpp:118, zmqnotificationinterface.cpp:224-229` See original finding #11. --- ## Medium ### M1. RDTS Consent Logic Diverges Test vs Mainnet `src/validation.cpp:6759-6761` ```cpp if (GetParams().IsTestChain() ? (!g_enable_rdts) : GetConsensus().vDeployments[Consensus::DEPLOYMENT_REDUCED_DATA].nStartTime == Consensus::BIP9Deployment::NEVER_ACTIVE) { ``` Different logic for test chains vs mainnet when checking RDTS consent. Non-consenting nodes on mainnet with `g_enable_rdts==false` but `nStartTime != NEVER_ACTIVE` won't see the enforcement warning. ### M2. TRUC Assume() Removes Size Checks in Release Builds `src/policy/truc_policy.cpp:73` ```cpp if (!Assume(vsize <= TRUC_MAX_VSIZE || ignore_rejects.count(reason_prefix + "vsize-toobig"))) { ``` `Assume()` compiles to no-op in release. If a caller bypasses `SingleTRUCChecks`, the TRUC size guard is completely absent in production. Replace with explicit `if` check. ### M3. Fee Estimation Returns 0 for Multiple Error Conditions `src/policy/fees.cpp:726-759` ```cpp if (confTarget <= 0 || (unsigned int)confTarget > stats->GetMaxConfirms()) return CFeeRate(0); // indistinguishable from "0 fee is sufficient" ``` Three different error conditions all return `CFeeRate(0)`. Callers creating transactions may send 0-fee txs on estimation failure. **Fix:** Return `std::optional`. ### M4. Double Precision Loss in Coin Age Priority `src/policy/coin_age_priority.cpp:61` ```cpp r.inputs_coin_age += (double)(coin.out.nValue) * (nHeight - coin.nHeight); ``` For UTXO of 21M BTC aged 100k blocks, the product (~2.1e20) exceeds `double`'s 53-bit mantissa (~9e15). Loses integer precision, enabling fee sniping via priority collision. **Fix:** Use integer arithmetic or 128-bit intermediate calculation. ### M5. CDiskBlockIndex Unvalidated Fields from LevelDB `src/node/blockstorage.cpp:158-166` ```cpp pindexNew->nHeight = diskindex.nHeight; pindexNew->nFile = diskindex.nFile; pindexNew->nDataPos = diskindex.nDataPos; pindexNew->nUndoPos = diskindex.nUndoPos; ``` No validation that nHeight >= 0, nFile is valid, or file positions are within bounds. A corrupted BlockTreeDB with malicious entries causes UB on subsequent `ReadBlock`/`ReadBlockUndo`. **Fix:** Add bounds checks on all loaded index fields. ### M6. DB Dirty Sets Cleared Before WriteBatchSync Commit `src/node/blockstorage.cpp:598-614` ```cpp m_dirty_fileinfo.erase(it++); // ERASED BEFORE WriteBatchSync // ... if (!m_block_tree_db->WriteBatchSync(vFiles, max_blockfile, vBlocks, m_prune_locks)) { return false; // BUT DIRTY SETS ALREADY CLEARED } ``` If `WriteBatchSync` fails, the dirty sets are already empty - those changes are lost permanently. On restart, block index is inconsistent with on-disk state. **Fix:** Erase dirty sets only after successful WriteBatchSync. ### M7. Unbounded Fee Deltas in Mempool Persistence `src/node/mempool_persist.cpp:136-144` ```cpp CAmount amountdelta = nFeeDelta; pool.PrioritiseTransaction(tx->GetHash(), amountdelta); ``` `nFeeDelta` deserialized without bounds checking. Up to 9.2e18 satoshis could be applied, corrupting internal accounting across restarts. **Fix:** Bound `|nFeeDelta| <= actual_tx_fee`. ### M8. Unbounded mapDeltas During Mempool Load `src/node/mempool_persist.cpp:162-163` ```cpp std::map mapDeltas; file >> mapDeltas; ``` No size limit. Crafted mempool.dat can exhaust RAM. **Fix:** Add cap (e.g., 1M entries) and validate before deserialization. ### M9. FlatFilePos::IsNull Only Checks nFile `src/flatfile.h:36` ```cpp bool IsNull() const { return (nFile == -1); } ``` `FlatFilePos(0, 0)` is NOT null. Code checking `IsNull()` before using the position operates on real file 0, position 0 (genesis block data). ### M10. Prevector Insert Signed-to-Unsigned Wrap `src/prevector.h:417-419` ```cpp difference_type count = last - first; size_type new_size = _size + count; ``` If `last < first` (malicious serialization), `count` is negative. Signed-to-unsigned conversion wraps to near-SIZE_MAX, triggering OOM allocation. **Fix:** Assert `count >= 0` with runtime check. ### M11. Minisketch Benchmark OOB Access `src/node/minisketchwrapper.cpp:48-52` ```cpp if (!best || best->first > benches[5]) { best = std::make_pair(benches[5], impl); } ``` `benches[5]` accessed without checking `benches.size() > 5`. If fewer than 6 implementations are benchmarked (platform-dependent), this is out-of-bounds. **Fix:** Check `benches.size() > 5` before access. ### M12. SnapshotMetadata Blockhash Not Cross-Validated `src/node/utxo_snapshot.h:99-101`, `src/node/blockstorage.cpp:530` The metadata's `m_base_blockhash` is set from the block index without verification against the actual snapshot body contents hash. A crafted snapshot file with mismatched metadata/body blockhashes causes chainstate corruption. **Fix:** After loading snapshot body, verify `m_base_blockhash == actual_body_blockhash`. ### M13. Snapshot Base Hash File No Integrity Check `src/node/utxo_snapshot.cpp:39,74` WriteSnapshotBaseBlockhash writes raw 32 bytes with no magic/version/checksum. A crash during write leaves a truncated file that reads as a valid (but wrong) uint256. ### M14. No Duplicate Height Detection in LoadBlockIndex `src/node/blockstorage.cpp:554-558` Only detects gaps in height, not duplicate heights. Two blocks claiming the same height in a corrupted DB produces inconsistent chain state. **Fix:** Add duplicate height check. ### M15. Secp256k1 Wrong Context for xonly_tweak_add `src/key.cpp:421` ```cpp success = secp256k1_keypair_xonly_tweak_add(secp256k1_context_static, keypair, tweak.data()); ``` Requires non-static context (`secp256k1_extrakeys.h:165`). The call at line 418 correctly uses `secp256k1_context_sign`. **Fix:** Use `secp256k1_context_sign` for xonly_tweak_add. ### M16. Non-Constant-Time Private Key Comparison `src/key.h:96-98` ```cpp memcmp(a.data(), b.data(), a.size()) == 0; ``` Timing side-channel reveals prefix match length. **Fix:** Use `timingsafe_bcmp` or XOR-accumulation. ### M17. PSBT Leaks Wallet UTXO Structure `src/wallet/wallet.cpp:2354-2363` All previous transaction data attached to PSBT inputs. See original finding #14. ### M18. REST Mempool Exposure Without Authentication `src/rest.cpp:802-861` See original finding #15. ### M19. Coin Age Priority Reintroduction - Fee Sniping `src/policy/coin_age_priority.cpp:36-41,185` See original finding #16. ### M20. Software Expiry Clock-Based DoS `src/init.cpp:2565` See original finding #17. ### M21. RPC Auth Rate Limiting - Only 250ms `src/httprpc.cpp:176` See original finding #18. ### M22. ShellEscape Regex-Based Escaping Insufficient `src/common/system.cpp:40-45` ```cpp std::string ShellEscape(const std::string& arg) { std::string escaped = arg; ReplaceAll(escaped, "'", "'\"'\"'"); return "'" + escaped + "'"; } ``` Only handles single-quote escaping. Missing: backslash sequences, `$()`, backticks, newlines, and other shell metacharacters. Not a sound quoting mechanism. **Fix:** Use `execvp` with argument arrays instead of shell. If shell is needed, use a proper quoting function. ### M23. RunCommandParseJSON String-Split Argument Injection `src/common/run_command.cpp:16-47` ```cpp auto c = sp::Popen(str_command, ...); ``` The string constructor splits by whitespace naively (`subprocess.h:316-333`). Does not handle quoted arguments with spaces, causing argument splitting/smuggling. **Fix:** Use the `initializer_list` or `vector` constructors for argument arrays. ### M24. IPC Unix Socket TOCTOU Race `src/ipc/process.cpp:132-136` ```cpp if (fs::symlink_status(path).type() == fs::file_type::socket) { fs::remove(path); } // ... later: ::bind(fd, (struct sockaddr*)&addr, sizeof(addr)); ``` Attacker can replace the path with a symlink between the check and `bind()`, causing the server to bind to attacker-chosen file. **Fix:** Create socket in 0700 directory (already done at line 145-147 as defense-in-depth), or use abstract namespace sockets. ### M25. JSON Settings File No Size Limit `src/common/settings.cpp:92-93` ```cpp if (!in.read(std::string{std::istreambuf_iterator(file), std::istreambuf_iterator()})) { ``` Entire settings.json read into memory unbounded. Crafted file can cause OOM. **Fix:** Add file size check before read (e.g., 10MB). ### M26. ParseMoney Integer Overflow Before Range Check `src/util/moneystr.cpp:84-87` ```cpp int64_t nWhole = LocaleIndependentAtoi(strWhole); CAmount value = nWhole * COIN + nUnits; ``` `nWhole * COIN` overflows before `MoneyRange` check at line 87. The length check at line 80 (`<= 10 digits`) is insufficient guarantee. **Fix:** Check `nWhole <= MAX_MONEY / COIN` before multiplication. ### M27. V2Transport Per-Byte Garbage CPU Exhaustion `src/net.cpp:1319-1321` ```cpp case RecvState::GARB_GARBTERM: return 1; // Process garbage bytes one by one ``` Up to 4095 bytes processed at 1 byte per socket-read cycle. Many slow connections can cause CPU churn in the socket handler thread. No connection-level rate limit on garbage byte ingestion during the BIP324 handshake phase. ### M28. Sock::Wait shared_ptr Dangling Risk `src/util/sock.cpp:143-144` ```cpp std::shared_ptr shared{this, [](const Sock*) {}}; ``` No-op deleter doesn't extend object lifetime. If `Sock` destroyed while `WaitMany` is pending (e.g., `m_sock.reset()` from `CloseSocketDisconnect()` on another thread), the shared_ptr holds a dangling pointer. ### M29. Coin Age Priority Double Precision Loss in Transaction Priority `src/policy/coin_age_priority.cpp:70-71` ```cpp double deltaPriority = ((double)heightDiff*inChainInputValue)/nModSize; ``` Same double precision issue as M4. For large `inChainInputValue` (>2^53 satoshis), the product loses integer precision before division. ### M30. `walletnotify` Detaches Unlimited Threads `src/wallet/wallet.cpp:1317` ```cpp t.detach(); ``` No rate limiting. Many incoming transactions create unbounded threads. **Fix:** Use a bounded thread pool or single worker thread. ### M31. `assert(false)` Crash on Partial Wallet Encryption Failure `src/wallet/wallet.cpp:976-978` ```cpp assert(false); // "half of our keys encrypted in memory, half not" ``` Database transaction is aborted (`TxnAbort()`), so on-disk state is consistent - but a hard crash is excessive. Use controlled shutdown with cleanup. ### M32. RPC Console Command History in Plaintext QSettings `src/qt/rpcconsole.cpp:696-704` Command history stored in `~/.config/Limenka/Limenka-Qt.conf` unencrypted. While sensitive commands (`dumpprivkey`, `walletpassphrase`, etc.) are filtered from history, `getdescriptorinfo`, `deriveaddresses`, and `importdescriptors` with sensitive arguments are not. ### M33. m_tx_inventory_to_send Unbounded for Whitelisted Peers `src/net_processing.cpp:276` ```cpp std::set m_tx_inventory_to_send GUARDED_BY(m_tx_inventory_mutex); ``` No size limit for peers with `NetPermissionFlags::Relay`. Grows without bound if peer is slow to receive INV messages, consuming RAM. ### M34. FSChaCha20Poly1305 Decrypt Advances State on Tag Failure `src/crypto/chacha20poly1305.cpp:130-135` ```cpp bool ret = m_aead.Decrypt(...); NextPacket(); // ALWAYS advances, even if ret==false ``` Failed decryption advances packet counter, desynchronizing stream state and making the stream unrecoverable. **Fix:** Only call `NextPacket()` when `ret == true`. On failure, terminate connection. ### M35. P2SPKH in MANDATORY_SCRIPT_VERIFY_FLAGS While NEVER_ACTIVE `src/policy/policy.h:162`, `src/kernel/chainparams.cpp:144` `SCRIPT_VERIFY_P2SPKH` is in mandatory flags but deployment is `NEVER_ACTIVE`. Currently a no-op (only affects P2SPKH inputs), but if P2SPKH is later activated without updating policy flags, legitimate transactions could be rejected. ### M36. Undo Checksum Excludes Block Hash `src/node/blockstorage.cpp:1089-1092` ```cpp HashWriter hasher{}; hasher << block.pprev->GetBlockHash() << blockundo; ``` Undo file integrity check authenticates `pprev_hash || undo_data` but not the block hash itself. A swap of pprev_hash and undo_data could produce a valid checksum. **Fix:** Include `block.GetBlockHash()` in checksum. ### M37. Block Index Non-Contiguity Detection Height-Only `src/node/blockstorage.cpp:554-558` Only detects height gaps. Misses duplicate heights, fork conflicts, and chain discontinuities at the same height level. ### M38. Initialize m_recv_len Fragile Path in V2Transport `src/net.cpp:1243-1253` The `else if` branch at line 1250 reads `m_recv_len` which may not have been set if bytes arrived in unexpected order. Guarded by `GetMaxBytesToProcess()` invariants but fragile. ### M39. Fee Estimator HistoricalBlockSpan Unsigned Wrap-Around `src/policy/fees.cpp:792` ```cpp if (nBestSeenHeight - historicalBest > OLDEST_ESTIMATE_HISTORY) return 0; ``` Unsigned subtraction wraps if `nBestSeenHeight < historicalBest` (deep reorg). Produces large positive number that never exceeds `OLDEST_ESTIMATE_HISTORY`, retaining stale data. **Fix:** Check `nBestSeenHeight >= historicalBest` before subtraction. ### M40. Compressor Special Script Size Extension Hazard `src/compressor.h:62-93` Six special script cases mapped by index 0-5. Adding cases 6+ without updating `GetSpecialScriptSize` causes 0-byte decompression, embedding invalid scripts silently. **Fix:** Add static_assert or bounds check. ### M41. Third-party Tx URL Scheme Not Validated `src/qt/optionsdialog.cpp:844` `thirdPartyTxUrls` accepts arbitrary URL schemes. Non-`https://` URLs could trigger unexpected handlers. **Fix:** Validate scheme is `https://` on save. ### M42. `importwallet` Reads Arbitrary Files Through RPC `src/wallet/rpc/backup.cpp:568` ```cpp file.open(fs::u8path(request.params[0].get_str()), std::ios::in | std::ios::ate); ``` No path allowlist. Authenticated RPC user can read any file the process has access to (content fails wallet parsing, but error messages may leak information). ### M43. `dumpwallet` Writes to Arbitrary Path Through RPC `src/wallet/rpc/backup.cpp:805-806` ```cpp fs::path filepath = fs::absolute(fs::u8path(request.params[0].get_str())); ``` No path allowlist beyond preventing overwrite. Can write to any writeable location. ### M44. External Signer PSBT Leak in Error Message `src/external_signer.cpp:94-100` ```cpp error = "Signer fingerprint " + m_fingerprint + " does not match:\n" + EncodeBase64(ssTx.str()); ``` On fingerprint mismatch, the full base64-encoded PSBT is included in the user-facing error message. ### M45. Prevector malloc Overflow Check Depends on assert `src/prevector.h:203-204` ```cpp assert(alloc_bytes / sizeof(T) == new_capacity); ``` Overflow check compiled out in release mode where `NDEBUG` is defined (though Limenka's `util/check.h:49` prevents this via `#error`). Defense-in-depth should use explicit check. --- ## Low ### L1. Batch RPC Array Size Unbounded `src/httprpc.cpp:243` — Memory exhaustion with very large JSON arrays (32MB body limit). See original finding #21. ### L2. prevector raw malloc/realloc with assert `src/prevector.h:189-197` — See original finding #22. ### L3. Dead REST Endpoint `src/rest.cpp:1246-1247` — See original finding #23. ### L4. sethdseed Accepts Arbitrary WIF Key `src/wallet/rpc/wallet.cpp:552-591` — See original finding #24. ### L5. External Signer Arbitrary Binary Execution `src/wallet/external_signer_scriptpubkeyman.cpp:55` — See original finding #25. ### L6. DumpWallet Writes Private Keys to User-Specified Path `src/wallet/rpc/backup.cpp:805-893` — See original finding #26. ### L7. Signal Handlers Call Non-Async-Signal-Safe Operations `src/init.cpp:430-440` — `HandleSIGTERM` dereferences function pointer; `HandleSIGHUP` accesses logger mutex without lock. ### L8. No fork() Safety for RNG State `src/random.cpp:643-687` — Process-global RNG singleton after `fork()` produces identical output in parent and child. ### L9. RNGState Initialized with Zero-Entropy State `src/random.cpp:352` ```cpp unsigned char m_state[32] GUARDED_BY(m_mutex) = {0}; ``` Zero-state on first use. While `SeedStartup` provides real entropy before first output, defense-in-depth should initialize from OS entropy immediately. ### L10. ECDHShared Secret Not in Locked/Secure Memory `src/key.h:29` BIP324 ECDH shared secrets (32 bytes) live in ordinary `std::array`, not `mlock`ed. If swapped to disk, physical access could recover v2 transport session keys. ### L11. HMAC Constructor Leaves Key-Derived Material on Stack `src/crypto/hmac_sha256.cpp:11`, `src/crypto/hmac_sha512.cpp:11` `unsigned char rkey[64/128]` holds ipad-XOR'd and opad-XOR'd key material. Not zeroed before constructor returns. Stack inspection or core dump could recover key material. **Fix:** Add `memory_cleanse(rkey, sizeof(rkey))` before return. ### L12. Base58 Decode Integer Overflow on 32-bit `src/base58.cpp:54` ```cpp int size = strlen(psz) * 733 / 1000 + 1; ``` `strlen * 733` overflows 32-bit `int` for strings > 2.9M chars. Use `size_t` and explicit overflow check. ### L13. WALLET_CRYPTO_SALT_SIZE = 8 Bytes `src/wallet/crypter.h:15` — Below 16-byte recommendation. See original finding #6. ### L14. Deterministic IV for Key Encryption `src/wallet/crypter.cpp:115` — IV derived from pubkey hash. See original finding #7. ### L15. Minimal PBKDF2 Iterations Floor at 25,000 `src/wallet/wallet.cpp:684-685` — Below OWASP 2021 recommendation of 210,000 for PBKDF2-SHA512. ### L16. Passphrase in Unprotected std::string in Request Params `src/wallet/rpc/encrypt.cpp:51-52` — Passphrase in JSON param (heap, not mlocked) before copy to `SecureString`. ### L17. No Transport Encryption for RPC (HTTP Only) `src/httpserver.cpp` — Plain HTTP. Credentials and private keys travel unencrypted. Mitigated by default `127.0.0.1` binding. ### L18. BIP32 ParseHDKeypath No Maximum Path Length `src/util/bip32.cpp:18-50` — No limit on derivation levels. Crafted input with millions of entries causes excessive iteration. **Fix:** Enforce max path length (e.g., 255). ### L19. MerkleComputation Inner Array Hardcoded to 32 `src/consensus/merkle.cpp:94-108` ```cpp uint256 inner[32]; Assume(leaves.size() <= UINT32_MAX); ``` If `Assume` is compiled out, a tree with 2^32+1 leaves writes past the array boundary. **Fix:** Replace `Assume` with explicit `if` check. ### L20. AccessByTxid Unbounded Loop `src/coins.cpp:348-360` Up to `MAX_OUTPUTS_PER_BLOCK` (~120k) iterations per undo record with missing height metadata. Deep reorgs could trigger CPU exhaustion. ### L21. CheckOutputSizes Blocks Taproot But Not Future Witness Versions `src/consensus/tx_verify.cpp:164-178` Blocks witness v1 at 34 bytes but allows v2-v16 with 32-byte programs, delegating to `VerifyWitnessProgram`. Intentional forward-compatibility but coarse-grained. ### L22. SignatureCache Lacks Chain/Network Separation `src/script/sigcache.h:40-48` Cache key is `SHA256(salt || hash || pubkey || sig)` without chain identifier. Multi-chain nodes could have cache poisoning between networks. ### L23. TOCTOU in LockDirectory `src/util/fs_helpers.cpp:69-72` Between `fopen` and `FileLock` creation, another process could replace the lock file with a symlink. ### L24. IsDirWritable Symlink TOCTOU `src/util/fs_helpers.cpp:435-436` Between path construction and `fopen("wbx")`, a concurrent symlink attack could redirect writes. ### L25. UrlDecode %00 to NUL Byte `src/common/url.cpp:17-38` — Decodes `%00` to NUL byte appended to result string. If passed to C-string APIs, causes silent truncation. ### L26. CBloomFilter Division by Zero When nElements=0 `src/common/bloom.cpp:31-37` — Constructor divides by `nElements`. Guarded by `insert()/contains()` early returns but not construction-time. ### L27. CRollingBloomFilter Data Size Odd Check Missing `src/common/bloom.cpp:231-233` — `data.size()` must be even for `pos | 1` access. ### L28. SplitHostPort Bracketed IPv6 Fragile Logic `src/util/strencodings.cpp:76-101` — `in[colon-1]` access depends on `fHaveColon` guaranteeing `colon != 0`. Logically correct but fragile. ### L29. m_blockfile_info Out-of-Bounds Access Potential `src/node/blockstorage.cpp:942-943` ```cpp if (static_cast(m_blockfile_info.size()) <= nFile) { ``` Casting `size()` to signed `int`. If `size() > INT_MAX`, the cast produces weird results. ### L30. Flatfile Allocate n_old_chunks/n_new_chunks Overflow `src/flatfile.cpp:61-62` — For very large `add_size` near UINT_MAX, `n_new_chunks` wraps. Mitigated by block size limits. ### L31. MAX_BLOCKFILE_SIZE Assert on Oversize Block `src/node/blockstorage.h:81, blockstorage.cpp:957` — 128 MiB file limit. If `blockmaxweight` is configured very high, assertion fails (crash). Should handle gracefully. ### L32. Mempool Load Caps Total Transactions But Not Individual Sizes `src/node/mempool_persist.cpp:108-111` — Max `MAX_MEMPOOL_LOAD_TXNS` (500k) but no per-tx size validation beyond deserialization MAX_SIZE. ### L33. BaseIndex::Commit No Atomicity for Rewind+Commit `src/index/base.h:84-96` — Crash between `Rewind` and `Commit` leaves index in inconsistent state on restart. ### L34. SnapshotMetadata m_supported_versions Non-Static `src/node/utxo_snapshot.h:80-82` — Local `const set` per instance, not `static const`. Memory waste, no security impact. ### L35. URIParseAmount Hex Amount Accepts Encoded Data `src/qt/guiutil.cpp:227-258` — `x`/`X` hex prefix allows encoding arbitrary patterns in `limenka:` URI amount field. ### L36. QFile::exists IPC Path Probing `src/qt/paymentserver.cpp:231` — IPC-connected local process can probe for file existence by observing error responses. ### L37. RPC LineEdit 16MB Input Limit `src/qt/rpcconsole.cpp:628` — 16MB single-line RPC command can cause brief client-side memory DoS. ### L38. Font Family CSS Injection `src/qt/guiutil.cpp:121-129` — `fontToCss` escapes `"` and `\` but not `;`, `{`, `}`. System-provided font names unlikely malicious. ### L39. Dual Clipboard Exposure on X11 `src/qt/guiutil.cpp:909-916` — Address copied to both `Clipboard` and `Selection`. Doubles attack surface for clipboard-monitoring malware. ### L40. `includeconf` Path Traversal Potential `src/common/config.cpp:190-194` — Absolute include paths bypass datadir. Low risk (config file is local and trusted). ### L41. ModifyRWConfigFile No Backup Before Overwrite `src/common/args.cpp:1085-1107` — Explicit `remove` before `ofstream` opens a window with no backup. Rename-over provides atomicity. ### L42. Subprocess close_all_fds Ignores EINTR on close() `src/util/subprocess.cpp:25-49` — On EINTR, file descriptor leaks into child process. ### L43. execvp PATH Lookup in Subprocess `src/subprocess.h:1429` — If `PATH` is attacker-controlled, arbitrary binary execution. ### L44. Netlink Socket 4096-Byte Fixed Buffer `src/common/netif.cpp:89-96` — Large routing tables exceed 4096-byte netlink response buffer. ### L45. Windows GetLocalAddresses DNS Leak `src/common/netif.cpp:277-278` — Hostname DNS lookup on Windows leaks hostname to DNS servers. ### L46. BIP21 r Parameter - Old BIP70 Path Bypassable `src/qt/paymentserver.cpp:209` — BIP70 is correctly rejected but the `r` parameter handling could be exploited for phishing via wallet confusion with old URIs. ### L47. No `-ftrivial-auto-var-init=pattern` Harden Flag `CMakeLists.txt:191` — Only in fuzzing presets. Missing in default hardening. Leaves uninitialized stack variables. ### L48. REDUCE_EXPORTS Off by Default `CMakeLists.txt:192` — Hidden visibility and `--exclude-libs,ALL` only with `REDUCE_EXPORTS=ON`. Increases attack surface. ### L49. No `_GLIBCXX_ASSERTIONS` in Debug Mode `CMakeLists.txt:646` — Only in hardening config. Should also be in debug builds for STL bounds checking. ### L50. V2Transport Garbage Terminator Match Timing Side Channel `src/net.cpp:1208-1226` — `std::ranges::equal` byte-by-byte comparison. Attacker needs shared secret already (circular; not exploitable). ### L51. RNGState Zero-Initialized m_state `src/random.cpp:352` — Zero-state feeds into CSHA512 mixer before first seed. ### L52. Fee Estimates File Version Stale `src/policy/fees.cpp:37` — `CURRENT_FEES_FILE_VERSION` at 149900 despite Knots-specific features added. Should be >= 289901 per the file's own comment. --- ## Vendored Dependencies | Dependency | Version | CVEs | Status | |-----------|---------|------|--------| | secp256k1 | 0.6.0 + post-release patches | None | Current | | leveldb | 1.22.0 + Limenka patches | None | Current (maintained fork) | | minisketch | 0.0.1 + patches | None | Current | | univalue | Limenka fork | None | Current | | crc32c | 1.1.0 | None | Current | All vendored dependencies at latest stable versions with maintenance patches. No known CVEs. Specific deep-dive findings: - **secp256k1:** All API return values checked. Context randomization correct. RFC6979 deterministic nonces correct. Schnorr uses tagged hashes correctly. Self-verification after signing catches corrupted signatures. - **leveldb:** Varint32/64 decoding has proper shift bounds. MMap limit set to 4096 files. Key obfuscation correct. memenv LockFile is no-op (tests only). - **minisketch:** Benchmark code has OOB access (M11 above). Wrapper correctly handles decode failures. - **univalue:** Correct UTF-8 validation (surrogate pairs, reject overlong). MAX_JSON_DEPTH=512 prevents stack overflow. All ASCII control chars escaped. - **crc32c:** Correct CPU feature detection (SSE4.2, ARM CRC32+PMULL). Alignment-safe SIMD paths. Fallback portable implementation correct. --- ## Knots-Specific Consensus Divergences vs Core | Feature | Knots | Core | |---------|-------|------| | MAX_OUTPUT_SCRIPT_SIZE | 34 (consensus) | none (policy only) | | Coin age priority | Reintroduced | Removed in v0.15.0 | | Witness v3 | P2SPKH enforcement | Always-success forward-compat | | Script element size | 256 (RDTS active) | 520 | | Taproot control nodes | 7 (RDTS active) | 128 | | Annex in tapscript | Banned (RDTS active) | Allowed | | OP_IF in tapscript | Banned (RDTS active) | Allowed | | NODE_REDUCED_DATA (bit 27) | Required for outbound | Not present | | Subdust fee penalty | Default ON | Not present | | MAX_DUST_OUTPUTS_PER_TX | 1 | No explicit cap | | Software expiry | Forced at ~2 years | Not present | | RDTS threshold | 55% | N/A | | RDTS mandatory activation | Height 965664 | N/A | | RDTS expiry | 52416 blocks (~1 year) | N/A | --- ## Build Hardening Status | Feature | Status | |---------|--------| | `_FORTIFY_SOURCE=3` | Enabled | | `-fstack-protector-all` | Enabled | | `-fstack-clash-protection` | Enabled | | `-fcf-protection=full` (CET) | Enabled | | `-mbranch-protection=standard` (aarch64) | Enabled | | PIE + Full RELRO | Enabled | | NX stack (`-z,noexecstack`) | Not explicitly set (toolchain default) | | `-ftrivial-auto-var-init=pattern` | **MISSING** | | `-fvisibility=hidden` + `--exclude-libs,ALL` | Only with `REDUCE_EXPORTS=ON` | | Thread safety annotations | Extensive | | Lock order debugging | DEBUG_LOCKORDER mode | | Assertions cannot be compiled out | `#error` if NDEBUG | --- ## Historical CVE Status All known Limenka consensus CVEs mitigated: CVE-2010-5137, CVE-2010-5139, CVE-2010-5141, CVE-2012-1909, CVE-2012-2459, CVE-2018-17144, CVE-2021-31876. SIGHASH_SINGLE `uint256::ONE` bug (`interpreter.cpp:1614-1618`) and `FindAndDelete` signature malleability (`interpreter.cpp:330-334`) remain in pre-segwit paths - consensus-critical, not fixable without hard fork. --- ## Recommended Actions (ordered by priority) ### Immediate (Critical/High) 1. **Remove UNSUPPORTED_UNSAFE_NO_ENFORCEMENT compile flag** or gate forced activation on it (C1) 2. **Fix dead P2SPKH policy code** - move checks outside taproot/tapscript blocks (`policy.cpp:455-465`) 3. **Replace `::system()` with `execvp`** in `runCommand` (C3) 4. **Demote software expiry to warning** - never hard-reject blocks (C4) 5. **Fix TxValidationResult in CheckOutputSizes** - use `TX_CONSENSUS` (H6) 6. **Fix post-deserialization size checks** in ADDR/INV/GETDATA/NOTFOUND (H8) 7. **Add disconnect on repeated deserialization failure** in ProcessMessage (H9) ### Short-Term (High) 8. **Replace wallet legacy KDF** with PBKDF2-HMAC-SHA512 or Argon2id 9. **Add AEAD to wallet encryption** (method 0 -> method 2 migration) 10. **Increase WALLET_CRYPTO_SALT_SIZE** to 16+ bytes, use random IVs 11. **Fix secp256k1 context** at `key.cpp:421` - use `secp256k1_context_sign` 12. **Replace memcmp with constant-time comparison** in `CKey::operator==` 13. **Document P2SPKH UASF chain split risk** in deployment docs 14. **Add signet block verification flags** (MINIMALDATA, CLEANSTACK, etc.) 15. **Replace TRUC Assume() checks** with explicit runtime checks ### Medium-Term 16. **Validate CDiskBlockIndex fields** when loading from BlockTreeDB (M5) 17. **Move dirty set clearing after WriteBatchSync** success (M6) 18. **Bound mempool fee deltas and mapDeltas** during load (M7, M8) 19. **Fix FlatFilePos::IsNull** to check all fields (M9) 20. **Add runtime check for prevector insert count** (M10) 21. **Fix minisketch benchmark OOB access** (M11) 22. **Cross-validate snapshot metadata blockhash** (M12) 23. **Add snapshot base hash integrity check** (M13) 24. **Detect duplicate heights in LoadBlockIndex** (M14) 25. **Replace ShellEscape/runCommand** with argument-array process spawning 26. **Fix IPC socket creation race** with abstract namespace or O_NOFOLLOW 27. **Bound settings.json file size** before reading (M25) 28. **Fix ParseMoney overflow** with pre-multiplication range check (M26) 29. **Fix FSChaCha20Poly1305 state advancement** on decrypt failure (M34) 30. **Add ZMQ authentication** or document the exposure 31. **Add REST authentication option** or document mempool exposure 32. **Add `-ftrivial-auto-var-init=pattern`** to default hardening flags 33. **Consider enabling REDUCE_EXPORTS** by default in release builds 34. **Add `_GLIBCXX_ASSERTIONS`** to debug build configuration ### Low-Hanging (Low) 35. Memory_cleanse HMAC rkey before constructor return 36. Bound BIP32 path length to 255 37. Replace merkle Assume with explicit check 38. Bound batch RPC array size 39. Add `timingsafe_bcmp` for key comparison 40. Add thirdPartyTxUrls scheme validation 41. Fix font CSS injection escaping 42. Bound RPC line edit to reasonable size (< 64KB) 43. Reduce COPY+Selection dual clipboard exposure on X11 44. Validate hex amounts in BIP21 URI parsing