ROADMAP: the typed-values design ranks the type fixes by measured expectation, and records what it found

Three read-only designs and a judge (library prototypes into values, widths from reads, a proven
return or void) gave measured expectations well under the DecBench class ceilings: about +27 to +32
net union rows together against the 270 rows between r2s and kuna. D70 repairs the soundness gate
first (it splits function pointer parameters on every comma and accepts r2s's own `arg0` as a source
name), D71 and D72 are the two items to build, D73 waits for a measurement, D74 holds the real
findings (a dereferenced parameter declared integer, a silently wrong void, stack arguments past six
never recovered), and D75 names the structurer respellings as the larger measured lever.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01456qmqCGpCWJkrg6JUZNNM
This commit is contained in:
Priyanshu KumarandClaude Sonnet 5.5 committed 2026-10-23 17:33:03 +05:30
1 parent 32a74ae756
commit a0d9e34aa2
1 file changed
+7 -1
+7 -1
View File
@@ -124,9 +124,15 @@ Decisions
| D64 | `Values` keeps interior mutability (`RefCell` scratch for rendered, residual, effect, restored, owed, broken and the pending attempt, and the block being spelled, a `Cell`) and the D58 spelling pass is its only mutator; making the pass `&mut self` and `Values` immutable once spelled, with each attempt of `spell_function` a value rather than fresh mutable state, makes the single owner true by the type and not by the architecture test. Gate: no `RefCell` in `render::values` outside the pass; owner `r2dec` `render`; timing: deferred | SD, deferred |
| D65 | A function is an import's stub only where the program says its address is one and its body is only the transfer; the route asked the body alone, so a `.text` function that ends in a jump to an import's stub printed "import stub `free` has no body" and no C, though `afl` lists it as no import. Done (2026-10-23, branch `d65-stub-one-jump`, [adr-decompiler-rewrite](doc/adr-decompiler-rewrite.md) "The import-stub route"): `native::sealed` asks `Program::import_at(entry)` (discovery's `Linkage`, `O(log stubs)`) and `SealedFunctionAnalysis::stub_of` runs `stub::import_stub` only for a stated import, naming the stub as discovery does; `seal_function_from_input` and `decompile_function_from_input`, which have no program, no longer take the route. bzip2 `default_bzfree` (`0x5b00`: `test rsi, rsi; je; mov rdi, rsi; jmp free`) and `default_bzalloc` (`0x5b20`: `imul esi, edx; movsxd rdi, esi; jmp malloc`) now render the test and the `free` call, and `malloc` of the product, with the unread-parameter arguments as residuals. The 63 DecBench rows (2026-10-23): 52 write an argument register before the jump, 2 test first, 9 are a bare `jmp` to the stub; all 63 render, 22 without a residual, 24 with a parameter the body reads and nothing declares (P7), 16 with an `int` result whose upper half is a residual (W47), 1 with both. Over bash, ls-toybox, pigz, 0pack and pumasim (26,935 functions) 1,415 rendered as an import stub at the base: 1,200 are `afl`'s `sym.imp.*` and stay stubs, 215 are functions with a name or a body of their own (0pack 100, pumasim 90, bash 13, ls-toybox 12) and render; none is newly refused, no goto is added, +86 residual sites, and the base declared them away wrongly (pumasim `QLayoutItem::~QLayoutItem()` deleting destructor `0x295d10` printed as `extern void _ZdlPvm(void*, uint64_t)`, its constant size dropped; a static initialiser's `__cxa_atexit(...)` as the import itself). The census (442 goldens, 24 stable-temporaries) is unmoved, and the engine tests `a_guarded_wrapper_of_a_stub_is_a_body_not_the_stub`, `a_wrapper_that_computes_the_stub_s_argument_is_a_body_not_the_stub` and `a_function_that_only_jumps_to_a_stub_is_a_body_not_the_stub` fail at the base while `an_import_stub_is_its_import_s_declaration` and `an_effect_before_the_jump_keeps_a_stub_a_body` hold. Left: the DecBench no-output count 380 to about 317 is the main session's run; `stub::import_stub` keeps its body proof as the second half (`stub_start` already stops a container stub at any write the transfer does not read, so the two agree on every stub discovery names); a linkage section `section_stubs` rejects now renders bodies with a visible gap, a discovery defect `afl` and `pdd` both show | FS |
| D66 | A run that loses rows fails, and the backend checkpoints by time. `tests/decbench/run_gate.py` stops a run (exit 71) when a binary a decompiler rendered has no score key of that decompiler in `evaluated/<binary>.toml` (the 16-byte `binary = "bash"` of 2026-10-17: 55 of 192 cells of that tree, all bash) and refuses more than one evaluation worker (exit 72) for a binary whose decompiled C exceeds 8 MB (one worker holds 5.2 GB for r2s's bash), in `run_decbench.sh`, in the driver before `evaluate_project` (as `SystemExit`, which the driver's `except Exception` does not swallow) and in `evaluate_checkpointed.py`; `report_decbench.py` compares decompilers over the rows every one scored, prints the rows it left out and each decompiler's rows without a score, and exits 73 when one scored nothing in a project (it stops at `kuna scored 0 of 4144 rows of bash` on the 2026-10-17 results and passes the 2026-10-23 ones); `r2sleigh_raw.py` pickles its partial result every 30 s and at the end, not per function (2.5 MB each on bzip2, 18 MB on bash, quadratic: the bash backend took 1,253 s and 1,723 s against about 140 s for r2s alone) and resumes from a checkpoint of the same build, binary and targets. `r2s_batch.py` gave `on_answer` a process's answers only after the process had ended, so the per-function checkpoint never held an answer of a killed run; it streams them now. Next: a host run of bash with the 30 s backend, to put the wall time against r2s's 140 s | GT, immediate (landed on decbench-2, unit-tested; the driver wiring is proven by `--plan` and a stand-in, not a driver run) |
| D67 | The DecBench-ranked fixes after D65 and D66, each with the ceiling the 2026-10-23 run measured ([record](tests/decbench/RESULT-2026-10-23.md), section 5), graded by D47 and the source-gold soundness gate. (1) Library prototypes into values: r2abi states pointer-bearing prototypes for the 23 stdio and multibyte imports (945 renderings) and `error`'s `...`, r2types types a value by the callee parameter it flows into and a call result by its return type; 248 pointer-as-integer and 36 library-named functions where it is the only cause, 123 net union rows. (2) A proven return or `void` (W47): about +35 GED-perfect non-bash rows, a size-controlled estimate and not a counterfactual. (3) Widths from reads (W49, W50): 209 functions, 142 net union rows. (4) One C object per SSA web, stated by r2ssa and lowered by r2dec: 138 functions, 126 net union rows, and bash's C from 13.9 MB toward 2.6 MB; unproven for GED, graded on the mined set first. (5) Parameters that flow into residual calls (P7 call contracts): 402 functions, 138 only cause; the 410 functions whose parameter nothing reads need caller or function-pointer-slot evidence from r2engine and have no ceiling yet. (6) Structurer respellings: the noreturn arm as a guard (+12 / -1, sound by the callee's noreturn contract), the select respelling (+15 / 0) only after an ADR decision on rule 8, the do-while rotation's 10 lost rows when the bound is a constant. (7) Unexplained: r2s's mean GED is 39.0 on the non-bash rows against 18.7 to 20.9 for the others while its perfect count is competitive; find what drives the tail (the 380 no-output rows, the large functions) before choosing a fix. Deferred, as benchmark facts or without a shape to copy: large functions (1,345 rows of 20 blocks or more, 2 perfect for any tool), switches (115 rows), compound conditions (75), the project-named type cap (3,020 functions) and the unobservable cap (2,255) | T, P7, SD |
| D67 | The DecBench-ranked fixes after D65 and D66, each with the ceiling the 2026-10-23 run measured (the typed-values design of the same day replaced the type-class ceilings by measured expectations: D71 about +20 net union rows, D72 +7 to +12, D73 5 to 20, together about +27 to +32 against the 270 rows between r2s and kuna, so these items buy `type_match` and clean declarations and do not close the union gap; D75's respellings and the residual share are the larger measured levers, see D70 to D75) ([record](tests/decbench/RESULT-2026-10-23.md), section 5), graded by D47 and the source-gold soundness gate. (1) Library prototypes into values: r2abi states pointer-bearing prototypes for the 23 stdio and multibyte imports (945 renderings) and `error`'s `...`, r2types types a value by the callee parameter it flows into and a call result by its return type; 248 pointer-as-integer and 36 library-named functions where it is the only cause, 123 net union rows. (2) A proven return or `void` (W47): about +35 GED-perfect non-bash rows, a size-controlled estimate and not a counterfactual. (3) Widths from reads (W49, W50): 209 functions, 142 net union rows. (4) One C object per SSA web, stated by r2ssa and lowered by r2dec: 138 functions, 126 net union rows, and bash's C from 13.9 MB toward 2.6 MB; unproven for GED, graded on the mined set first. (5) Parameters that flow into residual calls (P7 call contracts): 402 functions, 138 only cause; the 410 functions whose parameter nothing reads need caller or function-pointer-slot evidence from r2engine and have no ceiling yet. (6) Structurer respellings: the noreturn arm as a guard (+12 / -1, sound by the callee's noreturn contract), the select respelling (+15 / 0) only after an ADR decision on rule 8, the do-while rotation's 10 lost rows when the bound is a constant. (7) Unexplained: r2s's mean GED is 39.0 on the non-bash rows against 18.7 to 20.9 for the others while its perfect count is competitive; find what drives the tail (the 380 no-output rows, the large functions) before choosing a fix. Deferred, as benchmark facts or without a shape to copy: large functions (1,345 rows of 20 blocks or more, 2 perfect for any tool), switches (115 rows), compound conditions (75), the project-named type cap (3,020 functions) and the unobservable cap (2,255) | T, P7, SD |
| D68 | A named value is read as a variable only where its definition dominates the read. Done (2026-10-23, branch `d68-dominating-reads`, [adr-decompiler-rewrite](doc/adr-decompiler-rewrite.md) "A name is read where its definition has run"). The skeptic's finding at D58's tip `15486ea7`: `narrowed_argument` took the first value of a lane's class in definition order as a call argument, a value that can be defined where the call is not dominated (pumasim `0x279d80` and `0x27b040`, 4 reads of `tmp_lane_..` assigned only on the other arm of a branch), and D58's reverse postorder spelling read it as a variable; the parents print the same wrong C on a mirrored layout and a residual on another, so D58 moved which functions show a fact error of r2ssa's. Fixed at the owners. r2ssa: `integrity::defined_before` (an entry value, a phi or an earlier operation of the block, a strictly dominating block; `O(1)`) keeps a lane only where its definition has run at the call and otherwise states the carrier the walk reached (`(uint32_t)xmm1_6`); `validate_boundary_values` checks every value a sealed fact reads (call arguments and carriers, call targets, return values, return address, exit stack pointer, branch conditions, switch selectors; `O(facts)`) and refuses the function otherwise (`IntegrityRule::BoundaryValueNotDominated`; call results are definitions and a comparison's operands are copy-class names, so neither is a read). r2dec: `read_name` keeps its own rule as a hard guard, not a `debug_assert` (a residual with refusal evidence and a proof-line cause), asking r2ssa's `value_defined_before` at the block being spelled. Gate: three engine tests under the four write orders (`a_call_argument_is_never_a_lane_that_only_the_other_arm_assigns_under_every_write_order` fails at `15486ea7` on the skeptic's `lane_nondom9` and `lane_nondom7`; with the pick and the check both off, `fact_mutants`, the guard alone makes the read a residual; with the pick off and the check kept, r2ssa refuses), five r2ssa tests (`function::tests::lanes`), the guard's table in r2dec; `scripts/mutants_d68.py`, 25 mutants, 22 killed and 3 alive with reasons. Measured over 25,735 functions of bash, ls-toybox, pigz, 0pack and pumasim and 14,034 of ten more: 2 change (pumasim `0x279d80`, `0x27b040`), none is newly refused, residual sites and gotos equal; the dominance audit reads 0 names out of scope under all four write orders (4 in 2 functions at the tip) and clang `-Wuninitialized` flags 56 of 1,920 generated float functions at the tip and 0 now, under all four write orders; `census.py orders` over 2,508 functions holds at 0, 0, 0 differing, now over refused functions' ledgers too. Also: the production write order (exits inlined, no list permuted) is compiled under test and pinned, and `census.py orders` fingerprints a refused function, which moved 7 guards to tip unseen. Left: a later member of the lane's class that dominates the call is not looked for (`low_lanes` holds the first of each class); the `canonical_source` of a stack reload and a comparison's operands are first-of-class names no renderer reads, unchecked; certificates repeat their facts' values and are not checked on their own | done (SD) |
| D69 | A function a change moves is executed against its base on real binaries, as a standing exit gate. Today the equivalence ratchet's 828 records come from eight small C files (`tests/corpus`, `tests/gold`), so none of the real-binary functions the structurer items change is covered by it: D58 moved 55, D59 12, D65 215 (2026-10-23). The skeptics ran the check by hand in scratch (`difftest2` over the changed lists: native vectors with a trap-aware harness, a control mutant that must show DIFF on 100 of 100 vectors), and found D58's wrong C by a second oracle that the 442 goldens, the 2,998 tests, the census and the equivalence run all missed (clang `-Wuninitialized` with the residual helpers marked noreturn, plus a dominance audit of every variable read). Move both into the tree: the changed-function differential execution beside `tests/equiv`, the uninitialized-read sweep into certification (`certify_render.py`), each with a control that must fail | GT |
| D70 | The soundness gate can grade what the typed-values items change. `scripts/differential_truth.py` splits a function pointer parameter on every comma (`parse_prototype`, `int (*f)(int, int), char *s` reads as three parameters) and accepts r2s's own `arg0` as a source name (`RECOVERED_PARAM_NAME` matches upper case registers only), so a debug build whose DWARF failed to load scores 1.0 instead of exiting 2; it scores at function granularity with the word `residual` as the marker, so a premised claim (closed-world `void`, whose proof line names the premise) is silent by construction and must stay so, and it reports no grade, qualifier or width bucket. Fix the four defects first, bucket the silent keys (return or parameter by width, sign, qualifier, pointee, pointer as integer, void against value), add `tests/gold/returns.c` (void to void, void to value, value to void, value to value), re-record the baseline once with every cause carried over (the fixes move review.c 24 to 28 percent, hashes.c 61 to 75), and measure M1 on the DecBench checkpoints: `type_causes.py collect`, `fp:integer_width` by argument and by wider or narrower (parameters 40 percent or more of the 209 means D73's B1 first, locals 60 percent or more means B2 first). Scripts and Python tests only; first, in parallel with D71 and D72 | GT, first |
| D71 | A value that flows into a declared pointer parameter, or is defined by a declared pointer return, has that pointer type at `Basis::Declared` unless other evidence refuses it. The solver already bounds both (`gather_callee_prototypes`, r2types `evidence.rs`) and two owners drop it: `apply_exact_source_signature` rebuilds each parameter from the carrier width graph and `requalify` ends in `(graph, _) => graph`, so an Int against a Pointer keeps the Int; r2dec declares every local from `MachineType` alone. A pointer forwarded to an internal function is bounded by that callee's carrier width and meets to Bottom (the interface carries one `Confidence` for all its slots). r2abi reads `FILE` as a generic opaque typedef, so `Prototype::of` is None for 21 imports and 525 renderings print `uint64_t fclose(uint64_t)`; the table has lowercase `file*` returns (`fopen`, `fdopen`, `freopen`, `popen`, `tmpfile`), `error` without its ellipsis, and no row for 59 of the 149 imports bzip2, grep and zlib call. Staged: A1 transport (cited r2abi rows, a platform handle row beside `named_scalar` for `FILE` and `DIR`, `c_type.rs` keeps the typedef name over an opaque tag, a declared-not-defined tag set in r2dec, `typedef struct _IO_FILE FILE;`), A2 parameters in r2types (the callee signature carries its `Confidence`, an integer parameter at `CarrierWidth` or `Convention` basis is a width and not a bound, ordered unsigned compares stop seeding integer evidence on a class holding a pointer bound, `apply_exact_source_signature` admits the pointer), A3 locals and call results after both (`ValueTypeFacts` beside `ReturnTypeFact`, dense, decided once in `SolvedFunction::of`, read by r2dec; `rax_1 = malloc(..)` is `void *`). Expected, measured from the 156 of 1,015 non-bash pointer parameters cast in their own body (15%): `type_match` perfect +45 (25 to 90, 328 to about 375), net union about +20 (10 to 40); it closes `fatal`, `ck_fclose`, `myMalloc`, not `set_color_palette` (P8) or the width rows. Gates: failing tests first, `type_causes.py` on the 48 binaries (`pointer_as_integer` and `library_named_type` fall, `wrong_pointee` rises by less), D70's soundness rate not below 43 and 21 percent, census casts disappear, certification, equivalence | T, r2abi, next |
| D72 | A function is declared with a value because its body leaves the carrier, though no caller reads it: 15 of 75 source `void` functions in pigz are `void`, 57 are read by no caller. Return half only (the parameter half waits for D71's rows): r2ssa states per call site the fate of the result bytes (`site_fates` replaces `reads_after_call`, which breaks at the first control flow op; a backward worklist over the caller's block successors with a byte lane algebra, a call kills the lanes unless it reads them, a budget gives Unknown), r2engine joins them under `Premise::ClosedWorld`, `inert` (`program/reads.rs`) accepts multi-byte `nop` and `hlt` padding (one `nop word [rax + rax]` after a call keeps `Escapes` from ever being asked), r2types projects a `Void` interface under the premise as `Decided{Void, by: Unobserved}` at `Basis::Solved` plus the premise and not `Certified`. Probe first: count how many of the 48 DecBench binaries reach `Some` from `Escapes::compute`; zero means fix that blocker first. Expected: pigz void 40 of 75, DecBench +12 to +20 GED-perfect, net union +7 to +12, `type_match` unchanged. Negatives that reject fake output: an exported symbol, an address taken, a variadic callee after the call and an unresolved indirect branch keep their residual; mutation target in the style of `scripts/mutants_d68.py`. Not in this row: void for a scratch result under the closed world (+18 on pigz, 3 more silently wrong) needs an ADR on rule 8 first, and forwarders (tail jump joins by Tarjan components) wait for the remainder | T, next |
| D73 | Integer widths are stated by what the body moved, with a basis per slot: a declared width is never narrower than a lane the body moved whole (`rotl(unsigned, int)` prints `uint8_t arg1`, `bsPutBit` prints `uint8_t arg0`, both because `observed_entry_read_storages` declares the demanded byte), never wider than the lane a callee states for a value only handed on (`int i_call(int x){return isalpha(x)+1;}` prints `uint32_t i_call(uint64_t arg0)` beside `int32_t isalpha(int32_t)`), and carries its basis (this mints the per-slot `Basis` that D71 reads and closes W46 by deleting the second narrowing path in `signature_infer.rs`). B1 parameters only, after D70 and M1 and after D71's A2 and D72 (shared files `function_facts/mod.rs`, `recover_interface.rs`, `SourceResultReads`). Measured on 200 integer parameters in 7 binaries: 13 wrong today, B1 closes the 3 narrower ones with certainty and up to 7 of 10 wider; DecBench plan 5 to 20 net union until M1 exists (the 142 ceiling assumed a parameter-local split nobody measured). B2 (locals: a compact written-lane record, a join over a web) waits for M1 and lands before the one object per web item; W50 (a narrow DWARF parameter's extension: a producer reader in r2image, a premise, a cited psABI row; five crates, DWARF route only, no DecBench effect) is deferred | T, conditional |
| D74 | Real findings of the typed-values design (2026-10-23), each with an owner and timing. Next: a parameter the body dereferences is declared integer (pigz `free_pool`, `get_space`: the proof line says "1 structure laid out from a parameter's accesses" and the header is `uint64_t arg0`; find why `certified_parameter_pointers` refuses; r2ssa and r2types; gate `fp:pointer_as_integer` rows whose body dereferences the parameter); a silently wrong `void` (pigz `alloc` at `0x4044b0`, `void *alloc(void *ptr, size_t size)`, renders `void fcn_4044b0(void)`, likely a tail into a noreturn callee; r2ssa; gate D70's return buckets); stack arguments past six are never recovered (9 pigz functions have DWARF arity above 6 and r2s at most 6; `argument_area` stays empty for an unread reason; r2ssa interface recovery; gate an arity census against DWARF); `mov eax, 0x811c9dc5` lifts as `RAX = COPY 0x811c9dc5:8`, so a returned constant is declared 8 bytes wide (19 of the 24 silent source-gold width errors; the constant fold loses the 32-bit write; r2ssa lanes or the lift; gate D70's return `WIDER` bucket). After D71's A2: printf-style conversions and a callee's dereferenced pointer parameter as evidence for the caller's argument (12 or more of pigz's 26 standard-pointee integer-declared pointers are handed to internal wrappers such as `fail(err, file, line, func)`; r2ssa `printf.rs` into r2types; gate `pointer_as_integer` falls without `wrong_pointee` rising by as much). After D71's A1: `iswalnum` is refused in objects with no platform scope ("the import's declaration has an argument this convention does not place"), confirm on grep before D73 claims `wordchar`; parameters that flow into residual calls (P7; 121 of D67 item 5's 402 rows are undeclared imports). Upstream: radare2 returns `file*` lowercase for `fopen`, `fdopen`, `freopen`, `popen`, `tmpfile` and gives `error` no ellipsis (steward, one idea per PR). Conditional: `certified_parameter_pointers` is a second owner of what `gather_memory_widths` asserts (fold into D71's meet or leave to P9) | r2ssa, r2types, r2abi |
| D75 | The structurer respellings D67 item 6 listed are the largest measured GED lever left in r2dec and cost no new fact: on the 594 GED-perfect non-bash rows the noreturn arm as a guard (`if (c) { f(); } rest`, 275 renderings) gives +12 perfect and -1, sound by the callee's noreturn contract; `x = c ? a : b; return x;` as `if (c) return a; else return b;` gives +15 and 0 and needs an ADR decision on rule 8 first (it spells a branch the machine does not have; the select and the `if` are equal, so the question is whether a pure select with side-effect-free arms may be spelled as control when the source has the shape) ; the three together +27 and -1 on 2,736 rows, bash not yet counterfactually measured (59% of the rows). Owner r2dec structure and render; gate the mined set (D47), GED not worse, certification, census read line by line. Immediate: the noreturn guard; the select respelling after the ADR | SD, next |
The Sleigh crates (`libsla`, `libsla-sys`, `sleigh-config`) are vendored (`vendor/`).