From 2623d6047a21089896b6662d611bb4e1b83c54b1 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Mon, 12 Jan 2026 13:38:17 -0600 Subject: [PATCH 1/3] Run 'python3-libraries' fuzzer in CI using CIFuzz --- .github/workflows/build.yml | 40 +++++++++++ ...-01-12-13-37-14.gh-issue-143572.WKV_Jk.rst | 2 + Tools/build/compute-changes.py | 66 ++++++++++++++++++- 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Tools-Demos/2026-01-12-13-37-14.gh-issue-143572.WKV_Jk.rst diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6769cdd4531a0c..608be81fd89c21 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -676,6 +676,45 @@ jobs: sarif_file: cifuzz-sarif/results.sarif checkout_path: cifuzz-sarif + cifuzz-libraries: + name: CIFuzz-libraries + runs-on: ubuntu-latest + timeout-minutes: 60 + needs: build-context + if: needs.build-context.outputs.run-ci-fuzz-libraries == 'true' + permissions: + security-events: write + strategy: + fail-fast: false + matrix: + sanitizer: [address, undefined, memory] + steps: + - name: Build fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: python3-libraries + sanitizer: ${{ matrix.sanitizer }} + - name: Run fuzzers (${{ matrix.sanitizer }}) + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + fuzz-seconds: 600 + oss-fuzz-project-name: python3-libraries + output-sarif: true + sanitizer: ${{ matrix.sanitizer }} + - name: Upload crash + if: failure() && steps.build.outcome == 'success' + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.sanitizer }}-artifacts + path: ./out/artifacts + - name: Upload SARIF + if: always() && steps.build.outcome == 'success' + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: cifuzz-sarif/results.sarif + checkout_path: cifuzz-sarif + all-required-green: # This job does nothing and is only used for the branch protection name: All required checks pass runs-on: ubuntu-latest @@ -698,6 +737,7 @@ jobs: - build-san - cross-build-linux - cifuzz + - cifuzz-libraries if: always() steps: diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-01-12-13-37-14.gh-issue-143572.WKV_Jk.rst b/Misc/NEWS.d/next/Tools-Demos/2026-01-12-13-37-14.gh-issue-143572.WKV_Jk.rst new file mode 100644 index 00000000000000..8ea707a06ec431 --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-01-12-13-37-14.gh-issue-143572.WKV_Jk.rst @@ -0,0 +1,2 @@ +Run the 'python3-libraries' fuzzer using CIFuzz. Automatically detect when +modules are changed. diff --git a/Tools/build/compute-changes.py b/Tools/build/compute-changes.py index 524d3066fbffa7..74653beda2beef 100644 --- a/Tools/build/compute-changes.py +++ b/Tools/build/compute-changes.py @@ -50,11 +50,57 @@ MACOS_DIRS = frozenset({"Mac"}) WASI_DIRS = frozenset({Path("Tools", "wasm")}) +LIBRARY_FUZZER_PATHS = frozenset({ + # All C/CPP fuzzers. + Path("configure"), + # ast + Path("Lib/ast.py"), + Path("Python/ast.c"), + # configparser + Path("Lib/configparser.py"), + # csv + Path("Lib/csv.py"), + Path("Modules/_csv.c"), + # decode + Path("Lib/encodings/"), + Path("Modules/_codecsmodule.c"), + Path("Modules/cjkcodecs/"), + Path("Modules/unicodedata*"), + # difflib + Path("Lib/difflib.py"), + # email + Path("Lib/email/"), + # html + Path("Lib/html/"), + Path("Lib/_markupbase.py"), + # http.client + Path("Lib/http/client.py"), + # json + Path("Lib/json/"), + Path("Modules/_json.c"), + # plist + Path("Lib/plistlib.py"), + # re + Path("Lib/re/"), + Path("Modules/_sre/"), + # tarfile + Path("Lib/tarfile.py"), + # tomllib + Path("Modules/tomllib/"), + # xml + Path("Lib/xml/"), + Path("Lib/_markupbase.py"), + Path("Modules/expat/"), + # zipfile + Path("Lib/zipfile/"), +}) + @dataclass(kw_only=True, slots=True) class Outputs: run_android: bool = False run_ci_fuzz: bool = False + run_ci_fuzz_libraries: bool = False run_docs: bool = False run_ios: bool = False run_macos: bool = False @@ -94,6 +140,11 @@ def compute_changes() -> None: else: print("Branch too old for CIFuzz tests; or no C files were changed") + if outputs.run_ci_fuzz_libraries: + print("Run CIFuzz tests for libraries") + else: + print("Branch too old for CIFuzz tests; or no library files were changed") + if outputs.run_docs: print("Build documentation") @@ -144,9 +195,18 @@ def get_file_platform(file: Path) -> str | None: return None +def is_fuzzable_library_file(file: Path) -> bool: + return any( + (file.is_relative_to(needs_fuzz) and needs_fuzz.is_dir()) + or (file == needs_fuzz and file.is_file()) + for needs_fuzz in LIBRARY_FUZZER_PATHS + ) + + def process_changed_files(changed_files: Set[Path]) -> Outputs: run_tests = False run_ci_fuzz = False + run_ci_fuzz_libraries = False run_docs = False run_windows_tests = False run_windows_msi = False @@ -161,7 +221,7 @@ def process_changed_files(changed_files: Set[Path]) -> Outputs: if file.parent == GITHUB_WORKFLOWS_PATH: if file.name == "build.yml": - run_tests = run_ci_fuzz = True + run_tests = run_ci_fuzz = run_ci_fuzz_libraries = True has_platform_specific_change = False if file.name == "reusable-docs.yml": run_docs = True @@ -196,6 +256,8 @@ def process_changed_files(changed_files: Set[Path]) -> Outputs: ("Modules", "_xxtestfuzz"), }: run_ci_fuzz = True + if not run_ci_fuzz_libraries and is_fuzzable_library_file(file): + run_ci_fuzz_libraries = True # Check for changed documentation-related files if doc_file: @@ -229,6 +291,7 @@ def process_changed_files(changed_files: Set[Path]) -> Outputs: return Outputs( run_android=run_android, run_ci_fuzz=run_ci_fuzz, + run_ci_fuzz_libraries=run_ci_fuzz_libraries, run_docs=run_docs, run_ios=run_ios, run_macos=run_macos, @@ -265,6 +328,7 @@ def write_github_output(outputs: Outputs) -> None: with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f: f.write(f"run-android={bool_lower(outputs.run_android)}\n") f.write(f"run-ci-fuzz={bool_lower(outputs.run_ci_fuzz)}\n") + f.write(f"run-ci-fuzz-libraries={bool_lower(outputs.run_ci_fuzz_libraries)}\n") f.write(f"run-docs={bool_lower(outputs.run_docs)}\n") f.write(f"run-ios={bool_lower(outputs.run_ios)}\n") f.write(f"run-macos={bool_lower(outputs.run_macos)}\n") From 88a5b0afcb567fe1c76097ba31bcfc0d0efab451 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Mon, 12 Jan 2026 13:50:57 -0600 Subject: [PATCH 2/3] Add outputs to compute-changes job --- .github/workflows/reusable-context.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-context.yml b/.github/workflows/reusable-context.yml index ce5562f2d51fbb..02ff2a93fda67c 100644 --- a/.github/workflows/reusable-context.yml +++ b/.github/workflows/reusable-context.yml @@ -21,7 +21,10 @@ on: # yamllint disable-line rule:truthy description: Whether to run the Android tests value: ${{ jobs.compute-changes.outputs.run-android }} # bool run-ci-fuzz: - description: Whether to run the CIFuzz job + description: Whether to run the CIFuzz job for 'cpython' fuzzer + value: ${{ jobs.compute-changes.outputs.run-ci-fuzz }} # bool + run-ci-fuzz-libraries: + description: Whether to run the CIFuzz job for 'python3-libraries' fuzzer value: ${{ jobs.compute-changes.outputs.run-ci-fuzz }} # bool run-docs: description: Whether to build the docs @@ -56,6 +59,7 @@ jobs: outputs: run-android: ${{ steps.changes.outputs.run-android }} run-ci-fuzz: ${{ steps.changes.outputs.run-ci-fuzz }} + run-ci-fuzz-libraries: ${{ steps.changes.outputs.run-ci-fuzz-libraries }} run-docs: ${{ steps.changes.outputs.run-docs }} run-ios: ${{ steps.changes.outputs.run-ios }} run-macos: ${{ steps.changes.outputs.run-macos }} From 55ff6a46e890f1cf1115f05dd75c268dfaf61707 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Mon, 12 Jan 2026 16:14:04 -0600 Subject: [PATCH 3/3] Move to reusable workflow approach --- .github/workflows/build.yml | 85 +++---------------- .github/workflows/reusable-cifuzz.yml | 50 +++++++++++ .github/workflows/reusable-context.yml | 4 +- ...-01-12-13-37-14.gh-issue-143572.WKV_Jk.rst | 2 - Tools/build/compute-changes.py | 34 +++----- 5 files changed, 76 insertions(+), 99 deletions(-) create mode 100644 .github/workflows/reusable-cifuzz.yml delete mode 100644 Misc/NEWS.d/next/Tools-Demos/2026-01-12-13-37-14.gh-issue-143572.WKV_Jk.rst diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 608be81fd89c21..f8a42f64204c7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -636,84 +636,18 @@ jobs: run: | "$BUILD_DIR/cross-python/bin/python3" -m test test_sysconfig test_site test_embed - # CIFuzz job based on https://google.github.io/oss-fuzz/getting-started/continuous-integration/ cifuzz: - name: CIFuzz - runs-on: ubuntu-latest - timeout-minutes: 60 needs: build-context if: needs.build-context.outputs.run-ci-fuzz == 'true' - permissions: - security-events: write - strategy: - fail-fast: false - matrix: - sanitizer: [address, undefined, memory] - steps: - - name: Build fuzzers (${{ matrix.sanitizer }}) - id: build - uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master - with: - oss-fuzz-project-name: cpython3 - sanitizer: ${{ matrix.sanitizer }} - - name: Run fuzzers (${{ matrix.sanitizer }}) - uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master - with: - fuzz-seconds: 600 - oss-fuzz-project-name: cpython3 - output-sarif: true - sanitizer: ${{ matrix.sanitizer }} - - name: Upload crash - if: failure() && steps.build.outcome == 'success' - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.sanitizer }}-artifacts - path: ./out/artifacts - - name: Upload SARIF - if: always() && steps.build.outcome == 'success' - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: cifuzz-sarif/results.sarif - checkout_path: cifuzz-sarif - - cifuzz-libraries: - name: CIFuzz-libraries - runs-on: ubuntu-latest - timeout-minutes: 60 + uses: ./.github/workflows/reusable-cifuzz.yml + with: + oss-fuzz-project-name: cpython3 + cifuzz-stdlib: needs: build-context - if: needs.build-context.outputs.run-ci-fuzz-libraries == 'true' - permissions: - security-events: write - strategy: - fail-fast: false - matrix: - sanitizer: [address, undefined, memory] - steps: - - name: Build fuzzers (${{ matrix.sanitizer }}) - id: build - uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master - with: - oss-fuzz-project-name: python3-libraries - sanitizer: ${{ matrix.sanitizer }} - - name: Run fuzzers (${{ matrix.sanitizer }}) - uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master - with: - fuzz-seconds: 600 - oss-fuzz-project-name: python3-libraries - output-sarif: true - sanitizer: ${{ matrix.sanitizer }} - - name: Upload crash - if: failure() && steps.build.outcome == 'success' - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.sanitizer }}-artifacts - path: ./out/artifacts - - name: Upload SARIF - if: always() && steps.build.outcome == 'success' - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: cifuzz-sarif/results.sarif - checkout_path: cifuzz-sarif + if: needs.build-context.outputs.run-ci-fuzz-stdlib == 'true' + uses: ./.github/workflows/reusable-cifuzz.yml + with: + oss-fuzz-project-name: python3-libraries all-required-green: # This job does nothing and is only used for the branch protection name: All required checks pass @@ -737,7 +671,7 @@ jobs: - build-san - cross-build-linux - cifuzz - - cifuzz-libraries + - cifuzz-stdlib if: always() steps: @@ -763,6 +697,7 @@ jobs: }} ${{ !fromJSON(needs.build-context.outputs.run-windows-tests) && 'build-windows,' || '' }} ${{ !fromJSON(needs.build-context.outputs.run-ci-fuzz) && 'cifuzz,' || '' }} + ${{ !fromJSON(needs.build-context.outputs.run-ci-fuzz-stdlib) && 'cifuzz-stdlib,' || '' }} ${{ !fromJSON(needs.build-context.outputs.run-macos) && 'build-macos,' || '' }} ${{ !fromJSON(needs.build-context.outputs.run-ubuntu) diff --git a/.github/workflows/reusable-cifuzz.yml b/.github/workflows/reusable-cifuzz.yml new file mode 100644 index 00000000000000..4124e0c5fb8aaa --- /dev/null +++ b/.github/workflows/reusable-cifuzz.yml @@ -0,0 +1,50 @@ +# CIFuzz job based on https://google.github.io/oss-fuzz/getting-started/continuous-integration/ +name: Reusable CIFuzz + +on: + workflow_call: + inputs: + oss-fuzz-project-name: + description: OSS-Fuzz project name + required: true + type: string + +permissions: + contents: read + security-events: write + +jobs: + cifuzz: + name: CIFuzz + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + sanitizer: [address, undefined, memory] + steps: + - name: Build fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: ${{ inputs.oss-fuzz-project-name }} + sanitizer: ${{ matrix.sanitizer }} + - name: Run fuzzers (${{ matrix.sanitizer }}) + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + fuzz-seconds: 600 + oss-fuzz-project-name: ${{ inputs.oss-fuzz-project-name }} + output-sarif: true + sanitizer: ${{ matrix.sanitizer }} + - name: Upload crash + if: failure() && steps.build.outcome == 'success' + uses: actions/upload-artifact@v6 + with: + name: ${{ matrix.sanitizer }}-artifacts + path: ./out/artifacts + - name: Upload SARIF + if: always() && steps.build.outcome == 'success' + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: cifuzz-sarif/results.sarif + checkout_path: cifuzz-sarif diff --git a/.github/workflows/reusable-context.yml b/.github/workflows/reusable-context.yml index 02ff2a93fda67c..f13f63932435c3 100644 --- a/.github/workflows/reusable-context.yml +++ b/.github/workflows/reusable-context.yml @@ -23,7 +23,7 @@ on: # yamllint disable-line rule:truthy run-ci-fuzz: description: Whether to run the CIFuzz job for 'cpython' fuzzer value: ${{ jobs.compute-changes.outputs.run-ci-fuzz }} # bool - run-ci-fuzz-libraries: + run-ci-fuzz-stdlib: description: Whether to run the CIFuzz job for 'python3-libraries' fuzzer value: ${{ jobs.compute-changes.outputs.run-ci-fuzz }} # bool run-docs: @@ -59,7 +59,7 @@ jobs: outputs: run-android: ${{ steps.changes.outputs.run-android }} run-ci-fuzz: ${{ steps.changes.outputs.run-ci-fuzz }} - run-ci-fuzz-libraries: ${{ steps.changes.outputs.run-ci-fuzz-libraries }} + run-ci-fuzz-stdlib: ${{ steps.changes.outputs.run-ci-fuzz-stdlib }} run-docs: ${{ steps.changes.outputs.run-docs }} run-ios: ${{ steps.changes.outputs.run-ios }} run-macos: ${{ steps.changes.outputs.run-macos }} diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-01-12-13-37-14.gh-issue-143572.WKV_Jk.rst b/Misc/NEWS.d/next/Tools-Demos/2026-01-12-13-37-14.gh-issue-143572.WKV_Jk.rst deleted file mode 100644 index 8ea707a06ec431..00000000000000 --- a/Misc/NEWS.d/next/Tools-Demos/2026-01-12-13-37-14.gh-issue-143572.WKV_Jk.rst +++ /dev/null @@ -1,2 +0,0 @@ -Run the 'python3-libraries' fuzzer using CIFuzz. Automatically detect when -modules are changed. diff --git a/Tools/build/compute-changes.py b/Tools/build/compute-changes.py index 74653beda2beef..bff530d4fe22ea 100644 --- a/Tools/build/compute-changes.py +++ b/Tools/build/compute-changes.py @@ -11,7 +11,7 @@ import os import subprocess -from dataclasses import dataclass +from dataclasses import dataclass, fields from pathlib import Path TYPE_CHECKING = False @@ -53,6 +53,7 @@ LIBRARY_FUZZER_PATHS = frozenset({ # All C/CPP fuzzers. Path("configure"), + Path(".github/workflows/reusable-cifuzz.yml"), # ast Path("Lib/ast.py"), Path("Python/ast.c"), @@ -100,7 +101,7 @@ class Outputs: run_android: bool = False run_ci_fuzz: bool = False - run_ci_fuzz_libraries: bool = False + run_ci_fuzz_stdlib: bool = False run_docs: bool = False run_ios: bool = False run_macos: bool = False @@ -140,7 +141,7 @@ def compute_changes() -> None: else: print("Branch too old for CIFuzz tests; or no C files were changed") - if outputs.run_ci_fuzz_libraries: + if outputs.run_ci_fuzz_stdlib: print("Run CIFuzz tests for libraries") else: print("Branch too old for CIFuzz tests; or no library files were changed") @@ -206,7 +207,7 @@ def is_fuzzable_library_file(file: Path) -> bool: def process_changed_files(changed_files: Set[Path]) -> Outputs: run_tests = False run_ci_fuzz = False - run_ci_fuzz_libraries = False + run_ci_fuzz_stdlib = False run_docs = False run_windows_tests = False run_windows_msi = False @@ -220,8 +221,8 @@ def process_changed_files(changed_files: Set[Path]) -> Outputs: doc_file = file.suffix in SUFFIXES_DOCUMENTATION or doc_or_misc if file.parent == GITHUB_WORKFLOWS_PATH: - if file.name == "build.yml": - run_tests = run_ci_fuzz = run_ci_fuzz_libraries = True + if file.name == "build.yml" or file.name == "reusable-cifuzz.yml": + run_tests = run_ci_fuzz = run_ci_fuzz_stdlib = True has_platform_specific_change = False if file.name == "reusable-docs.yml": run_docs = True @@ -256,8 +257,8 @@ def process_changed_files(changed_files: Set[Path]) -> Outputs: ("Modules", "_xxtestfuzz"), }: run_ci_fuzz = True - if not run_ci_fuzz_libraries and is_fuzzable_library_file(file): - run_ci_fuzz_libraries = True + if not run_ci_fuzz_stdlib and is_fuzzable_library_file(file): + run_ci_fuzz_stdlib = True # Check for changed documentation-related files if doc_file: @@ -291,7 +292,7 @@ def process_changed_files(changed_files: Set[Path]) -> Outputs: return Outputs( run_android=run_android, run_ci_fuzz=run_ci_fuzz, - run_ci_fuzz_libraries=run_ci_fuzz_libraries, + run_ci_fuzz_stdlib=run_ci_fuzz_stdlib, run_docs=run_docs, run_ios=run_ios, run_macos=run_macos, @@ -326,17 +327,10 @@ def write_github_output(outputs: Outputs) -> None: return with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f: - f.write(f"run-android={bool_lower(outputs.run_android)}\n") - f.write(f"run-ci-fuzz={bool_lower(outputs.run_ci_fuzz)}\n") - f.write(f"run-ci-fuzz-libraries={bool_lower(outputs.run_ci_fuzz_libraries)}\n") - f.write(f"run-docs={bool_lower(outputs.run_docs)}\n") - f.write(f"run-ios={bool_lower(outputs.run_ios)}\n") - f.write(f"run-macos={bool_lower(outputs.run_macos)}\n") - f.write(f"run-tests={bool_lower(outputs.run_tests)}\n") - f.write(f"run-ubuntu={bool_lower(outputs.run_ubuntu)}\n") - f.write(f"run-wasi={bool_lower(outputs.run_wasi)}\n") - f.write(f"run-windows-msi={bool_lower(outputs.run_windows_msi)}\n") - f.write(f"run-windows-tests={bool_lower(outputs.run_windows_tests)}\n") + for field in fields(outputs): + name = field.name.replace("_", "-") + val = bool_lower(getattr(outputs, field.name)) + f.write(f"{name}={val}\n") def bool_lower(value: bool, /) -> str: