From 90e30af829116d13e12edc8642eb9a948faeb59c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 26 Oct 2026 19:03:01 -0500 Subject: [PATCH 1/7] PYTHON-6188 Fix fuzz Regex divergence and replay the seed corpus in PR CI --- .evergreen/scripts/run_tests.py | 17 ++++++++++++++--- .evergreen/scripts/setup_tests.py | 5 +++-- fuzz/README.md | 6 +++++- fuzz/fuzz_shared.py | 4 ++++ fuzz/regression/decode__regex_flags_repr | Bin 0 -> 15 bytes 5 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 fuzz/regression/decode__regex_flags_repr diff --git a/.evergreen/scripts/run_tests.py b/.evergreen/scripts/run_tests.py index b0273f1d80..fa168baf10 100644 --- a/.evergreen/scripts/run_tests.py +++ b/.evergreen/scripts/run_tests.py @@ -139,15 +139,26 @@ def handle_fuzz() -> None: targets = ["fuzz_decode", "fuzz_decode_all", "fuzz_is_valid"] if SUB_TEST_NAME == "regression": + # Replay the committed reproducers plus the seed corpus (PYTHON-6188). # Skip dotfiles (pathlib globbing matches them). inputs = sorted( str(p) for p in (ROOT / "fuzz" / "regression").glob("*") if not p.name.startswith(".") ) if not inputs: raise RuntimeError("No fuzz regression inputs found in fuzz/regression/") - # Each reproducer is passed as its own file argument, so libFuzzer - # runs each exactly once; the -atheris_runs budget equals the file - # count so atheris exits cleanly after the last one. + corpus_dir = os.environ.get("FUZZ_CORPUS_DIR", str(ROOT / "fuzz-scratch" / "corpus")) + # Fail fast on a missing or empty corpus; libFuzzer would silently + # skip it. Skip dotfiles so a dotfile-only dir counts as empty. + corpus_files = sorted( + str(p) for p in Path(corpus_dir).glob("*") if not p.name.startswith(".") + ) + if not corpus_files: + raise RuntimeError( + f"No fuzz seed corpus found in {corpus_dir}; was setup_tests.py run?" + ) + inputs.extend(corpus_files) + # Each input runs exactly once without mutating; the -atheris_runs + # budget equals the file count so atheris exits cleanly after the last. runs = len(inputs) for target in targets: run_command( diff --git a/.evergreen/scripts/setup_tests.py b/.evergreen/scripts/setup_tests.py index dbf87d28d5..44d1432c2c 100644 --- a/.evergreen/scripts/setup_tests.py +++ b/.evergreen/scripts/setup_tests.py @@ -530,8 +530,9 @@ def handle_test_env() -> None: else: TEST_ARGS = f"test/performance/async_perf_test.py {TEST_ARGS}" - if test_name == "fuzz" and sub_test_name != "regression": - # Build the seed corpus from the vendored spec data. + if test_name == "fuzz": + # Build the seed corpus from the vendored spec data. Regression runs + # replay it too (PYTHON-6188). scratch_dir = ROOT / "fuzz-scratch" shutil.rmtree(scratch_dir, ignore_errors=True) corpus_dir = scratch_dir / "corpus" diff --git a/fuzz/README.md b/fuzz/README.md index ab05d6096f..4a507b5cf9 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -47,6 +47,10 @@ just setup-tests fuzz regression just run-tests ``` +On every PR, the `fuzz-regression` task also replays the full generated seed +corpus (PYTHON-6188): the committed reproducers plus every corpus seed exactly +once, deterministically — no mutations, no time budget. + Or without the justfile: ```bash @@ -114,5 +118,5 @@ fuzz_decode.py Fuzz target: bson.decode() fuzz_decode_all.py Fuzz target: bson.decode_all() fuzz_is_valid.py Fuzz target: bson.is_valid() fuzz_shared.py Code shared by the fuzz targets -regression/ Known bug reproducers (no corpus setup required) +regression/ Known bug reproducers; replayed with the seed corpus on PRs ``` diff --git a/fuzz/fuzz_shared.py b/fuzz/fuzz_shared.py index 30bc47e373..255cc9a004 100755 --- a/fuzz/fuzz_shared.py +++ b/fuzz/fuzz_shared.py @@ -57,6 +57,10 @@ def normalize(obj: object) -> object: return [normalize(v) for v in obj] if isinstance(obj, float) and math.isnan(obj): return "nan" + if type(obj).__name__ == "Regex": + # int() canonicalizes the C extension's plain int flags and pure + # Python's re.RegexFlag. + return f"Regex:{obj.pattern!r}:{int(obj.flags)}" return f"{type(obj).__name__}:{obj!r}" diff --git a/fuzz/regression/decode__regex_flags_repr b/fuzz/regression/decode__regex_flags_repr new file mode 100644 index 0000000000000000000000000000000000000000..cfb1d7a91795769c60670664c6acb1d56c4df180 GIT binary patch literal 15 Wcmd;QU|`@*WJpX(X2{HCU;qFRCIWZ> literal 0 HcmV?d00001 From 570b30d1c02385afdf4897d4181fccb1fa3989d2 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 27 Oct 2026 08:32:15 -0500 Subject: [PATCH 2/7] Update .evergreen/scripts/run_tests.py Co-authored-by: Noah Stapp --- .evergreen/scripts/run_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.evergreen/scripts/run_tests.py b/.evergreen/scripts/run_tests.py index fa168baf10..f0628689b9 100644 --- a/.evergreen/scripts/run_tests.py +++ b/.evergreen/scripts/run_tests.py @@ -139,7 +139,7 @@ def handle_fuzz() -> None: targets = ["fuzz_decode", "fuzz_decode_all", "fuzz_is_valid"] if SUB_TEST_NAME == "regression": - # Replay the committed reproducers plus the seed corpus (PYTHON-6188). + # Run the committed reproducers plus the seed corpus. # Skip dotfiles (pathlib globbing matches them). inputs = sorted( str(p) for p in (ROOT / "fuzz" / "regression").glob("*") if not p.name.startswith(".") From 794f3d718779a2ca3e16a80449bdfdb5b92ef544 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 27 Oct 2026 08:32:24 -0500 Subject: [PATCH 3/7] Update .evergreen/scripts/run_tests.py Co-authored-by: Noah Stapp --- .evergreen/scripts/run_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.evergreen/scripts/run_tests.py b/.evergreen/scripts/run_tests.py index f0628689b9..896e4fbfdc 100644 --- a/.evergreen/scripts/run_tests.py +++ b/.evergreen/scripts/run_tests.py @@ -148,7 +148,7 @@ def handle_fuzz() -> None: raise RuntimeError("No fuzz regression inputs found in fuzz/regression/") corpus_dir = os.environ.get("FUZZ_CORPUS_DIR", str(ROOT / "fuzz-scratch" / "corpus")) # Fail fast on a missing or empty corpus; libFuzzer would silently - # skip it. Skip dotfiles so a dotfile-only dir counts as empty. + # skip it. corpus_files = sorted( str(p) for p in Path(corpus_dir).glob("*") if not p.name.startswith(".") ) From b1d1ecde250bf7bc88666cff5ecb25f3984a5e15 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 27 Oct 2026 08:32:33 -0500 Subject: [PATCH 4/7] Update .evergreen/scripts/setup_tests.py Co-authored-by: Noah Stapp --- .evergreen/scripts/setup_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.evergreen/scripts/setup_tests.py b/.evergreen/scripts/setup_tests.py index 44d1432c2c..e21a346480 100644 --- a/.evergreen/scripts/setup_tests.py +++ b/.evergreen/scripts/setup_tests.py @@ -532,7 +532,7 @@ def handle_test_env() -> None: if test_name == "fuzz": # Build the seed corpus from the vendored spec data. Regression runs - # replay it too (PYTHON-6188). + # run it too. scratch_dir = ROOT / "fuzz-scratch" shutil.rmtree(scratch_dir, ignore_errors=True) corpus_dir = scratch_dir / "corpus" From 99b0c34f87ae94c88d7d31c274ecdc0414958438 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 27 Oct 2026 08:32:44 -0500 Subject: [PATCH 5/7] Update fuzz/README.md Co-authored-by: Noah Stapp --- fuzz/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fuzz/README.md b/fuzz/README.md index 4a507b5cf9..42d57d8316 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -47,9 +47,8 @@ just setup-tests fuzz regression just run-tests ``` -On every PR, the `fuzz-regression` task also replays the full generated seed -corpus (PYTHON-6188): the committed reproducers plus every corpus seed exactly -once, deterministically — no mutations, no time budget. +On every PR, the `fuzz-regression` task runs the committed reproducers and the full generated seed +corpus. Or without the justfile: From 01c13f1133c45310ae744f8939d7f6cc37b0633c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 27 Oct 2026 08:32:54 -0500 Subject: [PATCH 6/7] Update fuzz/README.md Co-authored-by: Noah Stapp --- fuzz/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz/README.md b/fuzz/README.md index 42d57d8316..8069c0586c 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -117,5 +117,5 @@ fuzz_decode.py Fuzz target: bson.decode() fuzz_decode_all.py Fuzz target: bson.decode_all() fuzz_is_valid.py Fuzz target: bson.is_valid() fuzz_shared.py Code shared by the fuzz targets -regression/ Known bug reproducers; replayed with the seed corpus on PRs +regression/ Known bug reproducers; run with the seed corpus on PRs ``` From 9e8fecba3547a656c50e9a69d2564141e616eaf5 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 27 Oct 2026 08:42:51 -0500 Subject: [PATCH 7/7] address review --- .evergreen/scripts/run_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.evergreen/scripts/run_tests.py b/.evergreen/scripts/run_tests.py index 896e4fbfdc..9c3cdf13f5 100644 --- a/.evergreen/scripts/run_tests.py +++ b/.evergreen/scripts/run_tests.py @@ -157,7 +157,7 @@ def handle_fuzz() -> None: f"No fuzz seed corpus found in {corpus_dir}; was setup_tests.py run?" ) inputs.extend(corpus_files) - # Each input runs exactly once without mutating; the -atheris_runs + # Each input runs exactly once without random fuzzing; the -atheris_runs # budget equals the file count so atheris exits cleanly after the last. runs = len(inputs) for target in targets: