Skip to content

r2il: resolve the Custom(n) non-determinism at its source, and guard the row - #12

Merged
AdaWorldAPI merged 1 commit into
masterfrom
claude/c64-6502-falsifier-shztkk
Aug 27, 2026
Merged

AdaWorldAPI merged 1 commit into
masterfrom
claude/c64-6502-falsifier-shztkk

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

Closes both remaining open rows in probes/win32-census/EXECUTION-NOTES.md — Ram/Custom aliasing and persisted-row safety — because they turned out to be one defect.

The notes had misdiagnosed the root cause

The file recorded the symptom ("Custom(n) is non-deterministic") and left the mechanism open. Reading the lift path settled it, and the answer changed what the fix had to be. There are two producers of SpaceId::Custom, and only one was broken:

producer input verdict
Disassembler::translate_space the space's name fine — a byte-sum hash, stable across processes. Cleared by reading it, not by assuming.
space_from_index (3 impls) LOAD/STORE input-0 the defect

Ghidra p-code encodes a LOAD/STORE's target space as its input-0 constant, and that constant holds the host AddrSpace* pointer — not a space index. So spaces.get(ptr as usize) always missed and the fallback truncated the pointer into Custom(ptr as u32).

That is why the same instruction lifted to Custom(1062180976), Custom(2279590000) and Custom(1968052336) on three runs: ASLR, bottled into a Serialize-derived field. Both halves follow — an unreproducible lift and an unreproducible persisted row.

The fix

Drop the handle instead of bottling it — a new payload-free SpaceId::Unresolved. Two runs now lift identically because nothing process-specific reaches the IR. All unresolved handles compare equal: an over-match, never an under-match — we genuinely do not know whether two are the same space, and claiming they differ on the strength of a pointer is the stronger lie. r2conc fails closed (ConcError::UnresolvedSpace) rather than guessing RAM.

Aliasing — AddressSpace::aliases declares "same memory, second identity" without merging the ids. A space that is a window onto RAM keeps its own SpaceId (the lift stays faithful to the SLEIGH spec) while naming where its bytes live. Reserve, don't claim. Additive + serde(default), so old rows read unchanged. No lifter populates it yet.

A design correction mid-implementation

Recorded because a measurement caused it, not a preference.

The first cut made Serialize refuse Unresolved, on the reasoning that an unreproducible row must not be writable at all. It broke 24 of 124 r2sleigh-plugin tests on ordinary x86 fixtures — baselined against a stash first, so caused, not pre-existing.

Cause: types.rs's function_analysis_cache_key_parts calls hash_json_value(&blocks). serde here is how an in-process cache key is computed, and how the plugin's diagnostic JSON is exported — not only how a row is stored. A veto on the type hits all of that.

So the guarantee moved to where persistence actually happens:

  • Unresolved serializes, as the self-describing token "Unresolved" — never a plausible number the way a truncated pointer was.
  • Varnode::is_persistable() is the guard a write path calls.
  • r2conc still fails closed at execution, where guessing would corrupt memory silently.

Stated precisely: "cannot be written unnoticed", not "cannot be written." r2il has no row-persist entry point yet (serialize.rs persists ArchSpec, not lifted ops), so the guard is available for the write path when it lands rather than wired into one today. That is the honest boundary.

The failure also measured something. Those 24 tests passed before because the pointer serialized fine — meaning they had been hashing ASLR'd pointers into cache keys all along. Harmless within one process, and proof that the unresolved path is hit constantly on ordinary x86, not in some corner.

Falsifiers

Each verified red with the fix reverted; the disable is named in the test's own doc comment.

test disable
unresolvable_space_pointers_from_three_runs_convert_identically restore Custom(raw.space_idx) in convert_varnode
a_resolvable_space_index_still_resolves the silence twin — without it, a convert returning Unresolved for everything would pass the above
an_unresolved_space_persists_as_a_self_describing_token give Unresolved a u32 payload
is_persistable_refuses_only_the_unreproducible_varnode two-sided: always-true fails one half, always-false the other
an_aliased_space_names_ram_without_becoming_ram drop the aliases field
is_unresolved_discriminates —

The determinism test runs the real convert_varnode fallback on the three measured pointer values, with an anti-vacuity assert that they genuinely miss the space map. A first version compared Unresolved to itself and never touched the lifter — caught and replaced before it landed.

Also

Fixes a stale notes row: R2ILOp = 464 bytes still read "open prerequisite" three commits after Move A closed it (464 → 144 B, 3.22× measured).

