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)
| 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
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.
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.
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.
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.
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.
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.
src/consensus/consensus.h:37
See original finding #4.
src/wallet/crypter.cpp:15-39
See original finding #5.
src/wallet/crypter.cpp:76-92
AES-256-CBC with no MAC. See original finding #8.
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.
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.
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.
src/net_processing.cpp:5080-5083
All deserialization exceptions caught silently without disconnecting the peer. See original finding #10.
src/zmq/zmqpublishnotifier.cpp:118, zmqnotificationinterface.cpp:224-229
See original finding #11.
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.
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.
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>.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
src/wallet/wallet.cpp:2354-2363
All previous transaction data attached to PSBT inputs. See original finding #14.
src/rest.cpp:802-861
See original finding #15.
src/policy/coin_age_priority.cpp:36-41,185
See original finding #16.
src/init.cpp:2565
See original finding #17.
src/httprpc.cpp:176
See original finding #18.
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.
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.
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.
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).
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.
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.
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.
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.
walletnotify Detaches Unlimited Threadssrc/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.
assert(false) Crash on Partial Wallet Encryption Failuresrc/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.
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.
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.
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.
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.
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.
src/node/blockstorage.cpp:554-558
Only detects height gaps. Misses duplicate heights, fork conflicts, and chain discontinuities at the same height level.
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.
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.
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.
src/qt/optionsdialog.cpp:844
thirdPartyTxUrls accepts arbitrary URL schemes. Non-https:// URLs could trigger unexpected handlers.
Fix: Validate scheme is https:// on save.
importwallet Reads Arbitrary Files Through RPCsrc/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).
dumpwallet Writes to Arbitrary Path Through RPCsrc/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.
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.
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.
src/httprpc.cpp:243 — Memory exhaustion with very large JSON arrays (32MB body limit). See original finding #21.
src/prevector.h:189-197 — See original finding #22.
src/rest.cpp:1246-1247 — See original finding #23.
src/wallet/rpc/wallet.cpp:552-591 — See original finding #24.
src/wallet/external_signer_scriptpubkeyman.cpp:55 — See original finding #25.
src/wallet/rpc/backup.cpp:805-893 — See original finding #26.
src/init.cpp:430-440 — HandleSIGTERM dereferences function pointer; HandleSIGHUP accesses logger mutex without lock.
src/random.cpp:643-687 — Process-global RNG singleton after fork() produces identical output in parent and child.
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.
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.
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.
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.
src/wallet/crypter.h:15 — Below 16-byte recommendation. See original finding #6.
src/wallet/crypter.cpp:115 — IV derived from pubkey hash. See original finding #7.
src/wallet/wallet.cpp:684-685 — Below OWASP 2021 recommendation of 210,000 for PBKDF2-SHA512.
src/wallet/rpc/encrypt.cpp:51-52 — Passphrase in JSON param (heap, not mlocked) before copy to SecureString.
src/httpserver.cpp — Plain HTTP. Credentials and private keys travel unencrypted. Mitigated by default 127.0.0.1 binding.
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).
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.
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.
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.
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.
src/util/fs_helpers.cpp:69-72
Between fopen and FileLock creation, another process could replace the lock file with a symlink.
src/util/fs_helpers.cpp:435-436
Between path construction and fopen("wbx"), a concurrent symlink attack could redirect writes.
src/common/url.cpp:17-38 — Decodes %00 to NUL byte appended to result string. If passed to C-string APIs, causes silent truncation.
src/common/bloom.cpp:31-37 — Constructor divides by nElements. Guarded by insert()/contains() early returns but not construction-time.
src/common/bloom.cpp:231-233 — data.size() must be even for pos | 1 access.
src/util/strencodings.cpp:76-101 — in[colon-1] access depends on fHaveColon guaranteeing colon != 0. Logically correct but fragile.
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.
src/flatfile.cpp:61-62 — For very large add_size near UINTMAX, `nnew_chunks` wraps. Mitigated by block size limits.
src/node/blockstorage.h:81, blockstorage.cpp:957 — 128 MiB file limit. If blockmaxweight is configured very high, assertion fails (crash). Should handle gracefully.
src/node/mempool_persist.cpp:108-111 — Max MAX_MEMPOOL_LOAD_TXNS (500k) but no per-tx size validation beyond deserialization MAX_SIZE.
src/index/base.h:84-96 — Crash between Rewind and Commit leaves index in inconsistent state on restart.
src/node/utxo_snapshot.h:80-82 — Local const set per instance, not static const. Memory waste, no security impact.
src/qt/guiutil.cpp:227-258 — x/X hex prefix allows encoding arbitrary patterns in limenka: URI amount field.
src/qt/paymentserver.cpp:231 — IPC-connected local process can probe for file existence by observing error responses.
src/qt/rpcconsole.cpp:628 — 16MB single-line RPC command can cause brief client-side memory DoS.
src/qt/guiutil.cpp:121-129 — fontToCss escapes " and \ but not ;, {, }. System-provided font names unlikely malicious.
src/qt/guiutil.cpp:909-916 — Address copied to both Clipboard and Selection. Doubles attack surface for clipboard-monitoring malware.
includeconf Path Traversal Potentialsrc/common/config.cpp:190-194 — Absolute include paths bypass datadir. Low risk (config file is local and trusted).
src/common/args.cpp:1085-1107 — Explicit remove before ofstream opens a window with no backup. Rename-over provides atomicity.
src/util/subprocess.cpp:25-49 — On EINTR, file descriptor leaks into child process.
src/subprocess.h:1429 — If PATH is attacker-controlled, arbitrary binary execution.
src/common/netif.cpp:89-96 — Large routing tables exceed 4096-byte netlink response buffer.
src/common/netif.cpp:277-278 — Hostname DNS lookup on Windows leaks hostname to DNS servers.
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.
-ftrivial-auto-var-init=pattern Harden FlagCMakeLists.txt:191 — Only in fuzzing presets. Missing in default hardening. Leaves uninitialized stack variables.
CMakeLists.txt:192 — Hidden visibility and --exclude-libs,ALL only with REDUCE_EXPORTS=ON. Increases attack surface.
_GLIBCXX_ASSERTIONS in Debug ModeCMakeLists.txt:646 — Only in hardening config. Should also be in debug builds for STL bounds checking.
src/net.cpp:1208-1226 — std::ranges::equal byte-by-byte comparison. Attacker needs shared secret already (circular; not exploitable).
src/random.cpp:352 — Zero-state feeds into CSHA512 mixer before first seed.
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.
| 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:
| Feature | Knots | Core |
|---|---|---|
| MAXOUTPUTSCRIPT_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 |
| NODEREDUCEDDATA (bit 27) | Required for outbound | Not present |
| Subdust fee penalty | Default ON | Not present |
| MAXDUSTOUTPUTSPERTX | 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 |
| 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 |
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.
policy.cpp:455-465)runCommand (C3)TX_CONSENSUS (H6)key.cpp:421 - use secp256k1_context_signCKey::operator==timingsafe_bcmp for key comparison