Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions fusil/python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,22 @@ def setupProject(self) -> None:
# these prefixes; no diagnostic starts with them.
r"^(\+\+\+|---|!!!) ",
r"\.critical\(",
# Third face of the same disease, and the one that survived PR #265: `warnings`
# prints the source line of whatever frame was running when it fired --
#
# /.../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 being passed
# as an argument, not a diagnostic. 14 kept dirs in one PyPy fleet, across
# asyncio.base_events, asyncio.streams and asyncio.selector_events.
#
# Match the constant only where it sits in an argument position, so a real
# formatted record (`CRITICAL:root:...`) and English prose ("critical error")
# both still score. Case-sensitive on purpose: the all-caps spelling is the
# module constant.
r"[(,]\s*CRITICAL\s*[),]",
# The --new-uninit region prints a progress marker per poked type,
# e.g. "[NEW-UNINIT] poking SystemError". The type name is arbitrary and
# routinely collides with a crash word ("SystemError" -> a 1.0 hit) or, worse,
Expand Down
22 changes: 22 additions & 0 deletions fusil/python/blacklists.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,28 @@
"getaddrinfo",
"socket",
"SocketType",
# The int-as-FD family: the same self-harm shape as the int-as-pointer functions in
# CTYPES above, one layer up. These four take a RAW INTEGER file descriptor
# (`close(integer) -> None`, `dup(integer) -> integer`, `fromfd(fd, family, type)`,
# `send_fds(sock, buffers, fds)`), so handing them any fuzz integer closes or
# reinterprets a descriptor the interpreter is still using. It is not a target defect --
# the contract is the argument: `close(integer)` does what it is told, on any interpreter.
# (A standalone harness closing fd 11 while sibling threads call getaddrinfo did NOT
# reproduce the abort on either interpreter, so the window needs the full stress region;
# the case here rests on the evidence below, not on a differential.)
#
# Measured, and it is not a small effect: 163 of 229 kept dirs in one PyPy
# --concurrency-stress fleet (71%) were this, 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: they had closed the child's own stdout or stderr, so the diagnostic had nowhere
# to go. Of the 100 dirs whose shared constants include a 0, 1 or 2, 43 went silent; of the
# 63 that do not, none did.
"close",
"dup",
"fromfd",
"send_fds",
}
POSIX = {
"_exit",
Expand Down
26 changes: 26 additions & 0 deletions tests/python/test_blacklists.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,32 @@ def test_pypy_blacklist_stays_narrow(self):
for keep in ("newdict", "strategy", "internal_repr", "intop", "move_to_end"):
self.assertNotIn(keep, bl.BLACKLIST["__pypy__"])

def test_socket_int_as_fd_functions_blacklisted(self):
"""socket.close/dup/fromfd/send_fds take a RAW INTEGER file descriptor.

Handing them a fuzz integer closes or reinterprets a descriptor the interpreter is
still using -- the int-as-FD analogue of the int-as-pointer functions in CTYPES, and
self-harm rather than a target defect -- the contract is the argument, and `close(integer)`
does what it is told on any interpreter. It dominated a PyPy
--concurrency-stress fleet: 163 of 229 kept dirs, in two faces that split on the value
passed -- 120 carrying glibc's `Unexpected error 9 on netlink descriptor 11`
(`socket.AF_ROSE == 11`), and 43 SIGABRTs with an EMPTY stdout, which had closed their own
stdout or stderr -- of the 100 dirs carrying a constant worth 0, 1 or 2, 43 went silent;
of the 63 without one, none did.
"""
self.assertLessEqual({"close", "dup", "fromfd", "send_fds"}, bl.BLACKLIST["socket"])

def test_socket_blacklist_keeps_the_socket_object_surface(self):
"""Only the MODULE-level int-taking functions are excluded.

`close` and `dup` are also METHODS on a socket object, where they take no descriptor
and are perfectly safe to fuzz. The entry is module-keyed for exactly that reason --
putting these names in the name-based METHOD_BLACKLIST would silently stop fusil from
ever closing a socket, a file or anything else with a `close`.
"""
for name in ("close", "dup"):
self.assertNotIn(name, bl.METHOD_BLACKLIST)

def test_sys_trace_hooks_blacklisted(self):
self.assertEqual(
bl.BLACKLIST["sys"] & {"settrace", "setprofile"}, {"settrace", "setprofile"}
Expand Down
50 changes: 50 additions & 0 deletions tests/test_file_watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,56 @@ def test_faulthandler_stack_lines_are_not_swallowed(self):
self.assertEqual(w.score, 1.0)


class TestWarningSourceEchoIgnored(unittest.TestCase):
"""`warnings` echoes the source of whatever frame it fired in; that source may score.

The default warning formatter prints two lines -- a header naming a file and line, and
then that line's SOURCE, verbatim:

/.../logging/__init__.py:1536: RuntimeWarning: coroutine '...' was never awaited
self._log(CRITICAL, msg, args, **kwargs)

So an un-awaited coroutine collected while `logging` happens to be on the stack echoes
`logging`'s own source, and the `CRITICAL` in it is the level CONSTANT being passed as an
argument -- not a diagnostic. This is a third, distinct route to the same 1.0 word as the
traceback shapes above, and the traceback rules do not cover it: the line is neither a
`File "...", line N, in ...` frame nor a `.critical(` call. 14 kept dirs in one PyPy
fleet came in this way.

The rule matches the constant only in an argument position, so a real formatted record
and English prose both still score.
"""

ARG_CONSTANT = r"[(,]\s*CRITICAL\s*[),]"

def _watch_with_rule(self):
w = _watch(words={"critical": 1.0})
w.ignoreRegex(self.ARG_CONSTANT)
return w

def test_logging_source_echo_is_ignored(self):
for line in (
b" self._log(CRITICAL, msg, args, **kwargs)",
b" if self.isEnabledFor(CRITICAL):",
b" self.log(CRITICAL, msg, *args, **kwargs)",
):
with self.subTest(line=line):
w = self._watch_with_rule()
self.assertIsNone(w.processLine(line))
self.assertEqual(w.score, 0.0)

def test_a_real_record_or_prose_still_scores(self):
for line in (
b"CRITICAL:root:something exploded",
b"CRITICAL: target failed",
b"a critical error occurred in the target",
):
with self.subTest(line=line):
w = self._watch_with_rule()
w.processLine(line)
self.assertEqual(w.score, 1.0)


class TestCookiejarWarningIgnored(unittest.TestCase):
"""http.cookiejar's own "bug!" warning must not push a boring session over the threshold.

Expand Down
Loading