Gates

  • cargo test --workspace green, including r2sleigh-plugin 124/124
  • cargo clippy --workspace --all-targets clean
  • cargo fmt clean

CI cannot run on this repo yet — the workflow targets self-hosted and no runner is registered (run #1 is queued). All gates above were run locally.


Generated by Claude Code

…the row

Two open rows in probes/win32-census/EXECUTION-NOTES.md closed together,
because they turned out to be one defect: `Ram`/`Custom` aliasing and
persisted-row safety.

ROOT CAUSE, which the notes had misdiagnosed. The file recorded the symptom
("Custom(n) is non-deterministic") and left the mechanism open. There are two
producers of `SpaceId::Custom` and only one was broken:

  * `Disassembler::translate_space` hashes the space NAME. Deterministic.
    Cleared by reading it, not by assuming it.
  * `space_from_index` (three impls) takes a LOAD/STORE's input-0 constant.
    Ghidra p-code encodes the target space there as the host `AddrSpace*`
    POINTER, not an index -- so `spaces.get(ptr as usize)` always missed and
    the fallback truncated the pointer into `Custom(ptr as u32)`.

That is why one instruction lifted to Custom(1062180976), Custom(2279590000)
and Custom(1968052336) across three runs: ASLR, bottled into a
Serialize-derived field. Both an unreproducible lift and an unreproducible
persisted row.

FIX. The handle is dropped rather than bottled: a new payload-free
`SpaceId::Unresolved`. Two runs now lift identically because nothing
process-specific reaches the IR. All unresolved handles compare equal, which
is an over-match and never an under-match -- we do not know whether two are
the same space, and claiming they differ on the strength of a pointer is the
stronger lie. `r2conc` fails closed (`ConcError::UnresolvedSpace`) rather than
guessing RAM.

ALIASING. `AddressSpace::aliases` declares "same memory, second identity"
without merging the ids: a space that is a window onto RAM keeps its own
SpaceId (the lift stays faithful to the SLEIGH spec) while naming where its
bytes live. Reserve, don't claim. Additive and serde-default, so old rows
read unchanged. No lifter populates it yet.

A DESIGN CORRECTION MID-IMPLEMENTATION, recorded because the measurement
caused it. The first cut made `Serialize` REFUSE `Unresolved`. That broke 24
of 124 r2sleigh-plugin tests on ordinary x86 input -- baselined against a
stash first, so caused rather than pre-existing. Cause: serde here also backs
in-process cache keys (`hash_json_value(&blocks)`) and the plugin's diagnostic
JSON, not only persistence. The guarantee moved to the write path:
`Unresolved` serializes as the visible token "Unresolved" (never a plausible
number), and `Varnode::is_persistable()` is the guard a write path calls.
Precisely: "cannot be written unnoticed", not "cannot be written" -- r2il has
no row-persist entry point yet, so the guard is available rather than wired.

The failure also measured something: those 24 tests passed before because the
pointer serialized fine, i.e. they had been hashing ASLR'd pointers into cache
keys all along. Harmless in-process, and proof the unresolved path is hit
constantly on real x86 rather than in a corner.

Falsifiers, each verified red with the fix reverted, and the disable named in
the test's own doc comment:

  * unresolvable_space_pointers_from_three_runs_convert_identically -- runs
    the REAL `convert_varnode` fallback on the three measured pointer values,
    with an anti-vacuity assert that they genuinely miss the space map.
  * a_resolvable_space_index_still_resolves -- the silence twin; without it a
    convert that returned Unresolved for everything would pass the above.
  * an_unresolved_space_persists_as_a_self_describing_token
  * is_persistable_refuses_only_the_unreproducible_varnode -- two-sided.
  * an_aliased_space_names_ram_without_becoming_ram
  * is_unresolved_discriminates

A first version of the determinism test compared `Unresolved` to itself and
never touched the lifter. Caught and replaced before it landed.

Also fixes a stale notes row: `R2ILOp = 464 bytes` still read "open
prerequisite" three commits after Move A closed it.

Gates: cargo test --workspace green (incl. r2sleigh-plugin 124/124),
clippy --workspace --all-targets clean, fmt clean.
@coderabbitai

coderabbitai Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 33cb5ed6-f329-46d2-b80f-8d797c633ab3


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Aug 27, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_182b5a95-2b6d-4169-bf44-d44264c71843)

@AdaWorldAPI
AdaWorldAPI marked this pull request as ready for review August 27, 2026 12:48
@AdaWorldAPI
AdaWorldAPI merged commit 441f2c2 into master Aug 27, 2026
1 of 2 checks passed
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.

2 participants