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
21 changes: 21 additions & 0 deletions fusil/python/write_python_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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} ---"'
)
Expand Down
70 changes: 37 additions & 33 deletions tests/python/golden/fakemod_seed1234.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
59 changes: 59 additions & 0 deletions tests/python/test_golden_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading