Skip to content

Commit b56c816

Browse files
committed
fix: replay python env artifacts and harden fuzz policy
1 parent ca6ad9a commit b56c816

6 files changed

Lines changed: 274 additions & 18 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
name: Fuzz Python Env Replay
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
source_run_id:
7+
description: Comparison workflow run id to replay artifacts from
8+
required: true
9+
10+
permissions:
11+
actions: read
12+
contents: read
13+
14+
env:
15+
CARGO_TERM_COLOR: always
16+
RUST_BACKTRACE: 1
17+
18+
jobs:
19+
replay-python-env-artifacts:
20+
name: Replay Python env artifact (${{ matrix.case_name }}, ${{ matrix.policy_name }})
21+
runs-on: ubuntu-latest
22+
timeout-minutes: 20
23+
strategy:
24+
fail-fast: false
25+
matrix:
26+
include:
27+
- case_name: inherit-leak
28+
source_artifact_name: fuzz-python-env-inherit
29+
input_prefix: leak-
30+
policy_name: inherit
31+
env_policy: inherit
32+
- case_name: inherit-leak
33+
source_artifact_name: fuzz-python-env-inherit
34+
input_prefix: leak-
35+
policy_name: strip-setup-python
36+
env_policy: strip_setup_python
37+
- case_name: inherit-leak
38+
source_artifact_name: fuzz-python-env-inherit
39+
input_prefix: leak-
40+
policy_name: strip-setup-python-and-ld-library-path
41+
env_policy: strip_setup_python_and_ld_library_path
42+
- case_name: strip-setup-python-artifact
43+
source_artifact_name: fuzz-python-env-strip-setup-python
44+
input_prefix: crash-
45+
policy_name: inherit
46+
env_policy: inherit
47+
- case_name: strip-setup-python-artifact
48+
source_artifact_name: fuzz-python-env-strip-setup-python
49+
input_prefix: crash-
50+
policy_name: strip-setup-python
51+
env_policy: strip_setup_python
52+
- case_name: strip-setup-python-artifact
53+
source_artifact_name: fuzz-python-env-strip-setup-python
54+
input_prefix: crash-
55+
policy_name: strip-setup-python-and-ld-library-path
56+
env_policy: strip_setup_python_and_ld_library_path
57+
58+
steps:
59+
- name: Checkout code
60+
uses: actions/checkout@v4
61+
62+
- name: Install Rust toolchain
63+
uses: dtolnay/rust-toolchain@0f1b44df7e9cbb178d781a242338dfa5e243ad7f
64+
with:
65+
toolchain: nightly-2026-04-16
66+
67+
- name: Install Python
68+
uses: actions/setup-python@v5
69+
with:
70+
python-version: "3.11"
71+
72+
- name: Install cargo-fuzz
73+
run: cargo install cargo-fuzz --locked --version 0.13.1
74+
75+
- name: Resolve replay input
76+
id: input
77+
env:
78+
GH_TOKEN: ${{ github.token }}
79+
SOURCE_RUN_ID: ${{ github.event.inputs.source_run_id }}
80+
SOURCE_ARTIFACT_NAME: ${{ matrix.source_artifact_name }}
81+
INPUT_PREFIX: ${{ matrix.input_prefix }}
82+
run: |
83+
case "$SOURCE_RUN_ID" in
84+
''|*[!0-9]*)
85+
echo "::error::workflow_dispatch input 'source_run_id' must be a GitHub Actions run id"
86+
exit 1
87+
;;
88+
esac
89+
90+
mkdir -p fuzz/replay-input fuzz/artifacts/python-env-replay
91+
gh run download "$SOURCE_RUN_ID" -n "$SOURCE_ARTIFACT_NAME" -D fuzz/replay-input
92+
93+
input_file=$(find fuzz/replay-input -maxdepth 1 -type f -name "${INPUT_PREFIX}*" | sort | head -n 1)
94+
if [ -z "$input_file" ]; then
95+
echo "::error::No ${INPUT_PREFIX}* input found in artifact ${SOURCE_ARTIFACT_NAME}"
96+
find fuzz/replay-input -maxdepth 1 -type f -print
97+
exit 1
98+
fi
99+
100+
source_sha=$(gh api "repos/${{ github.repository }}/actions/runs/$SOURCE_RUN_ID" --jq .head_sha)
101+
102+
printf 'input_file=%s\n' "$input_file" >> "$GITHUB_OUTPUT"
103+
printf 'input_name=%s\n' "$(basename "$input_file")" >> "$GITHUB_OUTPUT"
104+
printf 'input_size=%s\n' "$(wc -c < "$input_file" | tr -d ' ')" >> "$GITHUB_OUTPUT"
105+
printf 'source_sha=%s\n' "$source_sha" >> "$GITHUB_OUTPUT"
106+
107+
- name: Record replay metadata
108+
run: |
109+
prefix="fuzz/artifacts/python-env-replay/${{ matrix.case_name }}-${{ matrix.policy_name }}"
110+
cp "${{ steps.input.outputs.input_file }}" "${prefix}-input.bin"
111+
{
112+
echo "source_run_id=${{ github.event.inputs.source_run_id }}"
113+
echo "source_sha=${{ steps.input.outputs.source_sha }}"
114+
echo "source_artifact_name=${{ matrix.source_artifact_name }}"
115+
echo "input_name=${{ steps.input.outputs.input_name }}"
116+
echo "input_size=${{ steps.input.outputs.input_size }}"
117+
echo "policy=${{ matrix.env_policy }}"
118+
echo "workflow_sha=${{ github.sha }}"
119+
} > "${prefix}-metadata.txt"
120+
121+
- name: Report effective Python environment
122+
env:
123+
PICKLE_FUZZ_PYTHON_ENV_POLICY: ${{ matrix.env_policy }}
124+
run: |
125+
prefix="fuzz/artifacts/python-env-replay/${{ matrix.case_name }}-${{ matrix.policy_name }}"
126+
cargo run --manifest-path fuzz/Cargo.toml --example report_python_env --quiet \
127+
> "${prefix}-env.txt"
128+
129+
- name: Replay saved fuzz artifact
130+
id: replay
131+
env:
132+
PICKLE_FUZZ_PYTHON_ENV_POLICY: ${{ matrix.env_policy }}
133+
run: |
134+
prefix="fuzz/artifacts/python-env-replay/${{ matrix.case_name }}-${{ matrix.policy_name }}"
135+
rm -rf fuzz/artifacts/validate_with_python
136+
mkdir -p fuzz/artifacts/validate_with_python
137+
138+
set -o pipefail
139+
status=0
140+
cargo fuzz run validate_with_python "${{ steps.input.outputs.input_file }}" -- \
141+
-runs=1 \
142+
-print_final_stats=1 \
143+
-verbosity=1 | tee "${prefix}-replay.log" || status=$?
144+
145+
printf 'cargo_status=%s\n' "$status" >> "$GITHUB_OUTPUT"
146+
147+
- name: Classify replay outcome
148+
id: classify
149+
env:
150+
CARGO_STATUS: ${{ steps.replay.outputs.cargo_status }}
151+
run: |
152+
prefix="fuzz/artifacts/python-env-replay/${{ matrix.case_name }}-${{ matrix.policy_name }}"
153+
input_name='${{ steps.input.outputs.input_name }}'
154+
input_size='${{ steps.input.outputs.input_size }}'
155+
outcome=clean
156+
found_files=''
157+
158+
for kind in crash timeout oom leak; do
159+
found=$(find fuzz/artifacts/validate_with_python -maxdepth 1 -type f -name "${kind}-*" | sort || true)
160+
if [ -n "$found" ]; then
161+
outcome=$kind
162+
found_files=$found
163+
break
164+
fi
165+
done
166+
167+
if [ "$outcome" = clean ] && [ "${CARGO_STATUS:-0}" != "0" ]; then
168+
outcome=nonzero-no-artifact
169+
fi
170+
171+
{
172+
echo "replay_outcome=$outcome"
173+
echo "cargo_status=${CARGO_STATUS:-0}"
174+
echo "matching_files<<EOF"
175+
printf '%s\n' "$found_files"
176+
echo "EOF"
177+
} >> "$GITHUB_OUTPUT"
178+
179+
{
180+
echo "replay_outcome=$outcome"
181+
echo "cargo_status=${CARGO_STATUS:-0}"
182+
if [ -n "$found_files" ]; then
183+
echo "matching_files:"
184+
printf '%s\n' "$found_files"
185+
fi
186+
} >> "${prefix}-metadata.txt"
187+
188+
{
189+
echo "### Replay result"
190+
echo
191+
echo "- Case: \`${{ matrix.case_name }}\`"
192+
echo "- Policy: \`${{ matrix.env_policy }}\`"
193+
echo "- Source run id: \`${{ github.event.inputs.source_run_id }}\`"
194+
echo "- Source input: \`${input_name}\` (${input_size} bytes)"
195+
echo "- Outcome: \`$outcome\`"
196+
echo "- cargo fuzz exit status: \`${CARGO_STATUS:-0}\`"
197+
if [ -n "$found_files" ]; then
198+
echo "- Matching files:"
199+
printf '%s\n' "$found_files" | while IFS= read -r file; do
200+
echo " - \`$file\`"
201+
done
202+
fi
203+
} >> "$GITHUB_STEP_SUMMARY"
204+
205+
- name: Upload replay artifacts
206+
if: always()
207+
uses: actions/upload-artifact@v4
208+
with:
209+
name: fuzz-python-env-replay-${{ matrix.case_name }}-${{ matrix.policy_name }}
210+
path: |
211+
fuzz/artifacts/validate_with_python/
212+
fuzz/artifacts/python-env-replay/${{ matrix.case_name }}-${{ matrix.policy_name }}-input.bin
213+
fuzz/artifacts/python-env-replay/${{ matrix.case_name }}-${{ matrix.policy_name }}-metadata.txt
214+
fuzz/artifacts/python-env-replay/${{ matrix.case_name }}-${{ matrix.policy_name }}-env.txt
215+
fuzz/artifacts/python-env-replay/${{ matrix.case_name }}-${{ matrix.policy_name }}-replay.log
216+
if-no-files-found: error

.github/workflows/fuzz.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +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
142+
PICKLE_FUZZ_PYTHON_ENV_POLICY: strip_setup_python_and_ld_library_path
143143
run: |
144144
cargo fuzz run validate_with_python -- \
145145
-max_total_time="$FUZZ_DURATION" \
@@ -238,7 +238,7 @@ jobs:
238238
env:
239239
FUZZ_TARGET: ${{ steps.target.outputs.target }}
240240
FUZZ_DURATION: ${{ steps.duration.outputs.duration }}
241-
PICKLE_FUZZ_PYTHON_ENV_POLICY: strip_setup_python
241+
PICKLE_FUZZ_PYTHON_ENV_POLICY: strip_setup_python_and_ld_library_path
242242
run: |
243243
cargo fuzz run "$FUZZ_TARGET" -- \
244244
-max_total_time="$FUZZ_DURATION" \

fuzz/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ If the variable is unset, the target defaults to `strip_setup_python`.
7171
To inspect the effective child-process environment that the fuzz target will use:
7272

7373
```bash
74-
PICKLE_FUZZ_PYTHON_ENV_POLICY=strip_setup_python \
74+
PICKLE_FUZZ_PYTHON_ENV_POLICY=strip_setup_python_and_ld_library_path \
7575
cargo run --manifest-path fuzz/Cargo.toml --example report_python_env --quiet
7676
```
7777

@@ -251,6 +251,12 @@ The project includes automated fuzzing via GitHub Actions (`.github/workflows/fu
251251
The repo also includes `.github/workflows/fuzz-python-env-comparison.yml`, a
252252
PR/workflow-dispatch matrix that compares the effective Python child-process
253253
environment for the three policy variants on `ubuntu-latest`.
254+
The scheduled/custom `validate_with_python` jobs in `.github/workflows/fuzz.yml`
255+
explicitly use `strip_setup_python_and_ld_library_path` on GitHub-hosted
256+
runners.
257+
For exact-input follow-up, `.github/workflows/fuzz-python-env-replay.yml`
258+
replays saved `validate_with_python` artifacts under all three policies on
259+
`ubuntu-latest`.
254260

255261
### Manual Runs
256262

fuzz/tests/python_env_contract.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ fn workflow_and_docs_stay_in_sync_with_supported_policies() {
102102
repo_root().join(".github/workflows/fuzz-python-env-comparison.yml"),
103103
)
104104
.expect("workflow should exist");
105+
let replay_workflow = std::fs::read_to_string(
106+
repo_root().join(".github/workflows/fuzz-python-env-replay.yml"),
107+
)
108+
.expect("replay workflow should exist");
105109
let readme = std::fs::read_to_string(repo_root().join("fuzz/README.md"))
106110
.expect("fuzz README should exist");
107111

@@ -115,9 +119,25 @@ fn workflow_and_docs_stay_in_sync_with_supported_policies() {
115119
workflow.contains(name),
116120
"workflow must mention policy {name}"
117121
);
122+
assert!(
123+
replay_workflow.contains(name),
124+
"replay workflow must mention policy {name}"
125+
);
118126
assert!(readme.contains(name), "README must mention policy {name}");
119127
}
120128

129+
for artifact_name in ["fuzz-python-env-inherit", "fuzz-python-env-strip-setup-python"] {
130+
assert!(
131+
replay_workflow.contains(artifact_name),
132+
"replay workflow must mention source artifact {artifact_name}"
133+
);
134+
}
135+
136+
assert!(
137+
readme.contains(".github/workflows/fuzz-python-env-replay.yml"),
138+
"README must mention the replay workflow"
139+
);
140+
121141
for path in [
122142
"fuzz/fuzz_targets/validate_with_python.rs",
123143
"fuzz/src/lib.rs",
@@ -133,9 +153,11 @@ fn workflow_and_docs_stay_in_sync_with_supported_policies() {
133153
let main_workflow =
134154
std::fs::read_to_string(repo_root().join(".github/workflows/fuzz.yml"))
135155
.expect("main fuzz workflow should exist");
136-
let policy_count = main_workflow.matches("PICKLE_FUZZ_PYTHON_ENV_POLICY: strip_setup_python").count();
156+
let policy_count = main_workflow
157+
.matches("PICKLE_FUZZ_PYTHON_ENV_POLICY: strip_setup_python_and_ld_library_path")
158+
.count();
137159
assert_eq!(
138160
policy_count, 2,
139-
"main fuzz workflow should set strip_setup_python for both validate_with_python entry points"
161+
"main fuzz workflow should set strip_setup_python_and_ld_library_path for both validate_with_python entry points"
140162
);
141163
}

plans/fuzz-and-security-ci-pr.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ comparison workflow for the Python validator environment hypothesis.
55

66
This updates `rand` from `0.9.2` to `0.9.4`, makes the
77
`validate_with_python` fuzz target explicitly configurable via
8-
`PICKLE_FUZZ_PYTHON_ENV_POLICY`, sets the main fuzz workflow to use the
9-
`strip_setup_python` policy, and adds a PR/workflow-dispatch comparison matrix
10-
that runs `inherit`, `strip_setup_python`, and
8+
`PICKLE_FUZZ_PYTHON_ENV_POLICY`, sets the main GitHub-hosted fuzz workflow to
9+
use the `strip_setup_python_and_ld_library_path` policy, and adds a
10+
PR/workflow-dispatch comparison matrix that runs `inherit`,
11+
`strip_setup_python`, and
1112
`strip_setup_python_and_ld_library_path` side by side on `ubuntu-latest`.
12-
It also adds a shared fuzz helper with unit and integration coverage, a
13+
It also adds a workflow-dispatch replay workflow for saved comparison
14+
artifacts, a shared fuzz helper with unit and integration coverage, a
1315
child-env reporting example, and the final Clippy fix for the PR.
1416

1517
## Related Issue
@@ -38,11 +40,15 @@ N/A
3840
effective child-process environment after the policy is applied.
3941
- Add fuzz-crate tests that verify both policy-to-child-env behavior and the
4042
workflow/README policy contract.
41-
- Keep the scheduled and custom fuzz workflow on the targeted
42-
`strip_setup_python` policy instead of broad leak suppression.
43+
- Switch the scheduled and custom GitHub-hosted fuzz workflow to the targeted
44+
`strip_setup_python_and_ld_library_path` policy instead of broad leak
45+
suppression.
4346
- Add `.github/workflows/fuzz-python-env-comparison.yml` so the PR can compare
4447
the three environment policies on GitHub-hosted x86_64 runners, using the
4548
same helper code that the fuzz target uses.
49+
- Add `.github/workflows/fuzz-python-env-replay.yml` so a saved `inherit`
50+
leak input and the `strip-setup-python` zero-byte artifact can be replayed
51+
under all three policies on GitHub-hosted x86_64 runners.
4652
- Pin the fuzz workflows to `nightly-2026-04-16` and `cargo-fuzz 0.13.1`,
4753
make the comparison workflow cache matrix-specific, and upload the env report
4854
artifact for each matrix job.
@@ -66,7 +72,7 @@ cargo audit
6672
cargo deny check advisories
6773
cd fuzz && cargo audit
6874
cargo +nightly-2026-04-16 test --manifest-path fuzz/Cargo.toml
69-
PICKLE_FUZZ_PYTHON_ENV_POLICY=strip_setup_python cargo +nightly-2026-04-16 run --manifest-path fuzz/Cargo.toml --example report_python_env --quiet
75+
PICKLE_FUZZ_PYTHON_ENV_POLICY=strip_setup_python_and_ld_library_path cargo +nightly-2026-04-16 run --manifest-path fuzz/Cargo.toml --example report_python_env --quiet
7076
```
7177

7278
## Checklist
@@ -101,6 +107,8 @@ Not applicable.
101107
input.
102108
- The comparison workflow uploads both the fuzz artifacts and the
103109
`fuzz-python-env-report-*` artifact for each policy.
110+
- The replay workflow is manual (`workflow_dispatch`) and downloads artifacts
111+
from a specified comparison run id using the repo's `GITHUB_TOKEN`.
104112
- The tracked plan and PR summary for this branch live under `plans/`.
105113

106114
## Breaking Changes

plans/fuzz-and-security-ci.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,25 @@ cargo audit
3434
cargo deny check advisories
3535
cd fuzz && cargo audit
3636
cargo +nightly-2026-04-16 test --manifest-path fuzz/Cargo.toml
37-
PICKLE_FUZZ_PYTHON_ENV_POLICY=strip_setup_python cargo +nightly-2026-04-16 run --manifest-path fuzz/Cargo.toml --example report_python_env --quiet
37+
PICKLE_FUZZ_PYTHON_ENV_POLICY=strip_setup_python_and_ld_library_path cargo +nightly-2026-04-16 run --manifest-path fuzz/Cargo.toml --example report_python_env --quiet
3838
```
3939

4040
GitHub evidence already observed on PR `#24`:
4141

4242
- `Security Audit` green
4343
- `Fuzz Testing` green
44-
- `Fuzz Python Env Comparison` green for `inherit`,
45-
`strip-setup-python`, and `strip-setup-python-and-ld-library-path`
44+
- Seeded `Fuzz Python Env Comparison` on April 16, 2026 showed:
45+
`inherit` leaked, `strip-setup-python` crashed with a zero-byte artifact,
46+
and `strip-setup-python-and-ld-library-path` finished clean
4647

4748
## Notes
4849

4950
- The comparison workflow is diagnostic. It verifies the effective Python child
5051
environment used by the fuzz target, records policy/duration/seed metadata,
5152
and uploads both the env report and fuzz artifacts for each policy.
52-
- The current branch default remains `strip_setup_python` for scheduled and
53-
custom `validate_with_python` runs because it is the least invasive CI-only
54-
change under active observation.
53+
- The main GitHub-hosted `validate_with_python` workflow now uses
54+
`strip_setup_python_and_ld_library_path`, while the fuzz target's unset local
55+
default remains `strip_setup_python`.
56+
- `.github/workflows/fuzz-python-env-replay.yml` can replay the saved
57+
`inherit` leak artifact and the `strip-setup-python` zero-byte artifact under
58+
all three policies on `ubuntu-latest`.

0 commit comments

Comments
 (0)