Skip to content

[AUTOMATED] fix(cli): xrefs --to <import name> answers instead of refusing it as ambiguous - #441

Merged
mahaloz merged 1 commit into
mainfrom
feat/re-name-based-xrefs-rejects
Sep 6, 2026
Merged

[AUTOMATED] fix(cli): xrefs --to <import name> answers instead of refusing it as ambiguous#441
mahaloz merged 1 commit into
mainfrom
feat/re-name-based-xrefs-rejects

Conversation

@mahaloz

@mahaloz mahaloz commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

The problem

kuna xrefs --to <name> refuses to answer for an import, because an import's
name is on two addresses and the selector model calls that ambiguous — while
either of the two addresses answers the question fine.

$ kuna xrefs decompiler/crates/kuna-analysis/tests/fixtures/pe_imports.exe --to VirtualProtect --json
error: selector "VirtualProtect" is ambiguous; candidates:
  VirtualProtect at synthetic 0x1400079b0
  VirtualProtect at synthetic 0x14000d234
use a section-qualified selector to choose one candidate
$ echo $?
1

$ kuna xrefs decompiler/crates/kuna-analysis/tests/fixtures/pe_imports.exe --to 0x1400079b0
# 2 references to VirtualProtect @ 0x1400079b0
# same import at 0x14000d234 (VirtualProtect) - a forwarding veneer and the pointer slot it jumps through
0x140001a9e	read	__write_memory.part.0+0x18e	CALL qword ptr [0x14000d234]
0x140001cce	read	_pei386_runtime_relocator+0x19e	MOV R12,qword ptr [0x14000d234]

The two candidates are one callable: a .text FF 25 veneer and the .rdata
IAT slot it jumps through. --to has answered over that alias class since
xrefs-unify-pe-import, so both addresses already return the same rows — the
name was the only spelling that could not reach them.

The fix

  • A contested name is no longer decided at lookup time. Which addresses are one
    callable is a property of the decoded forwarding jumps, and those only exist
    once the walk has run, so the candidates are carried into the walk as its focus
    set and the ambiguity is settled afterwards against the alias class.
  • Candidates that all lie in one class are one callable; the query proceeds at
    the class's code half — the veneer, which is the address the answer is next
    disassembled at — with the lowest address breaking a tie between several
    veneers through one slot.
  • Candidates that do not all share a class are distinct functions and keep the
    refusal, with every candidate still named. The fold rests on the decoded jump,
    never on the shared name, so two static duplicate_locals are never merged.
  • No option: a read-only query surface commits nothing into the engine and
    cannot change emitted C.

The tests

Three cases in kuna-cli/tests/xrefs_cli.rs: the import resolved by name answers
byte-for-byte what its veneer address answers; puts, reached through the veneer
rather than the slot, folds the same way; and duplicate_local in a relocatable
object is still refused with both candidates. The first two fail without the fix
(exit 1, "is ambiguous"). The acceptance probe is promoted to
tests/cli/name-based-xrefs-rejects.json.

A sweep over every duplicate-name entry in every binary fixture — 148 names in 14
images — folded 129 and refused 18, with every fold answering identically to each
of its own candidate addresses and every refusal naming all of them.

🤖 Generated with Claude Code

…ame> answers instead of refusing

An import's name is on two addresses — a forwarding veneer and the IAT/GOT slot
it jumps through — so `kuna xrefs --to VirtualProtect` was refused as ambiguous
while `--to 0x1400079b0` and `--to 0x14000d234` both answered it, identically,
over that alias class.

The check could not have consulted the class where it stood: `alias_class` is the
connected component of the DECODED `jmp [slot]` relation, and the walk that
decodes it runs after target resolution because it needs the target address to
focus on. So a contested name is no longer decided at lookup time. Its candidates
ride into the walk as its focus set and are settled afterwards: candidates that
all lie in one class are one callable and resolve to the class's code half — the
veneer — while candidates that do not are distinct functions and keep the
refusal with every candidate named. The fold therefore rests on the decoded jump
and never on the shared name.

A read-only query surface commits nothing into the engine and emits no C, so
there is no option, no stages case and no catalog counter.

Sweep over every duplicate-name entry in every binary fixture — 148 names in 14
images — folded 129 and refused 18, with every fold answering byte-identically to
each of its own candidate addresses and every refusal naming all of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mahaloz
mahaloz force-pushed the feat/re-name-based-xrefs-rejects branch from 6b1b57f to 7ee207c Compare September 6, 2026 03:41
@mahaloz mahaloz added the full-ci Run the full cargo workspace suite on this PR before merge (internal PRs skip it by default) label Sep 6, 2026
@mahaloz
mahaloz merged commit d48b3ed into main Sep 6, 2026
10 of 11 checks passed
@mahaloz
mahaloz deleted the feat/re-name-based-xrefs-rejects branch September 6, 2026 03:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

full-ci Run the full cargo workspace suite on this PR before merge (internal PRs skip it by default)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant