Skip to content

Commit e47aa74

Browse files
authored
Merge pull request #10 from cisco-ai-defense/expose-pickle-fuzzer-gh-action
Expose pickle fuzzer gh action
2 parents 88889b9 + 40ea346 commit e47aa74

9 files changed

Lines changed: 586 additions & 2 deletions

File tree

.github/workflows/action-smoke.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Action Smoke Test
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
inputs:
7+
version:
8+
description: Release tag to install (e.g., v1.2.3). Defaults to latest.
9+
required: false
10+
default: latest
11+
12+
jobs:
13+
smoke:
14+
name: Action Smoke Test (${{ matrix.os }})
15+
runs-on: ${{ matrix.os }}
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
os: [ubuntu-latest, macos-latest, windows-latest]
20+
defaults:
21+
run:
22+
shell: bash
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v4
26+
27+
- name: Install pickle-fuzzer
28+
uses: ./
29+
with:
30+
version: ${{ inputs.version }}
31+
mode: cli
32+
install_only: true
33+
34+
- name: Verify version
35+
run: pickle-fuzzer --version
36+
37+
- name: Generate samples
38+
uses: ./
39+
with:
40+
version: ${{ inputs.version }}
41+
mode: cli
42+
output_dir: samples
43+
samples: 5
44+
protocol: 4
45+
46+
- name: Verify samples
47+
run: |
48+
test -f samples/0.pkl
49+
ls -la samples
50+
51+
atheris:
52+
name: Action Smoke Test (atheris)
53+
runs-on: ubuntu-latest
54+
steps:
55+
- name: Checkout code
56+
uses: actions/checkout@v4
57+
58+
- name: Run Atheris harness
59+
uses: ./
60+
with:
61+
mode: atheris
62+
harness: python/examples/ci-harness.py
63+
harness_args: "-max_total_time=5"

.github/workflows/release.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,31 @@ jobs:
7878
if: matrix.os != 'windows-latest'
7979
run: strip target/${{ matrix.target }}/release/${{ matrix.artifact_name }}
8080

81+
- name: Generate checksum (Linux)
82+
if: matrix.os == 'ubuntu-latest'
83+
run: |
84+
artifact_path="target/${{ matrix.target }}/release/${{ matrix.artifact_name }}"
85+
checksum_path="target/${{ matrix.target }}/release/${{ matrix.asset_name }}.sha256"
86+
hash="$(sha256sum "$artifact_path" | awk '{print $1}')"
87+
echo "${hash} ${{ matrix.asset_name }}" > "$checksum_path"
88+
89+
- name: Generate checksum (macOS)
90+
if: matrix.os == 'macos-latest'
91+
run: |
92+
artifact_path="target/${{ matrix.target }}/release/${{ matrix.artifact_name }}"
93+
checksum_path="target/${{ matrix.target }}/release/${{ matrix.asset_name }}.sha256"
94+
hash="$(shasum -a 256 "$artifact_path" | awk '{print $1}')"
95+
echo "${hash} ${{ matrix.asset_name }}" > "$checksum_path"
96+
97+
- name: Generate checksum (Windows)
98+
if: matrix.os == 'windows-latest'
99+
shell: pwsh
100+
run: |
101+
$artifactPath = "target/${{ matrix.target }}/release/${{ matrix.artifact_name }}"
102+
$checksumPath = "target/${{ matrix.target }}/release/${{ matrix.asset_name }}.sha256"
103+
$hash = (Get-FileHash -Algorithm SHA256 $artifactPath).Hash.ToLower()
104+
"$hash ${{ matrix.asset_name }}" | Out-File -FilePath $checksumPath -Encoding ascii
105+
81106
- name: Upload Release Asset
82107
uses: actions/upload-release-asset@v1
83108
env:
@@ -88,6 +113,16 @@ jobs:
88113
asset_name: ${{ matrix.asset_name }}
89114
asset_content_type: application/octet-stream
90115

116+
- name: Upload checksum asset
117+
uses: actions/upload-release-asset@v1
118+
env:
119+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
120+
with:
121+
upload_url: ${{ needs.create-release.outputs.upload_url }}
122+
asset_path: ./target/${{ matrix.target }}/release/${{ matrix.asset_name }}.sha256
123+
asset_name: ${{ matrix.asset_name }}.sha256
124+
asset_content_type: text/plain
125+
91126
# publish-crate:
92127
# name: Publish to crates.io
93128
# needs: build-release

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ target/criterion/
2121
*.log
2222

