Skip to content

Commit edeebe2

Browse files
sethmlarsonwebknjazhugovk
authored
gh-143572: Run 'python3-libraries' fuzzer in CI using CIFuzz (#143749)
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <578543+webknjaz@users.noreply.github.com> Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
1 parent 19e64af commit edeebe2

File tree

4 files changed

+161
-47
lines changed

4 files changed

+161
-47
lines changed

.github/workflows/build.yml

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -641,45 +641,45 @@ jobs:
641641
run: |
642642
"$BUILD_DIR/cross-python/bin/python3" -m test test_sysconfig test_site test_embed
643643
644-
# CIFuzz job based on https://google.github.io/oss-fuzz/getting-started/continuous-integration/
645644
cifuzz:
646-
name: CIFuzz
647-
runs-on: ubuntu-latest
648-
timeout-minutes: 60
645+
# ${{ '' } is a hack to nest jobs under the same sidebar category.
646+
name: CIFuzz${{ '' }} # zizmor: ignore[obfuscation]
649647
needs: build-context
650-
if: needs.build-context.outputs.run-ci-fuzz == 'true'
648+
if: >-
649+
needs.build-context.outputs.run-ci-fuzz == 'true'
650+
|| needs.build-context.outputs.run-ci-fuzz-stdlib == 'true'
651651
permissions:
652652
security-events: write
653653
strategy:
654654
fail-fast: false
655655
matrix:
656-
sanitizer: [address, undefined, memory]
657-
steps:
658-
- name: Build fuzzers (${{ matrix.sanitizer }})
659-
id: build
660-
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
661-
with:
662-
oss-fuzz-project-name: cpython3
663-
sanitizer: ${{ matrix.sanitizer }}
664-
- name: Run fuzzers (${{ matrix.sanitizer }})
665-
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
666-
with:
667-
fuzz-seconds: 600
668-
oss-fuzz-project-name: cpython3
669-
output-sarif: true
670-
sanitizer: ${{ matrix.sanitizer }}
671-
- name: Upload crash
672-
if: failure() && steps.build.outcome == 'success'
673-
uses: actions/upload-artifact@v6
674-
with:
675-
name: ${{ matrix.sanitizer }}-artifacts
676-
path: ./out/artifacts
677-
- name: Upload SARIF
678-
if: always() && steps.build.outcome == 'success'
679-
uses: github/codeql-action/upload-sarif@v4
680-
with:
681-
sarif_file: cifuzz-sarif/results.sarif
682-
checkout_path: cifuzz-sarif
656+
sanitizer:
657+
- address
658+
- undefined
659+
- memory
660+
oss-fuzz-project-name:
661+
- cpython3
662+
- python3-libraries
663+
exclude:
664+
# Note that the 'no-exclude' sentinel below is to prevent
665+
# an empty string value from excluding all jobs and causing
666+
# GHA to create a 'default' matrix entry with all empty values.
667+
- oss-fuzz-project-name: >-
668+
${{
669+
needs.build-context.outputs.run-ci-fuzz == 'true'
670+
&& 'no-exclude'
671+
|| 'cpython3'
672+
}}
673+
- oss-fuzz-project-name: >-
674+
${{
675+
needs.build-context.outputs.run-ci-fuzz-stdlib == 'true'
676+
&& 'no-exclude'
677+
|| 'python3-libraries'
678+
}}
679+
uses: ./.github/workflows/reusable-cifuzz.yml
680+
with:
681+
oss-fuzz-project-name: ${{ matrix.oss-fuzz-project-name }}
682+
sanitizer: ${{ matrix.sanitizer }}
683683

684684
all-required-green: # This job does nothing and is only used for the branch protection
685685
name: All required checks pass
@@ -734,7 +734,12 @@ jobs:
734734
|| ''
735735
}}
736736
${{ !fromJSON(needs.build-context.outputs.run-windows-tests) && 'build-windows,' || '' }}
737-
${{ !fromJSON(needs.build-context.outputs.run-ci-fuzz) && 'cifuzz,' || '' }}
737+
${{
738+
!fromJSON(needs.build-context.outputs.run-ci-fuzz)
739+
&& !fromJSON(needs.build-context.outputs.run-ci-fuzz-stdlib)
740+
&& 'cifuzz,' ||
741+
''
742+
}}
738743
${{ !fromJSON(needs.build-context.outputs.run-macos) && 'build-macos,' || '' }}
739744
${{
740745
!fromJSON(needs.build-context.outputs.run-ubuntu)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# CIFuzz job based on https://google.github.io/oss-fuzz/getting-started/continuous-integration/
2+
name: Reusable CIFuzz
3+
4+
on:
5+
workflow_call:
6+
inputs:
7+
oss-fuzz-project-name:
8+
description: OSS-Fuzz project name
9+
required: true
10+
type: string
11+
sanitizer:
12+
description: OSS-Fuzz sanitizer
13+
required: true
14+
type: string
15+
16+
jobs:
17+
cifuzz:
18+
name: ${{ inputs.oss-fuzz-project-name }} (${{ inputs.sanitizer }})
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 60
21+
steps:
22+
- name: Build fuzzers (${{ inputs.sanitizer }})
23+
id: build
24+
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
25+
with:
26+
oss-fuzz-project-name: ${{ inputs.oss-fuzz-project-name }}
27+
sanitizer: ${{ inputs.sanitizer }}
28+
- name: Run fuzzers (${{ inputs.sanitizer }})
29+
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
30+
with:
31+
fuzz-seconds: 600
32+
oss-fuzz-project-name: ${{ inputs.oss-fuzz-project-name }}
33+
output-sarif: true
34+
sanitizer: ${{ inputs.sanitizer }}
35+
- name: Upload crash
36+
if: failure() && steps.build.outcome == 'success'
37+
uses: actions/upload-artifact@v6
38+
with:
39+
name: ${{ inputs.sanitizer }}-artifacts
40+
path: ./out/artifacts
41+
- name: Upload SARIF
42+
if: always() && steps.build.outcome == 'success'
43+
uses: github/codeql-action/upload-sarif@v4
44+
with:
45+
sarif_file: cifuzz-sarif/results.sarif
46+
checkout_path: cifuzz-sarif

.github/workflows/reusable-context.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ on: # yamllint disable-line rule:truthy
2121
description: Whether to run the Android tests
2222
value: ${{ jobs.compute-changes.outputs.run-android }} # bool
2323
run-ci-fuzz:
24-
description: Whether to run the CIFuzz job
24+
description: Whether to run the CIFuzz job for 'cpython' fuzzer
2525
value: ${{ jobs.compute-changes.outputs.run-ci-fuzz }} # bool
26+
run-ci-fuzz-stdlib:
27+
description: Whether to run the CIFuzz job for 'python3-libraries' fuzzer
28+
value: ${{ jobs.compute-changes.outputs.run-ci-fuzz-stdlib }} # bool
2629
run-docs:
2730
description: Whether to build the docs
2831
value: ${{ jobs.compute-changes.outputs.run-docs }} # bool
@@ -56,6 +59,7 @@ jobs:
5659
outputs:
5760
run-android: ${{ steps.changes.outputs.run-android }}
5861
run-ci-fuzz: ${{ steps.changes.outputs.run-ci-fuzz }}
62+
run-ci-fuzz-stdlib: ${{ steps.changes.outputs.run-ci-fuzz-stdlib }}
5963
run-docs: ${{ steps.changes.outputs.run-docs }}
6064
run-ios: ${{ steps.changes.outputs.run-ios }}
6165
run-macos: ${{ steps.changes.outputs.run-macos }}

Tools/build/compute-changes.py

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import os
1313
import subprocess
14-
from dataclasses import dataclass
14+
from dataclasses import dataclass, fields
1515
from pathlib import Path
1616

1717
TYPE_CHECKING = False
@@ -52,11 +52,59 @@
5252
MACOS_DIRS = frozenset({"Mac"})
5353
WASI_DIRS = frozenset({Path("Tools", "wasm")})
5454

55+
LIBRARY_FUZZER_PATHS = frozenset({
56+
# All C/CPP fuzzers.
57+
Path("configure"),
58+
Path(".github/workflows/reusable-cifuzz.yml"),
59+
# ast
60+
Path("Lib/ast.py"),
61+
Path("Python/ast.c"),
62+
# configparser
63+
Path("Lib/configparser.py"),
64+
# csv
65+
Path("Lib/csv.py"),
66+
Path("Modules/_csv.c"),
67+
# decode
68+
Path("Lib/encodings/"),
69+
Path("Modules/_codecsmodule.c"),
70+
Path("Modules/cjkcodecs/"),
71+
Path("Modules/unicodedata*"),
72+
# difflib
73+
Path("Lib/difflib.py"),
74+
# email
75+
Path("Lib/email/"),
76+
# html
77+
Path("Lib/html/"),
78+
Path("Lib/_markupbase.py"),
79+
# http.client
80+
Path("Lib/http/client.py"),
81+
# json
82+
Path("Lib/json/"),
83+
Path("Modules/_json.c"),
84+
# plist
85+
Path("Lib/plistlib.py"),
86+
# re
87+
Path("Lib/re/"),
88+
Path("Modules/_sre/"),
89+
# tarfile
90+
Path("Lib/tarfile.py"),
91+
# tomllib
92+
Path("Modules/tomllib/"),
93+
# xml
94+
Path("Lib/xml/"),
95+
Path("Lib/_markupbase.py"),
96+
Path("Modules/expat/"),
97+
Path("Modules/pyexpat.c"),
98+
# zipfile
99+
Path("Lib/zipfile/"),
100+
})
101+
55102

56103
@dataclass(kw_only=True, slots=True)
57104
class Outputs:
58105
run_android: bool = False
59106
run_ci_fuzz: bool = False
107+
run_ci_fuzz_stdlib: bool = False
60108
run_docs: bool = False
61109
run_ios: bool = False
62110
run_macos: bool = False
@@ -96,6 +144,11 @@ def compute_changes() -> None:
96144
else:
97145
print("Branch too old for CIFuzz tests; or no C files were changed")
98146

147+
if outputs.run_ci_fuzz_stdlib:
148+
print("Run CIFuzz tests for stdlib")
149+
else:
150+
print("Branch too old for CIFuzz tests; or no stdlib files were changed")
151+
99152
if outputs.run_docs:
100153
print("Build documentation")
101154

@@ -146,9 +199,18 @@ def get_file_platform(file: Path) -> str | None:
146199
return None
147200

148201

202+
def is_fuzzable_library_file(file: Path) -> bool:
203+
return any(
204+
(file.is_relative_to(needs_fuzz) and needs_fuzz.is_dir())
205+
or (file == needs_fuzz and file.is_file())
206+
for needs_fuzz in LIBRARY_FUZZER_PATHS
207+
)
208+
209+
149210
def process_changed_files(changed_files: Set[Path]) -> Outputs:
150211
run_tests = False
151212
run_ci_fuzz = False
213+
run_ci_fuzz_stdlib = False
152214
run_docs = False
153215
run_windows_tests = False
154216
run_windows_msi = False
@@ -162,8 +224,8 @@ def process_changed_files(changed_files: Set[Path]) -> Outputs:
162224
doc_file = file.suffix in SUFFIXES_DOCUMENTATION or doc_or_misc
163225

164226
if file.parent == GITHUB_WORKFLOWS_PATH:
165-
if file.name == "build.yml":
166-
run_tests = run_ci_fuzz = True
227+
if file.name in ("build.yml", "reusable-cifuzz.yml"):
228+
run_tests = run_ci_fuzz = run_ci_fuzz_stdlib = True
167229
has_platform_specific_change = False
168230
if file.name == "reusable-docs.yml":
169231
run_docs = True
@@ -194,6 +256,8 @@ def process_changed_files(changed_files: Set[Path]) -> Outputs:
194256
("Modules", "_xxtestfuzz"),
195257
}:
196258
run_ci_fuzz = True
259+
if not run_ci_fuzz_stdlib and is_fuzzable_library_file(file):
260+
run_ci_fuzz_stdlib = True
197261

198262
# Check for changed documentation-related files
199263
if doc_file:
@@ -227,6 +291,7 @@ def process_changed_files(changed_files: Set[Path]) -> Outputs:
227291
return Outputs(
228292
run_android=run_android,
229293
run_ci_fuzz=run_ci_fuzz,
294+
run_ci_fuzz_stdlib=run_ci_fuzz_stdlib,
230295
run_docs=run_docs,
231296
run_ios=run_ios,
232297
run_macos=run_macos,
@@ -261,16 +326,10 @@ def write_github_output(outputs: Outputs) -> None:
261326
return
262327

263328
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
264-
f.write(f"run-android={bool_lower(outputs.run_android)}\n")
265-
f.write(f"run-ci-fuzz={bool_lower(outputs.run_ci_fuzz)}\n")
266-
f.write(f"run-docs={bool_lower(outputs.run_docs)}\n")
267-
f.write(f"run-ios={bool_lower(outputs.run_ios)}\n")
268-
f.write(f"run-macos={bool_lower(outputs.run_macos)}\n")
269-
f.write(f"run-tests={bool_lower(outputs.run_tests)}\n")
270-
f.write(f"run-ubuntu={bool_lower(outputs.run_ubuntu)}\n")
271-
f.write(f"run-wasi={bool_lower(outputs.run_wasi)}\n")
272-
f.write(f"run-windows-msi={bool_lower(outputs.run_windows_msi)}\n")
273-
f.write(f"run-windows-tests={bool_lower(outputs.run_windows_tests)}\n")
329+
for field in fields(outputs):
330+
name = field.name.replace("_", "-")
331+
val = bool_lower(getattr(outputs, field.name))
332+
f.write(f"{name}={val}\n")
274333

275334

276335
def bool_lower(value: bool, /) -> str:

0 commit comments

Comments
 (0)