Skip to content

Commit 76f1e77

Browse files
authored
Merge pull request #128 from devdanzin/oom-dedup-rename-failure
oom_dedup: never let a dedupe failure abort the session keep/rename
2 parents f0f66de + 2506ce4 commit 76f1e77

4 files changed

Lines changed: 50 additions & 18 deletions

File tree

fusil/python/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -603,9 +603,14 @@ def _oom_keep_policy(self, session):
603603
try:
604604
with open(os.path.join(session_dir, "stdout"), errors="replace") as fh:
605605
text = fh.read()
606-
except OSError:
606+
return self._deduper.decide(text, source_path=os.path.join(session_dir, "source.py"))
607+
except Exception as err:
608+
# Never let a dedupe failure abort the session's keep/rename (deinit): keep the
609+
# crash dir unlabelled rather than lose it. (decide() resolves segvs via gdb, whose
610+
# captured output can be binary -- a past UnicodeDecodeError here left crash dirs
611+
# stuck as session-NNNN instead of renamed.)
612+
self.error("OOM keep-policy failed (%s); keeping crash dir unlabelled" % err)
607613
return True, None
608-
return self._deduper.decide(text, source_path=os.path.join(session_dir, "source.py"))
609614

610615
def exit(self, keep_log: bool = True) -> None:
611616
"""Clean up and exit the fuzzer, printing runtime statistics."""

fusil/python/oom_dedup.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,17 @@ def gdb_crash_site(python_bin, source_path, timeout=120, drop_uid=None, drop_gid
277277
],
278278
capture_output=True,
279279
text=True,
280+
# The re-run prints the fuzzed program's own stdout into gdb's captured output,
281+
# which is frequently binary (e.g. a gzip/zlib vehicle emits 0x1f 0x8b ...). Decode
282+
# tolerantly so it never raises UnicodeDecodeError -- otherwise the exception would
283+
# escape decide() and abort the session's keep/rename in deinit.
284+
errors="replace",
280285
timeout=timeout,
281286
cwd=cwd,
282287
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=0:abort_on_error=0"},
283288
**drop,
284289
).stdout
285-
except (OSError, subprocess.SubprocessError):
290+
except (OSError, ValueError, subprocess.SubprocessError):
286291
return []
287292
return extract_sites_from_bt(out)
288293

@@ -336,22 +341,29 @@ def __init__(
336341
self.kept = collections.Counter() # bug -> directories kept
337342

338343
def _resolve(self, source_path):
339-
"""Return a list of 'func@file:line' chain frames (normalising the resolver)."""
340-
r = (
341-
self._resolver(source_path)
342-
if self._resolver is not None
343-
else (
344-
gdb_crash_site(
345-
self.python_bin,
346-
source_path,
347-
self.gdb_timeout,
348-
drop_uid=self.drop_uid,
349-
drop_gid=self.drop_gid,
344+
"""Return a list of 'func@file:line' chain frames (normalising the resolver).
345+
346+
Any failure in the resolver is swallowed (-> no chain): segv resolution is a
347+
best-effort enrichment, and decide() must never raise into the caller's keep/rename.
348+
"""
349+
try:
350+
r = (
351+
self._resolver(source_path)
352+
if self._resolver is not None
353+
else (
354+
gdb_crash_site(
355+
self.python_bin,
356+
source_path,
357+
self.gdb_timeout,
358+
drop_uid=self.drop_uid,
359+
drop_gid=self.drop_gid,
360+
)
361+
if (self.python_bin and source_path)
362+
else None
350363
)
351-
if (self.python_bin and source_path)
352-
else None
353364
)
354-
)
365+
except Exception:
366+
return []
355367
if not r:
356368
return []
357369
return [r] if isinstance(r, str) else list(r)

tests/python/test_oom_dedup.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,16 @@ def test_unresolvable_segv_kept_as_segv(self):
328328
d = self._deduper(lambda sp: None)
329329
self.assertEqual(d.decide(SEGV, source_path="s"), (True, "oomSEGV"))
330330

331+
def test_resolver_exception_does_not_break_decide(self):
332+
# A resolver that raises (e.g. gdb's captured output is binary -> UnicodeDecodeError)
333+
# must not propagate out of decide(): segv resolution is best-effort, and a raise here
334+
# previously aborted the session's keep/rename in deinit, leaving dirs as session-NNNN.
335+
def boom(sp):
336+
raise UnicodeDecodeError("utf-8", b"\x8b", 0, 1, "invalid start byte")
337+
338+
d = self._deduper(boom)
339+
self.assertEqual(d.decide(SEGV, source_path="s"), (True, "oomSEGV"))
340+
331341
def test_resolved_known_segv_prunes_over_cap(self):
332342
d = self._deduper(
333343
lambda sp: "dictiter_dealloc@Objects/dictobject.c:5532", keep=1, prune=True

tests/python/test_oom_dedup_wiring.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ def _fake_self(self, prune=False, keep=5):
3636
with os.fdopen(fd, "w") as fh:
3737
fh.write(SNAPSHOT)
3838
self.addCleanup(os.unlink, path)
39-
return SimpleNamespace(_deduper=Deduper(path, keep=keep, prune=prune))
39+
# `error` mirrors the Application logger the real Fuzzer has; the keep-policy uses it
40+
# to report a dedupe failure before falling back to (True, None).
41+
return SimpleNamespace(
42+
_deduper=Deduper(path, keep=keep, prune=prune),
43+
error=lambda *a, **k: None,
44+
)
4045

4146
def _session(self, text):
4247
d = tempfile.mkdtemp()

0 commit comments

Comments
 (0)