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
+ # ${{ '' } is a hack to nest jobs under the same sidebar category.
+ name: CIFuzz${{ '' }} # zizmor: ignore[obfuscation]
needs: build-context
- if: needs.build-context.outputs.run-ci-fuzz == 'true'
+ if: >-
+ needs.build-context.outputs.run-ci-fuzz == 'true'
+ || needs.build-context.outputs.run-ci-fuzz-stdlib == '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@v6
- 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
+ sanitizer:
+ - address
+ - undefined
+ - memory
+ oss-fuzz-project-name:
+ - cpython3
+ - python3-libraries
+ exclude:
+ # Note that the 'no-exclude' sentinel below is to prevent
+ # an empty string value from excluding all jobs and causing
+ # GHA to create a 'default' matrix entry with all empty values.
+ - oss-fuzz-project-name: >-
+ ${{
+ needs.build-context.outputs.run-ci-fuzz == 'true'
+ && 'no-exclude'
+ || 'cpython3'
+ }}
+ - oss-fuzz-project-name: >-
+ ${{
+ needs.build-context.outputs.run-ci-fuzz-stdlib == 'true'
+ && 'no-exclude'
+ || 'python3-libraries'
+ }}
+ uses: ./.github/workflows/reusable-cifuzz.yml
+ with:
+ oss-fuzz-project-name: ${{ matrix.oss-fuzz-project-name }}
+ sanitizer: ${{ matrix.sanitizer }}
all-required-green: # This job does nothing and is only used for the branch protection
name: All required checks pass
|| ''
}}
${{ !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)
+ && !fromJSON(needs.build-context.outputs.run-ci-fuzz-stdlib)
+ && 'cifuzz,' ||
+ ''
+ }}
${{ !fromJSON(needs.build-context.outputs.run-macos) && 'build-macos,' || '' }}
${{
!fromJSON(needs.build-context.outputs.run-ubuntu)
--- /dev/null
+# 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
+ sanitizer:
+ description: OSS-Fuzz sanitizer
+ required: true
+ type: string
+
+jobs:
+ cifuzz:
+ name: ${{ inputs.oss-fuzz-project-name }} (${{ inputs.sanitizer }})
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ steps:
+ - name: Build fuzzers (${{ inputs.sanitizer }})
+ id: build
+ uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
+ with:
+ oss-fuzz-project-name: ${{ inputs.oss-fuzz-project-name }}
+ sanitizer: ${{ inputs.sanitizer }}
+ - name: Run fuzzers (${{ inputs.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: ${{ inputs.sanitizer }}
+ - name: Upload crash
+ if: failure() && steps.build.outcome == 'success'
+ uses: actions/upload-artifact@v6
+ with:
+ name: ${{ inputs.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
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-stdlib:
+ description: Whether to run the CIFuzz job for 'python3-libraries' fuzzer
+ value: ${{ jobs.compute-changes.outputs.run-ci-fuzz-stdlib }} # bool
run-docs:
description: Whether to build the docs
value: ${{ jobs.compute-changes.outputs.run-docs }} # bool
outputs:
run-android: ${{ steps.changes.outputs.run-android }}
run-ci-fuzz: ${{ steps.changes.outputs.run-ci-fuzz }}
+ 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 }}
import os
import subprocess
-from dataclasses import dataclass
+from dataclasses import dataclass, fields
from pathlib import Path
TYPE_CHECKING = False
MACOS_DIRS = frozenset({"Mac"})
WASI_DIRS = frozenset({Path("Tools", "wasm")})
+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"),
+ # 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/"),
+ Path("Modules/pyexpat.c"),
+ # zipfile
+ Path("Lib/zipfile/"),
+})
+
@dataclass(kw_only=True, slots=True)
class Outputs:
run_android: bool = False
run_ci_fuzz: bool = False
+ run_ci_fuzz_stdlib: bool = False
run_docs: bool = False
run_ios: bool = False
run_macos: bool = False
else:
print("Branch too old for CIFuzz tests; or no C files were changed")
+ if outputs.run_ci_fuzz_stdlib:
+ print("Run CIFuzz tests for stdlib")
+ else:
+ print("Branch too old for CIFuzz tests; or no stdlib files were changed")
+
if outputs.run_docs:
print("Build documentation")
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_stdlib = False
run_docs = False
run_windows_tests = False
run_windows_msi = False
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 = True
+ if file.name in ("build.yml", "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
("Modules", "_xxtestfuzz"),
}:
run_ci_fuzz = 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:
return Outputs(
run_android=run_android,
run_ci_fuzz=run_ci_fuzz,
+ run_ci_fuzz_stdlib=run_ci_fuzz_stdlib,
run_docs=run_docs,
run_ios=run_ios,
run_macos=run_macos,
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-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: