Skip to content

two more fleet-noise sources: socket's int-as-fd family and logging's level constant - #266

Merged
devdanzin merged 3 commits into
mainfrom
fleet08-noise-socket-fd-and-logging-constant
Sep 2, 2026
Merged

two more fleet-noise sources: socket's int-as-fd family and logging's level constant#266
devdanzin merged 3 commits into
mainfrom
fleet08-noise-socket-fd-and-logging-constant

Conversation

@devdanzin

@devdanzin devdanzin commented Aug 29, 2026

Copy link
Copy Markdown
Owner

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_fds take a raw integer fd — 163 dirs

The int-as-FD analogue of the int-as-pointer family already excluded in CTYPES. Handing any
of 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:

face dirs what happened
socket-fatal_python_error 120 closed some other descriptor; glibc's own Unexpected error 9 on netlink descriptor 11 survived to stdout — 11 being socket.AF_ROSE, which the stress region had picked as a shared object
socket-sigabrt 43 SIGABRT with an empty stdout — and all 43 shared a constant worth 0, 1 or 2. They closed the child's own stdout or stderr, so the diagnostic had nowhere to go

43/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
getaddrinfo is 0/6 on both interpreters, across two fd ranges and two rates — the window
needs the full stress region. I am not carrying that differential forward. Nothing above
depends on it: the message is glibc's __libc_fatal reporting EBADF on 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 test
pinning that: close and dup are also methods on sockets, files and much else, where they
take no descriptor and are fine to fuzz. A METHOD_BLACKLIST entry would stop fusil ever
closing 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. warnings echoing logging's source — 14 dirs, and #265 does not cover it

A third face of the self-echo disease. The default warning formatter 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 the CRITICAL in it is the level constant being passed as an argument. It is
neither a File "...", line N, in ... frame nor a .critical( call, so neither #265 rule
reaches it — I checked the two observed lines against the live core_ignore_regexes rather
than assuming, and both still score 1.0 today. 14 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 formatted record (CRITICAL:root:...) and English prose ("a critical error") both still
score. 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 and bdb will keep echoing arbitrary source past any
literal 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 check and ruff format --check both clean.

… 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.
@devdanzin
devdanzin merged commit 82f8d05 into main Sep 2, 2026
0 of 2 checks passed
@devdanzin
devdanzin deleted the fleet08-noise-socket-fd-and-logging-constant branch September 2, 2026 18:01
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.
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.

1 participant