Skip to content

Commit 3ac5a6b

Browse files
committed
Add GitHub CI fuzz env comparison
1 parent 4e5870e commit 3ac5a6b

3 files changed

Lines changed: 182 additions & 4 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
name: Fuzz Python Env Comparison
2+
3+
on:
4+
pull_request:
5+
branches: [ main ]
6+
paths:
7+
- .github/workflows/fuzz-python-env-comparison.yml
8+
- .github/workflows/fuzz.yml
9+
- fuzz/fuzz_targets/validate_with_python.rs
10+
- Cargo.toml
11+
- Cargo.lock
12+
- fuzz/Cargo.toml
13+
- fuzz/Cargo.lock
14+
workflow_dispatch:
15+
inputs:
16+
duration:
17+
description: Fuzzing duration in seconds
18+
required: false
19+
default: "1800"
20+
21+
permissions:
22+
contents: read
23+
24+
env:
25+
CARGO_TERM_COLOR: always
26+
RUST_BACKTRACE: 1
27+
28+
jobs:
29+
compare-python-env:
30+
name: Compare Python env policy (${{ matrix.name }})
31+
runs-on: ubuntu-latest
32+
timeout-minutes: 45
33+
strategy:
34+
fail-fast: false
35+
matrix:
36+
include:
37+
- name: inherit
38+
env_policy: inherit
39+
- name: strip-setup-python
40+
env_policy: strip_setup_python
41+
- name: strip-setup-python-and-ld-library-path
42+
env_policy: strip_setup_python_and_ld_library_path
43+
44+
steps:
45+
- name: Checkout code
46+
uses: actions/checkout@v4
47+
48+
- name: Install Rust toolchain
49+
uses: dtolnay/rust-toolchain@0f1b44df7e9cbb178d781a242338dfa5e243ad7f
50+
with:
51+
toolchain: nightly
52+
53+
- name: Install Python
54+
uses: actions/setup-python@v5
55+
with:
56+
python-version: "3.11"
57+
58+
- name: Install cargo-fuzz
59+
run: cargo install cargo-fuzz
60+
61+
- name: Cache fuzz corpus
62+
uses: actions/cache@v4
63+
with:
64+
path: fuzz/corpus/validate_with_python
65+
key: fuzz-python-env-compare-${{ github.sha }}
66+
restore-keys: |
67+
fuzz-python-env-compare-
68+
fuzz-corpus-validate-python-
69+
70+
- name: Determine fuzzing duration
71+
id: duration
72+
env:
73+
EVENT_NAME: ${{ github.event_name }}
74+
INPUT_DURATION: ${{ github.event.inputs.duration }}
75+
run: |
76+
duration=1800
77+
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
78+
case "$INPUT_DURATION" in
79+
''|*[!0-9]*)
80+
echo "::error::workflow_dispatch input 'duration' must be a whole number of seconds"
81+
exit 1
82+
;;
83+
esac
84+
duration="$INPUT_DURATION"
85+
fi
86+
printf 'duration=%s\n' "$duration" >> "$GITHUB_OUTPUT"
87+
88+
- name: Report Python environment
89+
env:
90+
PICKLE_FUZZ_PYTHON_ENV_POLICY: ${{ matrix.env_policy }}
91+
run: |
92+
echo "policy=$PICKLE_FUZZ_PYTHON_ENV_POLICY"
93+
python3 --version
94+
env | sort | grep -E '^(pythonLocation|PKG_CONFIG_PATH|Python_ROOT_DIR|Python2_ROOT_DIR|Python3_ROOT_DIR|LD_LIBRARY_PATH)=' || true
95+
96+
- name: Run Python-validation fuzzing
97+
env:
98+
FUZZ_DURATION: ${{ steps.duration.outputs.duration }}
99+
PICKLE_FUZZ_PYTHON_ENV_POLICY: ${{ matrix.env_policy }}
100+
run: |
101+
cargo fuzz run validate_with_python -- \
102+
-max_total_time="$FUZZ_DURATION" \
103+
-print_final_stats=1 \
104+
-verbosity=1
105+
continue-on-error: true
106+
107+
- name: Check for crashes
108+
if: always()
109+
run: |
110+
if [ -d "fuzz/artifacts/validate_with_python" ]; then
111+
if ls fuzz/artifacts/validate_with_python/crash-* 2>/dev/null || \
112+
ls fuzz/artifacts/validate_with_python/timeout-* 2>/dev/null || \
113+
ls fuzz/artifacts/validate_with_python/oom-* 2>/dev/null; then
114+
echo "::error::Fuzzing found crashes for policy ${{ matrix.env_policy }}!"
115+
ls -la fuzz/artifacts/validate_with_python/
116+
exit 1
117+
fi
118+
if ls fuzz/artifacts/validate_with_python/leak-* 2>/dev/null; then
119+
echo "::warning::Memory leaks detected for policy ${{ matrix.env_policy }}"
120+
ls -la fuzz/artifacts/validate_with_python/leak-*
121+
fi
122+
else
123+
echo "::error::Fuzzing did not create fuzz/artifacts/validate_with_python. cargo fuzz likely failed before execution."
124+
exit 1
125+
fi
126+
127+
- name: Upload fuzz artifacts
128+
if: always()
129+
uses: actions/upload-artifact@v4
130+
with:
131+
name: fuzz-python-env-${{ matrix.name }}
132+
path: fuzz/artifacts/validate_with_python/
133+
if-no-files-found: ignore

