Skip to content

Fix ClemoryView.find translating its bounds twice - #820

Open
zardus wants to merge 1 commit into
masterfrom
feature/clemoryview-find
Open

Fix ClemoryView.find translating its bounds twice#820
zardus wants to merge 1 commit into
masterfrom
feature/clemoryview-find

Conversation

@zardus

@zardus zardus commented Sep 6, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

ClemoryView.find reports nothing where the parent Clemory finds a match:

>>> m = cle.Clemory(arch, root=True)
>>> m.add_backer(0x1000, b'AAAABBBBCCCCDDDD')
>>> view = cle.ClemoryView(m, 0x1000, 0x1010)
>>> list(m.find(b'CCCC'))
[4104]
>>> list(view.find(b'CCCC'))
[]

An empty result reads as an answer rather than a failure, so a caller searching through a view
gets a wrong one silently.

Root cause

The bounds are translated twice. search_min and search_max are clamped against
self._start and self._end, which are addresses in the parent's space, and then
self._rebase is added to them:

if search_min is None or search_min < self._start:
    search_min = self._start
if search_max is None or search_max > self._end:
    search_max = self._end
return self._backer.find(data, search_min=search_min + self._rebase, search_max=search_max + self._rebase)

For the view above _rebase is 0x1000, so a default search runs over the parent's
0x2000-0x2010, which is past the end of the view and past the end of the backer. Which way
it goes wrong depends on the offset: where offset equals start, _rebase is zero and the
arithmetic comes out right by accident.
__getitem__, __setitem__, load and store all take an address in the view's space and add
_rebase once; only find starts from the parent's.

The results have the same problem in the other direction: find yields the parent's addresses,
so unless _rebase is zero a match cannot be handed back to view[...].

Fix

Clamp against self._offset and self._endoffset, which are the view's own bounds, and
subtract _rebase from each result. find takes and gives addresses in the view's space, like
the rest of the class.

This does not touch Clemory.find, which has bound bugs of its own that the view inherits
either way.

Nothing in cle, angr or angr-management constructs a ClemoryView, so this has no
in-tree caller; the class is exported from cle and the method is wrong for anyone who does.

Testing

tests/test_clemory.py gains test_clemory_view_find: that the view reports the match at the
offset that indexes it, that the byte there is the one expected, that a search_max below the
match excludes it, that a view built with a non-zero offset reports the match at that offset,
and that a view starting inside a backer reports only the match its window covers and not one
before it. It fails on master with assert [] == [8].

Validation: #820 (comment)

session: sharpen

find clamped search_min and search_max against self._start and self._end, which
are addresses in the parent's space, and then added self._rebase to them, so a
default search ran over a range that is not the view's. For a view of
0x1000-0x1010 built with the default offset it searched the parent's
0x2000-0x2010 and reported nothing; where the offset happens to equal the start,
_rebase is zero and the old arithmetic came out right by accident.

It also yielded the parent's addresses rather than the view's, so unless _rebase
is zero a match could not be handed back to __getitem__ or load. Every other
method of the class takes and returns addresses in the view's space.

Clamp against _offset and _endoffset, which are the view's own bounds, and
subtract _rebase from each result.
@zardus

zardus commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head eccd457b48fd56391678d95806221b32f177fa27 against baseline
0e77ade3c39a3cee05f65051e57955675e1ac21b, with angr/binaries at
003e82a2bfa641530924055695b36cec8af483ab.

  • Regression: the head's tests/test_clemory.py run against the baseline's cle gives
    FAILED test_clemory_view_find - assert [] == [8], 1 failed 3 passed. The same file at the
    head gives 4 passed
  • Focused: python -m pytest tests/test_clemory.py -q — baseline 3 passed, head 4 passed
  • Full suite: python -m pytest tests -q — baseline 261 passed, 9 skipped; head 262 passed,
    9 skipped. No failures on either arm
  • Lint/type: the merge-base comparison the hosted Lint and Typecheck jobs make, against
    0e77ade3 — pylint cle/memory.py 10.00 -> 10.00, tests/test_clemory.py 5.12 -> 5.82;
    pyright errors cle/memory.py 0 -> 0, tests/test_clemory.py 9 -> 9. The
    # type: ignore[arg-type] on each new cle.Clemory(None, root=True) line is load-bearing;
    there are two, both on new lines
  • Hooks: pre-commit run --all-files exit 0, all 24 hooks pass or skip, nothing rewritten
  • Guards: check-test-inputs.py exit 0; check-stale-pins.py against 0e77ade3 exit 0, and
    structurally null here — the diff touches no pyproject.toml

find becomes a generator function where it was a plain function, so work it used to do when
called now happens on the first next(). Two measured instances: a bad search_min type
raises TypeError: '<' not supported between instances of 'str' and 'int' at the call on the
baseline and on the first next() at the head, and a view over a ClemoryTranslator moves
TypeError: Cannot perform finds through address translation the same way. The exception is
the same in both; only when it arrives moves.

No corpus comparison is offered. No loader path constructs a ClemoryView, so one would be a
guaranteed null: git grep -n ClemoryView at cle 0e77ade3, angr
87411a719c96ddc3f3954590b1833b1789e19697 and angr-management
aa843e5c10645ba361620768d59642a460805e80 finds no construction and no subclass.

Workspace gate: not run. What ran instead is the scoped equivalent above: cle's own suite in an isolated
worktree, the merge-base lint and type comparison, and the full hook set. The angr,
angr-management, pyvex, archinfo, pypcode and native Rust suites did not run; hosted CI covers
them.

Caveats: git merge-tree puts this in conflict with 718, 721, 788 and 816, and clean against
809 — the five open pull requests touching cle/memory.py when this was measured. Every
conflict is in tests/test_clemory.py only, because each appends tests to the same file, and
cle/memory.py itself auto-merges in all five. Whichever merges first, the rest rebase.
Clemory.find has bound bugs of its own that this does not touch.

@zardus

zardus commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Searching through a ClemoryView, before and after this change. Same script on both arms.

Before — the view reports nothing where the parent reports a match:

cle master 0e77ade
>>> import archinfo, cle
>>> arch = archinfo.arch_from_id('x86_64')
>>> m = cle.Clemory(arch, root=True)
>>> m.add_backer(0x1000, b'AAAABBBBCCCCDDDD')
>>> view = cle.ClemoryView(m, 0x1000, 0x1010)
>>> list(m.find(b'CCCC'))
[4104]
>>> list(view.find(b'CCCC'))
[]
>>> list(view.find(b'CCCC', search_max=0x4))
[]
>>> view[0x8]
67

After — the view reports that match at the offset that indexes it:

with this change
>>> import archinfo, cle
>>> arch = archinfo.arch_from_id('x86_64')
>>> m = cle.Clemory(arch, root=True)
>>> m.add_backer(0x1000, b'AAAABBBBCCCCDDDD')
>>> view = cle.ClemoryView(m, 0x1000, 0x1010)
>>> list(m.find(b'CCCC'))
[4104]
>>> list(view.find(b'CCCC'))
[8]
>>> list(view.find(b'CCCC', search_max=0x4))
[]
>>> view[0x8]
67

@angr-bot

angr-bot commented Sep 6, 2026

Copy link
Copy Markdown
Member

Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_820

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