From 44c56f8a309324b3436ae7abbc07896763631094 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:21:54 -0300 Subject: [PATCH] The skip-trivial guard printed "Skipping" and then fuzzed anyway `skip_trivial_type` has correctly identified cffi objects since #268 -- a PyPy stdlib module hands out a live `FFI` as an attribute (`_curses_panel.ffi`, `_ssl.ffi`, `_lzma.ffi`, ... at least ten of them), and `type(ffi).__module__` really is `_cffi_backend`, so the check returns True. It just never mattered: two of the three emission sites put nothing in the guard's body except the "Skipping deep diving" print, and emitted the method fuzzing after the `if` statement rather than inside an `else`. The generated script therefore reported the skip and then called the object anyway: if skip_trivial_type(fuzz_target_module.ffi): print('Skipping deep diving on ...') # the entire body # General method fuzzing for fuzz_target_module.ffi try: res_obj0m1 = callMethod("obj0m1", fuzz_target_module.ffi, "typeof", ... `ffi.memmove()` and its siblings take their arguments as raw pointers and fault by contract on whatever they are handed -- exactly as ctypes does on CPython -- so a crash there is never a target defect. Five consecutive PyPy fleets kept dirs from it; the most recent is a `_curses_panel` SIGSEGV in fleet_10, whose last line before the fault is `[obj12m14] FFI.memmove()`. Both broken sites now open a real `else:`. The third site already did, which is why it never contributed. The plugin path is untouched: a dispatcher that opens its own trailing `else` still owns the fallback, and the new one is opened only when no dispatcher did. The golden snapshot moves accordingly -- the method-fuzzing block is one level deeper, inside the else. Asserting on the emitted text would not have caught this, because the print was always there. The new tests assert on the parsed structure instead: every `if skip_trivial_type(x):` must carry the work in its `orelse`, the taken branch must only report, and the emitted function must skip a `_cffi_backend`-typed instance while leaving an ordinary object alone. They fail on the pre-fix generator. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01WPBSmN87d2BqUnrDbojUT1 --- fusil/python/write_python_code.py | 21 ++++++++ tests/python/golden/fakemod_seed1234.py | 70 +++++++++++++------------ tests/python/test_golden_output.py | 59 +++++++++++++++++++++ 3 files changed, 117 insertions(+), 33 deletions(-) diff --git a/fusil/python/write_python_code.py b/fusil/python/write_python_code.py index 66d01bb..22c7c3e 100644 --- a/fusil/python/write_python_code.py +++ b/fusil/python/write_python_code.py @@ -2320,6 +2320,14 @@ def _dispatch_fuzz_on_instance( ) if level is not None: L_else_generic = level + # Without a plugin dispatcher no `else:` was ever opened, so the generic fallback + # ran even for an object the skip-trivial guard had just reported as skipped -- the + # guard printed and nothing else. Open the else ourselves in that case so the + # fallback is genuinely guarded; with a dispatcher, its own else still owns it. + if L_else_generic is None: + self.write(0, "else:") + L_else_generic = self.addLevel(1) + self.write(0, "pass") try: self.write(0, "try:") self.write_print_to_stderr( @@ -2443,12 +2451,22 @@ def _fuzz_methods_on_object_or_specific_types( f'f"--- Fuzzing instance: {target_obj_expr_str} (type hint: {target_obj_class_name}, prefix: {current_prefix}) ---"', ) + # The guard has to OWN the method fuzzing below: for four fleets it emitted only the + # "Skipping" print and then fuzzed the object anyway, so a cffi FFI object reached as a + # module attribute (PyPy's _curses_panel.ffi, _ssl.ffi, ...) still had ffi.memmove() and + # friends called on it -- raw-pointer entry points that fault by contract on whatever + # arguments they are handed. Hence the else: the print alone was never a skip. self.write(0, f"if skip_trivial_type({target_obj_expr_str}):") with self.indented(): self.write_print_to_stderr( 0, f"f'Skipping deep diving on {target_obj_expr_str} {{type({target_obj_expr_str})}}'", ) + self.write(0, "else:") + skip_guard_level = self.addLevel(1) + # Keeps the block syntactically valid when the object turns out to have no methods to + # fuzz (methods_dict empty), which would otherwise leave `else:` with only a comment. + self.write(0, "pass") # General method fuzzing for other types self.write(0, f"# General method fuzzing for {target_obj_expr_str}") @@ -2481,6 +2499,9 @@ def _fuzz_methods_on_object_or_specific_types( is_method_call=True, generation_depth=0, ) + # Close the `else:` opened by the skip-trivial guard above; the summary print belongs + # outside it so a skipped object still reports. + self.restoreLevel(skip_guard_level) self.write_print_to_stderr( 0, f'f"--- Finished fuzzing instance: {target_obj_expr_str} ---"' ) diff --git a/tests/python/golden/fakemod_seed1234.py b/tests/python/golden/fakemod_seed1234.py index a1f4520..6f60400 100644 --- a/tests/python/golden/fakemod_seed1234.py +++ b/tests/python/golden/fakemod_seed1234.py @@ -1456,50 +1456,54 @@ def callFunc(prefix, func_name_str, *arguments, verbose=True): except Exception as e: print(f"--- (Depth 0) Error calling repr() prefix: c1_widget_ops) ---", file=stderr) if instance_c1_widget is not None: - if skip_trivial_type(instance_c1_widget): - print(f'Skipping deep diving on instance_c1_widget {type(instance_c1_widget)}', file=stderr) - try: - print(f'Instance {instance_c1_widget!r} (actual type {type(instance_c1_widget).__name__}) has no specific fuzzer type, doing generic calls.', file=stderr) - except Exception as e: - print(f'Error printing instance repr() { e } (actual type {type(instance_c1_widget).__name__}) has no specific fuzzer type, doing generic calls.', file=stderr) if skip_trivial_type(instance_c1_widget): print(f'Skipping deep diving on instance_c1_widget {type(instance_c1_widget)}', file=stderr) else: - print(f'Instance instance_c1_widget (type {type(instance_c1_widget).__name__}) has no specific fuzzer, doing generic calls.', file=stderr) - c1_widget_ops_generic_methods = [] + pass try: - for c1_widget_ops_generic_attr_name in dir(instance_c1_widget): - if c1_widget_ops_generic_attr_name.startswith('_'): continue - try: - c1_widget_ops_generic_attr_val = getattr(instance_c1_widget, c1_widget_ops_generic_attr_name) - if callable(c1_widget_ops_generic_attr_val) and c1_widget_ops_generic_attr_name not in _FUSIL_METHOD_BLACKLIST: c1_widget_ops_generic_methods.append((c1_widget_ops_generic_attr_name, c1_widget_ops_generic_attr_val)) - except Exception: pass - except Exception: c1_widget_ops_generic_methods = [] # Failed to get methods - if c1_widget_ops_generic_methods: - print(f'Found {len(c1_widget_ops_generic_methods)} callable methods for generic fuzzing of instance_c1_widget', file=stderr) - for _i_c1_widget_ops_generic in range(min(len(c1_widget_ops_generic_methods), 2)): - c1_widget_ops_generic_method_name_to_call, c1_widget_ops_generic_method_obj_to_call = choice(c1_widget_ops_generic_methods) - # Conceptual call to generic method fuzzer - if c1_widget_ops_generic_method_name_to_call not in _FUSIL_METHOD_BLACKLIST: callMethod(f'c1_widget_ops_generic_gen{_i_c1_widget_ops_generic}', instance_c1_widget, c1_widget_ops_generic_method_name_to_call) + print(f'Instance {instance_c1_widget!r} (actual type {type(instance_c1_widget).__name__}) has no specific fuzzer type, doing generic calls.', file=stderr) + except Exception as e: + print(f'Error printing instance repr() { e } (actual type {type(instance_c1_widget).__name__}) has no specific fuzzer type, doing generic calls.', file=stderr) + if skip_trivial_type(instance_c1_widget): + print(f'Skipping deep diving on instance_c1_widget {type(instance_c1_widget)}', file=stderr) + else: + print(f'Instance instance_c1_widget (type {type(instance_c1_widget).__name__}) has no specific fuzzer, doing generic calls.', file=stderr) + c1_widget_ops_generic_methods = [] + try: + for c1_widget_ops_generic_attr_name in dir(instance_c1_widget): + if c1_widget_ops_generic_attr_name.startswith('_'): continue + try: + c1_widget_ops_generic_attr_val = getattr(instance_c1_widget, c1_widget_ops_generic_attr_name) + if callable(c1_widget_ops_generic_attr_val) and c1_widget_ops_generic_attr_name not in _FUSIL_METHOD_BLACKLIST: c1_widget_ops_generic_methods.append((c1_widget_ops_generic_attr_name, c1_widget_ops_generic_attr_val)) + except Exception: pass + except Exception: c1_widget_ops_generic_methods = [] # Failed to get methods + if c1_widget_ops_generic_methods: + print(f'Found {len(c1_widget_ops_generic_methods)} callable methods for generic fuzzing of instance_c1_widget', file=stderr) + for _i_c1_widget_ops_generic in range(min(len(c1_widget_ops_generic_methods), 2)): + c1_widget_ops_generic_method_name_to_call, c1_widget_ops_generic_method_obj_to_call = choice(c1_widget_ops_generic_methods) + # Conceptual call to generic method fuzzer + if c1_widget_ops_generic_method_name_to_call not in _FUSIL_METHOD_BLACKLIST: callMethod(f'c1_widget_ops_generic_gen{_i_c1_widget_ops_generic}', instance_c1_widget, c1_widget_ops_generic_method_name_to_call) if instance_c1_widget is not None and instance_c1_widget is not SENTINEL_VALUE: print(f"--- Fuzzing instance: instance_c1_widget (type hint: Widget, prefix: c1m) ---", file=stderr) if skip_trivial_type(instance_c1_widget): print(f'Skipping deep diving on instance_c1_widget {type(instance_c1_widget)}', file=stderr) - # General method fuzzing for instance_c1_widget - try: - res_c1m1 = callMethod("c1m1", instance_c1_widget, "method_two", - list[weird_classes['weird_OrderedDict']] | weird_classes['weird_set'] | big_union, - verbose=True) - except Exception as _argexc_c1m1: - print("[c1m1] call skipped (argument build failed):", repr(_argexc_c1m1), file=stderr) + else: + pass + # General method fuzzing for instance_c1_widget + try: + res_c1m1 = callMethod("c1m1", instance_c1_widget, "method_two", + list[weird_classes['weird_OrderedDict']] | weird_classes['weird_set'] | big_union, + verbose=True) + except Exception as _argexc_c1m1: + print("[c1m1] call skipped (argument build failed):", repr(_argexc_c1m1), file=stderr) - try: - res_c1m2 = callMethod("c1m2", instance_c1_widget, "method_one", - verbose=True) - except Exception as _argexc_c1m2: - print("[c1m2] call skipped (argument build failed):", repr(_argexc_c1m2), file=stderr) + try: + res_c1m2 = callMethod("c1m2", instance_c1_widget, "method_one", + verbose=True) + except Exception as _argexc_c1m2: + print("[c1m2] call skipped (argument build failed):", repr(_argexc_c1m2), file=stderr) print(f"--- Finished fuzzing instance: instance_c1_widget ---", file=stderr) diff --git a/tests/python/test_golden_output.py b/tests/python/test_golden_output.py index b1a26b9..ec36c92 100644 --- a/tests/python/test_golden_output.py +++ b/tests/python/test_golden_output.py @@ -236,6 +236,65 @@ class ffi: # noqa: N801 - deliberately named like the cffi attribute self.assertFalse(skip(ffi())) +class SkipTrivialTypeGuardOwnsTheBlockTests(unittest.TestCase): + """The skip-trivial guard must OWN the fuzzing it claims to skip. + + For four consecutive PyPy fleets the guard emitted only its "Skipping deep diving" print + and then fuzzed the object anyway. That is how ``_curses_panel.ffi`` -- a cffi ``FFI`` + object handed out as a module attribute, whose type IS defined in ``_cffi_backend`` and + which ``skip_trivial_type`` therefore correctly identified -- still had ``ffi.memmove()`` + called on it and took the process down: a raw-pointer entry point that faults by contract + on whatever arguments it is given, so never a target defect. + + Asserting on the emitted text is not enough (the print was always there); assert on the + parsed structure instead -- every ``if skip_trivial_type(x):`` must carry the real work in + its ``orelse``, never after the statement. + """ + + def _guards(self, tree): + found = [] + for node in ast.walk(tree): + if not isinstance(node, ast.If): + continue + test = node.test + if ( + isinstance(test, ast.Call) + and isinstance(test.func, ast.Name) + and test.func.id == "skip_trivial_type" + ): + found.append(node) + return found + + def test_every_guard_has_an_else_branch(self): + guards = self._guards(ast.parse(generate())) + self.assertTrue(guards, "no skip_trivial_type guard emitted at all") + for node in guards: + self.assertTrue( + node.orelse, + "skip_trivial_type guard at line %d has no else: it prints 'Skipping' and " + "then falls through to the fuzzing it claims to skip" % node.lineno, + ) + + def test_guard_body_only_reports_and_never_calls(self): + """The taken branch must not itself call the object -- it is the skip path.""" + for node in self._guards(ast.parse(generate())): + for sub in ast.walk(ast.Module(body=node.body, type_ignores=[])): + if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Name): + self.assertIn( + sub.func.id, + {"print", "type", "repr"}, + "the skip branch calls %s(); it must only report" % sub.func.id, + ) + + def test_emitted_guard_skips_a_cffi_typed_object(self): + """Exec the emitted function against an object typed like PyPy's ``ffi``.""" + skip = SkipTrivialTypeTests()._emitted_skip_trivial_type() + cffi_like = type("FFI", (), {}) + cffi_like.__module__ = "_cffi_backend" + self.assertTrue(skip(cffi_like()), "a _cffi_backend-typed instance must be skipped") + self.assertFalse(skip(object()), "an ordinary object must NOT be skipped") + + class SelfNoiseVocabularyTests(unittest.TestCase): """Emitted COMMENTS must not contain fusil's own crash vocabulary.