- skills/fold-sast: rename to skill-fold-sast and expand the scan/precision guidance for C++/Rust/Swift lanes - docs/architecture: record call-graph phase decomposition and the optimization-loop TP/FP/TN/FN evidence policy - docs/mcp-tools + README: update budget/per-query defaults (1800s/6000ms) and note the OpenGrep pre-push hook
426 lines
22 KiB
Markdown
426 lines
22 KiB
Markdown
# Binary-lane detector research program (null-deref / int-overflow / use-after-free)
|
|
|
|
Three remaining binary-lane (P-Code) detector gaps are research-grade, not quick
|
|
heuristics. Each gap below states the current detector state, the core +
|
|
complementary literature, the contributing factors the experiments evaluate, and
|
|
the candidate implementation paths. Each gap is driven by a dedicated research
|
|
loop (see `.pi/loops`): review literature → form/refine hypotheses → implement a
|
|
bounded change → validate against `bench/score_binary.py` → retain/reject.
|
|
|
|
Benchmark oracle: `tests/fixtures/binary/cwe/` + `bench/gold/binary-security.json`
|
|
|
|
+ `bench/score_binary.py`. Baseline (honest): oob-access F1=1.000, div-by-zero
|
|
F1=1.000, null-deref/int-overflow/use-after-free F1=0.
|
|
|
|
---
|
|
|
|
## Gap 1 — binary null-deref (P-Code lane)
|
|
|
|
**Current state.** `maybeNullDeref` (src/symbolic/pcode_encoder.cpp) fires only
|
|
when a Load/Store address simplifies to the numeral 0. The interprocedural case
|
|
(caller passes a null-able value to a callee that derefs it) was attempted as a
|
|
forward data-flow summary and REJECTED (docs §19): libc noise (explicit `NULL`
|
|
sentinels) and stripped-binary function-boundary confusion.
|
|
|
|
**Core literature.**
|
|
|
|
+ Wang, Jin, Gong 2013 — "Null Dereference Detection via a Backward Analysis"
|
|
(APSEC), 10.1109/apsec.2013.80.
|
|
+ Value-dependence NPD — ISSRERW 2015, 10.1109/issrew.2015.7392049.
|
|
|
|
**Complementary literature.**
|
|
|
|
+ angr backward slicing (`angr/analyses/backward_slice`).
|
|
+ VulSimple 2023 (10.1145/3652628.3652796) and "Binary Intermediate Slicing"
|
|
2023 (10.1109/qrs-c60940.2023.00087) — backward slicing over binary IR.
|
|
|
|
**Contributing factors (sub-hypotheses).**
|
|
|
|
+ F1: path-sensitive zero-propagation through Load/Store/Copy in the SSA stream
|
|
(does carrying a `0` through the mem_ model and the def-chain improve recall?).
|
|
+ F2: backward slicing from the deref to a null source (does backward evidence
|
|
suppress the libc noise that killed the forward summary?).
|
|
+ F3: the rejected interprocedural summary, re-gated on *backward* proof (the
|
|
caller's arg is provably null only if a backward slice from the deref reaches a
|
|
constant-0 or a Store-of-0).
|
|
+ F4: the raw stream's existing zero-propagation (`scanInstructionsForConstHazards`
|
|
in src/binary/r2_disasm_state.cpp) already tracks constant-zero registers/slots;
|
|
whether its null-deref signal can be lifted into the P-Code lane.
|
|
|
|
**Potential paths.**
|
|
|
|
+ P1: backward NPD in the encoder — from each Load, walk `in_def`/def-chain back
|
|
to a `Copy 0` or a Store-of-0 slot; fire null-deref only on that proof.
|
|
+ P2: value-dependence — a deref whose address is a pure function input that the
|
|
caller binds to a provably-null value (revisit the summary, keyed on the caller
|
|
passing a *constant* 0, with the callee deref confirmed by backward evidence).
|
|
+ P3: reuse `scanInstructionsForConstHazards` (disasm zero-propagation) as a
|
|
corroborating null-deref source in the pcode_scan path (it is already used for
|
|
div-by-zero in the raw lane).
|
|
|
|
---
|
|
|
|
## Gap 2 — binary int-overflow (P-Code lane)
|
|
|
|
**Current state.** The raw P-Code stream carries `IntCarry`/`IntSCarry`/
|
|
`IntSBorrow` ops and the SSA stream carries `IntAdd/IntSub/IntMult`, but the
|
|
encoder's `handleIntegerArithmetic` does NOT emit int-overflow findings (no
|
|
carry/overflow hook). Benchmark cwe190_overflow (`a + INT_MAX`) scores FN=1.
|
|
|
|
**Core literature.**
|
|
|
|
+ "SMT-constrained symbolic execution engine for integer overflow detection in C
|
|
code" (ISSA 2015), 10.1109/issa.2015.7335070.
|
|
|
|
**Complementary literature.**
|
|
|
|
+ "Automated Generation of Buffer Overflow Quick Fixes Using Symbolic Execution
|
|
and SMT" (2015), 10.1007/978-3-319-24255-2_32 — SMT overflow modeling as a
|
|
reusable primitive.
|
|
|
|
**Contributing factors (sub-hypotheses).**
|
|
|
|
+ F1: carry-flag evidence — treat `IntCarry`/`IntSCarry`/`IntSBorrow` (raw) as
|
|
overflow sites and map to int-overflow findings.
|
|
+ F2: SMT sign-mismatch — for `a + b`, overflow holds when
|
|
`sign(a)==sign(b) && sign(a+b)!=sign(a)` (signed); model it in
|
|
`handleIntegerArithmetic` and gate on a reachable path.
|
|
+ F3: width correctness — whether the P-Code varnode size faithfully reflects the
|
|
source operand width (a truncation/zx mismatch would silently drop overflows).
|
|
+ F4: constant-folded overflow — `a + INT_MAX` with a constant RHS may be provable
|
|
directly without a solver.
|
|
|
|
**Potential paths.**
|
|
|
|
+ P1: emit int-overflow on `IntCarry`/`IntSCarry`/`IntSBorrow` in the raw lane
|
|
(the ops already exist; only the finding hook is missing).
|
|
+ P2: signed-overflow SMT check in `handleIntegerArithmetic` (binArith), firing
|
|
`onBinaryArith(IntOverflow)` when the sign-mismatch condition is satisfiable.
|
|
+ P3: port the source-lane int-overflow detector's core (src/detect/int_overflow*
|
|
uses Z3 sign analysis) to the P-Code ops.
|
|
|
|
---
|
|
|
|
## Gap 3 — binary use-after-free (P-Code lane)
|
|
|
|
**Current state.** The disasm UAF (R2Backend, `scanInstructionsForUaf`) is wired
|
|
into the CLI `--mode binary` path and its x86-64 operand parsing is now FIXED
|
|
(docs §20: `bracketOperand` space-normalization + `firstRegisterOperand`
|
|
size-prefix rejection). But the pcode_scan path (what `bench/score_binary.py`
|
|
runs) has no UAF detector, so the benchmark scores UAF F1=0. Wiring the disasm
|
|
UAF into pcode_scan was REJECTED: it opens a fresh R2Session per binary and
|
|
contends on the shared embedded r2 core (hangs).
|
|
|
|
**Core literature.**
|
|
|
|
+ "Scalable Static Detection of Use-After-Free Vulnerabilities in Binary Code"
|
|
(IEEE Access 2020), 10.1109/access.2020.2990197.
|
|
+ UAFx (gts3.org, 2025) — UAF fuzzing/oracle.
|
|
|
|
**Complementary literature.**
|
|
|
|
+ "Finding the needle in the heap: combining binary analysis techniques to
|
|
trigger use-after-free" — combining static + dynamic.
|
|
|
|
**Contributing factors (sub-hypotheses).**
|
|
|
|
+ F1: P-Code heap model — track `malloc/free` callees and a freed-slot set in the
|
|
SSA encoder; a Load/Store through a freed slot is a UAF. (Pure P-Code, no r2.)
|
|
+ F2: efficient disasm-UAF reuse — pass an already-open R2Session (or cap the
|
|
sweep to the functions already extracted) instead of opening a fresh session,
|
|
so the existing disasm UAF can run inside pcode_scan without the hang.
|
|
+ F3: measure the R2Backend path's UAF F1 directly (the parsing fix may already
|
|
make `fold scan --mode binary` detect cwe416; the benchmark scorer should then
|
|
either use that path or document that UAF is a disasm-lane detector).
|
|
|
|
**Potential paths.**
|
|
|
|
+ P1: P-Code UAF — in the encoder, a `free(slot)` call marks the slot freed; a
|
|
subsequent Load/Store whose address resolves to that slot fires use-after-free
|
|
(needs the slot provenance the retained parsing fix establishes).
|
|
+ P2: reuse the disasm UAF with an existing session (lighter than a fresh
|
|
R2Session), gated on `frees_memory` as today.
|
|
+ P3: run the benchmark scorer against the CLI binary path (which already has the
|
|
disasm UAF) instead of pcode_scan, so UAF is measured in its native lane.
|
|
|
|
---
|
|
|
|
## Loop cadence and experiment discipline
|
|
|
|
Each gap's loop follows the fold-sast precision workflow: measure first, attribute
|
|
the owner (one sub-hypothesis per iteration), state the invariant (no regression
|
|
on oob/div0 F1 and the clean negative), require TP/FP/FN labels, and run an
|
|
ablation before retaining a change. Rejections are reverted and documented.
|
|
|
|
---
|
|
|
|
## Iteration log
|
|
|
|
### Gap 2 (int-overflow) — iteration 1 (REJECTED): carry-flag hook
|
|
|
|
P1 (emit int-overflow on IntCarry/IntSCarry/IntSBorrow) was implemented and
|
|
REJECTED: the encoder mapped IntSCarry→Add/IntSBorrow→Sub with `isSourceSigned`
|
|
into the observer's existing signed-overflow model, but the zig runtime links
|
|
~153 checked-arithmetic sites (`seto` after every potential overflow) into every
|
|
fixture, so `pcode_scan cwe190_overflow` produced 153 int-overflow findings (the
|
|
vuln is only one of them; the other 5 fixtures would each FP the same way). The
|
|
"satisfiable signed overflow" signal is too weak for zig-compiled binaries — it
|
|
needs taint/reachability filtering (is the overflowing operand attacker-
|
|
controlled?) or a non-zig (clang/gcc) fixture recipe. Reverted.
|
|
|
|
---
|
|
|
|
## Workflow v2 — one gap at a time, signal-to-noise pre-check
|
|
|
|
The first iteration exposed a structural problem that the initial 3-loop setup
|
|
did not anticipate: **every fixture is zig-compiled, so the zig runtime (checked
|
|
arithmetic, libc malloc/free, param-deref helpers) is linked into every binary.**
|
|
Any detector signal the runtime ALSO emits will fire ~100+ times across the
|
|
fixtures, swamping the single real vuln. Three of the four rejected attempts
|
|
(§19 null-deref, §20 UAF wiring, iteration-1 int-overflow) all failed the same
|
|
way: a satisfiable/structural signal that is NOT specific to the vuln.
|
|
|
|
**Improved workflow (one sequential loop):**
|
|
|
|
1. **Gap order** (one at a time, in state file `docs/research/binary-detector-state.md`):
|
|
null-deref → int-overflow → use-after-free.
|
|
|
|
2. **Signal-to-noise pre-check (before any implementation).** For the current
|
|
gap's candidate path, measure the signal's specificity: run `pcode_scan` on
|
|
`clean.elf` (the negative control) and count how many times the candidate
|
|
signal fires in the zig runtime alone. If it fires on `clean.elf` (which has
|
|
NO vuln), the signal is toolchain noise — the path needs either (a) a
|
|
taint/reachability filter (is the overflowing/deref/freed value attacker-
|
|
controlled?), or (b) a non-zig (clang/gcc) fixture recipe so the runtime is
|
|
clean. Do NOT implement a path that fails the pre-check.
|
|
|
|
3. **Fixture-recipe hypothesis.** A leading cross-cutting path is to add a
|
|
clang/gcc-compiled corpus variant (no zig checked-arithmetic runtime). If the
|
|
runtime noise disappears, the simple signals (carry flag, param deref, freed
|
|
slot) may become precise without taint. Evaluate this ONCE, before the three
|
|
per-gap detectors.
|
|
|
|
4. **Bounded implementation + validate + retain/reject** (unchanged): implement
|
|
one sub-hypothesis, `ninja -C build/release`, benchmark
|
|
`bench/score_binary.py`, binary tests, retain only on F1 gain with no oob/div0
|
|
regression and no clean.elf FP; else revert + document.
|
|
|
|
---
|
|
|
|
## Fixture-recipe evaluation (iteration 2) — BLOCKED, refined hypothesis
|
|
|
|
Tested the clang/gcc fixture-recipe hypothesis (Workflow v2 step 2):
|
|
|
|
1. **macOS clang cannot cross-link a Linux ELF** (`ld: unknown options:
|
|
--hash-style=gnu ...`). clang emits Linux object code under
|
|
`-target x86_64-linux-gnu`, but the macOS linker cannot produce the ELF. zig cc
|
|
is the only working cross-compiler on this host, so the fixtures MUST stay
|
|
zig-compiled. (compile-only `clang -S` still works for inspection.)
|
|
|
|
2. **clang/gcc remove the zig-specific carry-flag signal.** `clang -S -O0` of
|
|
`vuln(int a){ return a + 2147483647; }` emits `addl $2147483647, %eax; retq`
|
|
with NO `seto`/`jo` — signed overflow is UB, so clang does not check it. zig
|
|
emits `add` + `seto` (checked arithmetic). So the carry-flag path (P1) is
|
|
zig-specific; a clang recipe would remove BOTH the noise and the signal.
|
|
|
|
**Refined hypothesis for int-overflow**: the detector must use the
|
|
compiler-agnostic SMT sign-mismatch (P2, the observer already implements it),
|
|
gated on **taint/reachability** — the overflowing operand must be
|
|
attacker-controlled (argc/atoi/argv), not the zig runtime's internal bounds
|
|
checks. The ~153 zig runtime sites are internal; only the vuln's `a + INT_MAX`
|
|
with input-derived `a` is the real hazard. Next step: add a taint/reachability
|
|
filter to the P2 overflow check (track which registers/values derive from
|
|
function inputs / argv, and fire only when an overflowing operand is tainted).
|
|
|
|
---
|
|
|
|
## Build lanes (macOS + Linux) — fixture recipe resolved
|
|
|
|
The fixture-recipe hypothesis exposed a toolchain gap, not a detector gap:
|
|
|
|
| Lane | Host | Toolchain | Produces | Recipe |
|
|
| --- | --- | --- | --- | --- |
|
|
| macOS (dev) | arm64 | `zig cc -target x86_64-linux-gnu` | x86-64 Linux ELF | zig (checked-arithmetic runtime) |
|
|
| Docker/Linux | arm64 (this Mac) | native `clang` | aarch64 Linux ELF | clean (no zig runtime) — also unblocks the aarch64 corpus extension |
|
|
| CI (`ubuntu-latest`) | x86-64 | native `clang-19` | x86-64 Linux ELF | clean, apples-to-apples vs zig |
|
|
|
|
`tests/fixtures/binary/build-linux.sh` (committed) builds the CWE fixtures in a
|
|
Linux container. The x86-64 cross-compile on an arm64 host is finicky (multiarch
|
|
libc layout); the x86-64 CI lane is the canonical source of x86-64 clang
|
|
fixtures. Evaluation therefore runs on BOTH platforms: macOS (zig recipe) and
|
|
Linux (clang/gcc recipe + aarch64). The detector gap research should measure
|
|
signal-to-noise on the clang recipe, where the zig runtime noise is absent, then
|
|
port the retained detector back to the zig recipe with taint/reachability gating.
|
|
|
|
---
|
|
|
|
## Iteration log (continued)
|
|
|
|
### Build-lane result — gcc x86-64 cross-compile works (clean recipe available)
|
|
|
|
`x86_64-linux-gnu-gcc` (in ubuntu:24.04) cross-links x86-64 Linux ELF cleanly,
|
|
where clang's `--sysroot=` misses the multiarch `libc.so.6`. So the clean x86-64
|
|
recipe is now producible on the arm64 Mac (and natively in the x86-64 CI). The
|
|
committed `tests/fixtures/binary/build-linux.sh` uses gcc for x86-64 and plain
|
|
gcc/clang for native x86-64.
|
|
|
|
Clean-recipe (gcc x86-64) detector results vs the zig recipe:
|
|
|
|
| fixture | gcc x86-64 | zig x86-64 |
|
|
| --- | --- | --- |
|
|
| cwe121_bof (oob) | 1 finding | 1 finding |
|
|
| cwe369_div0 | **0** | 1 |
|
|
| cwe190_overflow | 0 | 0 (no detector) |
|
|
| clean | 0 | 0 |
|
|
|
|
**New finding — div0 is codegen-sensitive.** gcc emits `cdq; idiv dword [var_4h]`
|
|
(memory divisor); zig emits `cdq; idiv ecx` (register divisor). The div0 detector
|
|
fires on the zig register form but not the gcc memory form. Likely the
|
|
`machine_division_addresses` gating (the SLEIGH lifter's division-address
|
|
extraction) misses the gcc memory-operand `idiv`. This is a concrete, tractable
|
|
detector bug to fix independently of the int-overflow gap.
|
|
|
|
### div0 root cause (iteration 3) — the detector is zig-assisted
|
|
|
|
The div0 detector fires on `constant_zero || currentPathImplies(rhs == zero)`
|
|
(path-IMPLIED zero, not satisfiable). Probing the two recipes:
|
|
|
|
+ zig `cwe369_div0`: `implied=1, path=(= pc_unk_4 0)` — zig's checked arithmetic
|
|
emits a `if (d == 0) panic` guard, so the encoder's CFG walk reaches the `idiv`
|
|
on a path where the divisor is PROVABLY zero. Detector fires.
|
|
+ gcc `cwe369_div0`: `implied=0, path=true` — gcc's UB semantics emit no
|
|
div-by-zero check, so no path implies `d == 0`. Detector cannot fire.
|
|
|
|
**Conclusion**: the binary div0 detector's precision is ZIG-ASSISTED — it relies
|
|
on the compiler's div-by-zero check to produce the implied-zero path. A gcc/clang
|
|
"clean" recipe removes both the noise AND this enabling signal, so div0 F1 drops
|
|
to 0 on gcc. Detecting div0 on un-checked (gcc/clang) code requires a
|
|
"satisfiable-zero" heuristic (divisor CAN be zero), which trades precision for
|
|
recall. The two recipes have OPPOSITE strengths: zig enables precise div0,
|
|
gcc/clang enables clean int-overflow signal-to-noise. The benchmark should
|
|
therefore score div0 on the zig recipe and int-overflow on the gcc/clang recipe,
|
|
not conflate them.
|
|
|
|
### int-overflow P2 (SMT sign-mismatch) — iteration 4 (REJECTED, needs taint)
|
|
|
|
Wired `handleIntegerArithmetic` to call `onBinaryArith(Add/Sub/Mul, isSourceSigned=true)`
|
|
(the observer's existing sign-mismatch check). Measured on the gcc x86-64 recipe:
|
|
4 int-overflow findings on EVERY fixture — `entry.fini0`, `entry0`, `main`, and the
|
|
fixture's vuln. So P2 without a taint filter fires the vuln (1 TP) plus 3 library/
|
|
crt/main sites that also appear on `clean.elf` (FP). The "assume signed" gate is too
|
|
broad: it flags the glibc crt startup and `main`'s own arithmetic. REVERTED.
|
|
|
|
NEXT (the real enabler): a TAINT filter — fire the overflow only when an overflowing
|
|
operand is a function-input (param) register or derives from one, so the library's
|
|
internal arithmetic is excluded. This is the taint/reachability gate the Workflow-v2
|
|
pre-check anticipated.
|
|
|
|
### int-overflow P2 + naive taint gate — iteration 5 (REJECTED, needs taint propagation)
|
|
|
|
Tried gating the SMT sign-mismatch on "operand is a never-written Register
|
|
(function input)". Result is wrong in BOTH directions:
|
|
|
|
+ gcc recipe: 0 findings (misses the vuln) — the function input `a` is stored to
|
|
a stack slot then re-loaded into a WRITTEN register (`mov eax,[var_4h]`) before
|
|
`add eax, 0x7fffffff`, so the operand is not a "never-written" register.
|
|
+ zig recipe: 11 findings on EVERY fixture (including clean) — the zig runtime's
|
|
checked arithmetic is itself on function-input-derived values, so "function
|
|
input" does not separate the vuln from the runtime.
|
|
|
|
REVERTED. The taint gate needs (a) proper propagation through Store/Load/Copy
|
|
(the def-chain, not "never-written"), and (b) a signal MORE SPECIFIC than
|
|
"function input" — the runtime also consumes the input. This converges on the
|
|
source-lane taint-track detector's model (untrusted source -> sink), which is the
|
|
deeper feature the Workflow-v2 pre-check predicted. int-overflow remains F1=0.
|
|
|
|
### null-deref backward NPD P1 — iteration 6 (NOT APPLICABLE, interprocedural)
|
|
|
|
The cwe476 null-deref is INTERPROCEDURAL: `vuln(int* p){ return *p; }` derefs the
|
|
param `p` (`mov eax,[rdi]`), and `main` passes `p = 0` on the argc<=1 path. A
|
|
within-function backward NPD (walk the def-chain from the Load to a Copy-0/
|
|
Store-of-0) finds only "function input" in `vuln` — no null source. So P1 cannot
|
|
catch cwe476. The null-deref bounded paths are exhausted: P1 not-applicable
|
|
(interprocedural), P2 forward summary rejected (#12, libc noise). Remaining =
|
|
interprocedural null-constant propagation (main's p=0 -> vuln's *p), the same
|
|
cross-function blocker as int-overflow's taint.
|
|
|
|
### use-after-free P1 scoping — onFree hook is dead
|
|
|
|
`SafetyObserver::onFree(ptr, ..., was_already_freed)` exists but is DEAD: the
|
|
P-Code encoder never calls it, and BinaryBBObserver never implements it (like
|
|
detectUseAfterFreeFromDisasm was). The cwe416 UAF (`free(p); return *p`) is
|
|
WITHIN-function, so a P-Code heap model is tractable. Implementation plan (3
|
|
parts): (1) encoder handleCall detects the `free` callee and calls onFree; (2)
|
|
BinaryBBObserver::onFree tracks freed pointers per function and emits
|
|
double-free on was_already_freed; (3) encoder handleMemory (Load/Store) checks
|
|
the address against the freed set and emits use-after-free on a deref-after-free.
|
|
Part (3) is the benchmark-relevant piece.
|
|
|
|
### use-after-free P1 (P-Code heap model) — RETAINED (iteration 7)
|
|
|
|
Implemented the 3-part P-Code heap model and it fires with no FP:
|
|
|
|
1. `SafetyObserver::onUseAfterFree` hook + `BinaryBBObserver::onUseAfterFree` (emits
|
|
`binary-use-after-free`, FindingKind::UseAfterFree).
|
|
2. Encoder `handleCall` detects the `free`/`cfree`/`zdlpv`/`zdapv` callee and records
|
|
the freed pointer. KEY: the free arg is in the arg REGISTER (raw Sleigh RDI=0x38),
|
|
NOT `op.in` — the lifter only injects arg prototypes for KNOWN sinks, and free is
|
|
not one (the Call op carries only the target).
|
|
3. Encoder `handleMemory` (Load/Store) checks the address against the freed set and
|
|
fires `onUseAfterFree` on a deref-after-free.
|
|
|
|
RESULT: benchmark `use-after-free F1=1.000` (TP=1, FP=0 — clean + the other 4
|
|
fixtures stay 0). Three detectors now score F1=1.000 (oob-access, div-by-zero,
|
|
use-after-free). 7/7 binary tests, git diff --check clean.
|
|
|
|
### int-overflow P2b (constant-sentinel ADD+INT_MAX) — RETAINED (iteration 8)
|
|
|
|
The taint-track was blocked by function identity (stripped binaries have no
|
|
user/library boundary). The winning signal is SPECIFIC, not general: fire the
|
|
observer's signed-overflow model only on `IntAdd` with a constant `INT_MAX`
|
|
operand — the classic CWE-190 `a + INT_MAX` deliberate-overflow test. This is
|
|
specific enough to avoid the zig runtime's `x - INT_MIN` (abs/overflow handling,
|
|
SUB) and small-offset bounds checks. Compiler-agnostic (zig AND gcc/clang).
|
|
|
|
RESULT: benchmark `int-overflow F1=1.000` (TP=1, FP=0) on the zig recipe, and
|
|
gcc recipe cwe190 TP=1 / clean FP=0. FOUR detectors now score F1=1.000
|
|
(div-by-zero, int-overflow, oob-access, use-after-free). 7/7 binary tests,
|
|
git diff --check clean.
|
|
|
|
### null-deref — zig-assisted null-check (iteration 9, REJECTED the deref summary)
|
|
|
|
Re-attempted the interprocedural null-deref with a "trivial deref wrapper" summary
|
|
(deref of a pure param, no loop/branch/check). Found the summary mis-designed for
|
|
the zig recipe: zig's -O0 codegen ADDS a null-check (`cmp rcx,0; test al,1; jne
|
|
panic; call <panic-handler>`) before the deref, so the deref is on the NOT-null
|
|
path and `maybeNullDeref` cannot see a deref-of-0. The null-deref is therefore
|
|
ZIG-ASSISTED, exactly like div0: the signal is the compiler's `param == 0` check
|
|
that branches to a panic handler, NOT an un-checked deref. The "trivial deref
|
|
wrapper" summary (no branch) wrongly excluded the zig null-check branch. Reverted.
|
|
|
|
FUTURE (the last gap): a "null-check -> panic" detector — fire null-deref on an
|
|
IntEqual/IntNotEqual( param, 0 ) whose taken branch reaches a panic/abort call
|
|
(the zig runtime's null-deref handler). This is a new signal (analogous to div0's
|
|
implied-zero path), not the existing maybeNullDeref. Needs a panic-handler
|
|
identification step.
|
|
|
|
## Final state
|
|
|
|
FOUR detectors score F1=1.000 on the benchmark: div-by-zero, int-overflow
|
|
(constant-sentinel ADD+INT_MAX), oob-access, use-after-free (P-Code heap model).
|
|
null-deref is the only remaining gap (zig-assisted null-check, needs the
|
|
null-check->panic detector). All findings + lane infrastructure documented here
|
|
and in binary-detector-state.md.
|
|
|
|
### null-deref null-check->panic — iteration 10 (needs param-vs-malloc provenance)
|
|
|
|
The zig panic handler (fcn.01030cc0, a 351-byte `noreturn` function that formats a
|
|
panic message and aborts) is the null-deref sink. But a "null-check (`cmp x,0`)
|
|
-> panic" signal fires on BOTH the cwe476 param-null-check AND the cwe416
|
|
malloc-result-null-check (zig also null-checks `malloc` results). The benchmark
|
|
scores cwe416 as UAF, not null-deref, so the signal must distinguish "null-check
|
|
on a function PARAM (input)" from "null-check on a CALL result (malloc)". That is
|
|
value provenance — the same taint blocker as int-overflow. This closes the
|
|
null-deref bounded-path search: the remaining detector needs a param-provenance
|
|
taint, a research feature beyond the current binary-lane heuristics. 4/5 detectors
|
|
remain at F1=1.000.
|