.github/workflows/fuzz.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ jobs:
139139
- name: Run thorough fuzzing with Python validation
140140
env:
141141
FUZZ_DURATION: ${{ steps.duration.outputs.duration }}
142+
PICKLE_FUZZ_PYTHON_ENV_POLICY: strip_setup_python
142143
run: |
143144
cargo fuzz run validate_with_python -- \
144145
-max_total_time="$FUZZ_DURATION" \
@@ -237,6 +238,7 @@ jobs:
237238
env:
238239
FUZZ_TARGET: ${{ steps.target.outputs.target }}
239240
FUZZ_DURATION: ${{ steps.duration.outputs.duration }}
241+
PICKLE_FUZZ_PYTHON_ENV_POLICY: strip_setup_python
240242
run: |
241243
cargo fuzz run "$FUZZ_TARGET" -- \
242244
-max_total_time="$FUZZ_DURATION" \

fuzz/fuzz_targets/validate_with_python.rs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ use pickle_fuzzer::mutators::{
6565
BitFlipMutator, BoundaryMutator, CharacterMutator, OffByOneMutator, StringLengthMutator,
6666
};
6767
use pickle_fuzzer::{Generator, Version};
68+
use std::env;
6869
use std::io::Write;
6970
use std::process::{Command, Stdio};
71+
use std::sync::OnceLock;
7072

7173
const STRICT_PICKLETOOLS_VALIDATOR: &str = r#"import io
7274
import pickletools
@@ -83,20 +85,61 @@ if stop_pos + 1 != len(data):
8385
pickletools.dis(data, out=io.StringIO())
8486
"#;
8587

86-
const PYTHON_ENV_REMOVALS: &[&str] = &[
88+
const SETUP_PYTHON_ENV_REMOVALS: &[&str] = &[
8789
"pythonLocation",
8890
"Python_ROOT_DIR",
8991
"Python2_ROOT_DIR",
9092
"Python3_ROOT_DIR",
9193
"PKG_CONFIG_PATH",
9294
];
9395

96+
static PYTHON_ENV_POLICY: OnceLock<PythonEnvPolicy> = OnceLock::new();
97+
98+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99+
enum PythonEnvPolicy {
100+
Inherit,
101+
StripSetupPython,
102+
StripSetupPythonAndLdLibraryPath,
103+
}
104+
105+
impl PythonEnvPolicy {
106+
fn current() -> Self {
107+
*PYTHON_ENV_POLICY.get_or_init(|| match env::var("PICKLE_FUZZ_PYTHON_ENV_POLICY") {
108+
Ok(value) => match value.as_str() {
109+
"inherit" => Self::Inherit,
110+
"strip_setup_python" => Self::StripSetupPython,
111+
"strip_setup_python_and_ld_library_path" => {
112+
Self::StripSetupPythonAndLdLibraryPath
113+
}
114+
_ => panic!(
115+
"invalid PICKLE_FUZZ_PYTHON_ENV_POLICY: {value}; expected one of \
116+
inherit, strip_setup_python, strip_setup_python_and_ld_library_path"
117+
),
118+
},
119+
Err(_) => Self::StripSetupPython,
120+
})
121+
}
122+
123+
fn apply(self, command: &mut Command) {
124+
match self {
125+
Self::Inherit => {}
126+
Self::StripSetupPython | Self::StripSetupPythonAndLdLibraryPath => {
127+
for key in SETUP_PYTHON_ENV_REMOVALS {
128+
command.env_remove(key);
129+
}
130+
}
131+
}
132+
133+
if self == Self::StripSetupPythonAndLdLibraryPath {
134+
command.env_remove("LD_LIBRARY_PATH");
135+
}
136+
}
137+
}
138+
94139
/// validate pickle using Python's pickletools plus a whole-file STOP check
95140
fn validate_with_python(pickle_bytes: &[u8]) -> bool {
96141
let mut command = Command::new("python3");
97-
for key in PYTHON_ENV_REMOVALS {
98-
command.env_remove(key);
99-
}
142+
PythonEnvPolicy::current().apply(&mut command);
100143

101144
let mut child = match command
102145
.arg("-c")

0 commit comments

Comments
 (0)