Skip to content

feat(reachability): distinguish present-static-binary from not-observed (JEF-404)#229

Merged
thejefflarson merged 2 commits into
mainfrom
thejefflarson/jef-404-reachability-blind-to-statically-linked-binaries-gorustmusl
Jul 12, 2026
Merged

feat(reachability): distinguish present-static-binary from not-observed (JEF-404)#229
thejefflarson merged 2 commits into
mainfrom
thejefflarson/jef-404-reachability-blind-to-statically-linked-binaries-gorustmusl

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

Closes JEF-404

Problem

Reachability is proven by correlating a CVE's package against runtime LibraryLoaded events. Statically linked binaries (Go, CGO-disabled/musl-static) compile everything into one executable — there are no per-.so loads — so LibraryLoaded never names the vulnerable package and every CVE renders not-observed, even when the vulnerable code is genuinely on a hot path. Two harms: (a) false negatives on reachability for a whole image class; (b) not-observed reads to the adjudicator as "not running", so absence-of-evidence was being read as evidence-of-absence.

What changed

  • New Reachability::PresentStaticBinary (label present-static-binary): distinct from not-observed ("observed absent"). The library-load correlation cannot fire for a static binary, so a would-be-not-observed CVE on such an image is indeterminate, not absent.
  • Minimal, zero-dependency ELF static-linkage classifier (engine::observe::elf): a static ELF carries no PT_INTERP program header. Pure, byte-only, bounds-checked; honestly returns None on any malformed/unknown input. No new heavy crate, and it only inspects bytes already in hand — the zero-egress invariant holds.
  • Image::static_binary: Option<bool> — the per-image signal the classifier populates. CveReachabilityAdapter tags PresentStaticBinary when the image is static and no load matched; a real LoadedAtRuntime match still wins (some static binaries dlopen a plugin).
  • Prompt fed honestly: the tag is CONTEXT, never exploitation evidence (only loaded-at-runtime is), but explicitly NOT reassurance — its lack of a runtime load must not be read as safety. The JEF-402/405 evidence rules are untouched (loaded-at-runtime remains the sole CVE evidence).

Static-linkage signal chosen (and why)

The cheapest reliable signal is the ELF header (no PT_INTERP ⇒ static). I implemented a minimal, well-scoped ELF-header parser rather than pulling in goblin/object (zero-egress + no heavy dep). It reads only the ELF header and program-header table, short-circuits at the first PT_INTERP, and is fully unit-tested with tiny synthetic byte fixtures.

DECISION NEEDED (architect): the engine has no in-cluster access to the entrypoint binary bytes today (zero-egress, node-local agent), so static_binary stays None in production until an ELF-byte signal is plumbed in — e.g. the agent POSTing the entrypoint's linkage over the behavioral wire (ADR-0014), or a scanner-reported field. The enum + classifier + adapter + prompt are all wired and tested end-to-end; only the byte source is pending. None linkage means reachability behaves exactly as before this change, so nothing regresses in the interim.

Tests

  • ELF fixtures: static vs dynamic classify differently; 32/64-bit; big-endian; non-ELF/truncated/malformed → None (never guess, never panic).
  • A static image tags PresentStaticBinary while the same image dynamically-linked stays NotObserved, and a real matching load still yields LoadedAtRuntime.
  • Reachability label round-trip (distinct token from not-observed).
  • Prompt renders the tag and frames it as unknowable, not reassurance, without weakening the loaded-at-runtime-only CVE-evidence rule.

Checks

cargo fmt clean; cargo clippy --all-targets clean (warnings as errors); cargo test -p protector → 772 passed, 0 failed. All touched files under the 1000-line cap (tests split into elf/tests.rs).

🤖 Generated with Claude Code

https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

…ed so a Go/musl CVE on a hot path doesn't read as safe (JEF-404)

Reachability is proven by correlating a CVE's package against runtime
LibraryLoaded events. Statically linked binaries (Go, CGO-disabled/musl-static)
compile everything into one executable — no per-.so loads — so LibraryLoaded
never names the vulnerable package and every CVE renders `not-observed` even on a
hot path. Worse, `not-observed` reads to the adjudicator as "not running", so
absence-of-evidence became evidence-of-absence for a whole image class.

