Don't fuzz a module's own mutex: fusil was releasing it - #276
Merged
Conversation
PyPy implements `grp` in Python over cffi with `_lock = _thread.allocate_lock()`,
and `getgrall()` holds that lock across its loop over libc's STATIC group buffer.
The lock is the only thing making the iteration safe.
fusil selected `grp._lock` as a module-level object -- it is just an attribute --
and fuzzed it:
--- Fuzzing instance: fuzz_target_module._lock (type hint: lock, prefix: obj0m) ---
[obj0m11] lock.release_lock()
[obj0m12] lock.release_lock()
Releasing it mid-`getgrall()` drops the mutual exclusion, a second thread re-enters
and calls setgrent()/getgrnam(), libc reuses the static buffer under the first
thread's feet, and `while res.gr_mem[i]:` walks freed memory. Measured: 6/6 SIGSEGV
with the release thread, 6/6 clean without it -- necessary and sufficient. That is
the `grp` crash open since the fleet_08 triage, where it was left INCONCLUSIVE
because 22 of 24 replays timed out; releasing a lock hangs the harness as readily
as it crashes it, which is why replay never worked.
METHOD_BLACKLIST was no defence, and the shape of the gap is worth recording: it
blocks every way to TAKE a lock (acquire, acquire_lock, _acquire_lock,
_acquire_restore, wait) and none of the ways to DROP one (release, release_lock,
_release_lock, _release_save, _at_fork_reinit). fusil had blocked the methods that
hang IT and left the ones that corrupt the TARGET.
So the filter is on the object's TYPE, at selection time, and only for module
attributes: a lock fusil instantiated itself is nobody's mutex, so fuzzing
`threading` still covers Lock/RLock/Condition through the class path. A name-based
ban on "release" would instead have cost `memoryview.release()`, which is the
surface PYPY-FUZZ-011 came from.
It has to hold on both discovery paths. In `--discover-in-target` mode the runner
only ever sees a proxy carrying the type's NAME, so the target subprocess filters
too, mirroring how TRIVIAL_TYPES is already split across the two.
Verified against the real module: introspecting PyPy's `grp` now yields
['__loader__', '__spec__', 'ffi'] -- no `_lock`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPBSmN87d2BqUnrDbojUT1
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.
The bug
PyPy implements
grpin Python over cffi with a module-level mutex, andgetgrall()holds it across its loop over libc's static group buffer:That lock is the only thing making the iteration safe. fusil selected
grp._lockas a module-level object — it's just an attribute — and fuzzed it:Release it mid-
getgrall(), a second thread re-enters and callssetgrent()/getgrnam(), libc reuses the buffer under the first thread's feet, andwhile res.gr_mem[i]:walks freed memory.getgrall()+_lock.release_lock()_group_from_gstructNecessary and sufficient. This is the
grpcrash open since the fleet_08 triage, left INCONCLUSIVE there because 22 of 24 replays timed out — releasing a lock hangs the harness as readily as it crashes it, which is why replay never worked.Why
METHOD_BLACKLISTdidn't stop itThe shape of the gap is worth recording:
acquirereleaseacquire_lockrelease_lock_acquire_lock_release_lock_acquire_restore_release_savewait_at_fork_reinitfusil had blocked the methods that hang it and left the ones that corrupt the target.
The fix
Filter on the object's type, at selection time, and only for module attributes. A lock fusil instantiated itself is nobody's mutex, so fuzzing
threadingstill coversLock/RLock/Conditionthrough the class path. A name-based ban onreleasewould instead have costmemoryview.release()— the surface PYPY-FUZZ-011 came from.It has to hold on both discovery paths: in
--discover-in-targetmode the runner only sees a proxy carrying the type's name, so the target subprocess filters too — mirroring howTRIVIAL_TYPESis already split across the two.Verified against the real module. Introspecting PyPy's
grpnow yields['__loader__', '__spec__', 'ffi']— no_lock.Tests
Five, using real stdlib locks (
tempfile._once_lock,logging._lock) on both paths, plus over-filtering guards: a lock fusil made itself is still a fuzz target, the class isn't skipped, and ordinary module objects survive. They settest_private=Trueas the PyPy fleets do — otherwise the underscore-prefixed names are filtered before the type is looked at and the test proves nothing. Four of five fail without the fix.1308 tests pass;
ruff check+ruff format --checkclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01WPBSmN87d2BqUnrDbojUT1