From 0b27d1176b5e2d1faa3dca39b7e4fcbe489add79 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:11:27 -0300 Subject: [PATCH] skip cffi objects reached as module attributes -- the int-as-pointer surface --blacklist misses --blacklist excludes _cffi_backend / _rawffi / ctypes by MODULE NAME, but PyPy's stdlib hands out live cffi objects as module ATTRIBUTES -- resource.ffi, _ssl.ffi, _sqlite3._ffi, _lzma.ffi, _gdbm.ffi, _curses.ffi, _tkinter.tkffi, _sha3._ffi, _posixshmem.ffi and their matching .lib -- so the surface is reachable regardless of the module blacklist. ffi.memmove(int, int, n) treats its arguments as raw pointers and dies by contract on whatever integer it is handed, exactly as CPython's ctypes does, so a fault there is never a target defect. Four consecutive PyPy fleets kept dirs from it (05, 06, 07, 08), which is why this is being applied now rather than flagged a fifth time. skip_trivial_type is already emitted into every generated script and already consulted at all three object/class dive sites, so the change is one condition there. It matches on the TYPE's defining module, not on the attribute name: an OBJECT_BLACKLIST entry for "ffi"/"lib"/"_ffi"/"tkffi" would match those names on unrelated targets that legitimately expose them, and there is a test pinning that distinction. Two things worth noting for future edits here: The first version of the comment used the words "segfault"/"segfaults", and SelfNoiseVocabularyTests caught it. That guard exists because help() echoes emitted comments through inspect.getcomments, so fusil's own crash vocabulary in a generated comment manufactures a crash -- the comment now says so in place, since this is the second time that trap has been walked into. The emitted function is SOURCE, so a golden snapshot cannot catch a logic error in it. The new tests parse it out of the generated script, exec it, and exercise it. --- fusil/python/write_python_code.py | 18 +++++++ tests/python/golden/fakemod_seed1234.py | 18 +++++++ tests/python/test_golden_output.py | 63 +++++++++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/fusil/python/write_python_code.py b/fusil/python/write_python_code.py index 96f1561..a322311 100644 --- a/fusil/python/write_python_code.py +++ b/fusil/python/write_python_code.py @@ -471,6 +471,24 @@ def _write_script_header_and_imports(self) -> None: def skip_trivial_type(obj_instance_or_class): if type(obj_instance_or_class) in TRIVIAL_TYPES: return True + # The int-as-pointer family, reached as a module ATTRIBUTE rather than as a + # module. --blacklist already excludes _cffi_backend/_rawffi/ctypes by NAME, but + # PyPy's stdlib hands out live cffi objects as attributes -- resource.ffi, + # _ssl.ffi, _sqlite3._ffi, _lzma.ffi, _gdbm.ffi, _curses.ffi, _tkinter.tkffi, + # _sha3._ffi, _posixshmem.ffi and their matching .lib -- so the surface is + # reachable regardless of the module blacklist. ffi.memmove(int, int, n) treats + # its arguments as raw pointers and dies BY CONTRACT on whatever integer it is + # handed, exactly as CPython's ctypes does on the same input, so a fault here is + # never a target defect. Seen in four consecutive PyPy fleets (05, 06, 07, 08). + # + # (Worded without fusil's own crash vocabulary on purpose: help() echoes emitted + # comments via inspect.getcomments, and SelfNoiseVocabularyTests enforces it.) + # + # Matched on the TYPE's defining module, not on the attribute name: an + # OBJECT_BLACKLIST entry for "ffi"/"lib"/"_ffi"/"tkffi" would match those names + # on unrelated targets that legitimately expose them. + if type(obj_instance_or_class).__module__ == "_cffi_backend": + return True return False """), ) diff --git a/tests/python/golden/fakemod_seed1234.py b/tests/python/golden/fakemod_seed1234.py index fbd6df9..a1f4520 100644 --- a/tests/python/golden/fakemod_seed1234.py +++ b/tests/python/golden/fakemod_seed1234.py @@ -42,6 +42,24 @@ def skip_trivial_type(obj_instance_or_class): if type(obj_instance_or_class) in TRIVIAL_TYPES: return True + # The int-as-pointer family, reached as a module ATTRIBUTE rather than as a + # module. --blacklist already excludes _cffi_backend/_rawffi/ctypes by NAME, but + # PyPy's stdlib hands out live cffi objects as attributes -- resource.ffi, + # _ssl.ffi, _sqlite3._ffi, _lzma.ffi, _gdbm.ffi, _curses.ffi, _tkinter.tkffi, + # _sha3._ffi, _posixshmem.ffi and their matching .lib -- so the surface is + # reachable regardless of the module blacklist. ffi.memmove(int, int, n) treats + # its arguments as raw pointers and dies BY CONTRACT on whatever integer it is + # handed, exactly as CPython's ctypes does on the same input, so a fault here is + # never a target defect. Seen in four consecutive PyPy fleets (05, 06, 07, 08). + # + # (Worded without fusil's own crash vocabulary on purpose: help() echoes emitted + # comments via inspect.getcomments, and SelfNoiseVocabularyTests enforces it.) + # + # Matched on the TYPE's defining module, not on the attribute name: an + # OBJECT_BLACKLIST entry for "ffi"/"lib"/"_ffi"/"tkffi" would match those names + # on unrelated targets that legitimately expose them. + if type(obj_instance_or_class).__module__ == "_cffi_backend": + return True return False diff --git a/tests/python/test_golden_output.py b/tests/python/test_golden_output.py index d3d713d..b1a26b9 100644 --- a/tests/python/test_golden_output.py +++ b/tests/python/test_golden_output.py @@ -173,6 +173,69 @@ def test_full_script_matches_snapshot(self): ) +class SkipTrivialTypeTests(unittest.TestCase): + """The emitted ``skip_trivial_type`` must refuse to dive into cffi objects. + + ``--blacklist`` excludes ``_cffi_backend`` / ``_rawffi`` / ``ctypes`` by MODULE NAME, but + PyPy's stdlib hands out live cffi objects as module ATTRIBUTES -- ``resource.ffi``, + ``_ssl.ffi``, ``_sqlite3._ffi``, ``_lzma.ffi``, ``_tkinter.tkffi`` and their matching + ``.lib`` -- so the int-as-pointer surface was reachable regardless. Four consecutive PyPy + fleets kept dirs from it. + + The function is emitted as SOURCE into every generated script, so testing the snapshot + text alone would not catch a logic error in it. Exec the emitted definition and exercise + it, matching on the type's defining module rather than on the attribute name. + """ + + def _emitted_skip_trivial_type(self): + source = generate() + tree = ast.parse(source) + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name == "skip_trivial_type": + namespace = {} + # TRIVIAL_TYPES is assigned just above the def in the same emitted block. + for prior in tree.body: + if ( + isinstance(prior, ast.Assign) + and getattr(prior.targets[0], "id", "") == "TRIVIAL_TYPES" + ): + exec(compile(ast.Module([prior], []), "", "exec"), namespace) + exec(compile(ast.Module([node], []), "", "exec"), namespace) + return namespace["skip_trivial_type"] + self.fail("skip_trivial_type was not emitted") + + def test_cffi_objects_are_skipped_and_ordinary_objects_are_not(self): + skip = self._emitted_skip_trivial_type() + + class _FakeFFI: + pass + + # cffi types report _cffi_backend as their defining module; that is the signal, not + # the attribute name the object happens to be bound to. + _FakeFFI.__module__ = "_cffi_backend" + self.assertTrue(skip(_FakeFFI())) + + class _OrdinaryTarget: + pass + + self.assertFalse(skip(_OrdinaryTarget())) + + def test_trivial_types_are_still_skipped(self): + skip = self._emitted_skip_trivial_type() + for value in (1, "s", 1.0, True, b"b", (), [], {}, set(), None): + with self.subTest(value=value): + self.assertTrue(skip(value)) + + def test_an_attribute_merely_NAMED_ffi_is_not_skipped(self): + """Name-based matching would collide with legitimate targets; this is type-based.""" + skip = self._emitted_skip_trivial_type() + + class ffi: # noqa: N801 - deliberately named like the cffi attribute + pass + + self.assertFalse(skip(ffi())) + + class SelfNoiseVocabularyTests(unittest.TestCase): """Emitted COMMENTS must not contain fusil's own crash vocabulary.