2323
# python fuzzing corpus (keep directory structure, ignore contents)
24-
python/examples/fuzz-corpus/
24+
python/examples/fuzz-corpus/
25+
.DS_Store

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,49 @@ The binary will be available at `target/release/pickle-fuzzer`.
5151
cargo install pickle-fuzzer
5252
```
5353

54+
## GitHub Action
55+
56+
Use `pickle-fuzzer` directly in CI with the bundled GitHub Action. The action
57+
supports two modes: a CLI generator (`mode: cli`) and an Atheris harness runner
58+
(`mode: atheris`).
59+
60+
```yaml
61+
name: Pickle Fuzzing
62+
on: [pull_request]
63+
jobs:
64+
fuzz:
65+
runs-on: ubuntu-latest
66+
steps:
67+
- uses: actions/checkout@v4
68+
- uses: cisco-ai-defense/pickle-fuzzer@v1
69+
with:
70+
mode: cli
71+
output_dir: samples
72+
samples: 200
73+
```
74+
75+
You can also pass raw arguments via `args`, which overrides other inputs:
76+
77+
```yaml
78+
- uses: cisco-ai-defense/pickle-fuzzer@v1
79+
with:
80+
args: "--dir samples --samples 200 --protocol 4"
81+
```
82+
83+
Set `version` to download a specific release tag, or `install_only: true` to
84+
just add the binary to `PATH` without running it.
85+
86+
To run a custom Atheris harness (see `python/examples/harness.py` for a starter
87+
template), use `mode: atheris` and provide a harness path:
88+
89+
```yaml
90+
- uses: cisco-ai-defense/pickle-fuzzer@v1
91+
with:
92+
mode: atheris
93+
harness: fuzz_harness.py
94+
harness_args: "-max_total_time=60"
95+
```
96+
5497
## Usage
5598

5699
### Generate a Single Pickle File
@@ -503,4 +546,3 @@ Pre-1.0 versions (0.x.x) may introduce breaking changes in minor versions as the
503546
Distributed under the Apache 2.0 License. See [LICENSE](LICENSE) for more information.
504547

505548
Project Link: [https://github.com/cisco-ai-defense/pickle-fuzzer](https://github.com/cisco-ai-defense/pickle-fuzzer)
506-

action.yml

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
name: pickle-fuzzer
2+
description: Install and run the pickle-fuzzer CLI to generate pickle bytecode.
3+
author: Cisco AI Defense
4+
5+
inputs:
6+
mode:
7+
description: "Mode to run: cli (default) or atheris."
8+
required: false
9+
default: "cli"
10+
version:
11+
description: >
12+
Release tag to download (e.g., v1.2.3). Defaults to the action ref if it
13+
looks like a tag; otherwise uses the latest release.
14+
required: false
15+
default: ""
16+
args:
17+
description: >
18+
Arguments passed directly to pickle-fuzzer. If set, other inputs are
19+
ignored.
20+
required: false
21+
default: ""
22+
output_dir:
23+
description: Output directory for batch generation (maps to --dir).
24+
required: false
25+
default: ""
26+
output_file:
27+
description: Output file path for single file mode (positional FILE).
28+
required: false
29+
default: ""
30+
samples:
31+
description: Number of samples to generate (maps to --samples).
32+
required: false
33+
default: ""
34+
protocol:
35+
description: Pickle protocol version 0-5 (maps to --protocol).
36+
required: false
37+
default: ""
38+
seed:
39+
description: Seed for reproducible generation (maps to --seed).
40+
required: false
41+
default: ""
42+
min_opcodes:
43+
description: Minimum opcodes to generate (maps to --min-opcodes).
44+
required: false
45+
default: ""
46+
max_opcodes:
47+
description: Maximum opcodes to generate (maps to --max-opcodes).
48+
required: false
49+
default: ""
50+
mutators:
51+
description: >
52+
Comma or space separated mutators (maps to repeated --mutators flags).
53+
required: false
54+
default: ""
55+
mutation_rate:
56+
description: Mutation probability 0.0-1.0 (maps to --mutation-rate).
57+
required: false
58+
default: ""
59+
unsafe_mutations:
60+
description: Allow mutations that may produce invalid pickles.
61+
required: false
62+
default: "false"
63+
allow_ext:
64+
description: Allow EXT* opcodes (requires extension registry).
65+
required: false
66+
default: "false"
67+
allow_buffer:
68+
description: Allow buffer opcodes (requires out-of-band buffer support).
69+
required: false
70+
default: "false"
71+
install_only:
72+
description: Only install pickle-fuzzer without running it.
73+
required: false
74+
default: "false"
75+
python_version:
76+
description: Python version for atheris mode.
77+
required: false
78+
default: "3.11"
79+
harness:
80+
description: Path to the Atheris harness script (required for atheris mode).
81+
required: false
82+
default: ""
83+
harness_args:
84+
description: Arguments passed to the harness script (atheris mode).
85+
required: false
86+
default: ""
87+
88+
outputs:
89+
binary-path:
90+
description: Path to the installed pickle-fuzzer binary.
91+
value: ${{ steps.install.outputs.binary-path }}
92+
resolved-version:
93+
description: Version tag resolved for download.
94+
value: ${{ steps.install.outputs.resolved-version }}
95+
96+
runs:
97+
using: composite
98+
steps:
99+
- name: Validate mode
100+
shell: bash
101+
run: |
102+
case "${{ inputs.mode }}" in
103+
cli|atheris) ;;
104+
*) echo "Unsupported mode: ${{ inputs.mode }}" >&2; exit 1 ;;
105+
esac
106+
107+
- id: install
108+
if: ${{ inputs.mode == 'cli' }}
109+
env:
110+
INPUT_VERSION: ${{ inputs.version }}
111+
INPUT_ARGS: ${{ inputs.args }}
112+
INPUT_OUTPUT_DIR: ${{ inputs.output_dir }}
113+
INPUT_OUTPUT_FILE: ${{ inputs.output_file }}
114+
INPUT_SAMPLES: ${{ inputs.samples }}
115+
INPUT_PROTOCOL: ${{ inputs.protocol }}
116+
INPUT_SEED: ${{ inputs.seed }}
117+
INPUT_MIN_OPCODES: ${{ inputs.min_opcodes }}
118+
INPUT_MAX_OPCODES: ${{ inputs.max_opcodes }}
119+
INPUT_MUTATORS: ${{ inputs.mutators }}
120+
INPUT_MUTATION_RATE: ${{ inputs.mutation_rate }}
121+
INPUT_UNSAFE_MUTATIONS: ${{ inputs.unsafe_mutations }}
122+
INPUT_ALLOW_EXT: ${{ inputs.allow_ext }}
123+
INPUT_ALLOW_BUFFER: ${{ inputs.allow_buffer }}
124+
INPUT_INSTALL_ONLY: ${{ inputs.install_only }}
125+
shell: bash
126+
run: |
127+
"${GITHUB_ACTION_PATH}/scripts/action-install.sh"
128+
129+
- name: Run pickle-fuzzer
130+
if: ${{ inputs.mode == 'cli' && inputs.install_only != 'true' && inputs.install_only != '1' && inputs.install_only != 'yes' }}
131+
env:
132+
INPUT_VERSION: ${{ inputs.version }}
133+
INPUT_ARGS: ${{ inputs.args }}
134+
INPUT_OUTPUT_DIR: ${{ inputs.output_dir }}
135+
INPUT_OUTPUT_FILE: ${{ inputs.output_file }}
136+
INPUT_SAMPLES: ${{ inputs.samples }}
137+
INPUT_PROTOCOL: ${{ inputs.protocol }}
138+
INPUT_SEED: ${{ inputs.seed }}
139+
INPUT_MIN_OPCODES: ${{ inputs.min_opcodes }}
140+
INPUT_MAX_OPCODES: ${{ inputs.max_opcodes }}
141+
INPUT_MUTATORS: ${{ inputs.mutators }}
142+
INPUT_MUTATION_RATE: ${{ inputs.mutation_rate }}
143+
INPUT_UNSAFE_MUTATIONS: ${{ inputs.unsafe_mutations }}
144+
INPUT_ALLOW_EXT: ${{ inputs.allow_ext }}
145+
INPUT_ALLOW_BUFFER: ${{ inputs.allow_buffer }}
146+
INPUT_INSTALL_ONLY: ${{ inputs.install_only }}
147+
shell: bash
148+
run: |
149+
"${GITHUB_ACTION_PATH}/scripts/action-run.sh"
150+
151+
- name: Setup Python (atheris)
152+
if: ${{ inputs.mode == 'atheris' }}
153+
uses: actions/setup-python@v5
154+
with:
155+
python-version: ${{ inputs.python_version }}
156+
157+
- name: Install Rust toolchain (atheris)
158+
if: ${{ inputs.mode == 'atheris' }}
159+
uses: dtolnay/rust-toolchain@stable
160+
161+
- name: Run Atheris harness
162+
if: ${{ inputs.mode == 'atheris' }}
163+
env:
164+
INPUT_HARNESS: ${{ inputs.harness }}
165+
INPUT_HARNESS_ARGS: ${{ inputs.harness_args }}
166+
shell: bash
167+
run: |
168+
"${GITHUB_ACTION_PATH}/scripts/action-atheris.sh"

python/examples/ci-harness.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""minimal Atheris harness for CI smoke tests"""
2+
3+
import sys
4+
5+
import atheris
6+
from pickle_fuzzer.fuzzer import PickleMutator
7+
8+
with atheris.instrument_imports():
9+
import pickletools
10+
11+
12+
def TestOneInput(data: bytes) -> None:
13+
if not data:
14+
return
15+
proto = data[0] % 6
16+
mutator = PickleMutator(protocol=proto)
17+
pickle_bytes = mutator.mutate(data[1:], max_size=256)
18+
try:
19+
for _ in pickletools.genops(pickle_bytes):
20+
pass
21+
except Exception:
22+
pass
23+
24+
25+
def main() -> None:
26+
atheris.Setup(sys.argv, TestOneInput)
27+
atheris.Fuzz()
28+
29+
30+
if __name__ == "__main__":
31+
main()

0 commit comments

Comments
 (0)