Skip to content

feat(reachability): plumb an ELF-byte source so static-linkage reachability activates in prod (JEF-407)#235

Merged
thejefflarson merged 2 commits into
mainfrom
thejefflarson/jef-407-plumb-an-elf-byte-source-so-static-linkage-reachability-jef
Jul 12, 2026
Merged

feat(reachability): plumb an ELF-byte source so static-linkage reachability activates in prod (JEF-407)#235
thejefflarson merged 2 commits into
mainfrom
thejefflarson/jef-407-plumb-an-elf-byte-source-so-static-linkage-reachability-jef

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

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_binary was None at every prod construction site. The ELF classifier never ran outside tests, and Go / musl-static CVEs still rendered not-observed even 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:

  • The agent already sees /proc/<pid>/exe, so no new capability and no new egress — the linkage rides the same /behavior channel every observation already uses (zero-egress invariant holds).
  • The alternative (a scanner/image-config linkage field) isn't cleanly reachable — the trivy/image path carries no such signal today, so it would have been larger new plumbing.

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

  • New Behavior::ImageLinkage { static_linkage: bool } in protector-behavior (serde-tagged {"kind":"image_linkage","static_linkage":...}), with variant_label / summary / fingerprint_key and round-trip tests.
  • The ELF classifier moved from engine::observe::elf into protector-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.
  • The engine's RuntimeAdapter diverts the linkage report onto Image::static_binary (walking RunsImage edges) before CveReachabilityAdapter reads it. It is never pushed onto workload runtime, so it can't pollute the adjudication prompt or the corroboration path.

checked_add hardening (architect-flagged on #229)

The ELF read helpers now form every off + N slice bound with checked_add, so a crafted e_phoff/e_phnum 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.

Invariants preserved

  • Backward-tolerant: an older agent that never sends ImageLinkage simply leaves static_binary == None — the exact prior behavior.
  • ImageLinkage is 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 only loaded-at-runtime is CVE evidence is untouched. PresentStaticBinary remains indeterminate, never reassurance, never exploitation evidence.

How I tested

  • Agent unit: classifies a static ELF (Some(true)) vs dynamic (Some(false)), drops unknown/non-ELF/unreadable (None); read_exe_head is cap-bounded and never panics.
  • Engine end-to-end: an ImageLinkage observation flows through the full adapter pipeline → sets Image::static_binary → a static report makes a would-be not-observed critical CVE render present-static-binary, a dynamic report stays not-observed, and a real matching load stays loaded-at-runtime. Also asserts ImageLinkage never lands on workload runtime.
  • Hardening: a crafted u64::MAX e_phoff returns None with no panic.
  • Wire: ImageLinkage and its full RuntimeObservation round-trip byte-for-byte.
  • Gates: 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-review clean (0 Critical/High); /simplify found 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, and aya doesn'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_bakeoff run is optional (per the ticket) and was not run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

…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
@thejefflarson thejefflarson enabled auto-merge (squash) July 12, 2026 22:14
@thejefflarson thejefflarson merged commit 645572b into main Jul 12, 2026
7 checks passed
@thejefflarson thejefflarson deleted the thejefflarson/jef-407-plumb-an-elf-byte-source-so-static-linkage-reachability-jef branch July 12, 2026 22:22
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