Skip to content

Implement __contains__ on ClemoryReadOnlyView - #822

Open
zardus wants to merge 1 commit into
masterfrom
feature/clemory-ro-view-contains
Open

zardus wants to merge 1 commit into
masterfrom
feature/clemory-ro-view-contains

Conversation

@zardus

@zardus zardus commented Sep 6, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

addr in view raises on a ClemoryReadOnlyView. On
binaries/tests/x86_64/fauxware:

>>> loader.gen_ro_memview()
>>> entry = loader.main_object.entry
>>> entry in loader.memory
True
>>> entry in loader.memory_ro_view
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "cle/memory.py", line 33, in __contains__
    raise NotImplementedError
NotImplementedError

Loader.gen_ro_memview() builds one of these for every CFGFast run and the
class is exported from cle, so a question the Clemory behind it answers
cannot be asked of the view. No caller asks it today: nothing in angr or
angr-management tests membership against the view. Nothing is broken; a public
read method cannot be called.

Root cause

ClemoryReadOnlyView does not define __contains__, so it reaches
ClemoryBase.__contains__, whose body is raise NotImplementedError.

Fix

Answer it the way Clemory.__contains__ answers it once its own fast paths are
exhausted: look the address up and report whether that raised. This reuses the
last-backer cache __getitem__ maintains and needs no bounds the class does not
carry.

>>> entry in loader.memory_ro_view
True
>>> (entry - 0x10000) in loader.memory_ro_view
False

find is inherited from ClemoryBase here too and still raises. It is left
alone: it needs a decision about whether to search the flattened backers or
delegate to the clemory they were flattened from, which is a separate change.

Testing

test_clemory_read_only_view_contains loads fauxware, builds the read-only view
and asserts that the entry point is in it, that an address 0x10000 below it is
not, and that the view answers the same as the clemory it was flattened from at
every backer boundary, gaps included. It fails on master with
NotImplementedError.

Validation: #822 (comment)

session: sharpen

The class does not define __contains__, so `addr in view` reaches
ClemoryBase.__contains__, whose body is `raise NotImplementedError`.

Answer it the way Clemory.__contains__ answers it once its own fast paths are
exhausted: look the address up and report whether that raised. This reuses the
last-backer cache __getitem__ maintains.

No caller asks the question today. angr builds one of these for every CFGFast
run through Loader.gen_ro_memview(), but nothing in angr or angr-management
tests membership against the view, so this closes a hole in a public API rather
than repairing a broken analysis.
@zardus

zardus commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head cf201f6c114d4cb5b50bb2b8670b591d4bdcdab3 against baseline 0e77ade3c39a3cee05f65051e57955675e1ac21b.

  • Regression: pytest tests/test_clemory.py::test_clemory_read_only_view_contains — fails on baseline with NotImplementedError from ClemoryBase.__contains__, passes on head
  • Assertion audit: each of the four assertions was made to fail by a deliberate mutation of the head — __contains__ returning True unconditionally breaks the negative case, returning False unconditionally breaks the positive one, answering from the outermost bounds instead of the backers breaks the boundary walk, and making gen_ro_memview() a no-op breaks the view is not None narrowing
  • Full suite: pytest tests — head 262 passed / 9 skipped / 0 failed; baseline 261 passed / 9 skipped / 0 failed
  • Lint/type: run-ci-diff-checks.py, which reproduces angr/ci-settings lint.py and typecheck.py — pylint cle/memory.py 10.00 -> 10.00, tests/test_clemory.py 5.12 -> 5.79; pyright errors cle/memory.py 0 -> 0, tests/test_clemory.py 9 -> 9; exit 0
  • Hooks: pre-commit run --files cle/memory.py tests/test_clemory.py — exit 0, no file rewritten
  • Push guards: check-test-inputs.py and check-stale-pins.py at the head — both exit 0

Caller survey behind the "no caller asks the question" claim in the description:

  • git grep -nE 'memory_ro_view|gen_ro_memview' -- '*.py' at angr master 87411a719c96ddc3f3954590b1833b1789e19697 returns six lines at four sites. analyses/cfg/cfg_fast.py:1690 constructs the view. analyses/decompiler/clinic.py:1734 reads it and calls backers() on it. block.py:499 reads it and calls load(). block.py:375 reads it and hands it to the project's lifter, which reaches it through backers() -- engines/vex/lifter.py and engines/pcode/lifter.py both do. The other two lines, block.py:376 and block.py:500, are is not None guards. No site tests membership
  • the same grep over angr-management master aa843e5c10645ba361620768d59642a460805e80 exits 1 with no output, and a positive control on the same tree (loader\.memory) returns hits, so the empty result is a real absence and not a broken search
  • inside cle, membership is tested against a Clemory in five places — backends/blob.py, backends/elf/elf.py, backends/cgc/backedcgc.py, backends/tls/pe_tls.py and loader.py — and against the read-only view in none

Caveats: the workspace-wide gate was not run — this machine's native libraries are pinned by running corpus sweeps and rebuilding them is forbidden, so validation is the cle suite, the two CI diff checks and the survey above. No corpus A/B: the change adds a method that nothing calls, so there is no behaviour for a sweep to move.

@zardus

zardus commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Membership tests against the read-only view of
binaries/tests/x86_64/fauxware, loaded with auto_load_libs=False and with
loader.gen_ro_memview() called, before and after this change.

Before — the same question the Clemory answers raises on the view:

angr/cle master 0e77ade
>>> entry = loader.main_object.entry; hex(entry)
'0x400580'
>>> entry in loader.memory
True
>>> entry in loader.memory_ro_view
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "cle/memory.py", line 33, in __contains__
    raise NotImplementedError
NotImplementedError
>>> (entry - 0x10000) in loader.memory_ro_view
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "cle/memory.py", line 33, in __contains__
    raise NotImplementedError
NotImplementedError

After — the view answers, and agrees with the clemory it was flattened from:

with this change
>>> entry = loader.main_object.entry; hex(entry)
'0x400580'
>>> entry in loader.memory
True
>>> entry in loader.memory_ro_view
True
>>> (entry - 0x10000) in loader.memory_ro_view
False

@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_822

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