Harden daemon wire, durability, and fragment-ingest boundaries - #7
Merged
Conversation
Four verified latent defects on the daemon's trust and durability boundaries, plus the host-contract gap they expose: - Wire DoS: read_frame allocated a buffer straight from the uint32 length read off the socket (a 0xFFFFFFFF header forced a ~4 GB allocation). A shared kMaxFrameBodyBytes cap (64 MiB) in protocol.hpp is now enforced by both encode_frame and the daemon receive path; oversized frames are rejected before allocation. - Hang: no socket timeouts existed, so a client stalling mid-frame froze the single-threaded serve loop (queries, watcher, persistence, idle shutdown). SO_RCVTIMEO/SO_SNDTIMEO (5 s) now apply to every accepted connection; stalled peers are dropped and the loop continues. - status() data race: the Status op read enrichment counters and iterated the unextracted map while enrichment refresh, drop ingest, and rescans wrote them under a different lock. A dedicated enrichment_mutex in DaemonState is now taken by every reader and writer; status snapshots under the lock. - Destructive persist fallback: on rename failure persist_graph_snapshot deleted the last-known-good graph.json and retried, so a double failure destroyed the graph. Now atomic-rename-only: failure leaves the prior file untouched, removes the orphan temp, surfaces the error. - Fragment referential integrity: semantic ingest now rejects a fragment atomically when an edge endpoint exists in neither the fragment (keyed via the now-exported graph_builder node_key) nor the live graph, keeping the host contract's 'malformed -> rejected, graph unchanged' true for referential breakage. merge_fragment semantics unchanged (Graphify parity). Tests: 64/64 passing, including raw-socket hostile-frame coverage (oversized header, stalled half-frame) against a live daemon. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
taylor009
pushed a commit
that referenced
this pull request
Jul 15, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Four verified latent defects sit on the daemon's externally reachable trust boundary and durability path, plus the host-contract gap they expose. None break the happy path (64/64 tests were green before this change) — each is a real bug under adversarial or failure conditions:
daemon_server.cppread_frame0xFFFFFFFF) forces a ~4 GB allocation before any body is readstatus()data racedaemon_ops.cppstatus opunextractedmap were read while written under a different lock — races constantly because the enrichment drainer is status-gated and pollsdaemon_lifecycle.cpppersist_graph_snapshotgraph.jsonand retried; double failure = no graph at allsemantic_ingest.cppWhat changed
kMaxFrameBodyBytescap (64 MiB) inprotocol.hpp, enforced on both send (encode_frame) and receive (daemonread_frame) — oversized declared lengths rejected before allocation.SO_RCVTIMEO/SO_SNDTIMEO(5 s) on every accepted connection in both serve loops; stalled peers are dropped, the loop continues.enrichment_mutexinDaemonStatetaken by every writer (enrichment refresh, drop ingest, rescan/incremental update,EnrichmentRunningScope) and by thestatusreader, which snapshots under the lock.persist_graph_snapshotis atomic-rename-only: failure leaves the priorgraph.jsonuntouched, cleans up the temp, surfaces the error.graph_builder::node_key— linkage change only) nor the live graph.merge_fragment/merge_fragmentssemantics unchanged (Graphify parity is a hard contract).Verification
ctest --preset default, macOS).daemon_server_testagainst a live daemon: a0xFFFFFFFFlength header is rejected without allocation and the daemon still answers status; a stalled half-frame reader is timed out and the serve loop recovers.OpenSpec change:
openspec/changes/harden-daemon-boundaries/(spec deltas forgraph-daemon-clientandsemantic-fragment-ingest).🤖 Generated with Claude Code