SECURITY_AUDIT.md raw

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

SeverityCountKey Areas
Critical4Chainsplit vectors, dead policy code, shell injection
High10Wallet crypto, RDTS deployment, wrong validation result, signet flags, TRUC guard, coin-age double, serialization
Medium45DoS, crypto side-channels, mempool persistence, index corruption, race conditions, fee estimation, input validation
Low52TOCTOU, buffer management, hardening gaps, prevector, clipboard, encoding edge cases
Info25Documentation, 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:

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.

// versionbits.cpp:99-102 - Forces LOCKED_IN regardless of miner signaling
} else if (max_activation_height < std::numeric_limits<int>::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<int>::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

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

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 LOCKEDIN). The `activeduration = INTMAX` means this is permanent on mainnet, not temporary. On testnet/signet, `maxactivationheight = INTMAX` 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. MAXOUTPUTSCRIPT_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

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

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

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

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

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<CFeeRate>.

M4. Double Precision Loss in Coin Age Priority

src/policy/coin_age_priority.cpp:61

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

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

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

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

std::map<uint256, CAmount> 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

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

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

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 xonlytweakadd

src/key.cpp:421

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 xonlytweakadd.

M16. Non-Constant-Time Private Key Comparison

src/key.h:96-98

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

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

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<const char*> or vector<string> constructors for argument arrays.

M24. IPC Unix Socket TOCTOU Race

src/ipc/process.cpp:132-136

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

if (!in.read(std::string{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()})) {

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

int64_t nWhole = LocaleIndependentAtoi<int64_t>(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

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

std::shared_ptr<const Sock> 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

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

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

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. mtxinventorytosend Unbounded for Whitelisted Peers

src/net_processing.cpp:276

std::set<uint256> 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

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 MANDATORYSCRIPTVERIFYFLAGS While NEVERACTIVE

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

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 pprevhash and undodata 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 mrecvlen 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

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

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

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

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

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-440HandleSIGTERM 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

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 mlocked. 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

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. WALLETCRYPTOSALT_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

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-233data.size() must be even for pos | 1 access.

L28. SplitHostPort Bracketed IPv6 Fragile Logic

src/util/strencodings.cpp:76-101in[colon-1] access depends on fHaveColon guaranteeing colon != 0. Logically correct but fragile.

L29. mblockfileinfo Out-of-Bounds Access Potential

src/node/blockstorage.cpp:942-943

if (static_cast<int>(m_blockfile_info.size()) <= nFile) {

Casting size() to signed int. If size() > INT_MAX, the cast produces weird results.

L30. Flatfile Allocate noldchunks/nnewchunks Overflow

src/flatfile.cpp:61-62 — For very large add_size near UINTMAX, `nnew_chunks` wraps. Mitigated by block size limits.

L31. MAXBLOCKFILESIZE 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 msupportedversions 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-258x/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-129fontToCss 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 closeallfds 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-1226std::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:37CURRENT_FEES_FILE_VERSION at 149900 despite Knots-specific features added. Should be >= 289901 per the file's own comment.

Vendored Dependencies

DependencyVersionCVEsStatus
secp256k10.6.0 + post-release patchesNoneCurrent
leveldb1.22.0 + Limenka patchesNoneCurrent (maintained fork)
minisketch0.0.1 + patchesNoneCurrent
univalueLimenka forkNoneCurrent
crc32c1.1.0NoneCurrent

All vendored dependencies at latest stable versions with maintenance patches. No known CVEs. Specific deep-dive findings:

Knots-Specific Consensus Divergences vs Core

FeatureKnotsCore
MAXOUTPUTSCRIPT_SIZE34 (consensus)none (policy only)
Coin age priorityReintroducedRemoved in v0.15.0
Witness v3P2SPKH enforcementAlways-success forward-compat
Script element size256 (RDTS active)520
Taproot control nodes7 (RDTS active)128
Annex in tapscriptBanned (RDTS active)Allowed
OP_IF in tapscriptBanned (RDTS active)Allowed
NODEREDUCEDDATA (bit 27)Required for outboundNot present
Subdust fee penaltyDefault ONNot present
MAXDUSTOUTPUTSPERTX1No explicit cap
Software expiryForced at ~2 yearsNot present
RDTS threshold55%N/A
RDTS mandatory activationHeight 965664N/A
RDTS expiry52416 blocks (~1 year)N/A

Build Hardening Status

FeatureStatus
_FORTIFY_SOURCE=3Enabled
-fstack-protector-allEnabled
-fstack-clash-protectionEnabled
-fcf-protection=full (CET)Enabled
-mbranch-protection=standard (aarch64)Enabled
PIE + Full RELROEnabled
NX stack (-z,noexecstack)Not explicitly set (toolchain default)
-ftrivial-auto-var-init=patternMISSING
-fvisibility=hidden + --exclude-libs,ALLOnly with REDUCE_EXPORTS=ON
Thread safety annotationsExtensive
Lock order debuggingDEBUG_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)

  1. Replace wallet legacy KDF with PBKDF2-HMAC-SHA512 or Argon2id
  2. Add AEAD to wallet encryption (method 0 -> method 2 migration)
  3. Increase WALLET_CRYPTO_SALT_SIZE to 16+ bytes, use random IVs
  4. Fix secp256k1 context at key.cpp:421 - use secp256k1_context_sign
  5. Replace memcmp with constant-time comparison in CKey::operator==
  6. Document P2SPKH UASF chain split risk in deployment docs
  7. Add signet block verification flags (MINIMALDATA, CLEANSTACK, etc.)
  8. Replace TRUC Assume() checks with explicit runtime checks

Medium-Term

  1. Validate CDiskBlockIndex fields when loading from BlockTreeDB (M5)
  2. Move dirty set clearing after WriteBatchSync success (M6)
  3. Bound mempool fee deltas and mapDeltas during load (M7, M8)
  4. Fix FlatFilePos::IsNull to check all fields (M9)
  5. Add runtime check for prevector insert count (M10)
  6. Fix minisketch benchmark OOB access (M11)
  7. Cross-validate snapshot metadata blockhash (M12)
  8. Add snapshot base hash integrity check (M13)
  9. Detect duplicate heights in LoadBlockIndex (M14)
  10. Replace ShellEscape/runCommand with argument-array process spawning
  11. Fix IPC socket creation race with abstract namespace or O_NOFOLLOW
  12. Bound settings.json file size before reading (M25)
  13. Fix ParseMoney overflow with pre-multiplication range check (M26)
  14. Fix FSChaCha20Poly1305 state advancement on decrypt failure (M34)
  15. Add ZMQ authentication or document the exposure
  16. Add REST authentication option or document mempool exposure
  17. Add `-ftrivial-auto-var-init=pattern` to default hardening flags
  18. Consider enabling REDUCE_EXPORTS by default in release builds
  19. Add `_GLIBCXX_ASSERTIONS` to debug build configuration

Low-Hanging (Low)

  1. Memory_cleanse HMAC rkey before constructor return
  2. Bound BIP32 path length to 255
  3. Replace merkle Assume with explicit check
  4. Bound batch RPC array size
  5. Add timingsafe_bcmp for key comparison
  6. Add thirdPartyTxUrls scheme validation
  7. Fix font CSS injection escaping
  8. Bound RPC line edit to reasonable size (< 64KB)
  9. Reduce COPY+Selection dual clipboard exposure on X11
  10. Validate hex amounts in BIP21 URI parsing