@@ -40,34 +40,42 @@ The single most important phase. Today a **one-byte UDP packet from anyone** —
4040chain response from a peer, or a mistyped ` peer add ` — unwinds out of the event loop and exits
4141the process ([ lotid.cpp:889] ( ../../src/app/lotid/lotid.cpp#L889 ) ).
4242
43- - [ ] ** 1.1 — Isolate every reactor callback.** Wrap the timer-callback and fd-readable dispatch
43+ ** Status: DONE** (commits ` harden: malformed/dishonest datagrams… ` and `harden: decoder length
44+ caps…` ). T1/T2 reproduced both crashes before the fix; ` test/acceptance/fuzz_udp.sh` confirms a
45+ running daemon now survives a 2100-datagram flood + a bad control command.
46+
47+ - [x] ** 1.1 — Isolate every reactor callback.** Wrap the timer-callback and fd-readable dispatch
4448 in [ reactor.hpp] ( ../../src/adapters/os/reactor.hpp#L87 ) (` fire_due_timers ` , the ` run() ` dispatch
4549 loop) in ` try { … } catch (const std::exception& e) { log; } ` so one throwing callback is
4650 logged and dropped, never fatal. This is the structural root fix — every finding below that ends
4751 in "→ crash" is downgraded to "→ one dropped packet/command" once this lands.
48- - [ ] ** 1.2 — Guard the UDP receive path.** In the reader lambda at
52+ - [x ] ** 1.2 — Guard the UDP receive path.** In the reader lambda at
4953 [ lotid.cpp:305-311] ( ../../src/app/lotid/lotid.cpp#L305 ) , wrap ` on_packet_received(datagram) ` in a
5054 per-datagram ` try/catch ` (drop + optional debug log). Belt-and-suspenders with 1.1; keeps a
5155 storm of bad packets from spamming the generic reactor logger.
52- - [ ] ** 1.3 — Decode is validation, not a fatal parse.** ` wire::decode `
56+ - [x ] ** 1.3 — Decode is validation, not a fatal parse.** ` wire::decode `
5357 ([ packets.cpp:80] ( ../../src/core/wire/packets.cpp#L80 ) ) and the ` Reader ` ` need() ` guard
5458 ([ codec.hpp:151] ( ../../src/core/wire/codec.hpp#L151 ) ) throw on any malformed field. Confirm the
5559 only callers are inside the now-guarded paths (transport receive, ` proof::deserialize ` ). Keep
5660 ` decode ` throwing (it is the right signal); the fix is that callers must catch.
57- - [ ] ** 1.4 — Validation on the live path returns, it does not throw.** Change
61+ - [x ] ** 1.4 — Validation on the live path returns, it does not throw.** Change
5862 ` Node::validate_chain_discovery_result ` ([ node.cpp:580] ( ../../src/core/node.cpp#L580 ) ) so a
5963 failed ` validate::verify_chain ` ** aborts the discovery** (like an expiry) instead of
6064 ` throw std::runtime_error ` at [ node.cpp:585] ( ../../src/core/node.cpp#L585 ) . A dishonest peer is
6165 the * expected* case in a trustless network; it must not be fatal. (The offline ` proof::verify `
6266 path already returns a result — this makes the two symmetric.)
63- - [ ] ** 1.5 — Guard the control/stdin command path.** Every command in ` Lotid::dispatch ` is called
67+ - [x ] ** 1.5 — Guard the control/stdin command path.** Every command in ` Lotid::dispatch ` is called
6468 bare except ` db-restore ` ; ` peer add <bad-ip> ` throws from ` UdpTransport::set_peer `
6569 ([ transport.hpp:59] ( ../../src/adapters/os/transport.hpp#L59 ) ) and kills the daemon. Wrap command
6670 dispatch so a bad command returns an ` ERR ` reply, never exits. Validate numeric/address args
6771 (` strtoull ` /` atoi ` /` parse_range ` currently accept garbage silently) and reply ` ERR ` on bad input.
6872- [ ] ** 1.6 — Preserve shutdown cleanup on fatal exit.** If the outer handler at
6973 [ lotid.cpp:889] ( ../../src/app/lotid/lotid.cpp#L889 ) is ever reached, unlink the control socket and
7074 ` lmdb_->sync() ` before returning, so a crash in lazy-sync mode does not skip the final flush.
75+ ** Deferred (low value):** with 1.1 in place the reactor loop no longer throws, so the outer
76+ handler is only reachable from * startup* (before the socket exists / before any data is
77+ committed) — run()'s normal-exit cleanup is now reliably reached. Revisit only if a new throwing
78+ path into ` main ` is introduced. See decision log.
7179
7280** Acceptance:** a new test feeds ` on_packet_received ` a corpus of truncated/garbage/oversized
7381datagrams and a hostile ` ChainResponse ` (bad hash, bad linkage, wrong endpoint) and asserts the
@@ -280,8 +288,8 @@ doc↔doc conflicts). No code change; do this alongside the phases that settle e
280288The current suite never feeds hostile input through ` on_packet_received ` , never exercises LMDB
281289pruning, and uses a lossless transport. Add:
282290
283- - [ ] ** T1 — malformed-packet corpus** through ` on_packet_received ` → node survives (Phase 1).
284- - [ ] ** T2 — Byzantine ` ChainResponse ` ** (bad hash/linkage/endpoint) → discovery aborts, node
291+ - [x ] ** T1 — malformed-packet corpus** through ` on_packet_received ` → node survives (Phase 1).
292+ - [x ] ** T2 — Byzantine ` ChainResponse ` ** (bad hash/linkage/endpoint) → discovery aborts, node
285293 survives (Phase 1.4).
286294- [ ] ** T3 — forged all-unsigned proof** → ` verify ` rejects (Phase 2.1).
287295- [ ] ** T4 — key-file mode ` 0600 ` ** and ** backup-write-failure → ERR** (Phase 2.2/2.3).
@@ -307,3 +315,23 @@ robust. This plan is about **hardening what exists**, not extending the feature
307315Record non-obvious decisions here as items land (chosen ` NodeId ` width; gossip-auth mechanism;
308316clock-implausibility policy; fsync threading vs documented-stall; whether ` --id ` /NullSigner stays
309317in the production binary).
318+
319+ ** Phase 1 (landed):**
320+
321+ - ** 1.4 — validation returns a verdict, does not throw.** ` validate_chain_discovery_result ` now
322+ returns ` bool ` ; ` complete_chain_discovery ` aborts an unsound discovery (fires ` on_chain_aborted `
323+ + the aborted telemetry hook) instead of throwing. This makes the live discovery path symmetric
324+ with the offline ` proof::verify ` path, which already returned a verdict. No honest discovery
325+ changes — a sound chain still completes exactly as before.
326+ - ** 1.3 / decoder cap — down payment on 3.1.** Rather than only catching the decode exception, the
327+ ` Reader ` now rejects an implausible element count * before* reserving (` ensure_count ` , division to
328+ avoid 32-bit overflow). This forecloses the multi-GB ` reserve ` DoS at the codec layer; the full
329+ 3.1 (a first-class max-count/max-datagram policy + the T5 assertion that no large allocation is
330+ attempted) is still open in Phase 3. T1b covers the codec-level rejection today.
331+ - ** 1.6 — deferred, mooted by 1.1.** See the item above. Not implemented because the reactor loop
332+ no longer throws; adding a ` main ` -catch cleanup would be dead code today and risks double
333+ unlink/sync against run()'s normal-exit cleanup.
334+ - ** ` on_packet_received ` is robust by contract.** The core entry point itself now swallows
335+ malformed-datagram exceptions (returns/drops), so the protocol engine — not just the daemon
336+ shell — upholds "a bad datagram is a dropped packet." The reactor/reader catches are
337+ defense-in-depth for unexpected (non-decode) exceptions.
0 commit comments