feat(reachability): plumb an ELF-byte source so static-linkage reachability activates in prod (JEF-407)#235
Merged
thejefflarson merged 2 commits intoJul 12, 2026
Conversation
…bility activates in prod (JEF-407)
JEF-404 landed the static-linkage machinery but it was DORMANT: the engine has no
in-cluster access to a workload's entrypoint bytes, so `Image::static_binary` was
`None` at every prod construction site — the ELF classifier never ran outside tests
and Go/musl-static CVEs still rendered `not-observed`. This wires the byte SOURCE.
Signal source (DECISION): the node-local agent reports linkage over the EXISTING
behavioral wire (ADR-0014) — the ticket's preferred path. The agent already sees
`/proc/<pid>/exe`; on an exec it reads only the leading ELF header and classifies
linkage, emitting a new `Behavior::ImageLinkage { static_linkage }` over the same
`/behavior` channel (no new egress — zero-egress holds; no heavy dep). The engine's
RuntimeAdapter diverts that signal onto `Image::static_binary` (walking RunsImage
edges) before CveReachabilityAdapter reads it, so a static image's would-be
`not-observed` CVE now tags `present-static-binary`.
Shared classifier: the ELF parser moved from `engine::observe::elf` into the shared
`protector-behavior` crate (std-only, dependency-free) as the single source of truth,
so engine and agent classify identically and can't drift; the engine module keeps a
thin re-export.
Pre-activation hardening (architect-flagged on #229): the ELF read helpers now form
every `off + N` slice bound with `checked_add`, so a crafted `e_phoff` near
`usize::MAX` can't overflow-panic on a debug build — it returns `None`. The parser
stays allocation-free and returns `None` on malformed/truncated input.
Backward-tolerant: an older agent that doesn't send `ImageLinkage` simply leaves
`static_binary == None` — the exact prior behavior. `ImageLinkage` is CONTEXT only:
it never corroborates and is diverted before it becomes workload runtime state, so
the JEF-402/405 rule that only `loaded-at-runtime` is CVE evidence is untouched.
Tests: agent classifies static vs dynamic ELF (and drops unknown); the engine maps
the agent's report into `Image::static_binary` and an end-to-end pipeline test shows
a static report → `present-static-binary`, a dynamic report → `not-observed`, and a
real load stays `loaded-at-runtime`; the `checked_add` change is covered by a crafted
`u64::MAX` offset returning `None` with no panic; `ImageLinkage` serde round-trips and
is proven not to land on workload runtime.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
…ce-so-static-linkage-reachability-jef
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.
Closes JEF-407
Problem
JEF-404 (#229) landed the full static-linkage machinery but it was dormant: the engine has no in-cluster access to a workload's entrypoint bytes, so
Image::static_binarywasNoneat every prod construction site. The ELF classifier never ran outside tests, and Go / musl-static CVEs still renderednot-observedeven when the vulnerable code was genuinely present.Signal source (DECISION — for the architect)
I took the ticket's preferred path: the node-local agent reports linkage over the existing behavioral wire (ADR-0014). Rationale:
/proc/<pid>/exe, so no new capability and no new egress — the linkage rides the same/behaviorchannel every observation already uses (zero-egress invariant holds).A new
Behavior::ImageLinkage { static_linkage: bool }variant carries the bit. On an exec the agent reads only the leading ELF header of the entrypoint, classifies it with the shared parser, and emits the signal once per pod (linkage is a stable per-image fact — one ELF read per pod, not per exec).Wire / field change
Behavior::ImageLinkage { static_linkage: bool }inprotector-behavior(serde-tagged{"kind":"image_linkage","static_linkage":...}), withvariant_label/summary/fingerprint_keyand round-trip tests.engine::observe::elfintoprotector-behavior::elf(std-only, dependency-free) as the single source of truth so the engine and agent classify identically and can't drift; the engine module keeps a thin re-export.RuntimeAdapterdiverts the linkage report ontoImage::static_binary(walkingRunsImageedges) beforeCveReachabilityAdapterreads it. It is never pushed onto workload runtime, so it can't pollute the adjudication prompt or the corroboration path.checked_addhardening (architect-flagged on #229)The ELF read helpers now form every
off + Nslice bound withchecked_add, so a craftede_phoff/e_phnumnearusize::MAXcan't overflow-panic on a debug build — it returnsNone. The parser stays allocation-free and returnsNoneon malformed/truncated input.Invariants preserved
ImageLinkagesimply leavesstatic_binary == None— the exact prior behavior.ImageLinkageis CONTEXT only — it never corroborates (is_alert()is false, and it's diverted before it becomes runtime state), so the JEF-402/405 rule that onlyloaded-at-runtimeis CVE evidence is untouched.PresentStaticBinaryremains indeterminate, never reassurance, never exploitation evidence.How I tested
Some(true)) vs dynamic (Some(false)), drops unknown/non-ELF/unreadable (None);read_exe_headis cap-bounded and never panics.ImageLinkageobservation flows through the full adapter pipeline → setsImage::static_binary→ a static report makes a would-benot-observedcritical CVE renderpresent-static-binary, a dynamic report staysnot-observed, and a real matching load staysloaded-at-runtime. Also assertsImageLinkagenever lands on workload runtime.u64::MAXe_phoffreturnsNonewith no panic.ImageLinkageand its fullRuntimeObservationround-trip byte-for-byte.cargo fmt,cargo clippy(warnings=errors),cargo test -p protector(774 pass) and-p protector-behavior(30), plus the agent workspace (cargo test -p protector-agent, 45)./soundcheck:pr-reviewclean (0 Critical/High);/simplifyfound nothing to cut.Scope note / limitation
The agent's eBPF crate cannot build in this environment (macOS —
bpf-linker's LLVM BPF target is unavailable, andayadoesn't compile off Linux). I verified the default (no-eBPF) agent build + tests green and reviewed the eBPF-gated observer change by hand; the eBPF object build is a Linux/CI concern. The linkage classifier and its emit logic are fully unit-tested in the default build.The adjudication prompt didn't change, so a
judge_bakeoffrun is optional (per the ticket) and was not run.🤖 Generated with Claude Code
https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP