Repository navigation
r2il: resolve the Custom(n) non-determinism at its source, and guard the row - #12
Merged
Merged
Conversation
…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.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 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. Comment |
Bugbot couldn't run - usage limit reachedBugbot 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
marked this pull request as ready for review
August 27, 2026 12:48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes both remaining open rows in
probes/win32-census/EXECUTION-NOTES.md—Ram/Customaliasing 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 ofSpaceId::Custom, and only one was broken:Disassembler::translate_spacespace_from_index(3 impls)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. Sospaces.get(ptr as usize)always missed and the fallback truncated the pointer intoCustom(ptr as u32).That is why the same instruction lifted to
Custom(1062180976),Custom(2279590000)andCustom(1968052336)on three runs: ASLR, bottled into aSerialize-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.r2concfails closed (ConcError::UnresolvedSpace) rather than guessing RAM.Aliasing —
AddressSpace::aliasesdeclares "same memory, second identity" without merging the ids. A space that is a window onto RAM keeps its ownSpaceId(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
SerializerefuseUnresolved, on the reasoning that an unreproducible row must not be writable at all. It broke 24 of 124r2sleigh-plugintests on ordinary x86 fixtures — baselined against a stash first, so caused, not pre-existing.Cause:
types.rs'sfunction_analysis_cache_key_partscallshash_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:
Unresolvedserializes, 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.r2concstill fails closed at execution, where guessing would corrupt memory silently.Stated precisely: "cannot be written unnoticed", not "cannot be written."
r2ilhas no row-persist entry point yet (serialize.rspersistsArchSpec, 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.
unresolvable_space_pointers_from_three_runs_convert_identicallyCustom(raw.space_idx)inconvert_varnodea_resolvable_space_index_still_resolvesUnresolvedfor everything would pass the abovean_unresolved_space_persists_as_a_self_describing_tokenUnresolvedau32payloadis_persistable_refuses_only_the_unreproducible_varnodetruefails one half, always-falsethe otheran_aliased_space_names_ram_without_becoming_ramaliasesfieldis_unresolved_discriminatesThe determinism test runs the real
convert_varnodefallback on the three measured pointer values, with an anti-vacuity assert that they genuinely miss the space map. A first version comparedUnresolvedto itself and never touched the lifter — caught and replaced before it landed.Also
Fixes a stale notes row:
R2ILOp = 464 bytesstill read "open prerequisite" three commits after Move A closed it (464 → 144 B, 3.22× measured).Gates
cargo test --workspacegreen, includingr2sleigh-plugin124/124cargo clippy --workspace --all-targetscleancargo fmtcleanCI cannot run on this repo yet — the workflow targets
self-hostedand no runner is registered (run #1 is queued). All gates above were run locally.Generated by Claude Code