mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-26 23:19:07 +00:00
A walk that reaches a position only a leaf may occupy marks the map Invalid and abandons the descent. Reaching that position is otherwise fatal: SHAMapNodeID::getChildNodeID() throws std::logic_error past kLeafDepth, and there is no try/catch around getMissingNodes() in InboundLedger::trigger(), around job.doJob() in JobQueue, or in Workers::Worker::run(), so the exception leaves the thread function and reaches std::terminate(). It is reachable without any node passing through addKnownNode(): InboundLedgers::gotStaleData() stores any parseable node from an unsolicited liAS_NODE reply into the fetch pack keyed by its own hash, with no relatedness check, and getFetchPack() re-verifies only that hash, so such a node canonicalizes into the tree and the walk descends onto it. Steering which hash a node acquires needs it to have no trusted validations - starting up, or an empty or misconfigured UNL - with the attacker holding its peer slots, which makes this a conditioned remote denial of service rather than a single-packet one. As in addKnownNode(), the depth test precedes the full-below cache lookup, since that cache is keyed by node hash and shared across maps and a hash does not cover depth, so a hit would carry the whole branch past the guard. Four further tests of isValid() bound what the walk does once the verdict lands: it short-circuits on entry rather than re-deriving a verdict already reached; it breaks out of the descent but falls through to the deferred-read drain, since posted reads hold a reference to the MissingNodes block on this frame; it discards whatever was collected, which belongs to a tree that cannot exist; and it re-tests before clearSynching(), since another thread's addKnownNode() can write the verdict after the loop's own test. Callers therefore have to re-check isValid() before reading an empty result as nothing left to fetch, which getMissingNodes()'s docstring states. Three of those four guards are defensive and no test drives them; only the entry short-circuit and the verdict itself are pinned. DeepChain gains withDecoys(): the same chain, but with a second and unresolvable child at every level, so a backed map's descendAsync() posts a real asynchronous read at every level. That is what leaves reads in flight when a walk reaches kLeafDepth, which is what the sanitizer case below needs. Seven cases cover this. Five drive the walk through a ChainFilter, which stands in for a fetch pack by serving nodes by hash and never structurally: the walk reaches the verdict itself on an unbacked map; it does so on a backed map with the offending node already marked full below; it drains the reads a decoy child at every level leaves in flight, which only a sanitizer can see; it refuses an already-invalid map; and it leaves a walk that stops one level short alone, reporting the genuinely missing child. The sixth pins that addRootNode() cannot clear the synching flag on an invalid map, since that call site needs a leaf root and so a zero root hash. The seventh races a walk against setImmutable() under ThreadSanitizer, and asserts only what trySetState() offers: the verdict stands, whatever the interleaving. It is skipped at run time rather than compiled out, so every build parses it.