- Add Reachability::PresentStaticBinary (label `present-static-binary`): the
  library-load correlation CANNOT fire for a static binary, so a would-be
  `not-observed` CVE on such an image is indeterminate, not observed-absent.
- Add a minimal, zero-dependency ELF static-linkage classifier
  (engine::observe::elf): a static ELF carries no PT_INTERP program header. Pure,
  byte-only, bounds-checked, honestly returns None on any malformed/unknown input
  — no new heavy crate, no egress (it only inspects bytes already in hand).
- Add Image::static_binary: Option<bool> as the per-image signal the classifier
  populates; CveReachabilityAdapter tags PresentStaticBinary when the image is
  static and no load matched (a real LoadedAtRuntime match still wins).
- Feed it into the prompt honestly: the tag is CONTEXT, never exploitation
  evidence (only loaded-at-runtime is), but explicitly NOT reassurance either —
  its lack of a runtime load must not be read as safety. JEF-402/405 evidence
  rules are untouched (loaded-at-runtime remains the sole CVE evidence).

DECISION NEEDED (architect): the engine has no in-cluster access to the
entrypoint binary bytes today (zero-egress, node-local agent), so static_binary
stays None in production until an ELF-byte signal is plumbed in (e.g. the agent
POSTing the entrypoint's linkage on the behavioral wire, or a scanner field). The
classifier + enum + adapter + prompt are all wired and tested end-to-end; only
the byte source is pending.

Tests: ELF fixtures (static vs dynamic classify differently, 32/64-bit,
big-endian, malformed→None); a static image tags PresentStaticBinary while the
same dynamic image stays NotObserved and a real load still yields LoadedAtRuntime;
label round-trip; prompt renders the tag as unknowable, not reassurance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
@thejefflarson

Copy link
Copy Markdown
Owner Author

Architect ratification (INTEGRATE / JEF-404 DECISION NEEDED)

RATIFIED: land-dormant is the correct call. The classifier + Reachability::PresentStaticBinary + adapter + prompt are fully wired and tested, with the byte source deferred. In production Image::static_binary is None at every construction site, so:

  • reachability behaves exactly as before this PR (zero regression — None ⇒ prior NotObserved path),
  • elf_static_linkage is never invoked in prod (no new attack surface reachable, zero-egress intact — the parser is a pure &[u8] -> Option<bool>).

Independently re-verified against the repo invariants:

  • Decision logic unchanged: the breach definition (one of the three evidence types) is byte-for-byte unchanged; there is no Rust match mapping reachability to an evidence boolean — exploitability is the model's call (ADR-0013), driven by prompt text. present-static-binary is placed in the context-never-evidence register alongside not-observed, explicitly framed as NOT reassurance, and loaded-at-runtime remains the sole CVE evidence discriminator. PresentStaticBinary cannot become an exploitable path.
  • ELF parser: bounds-checked throughout (.get() / checked_add / checked_mul), allocation-free, malformed/truncated -> None. No attacker-reachable panic in release.
  • File-size cap honored (largest touched file 945 lines; ELF tests split into elf/tests.rs).

FOLLOW-UP (required, human): file a ticket to plumb an ELF-byte source so static_binary can be populated — either the agent posting linkage over the behavioral wire (ADR-0014) or a scanner-reported field. Until then the feature stays dormant by design.

HARDENING NIT (non-blocking, not reachable today): in the read helpers, bytes.get(off..off + N) computes off + N before the range is bounds-checked; a crafted e_phoff near u64::MAX would overflow that add and panic under a debug build (overflow-checks on). Not reachable in prod (release wraps to a start>end range -> None, and the parser isn't invoked in prod at all). Worth a checked_add in the helpers when the byte source lands and the parser goes live. Fold into the follow-up ticket.

@thejefflarson thejefflarson enabled auto-merge (squash) July 12, 2026 04:12
@thejefflarson thejefflarson merged commit f020b94 into main Jul 12, 2026
5 checks passed
@thejefflarson thejefflarson deleted the thejefflarson/jef-404-reachability-blind-to-statically-linked-binaries-gorustmusl branch July 12, 2026 04:20
thejefflarson added a commit that referenced this pull request Jul 12, 2026
…bility activates in prod (JEF-407) (#235)

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.


Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant