Skip to content

Commit 4029a58

Browse files
committed
plan: mark Phase 1 done (crash isolation) + decision log
Ticks 1.1-1.5 and tests T1/T2; records the 1.4 validate-returns-verdict design, the decoder cap as a down payment on 3.1, and 1.6 deferred (mooted by 1.1).
1 parent 2000080 commit 4029a58

1 file changed

Lines changed: 35 additions & 7 deletions

File tree

plan/pending/hardening.md

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,34 +40,42 @@ The single most important phase. Today a **one-byte UDP packet from anyone** —
4040
chain response from a peer, or a mistyped `peer add` — unwinds out of the event loop and exits
4141
the 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
7381
datagrams 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
280288
The current suite never feeds hostile input through `on_packet_received`, never exercises LMDB
281289
pruning, 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
307315
Record non-obvious decisions here as items land (chosen `NodeId` width; gossip-auth mechanism;
308316
clock-implausibility policy; fsync threading vs documented-stall; whether `--id`/NullSigner stays
309317
in 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

Comments
 (0)