two more fleet-noise sources: socket's int-as-fd family and logging's level constant - #266
Merged
Merged
Conversation
… level constant Both measured on one PyPy --concurrency-stress fleet (158 827 sessions, 229 kept dirs). Together they account for 177 of those 229 dirs -- 77% of everything the fleet kept was fusil hurting itself or scoring an echo. socket.close/dup/fromfd/send_fds take a RAW INTEGER file descriptor, so any fuzz integer closes or reinterprets a descriptor the interpreter is still using. This is the int-as-FD analogue of the int-as-pointer family already excluded in CTYPES, and it is not a target defect: CPython aborts on it too, measured 1/6 against PyPy's 6/6 (the gap is window width, not correctness). 163 of 229 dirs, in two faces that split exactly on the value passed. 120 closed some other descriptor and were captured with glibc's own `Unexpected error 9 on netlink descriptor 11` -- 11 being socket.AF_ROSE, which the stress region had picked as a shared object. The other 43 were SIGABRTs with an EMPTY stdout, and all 43 of them shared a constant worth 0, 1 or 2: they had closed the child's own stdout or stderr, so the diagnostic had nowhere to go. That is why the bucket looked like two problems. The entry is module-keyed (BLACKLIST["socket"]), not name-based: `close` and `dup` are also methods on socket, file and many other objects, where they take no descriptor and are fine to fuzz. A METHOD_BLACKLIST entry would stop fusil ever closing anything. The second fix is a third face of the self-echo disease PR #265 addressed, and one that survived it. `warnings` prints the source line of whatever frame it fired in: /.../logging/__init__.py:1536: RuntimeWarning: coroutine '...' was never awaited self._log(CRITICAL, msg, args, **kwargs) so any un-awaited coroutine collected while logging is on the stack echoes logging's own source, and `CRITICAL` there is the level constant passed as an argument. It is neither a `File "...", line N, in ...` frame nor a `.critical(` call, so neither #265 rule reaches it: 14 kept dirs across asyncio.base_events, asyncio.streams and asyncio.selector_events. The new rule matches the constant only in an argument position and is case-sensitive, so a real record (`CRITICAL:root:...`) and English prose both still score.
…cannot reproduce The first version of this comment said "CPython aborts on it too", carried over from the snapshot triage, which had measured PyPy 6/6 against CPython 1/6. Re-checking it before shipping: a standalone harness closing fd 11 while sibling threads call getaddrinfo is 0/6 on BOTH interpreters, across two fd ranges and two rates. The window evidently needs the full stress region, so that differential is not a number to stand behind here. Nothing about the disposition changes. `close(integer)` is documented as taking an integer and does what it is told on any interpreter; the captured message is glibc's own __libc_fatal reporting EBADF on a descriptor it owns; the manifests name the constant whose value is that descriptor; and 43 of 43 silent aborts carry a constant worth 0, 1 or 2. The case rests on that, and the comment now says so.
…mmetry 'All 43 shared a 0/1/2 constant' is true but incomplete on its own: 57 of the 120 noisy dirs carry such a constant too, so having one is necessary, not sufficient. The asymmetry is what carries the argument -- of the 100 dirs with a std-fd constant 43 went silent, of the 63 without one none did.
This was referenced Sep 2, 2026
Nortaq-PlayNexus
pushed a commit
to Nortaq-PlayNexus/fusil
that referenced
this pull request
Sep 4, 2026
CI has been red on main for five consecutive merges (devdanzin#261 through devdanzin#266). Both failures are tests asserting a property of the INTERPRETER BUILD rather than of fusil, so both pass on the local interpreters and fail on the python.org builds setup-python installs. A permanently red CI catches nothing, which is exactly how a real regression gets in. test_builtins_still_discoverable_under_only_c asserted `math` is discovered under --only-c. Whether math is a builtin or a shared extension is a build choice: it is compiled in on both local interpreters and is an extension on CI, so the test was naming a module CI does not have. The invariant it actually guards -- a builtin has no filename, so the file-based only-c check must not drop it -- is now asserted over sys.builtin_module_names, whatever this interpreter happens to contain. test_tricky_objects_runs_without_types_CapsuleType builds a shim `types` module with every attribute except CapsuleType, to prove the prelude survives a target that lacks it. It copied `__getattr__` along with everything else -- and CPython 3.13 defines CapsuleType LAZILY through a module-level __getattr__ (PEP 562), so the copy resolved the attribute straight back to the real module and the shim stopped shimming. 3.14 defines it eagerly via _types, which is why it passed here and failed only on CI's 3.13 job. Skipping __getattr__ fixes it; verified against a simulated 3.13-style lazy `types`, since no 3.13 is installed locally. Neither fix touches fusil itself.
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.
Both found while re-triaging the completed
fusil-pypy311_fleet_08(--concurrency-stress,158 827 sessions, 548 crashes, 229 kept dirs). Together they account for 177 of those 229
dirs — 77% of everything the fleet kept was fusil hurting itself or scoring an echo.
1.
socket.close/dup/fromfd/send_fdstake a raw integer fd — 163 dirsThe int-as-FD analogue of the int-as-pointer family already excluded in
CTYPES. Handing anyof them a fuzz integer closes or reinterprets a descriptor the interpreter is still using. Not
a target defect — the contract is the argument:
close(integer)does exactly what it is told,on any interpreter.
The bucket looked like two problems and is one. It splits exactly on the value passed:
socket-fatal_python_errorUnexpected error 9 on netlink descriptor 11survived to stdout — 11 beingsocket.AF_ROSE, which the stress region had picked as a shared objectsocket-sigabrt43/43 is the whole reason to believe the account rather than extrapolate from the noisy face.
One correction to the evidence, made before shipping. My first draft of this said "CPython
aborts on it too", carried over from the snapshot triage, which had measured PyPy 6/6 against
CPython 1/6. Re-checking it: a standalone harness closing fd 11 while sibling threads call
getaddrinfois 0/6 on both interpreters, across two fd ranges and two rates — the windowneeds the full stress region. I am not carrying that differential forward. Nothing above
depends on it: the message is glibc's
__libc_fatalreportingEBADFon a descriptor it owns,the manifests name the constant whose value is that descriptor, the function is documented as
taking an
integer, and 43/43 of the silent aborts carry a 0/1/2 constant.The entry is module-keyed (
BLACKLIST["socket"]), not name-based, and there is a testpinning that:
closeanddupare also methods on sockets, files and much else, where theytake no descriptor and are fine to fuzz. A
METHOD_BLACKLISTentry would stop fusil everclosing anything.
This one changes the default fuzzing surface, which is why I have raised it twice without
applying it. Four module-level functions, all documented as taking an
integer.2.
warningsechoinglogging's source — 14 dirs, and #265 does not cover itA third face of the self-echo disease. The default warning formatter prints the source line of
whatever frame it fired in:
So any un-awaited coroutine collected while
loggingis on the stack echoeslogging's ownsource, and the
CRITICALin it is the level constant being passed as an argument. It isneither a
File "...", line N, in ...frame nor a.critical(call, so neither #265 rulereaches it — I checked the two observed lines against the live
core_ignore_regexesratherthan assuming, and both still score 1.0 today. 14 dirs across
asyncio.base_events,asyncio.streamsandasyncio.selector_events.The new rule matches the constant only in an argument position and is case-sensitive, so a
real formatted record (
CRITICAL:root:...) and English prose ("a critical error") both stillscore. Tests cover both directions.
The structural question, still open
This is the fourth echo-noise face patched one at a time, and I do not think the series
converges —
warnings, tracebacks andbdbwill keep echoing arbitrary source past anyliteral list. The alternative I raised after fleet_07 stands: score only diagnostic-shaped
lines, or drop the sub-1.0 words. Not attempted here; this PR is the measured fix for what
fleet_08 actually produced.
Checks
1281 tests OK (
~/venvs/fusil_np_verify),ruff checkandruff format --checkboth clean.