* Make every documented command runnable under our own python shims
The modern-python plugin ships PATH shims that refuse `python <script>`,
`pip install`, `python -m pip` and `uv pip install`. Twelve other plugins
in this marketplace issued exactly those forms, so installing our own
plugin broke our own skills — and CI was green throughout.
The worst case was not theoretical. c-review and rust-review both call
their Phase 4 planner as `python3 "${PLUGIN_ROOT}/scripts/build_run_plan.py"`,
so with the shim installed every run died before spawning a worker.
Verified both directions: the new form exits 0 with the shim on PATH, the
old form exits 1.
Phase 1's reading pass named 16 skills. A mechanical sweep found 96
candidate lines across 44 files, and scanning shell scripts as well as
markdown found 10 more the docs sweep had missed. That gap is the reason
the check below exists.
The fix is not one substitution. Four classes needed different treatment:
- Our own scripts become `uv run --no-project <script>`. Not bare `uv run`,
because these execute inside the *target* repo, which may be a Python
project that cannot resolve; verified against a broken pyproject.toml and
against validate_artifacts.py's sibling import of generate_sarif.
- Package installs become `uv add` for a dependency, `uv tool install` for a
CLI, `uv sync` for a project's own editable install.
- Third-party CLIs we merely document — OSS-Fuzz's infra/helper.py, yarGen —
become `uv run --no-project python <script>`, which keeps upstream's exact
semantics rather than handing their script an environment we manage.
- atheris's instrumented build keeps its source build, as
`uv add --no-binary-package cbor2`. Dropping that flag would silently
produce an uninstrumented fuzzer, which is worse than a visible failure.
Its prose was updated to name the flag it now uses.
Two factual corrections fell out. `pip install caracal` was wrong twice
over: caracal is a Rust tool (Cargo.toml at its root), so it is now
upstream's own `cargo install --git`, not a uv equivalent that would fetch
an unrelated PyPI package. And `pip install uv` cannot bootstrap uv under
a shim that intercepts pip, so culture-index now points at the official
installer.
Thirteen lines stay as they are, each deliberately: Dockerfile `RUN` lines
and oss-fuzz's build.sh run in containers where our shims are absent;
codeql's pip calls install the *analysed* project's dependencies, and that
project is arbitrary; trailmark's dispatch skills must keep saying "Do NOT
run `pip install`"; and modern-python documents what it intercepts.
`make shell-suites` passes again as a result — exit 0 with the 1.6.0 shim,
where AGENTS.md previously recorded it as broken by variant-analysis.
The guardrail: check_python_invocations scans 698 markdown and shell files
and fails on the four refused forms, with structural exemptions for
dockerfile fences and an `allow-legacy-python: <reason>` marker that scopes
to its code block. Eleven self-test fixtures cover it, four asserting it
fires and seven asserting it stays quiet on the compliant forms. It was
mutation-tested in both languages, and it caught its own worst bug during
development: unanchored patterns first flagged `uv run --no-project python
fuzz.py`, the very form the advice recommends. Self-test goes 45 -> 56.
* Review pass: fix the atheris flow, drop a stray exemption, trim comments
Three corrections from reviewing the branch diff:
- atheris's install now opens with `uv init --bare`, without which the
documented `uv add atheris` errors in a bare harness directory. The old
pip form assumed an activated venv, so setup was always implicit; now
it is one explicit line.
- ossfuzz carried an allow-legacy-python marker on a C++ build block that
contains no python at all — yesterday's insertion matched the first of
three "Build in build.sh" headings instead of the python one. The
exemption now sits only on the block that needs it.
- The anti-vacuity message said "read no markdown" for a scan that also
covers shell scripts.
The rest is weight: the new check's comment blocks, the hardcoded-path
constants' commentary, the AGENTS.md bullets and the three exemption
markers all said the same things at two to three times the length. Each
keeps its one-line why; the narratives are gone. No behavioural change —
self-test still passes 56 assertions and the full scan is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Address the review: fix where packages land, widen the check to match the shim
The review's core insight was right twice over. Several substitutions had
changed WHERE a package lands, breaking the documented next step, and the
checker enforced a narrower invariant than the shim it exists to mirror.
Where packages land:
- trailmark is imported as a library from five skills, and a `uv tool
install` environment is not importable — the retry loop at
trailmark/SKILL.md:47-51 would have spun forever on the exact error it
names. The CLI install stays `uv tool install`; the import snippets now
run under `uv run --with trailmark python -`.
- `uv add` writes to the manifest of whatever project you are standing
in, which for sarif-parsing is the audited repo. Its scripting rows,
ijson comment and jsonschema example now use `uv run --with <pkg>`,
which leaves no trace. atheris keeps `uv add` deliberately: the fuzzing
harness is the user's own project, made explicit by `uv init --bare`.
- `uv sync` leaves ct-analyzer in .venv/bin, so the README's very next
line failed with command not found. Now `uv tool install .`, verified
end to end: the console script lands on PATH and --help runs.
- yarGen needs pefile/lxml/yara-python, which `--no-project` had detached;
now `uv run --with-requirements requirements.txt`.
- The cbor2 source-build preference now persists via
`no-binary-package = ["cbor2"]` under [tool.uv] (field verified against
uv's accepted-settings list), so a later `uv sync` cannot silently swap
in an uninstrumented wheel.
The checker, widened to the shim's actual behaviour:
- `python3 --version` and `python3 -u foo.py` are refused by the shim but
passed the old patterns; one live instance (constant-time-analysis
README) proved it. Both forms are now caught.
- Every `uv pip` subcommand is refused, not just install; `-t` joins the
allowed tool-managed flags.
- .py files are scanned too: usage strings and error messages told users
to run refused commands from ten scripts, including the --help of the
very planner this PR fixed. All rewritten.
- The evals/tests exemption now tests path parts relative to plugins/, so
a checkout under a directory named tests no longer exempts every file.
- An allow-marker's scope ends at a blank line as well as a fence, so one
marker cannot blanket a whole file; quality-assessment.md gains the
second marker that scoping made necessary.
Also from the review: zeroize's preflight gets `which python3` back (a
helper script still needs the binary; the shim never required removing
it), the Makefile's shell-suites note no longer describes an interception
that is gone, and the cairo CI example warns that it rebuilds caracal
from source each run.
Self-test 56 -> 63; every new pattern and exemption is fixture-covered
and was mutation-probed against the real tree. Full scan: 0 findings over
773 files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Address the second review: prerequisite probe, checker parity, package placement
The review's P2 was a regression this PR introduced for a population the
first fix ignored: c-review and rust-review now require uv, and a box
with python3 but no uv would die at Phase 4 exactly the way shimmed boxes
died before. Phase 1 (Prerequisites) in both skills now probes
`command -v uv` and aborts with install guidance. zeroize-audit's
preflight already checked uv. The four converted shell suites gain the
same guard with a clear message instead of a bare 127 mid-run.
Checker parity with the shims, second pass:
- pipx and the non-install pip subcommands are refused by catch-all shim
arms and passed the checker; both get named-subcommand patterns.
- A script named by variable or path (`python3 "$MERGE"`) has no `.py`
token; a new pattern covers it and immediately caught one live
instance — a codeql test stub that fakes uv itself, now carrying an
allow-marker with its reason.
- finditer everywhere: a compliant `uv run` earlier on a line no longer
masks a refused command later on it, which was exactly the table-cell
case the unanchored design exists for.
- Prohibition phrases now test the text BEFORE the match, so
"Use `pip install semgrep` instead of the tarball" is flagged while
"Do NOT run `pip install`" stays exempt.
- The uv-pip allowance matches whole flags after the command, so
`--target-dir` no longer counts as `--target` and a trailing `-t /tmp`
does; `uv pip` precedes `pip` in the pattern order so its lines get
the right advice; a pip match directly after `uv ` defers to the
uv-pip verdict instead of double-reporting.
Package placement, continued from the same insight as round one:
- yarGen regains --no-project alongside --with-requirements, plus a cd
into the checkout so requirements.txt resolves where it lives.
- sarif-parsing's jsonschema example no longer names a script that does
not exist, and the table's run-forms show a concrete script.py.
- culture-index's two messages now agree and name the actual remedy
(`uv run --project` on the scripts directory) instead of re-adding a
dependency its pyproject already declares.
- merge_sarif's usage line gains --no-project; the generator plugin's
install section stops prescribing a venv its own runner never uses.
- generate_poc declared requires-python >=3.9 while using `str | None`
in a signature, a TypeError on 3.9 that uv's interpreter selection
made reachable; now >=3.10.
- The GitLab CI example exports ~/.local/bin onto PATH, without which
`uv tool install` warns and the next line dies command-not-found.
Self-test 63 -> 71; the masking, prohibition-direction, flag-position
and pipx cases are all fixtures, and each new pattern was probed live
against the tree (plant, error, remove, clean — 0 findings over 773
files).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Address the third review: importable trailmark, honest probes, sturdier scan
The P2 was the residue of round two's own fix, applied to the siblings
but not the flagship: trailmark/SKILL.md told the model to cure an import
error with `uv tool install`, which cannot cure it — a tool env is not
importable — while forbidding every fallback. The install block now says
what each remedy is for: `uv tool install` for the CLI, `uv run --with
trailmark python -` for the snippets, and the other five library-first
docs carry the same one-line annotation next to their install command.
Empirically settled rather than taken from the review: `uv run python3
<script>` works fine under the shims — uv prepends its environment's bin
directory, so python3 resolves to a real interpreter, not the shim. The
review's claim to the contrary would have meant rewriting the Makefile
and a bats suite; a two-minute transcript said no. Also declined: a
zeroize uv-prerequisite (its preflight already lists uv and uvx; the
C/C++ `which` line now names uv too).
Real and fixed:
- ct-analyzer's availability probe ran `python3 --version` by subprocess
— the one refused form — so under the shims it reported "Python is not
available" on machines where it plainly is. It now probes
sys.executable, the interpreter the analyzer itself runs under.
Verified under the shim: probe returns True.
- The flag step-over in both script patterns handles long and
value-taking flags (`python3 -W ignore harness.py`, `--verbose
tool.py`), matching the shim's two-slot consumption.
- A bare `allow-legacy-python:` with no reason no longer exempts
anything; the reason the docs demand is now enforced.
- `uv run {baseDir}/...` gets --no-project at the ten semgrep and
culture-index call sites that round two missed, and the culture-index
remediation strings now name that same runnable command instead of a
--project mechanism nothing uses.
- pip gains cache/config; the pattern comment now says the subcommand
list is deliberately a subset.
- Both filesystem scans skip .venv/node_modules-style directories, after
a stray local .venv (left by this session's own uv probe, and invisible
to CI) turned the path scan red.
Smaller review items: the uv-probe prose says "Phase 4 onward" rather
than a wrong phase range, run_fixtures' comment stops claiming PEP 723
headers its stdlib-only helpers do not have, the yarGen one-liners say
to run from the checkout, `uv tool install` sites note or export the
tool bin dir the way a fresh container needs, and sarif-parsing's table
column says Install / run and stops naming a file that does not exist.
Self-test 71 -> 74. Full scan: 0 findings over 773 files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
16 KiB
name, description, model, tools
| name | description | model | tools |
|---|---|---|---|
| 5-poc-generator | Crafts bespoke proof-of-concept programs demonstrating that zeroize-audit findings are exploitable. Reads source code and finding details to generate tailored PoCs — each PoC is individually written, not templated. Each PoC exits 0 if the secret persists or 1 if wiped. Mandatory for every finding. | inherit | Read, Write, Bash, Grep, Glob |
5-poc-generator
Craft bespoke proof-of-concept programs for all zeroize-audit findings. Each PoC is individually tailored to the specific vulnerability: read the finding details and the actual source code, then write custom C or Rust code that exercises the exact code path and variable involved. Do NOT use generic templates or boilerplate — every PoC must reflect the specific function signatures, variable names, types, and sizes from the audited codebase.
Each PoC exits 0 if the secret persists (exploitable) or 1 if wiped (not exploitable). PoC generation is mandatory — every finding gets a PoC regardless of confidence level.
Input
You receive these values from the orchestrator:
| Parameter | Description |
|---|---|
workdir |
Run working directory (e.g. /tmp/zeroize-audit-{run_id}/) |
compile_db |
Path to compile_commands.json |
config_path |
Path to merged config file ({workdir}/merged-config.yaml) |
final_report |
Path to {workdir}/report/findings.json |
poc_categories |
Finding categories for which to generate PoCs |
poc_output_dir |
Output directory for PoCs (default: {workdir}/poc/) |
baseDir |
Plugin base directory (for tool paths) |
Process
Step 0 — Load Configuration and Findings
-
Read
config_pathto load the merged config. Extract PoC-relevant settings:secret_fill_byte(default:0xAA)stack_probe_max(default:4096)source_inclusion_threshold(default:5000lines)
-
Read
final_reportto load all findings. Filter to findings inpoc_categories.
Step 1 — Write Shared PoC Infrastructure
Write {poc_output_dir}/poc_common.h with these helpers:
POC_PASS()macro — prints "EXPLOITABLE: secret persists" and exits 0POC_FAIL()macro — prints "NOT EXPLOITABLE: secret wiped" and exits 1volatile_read_nonzero(ptr, len)— readslenbytes through avolatilepointer, returns 1 if any byte is non-zerovolatile_read_pattern(ptr, len, pattern)— readslenbytes through avolatilepointer, returns 1 if≥ len/4bytes matchpatternstack_probe(frame_size)—noinline/noclonefunction that readsframe_sizebytes of uninitialized stack locals, checks forSECRET_FILL_BYTEpatternheap_residue_check(alloc_size)— malloc/fill/free/re-malloc/check cycle to detect heap residue
Set SECRET_FILL_BYTE and STACK_PROBE_MAX from config values. Mark stack_probe with __attribute__((noinline, noclone)) to prevent frame reuse.
Step 2 — Craft Each PoC
For each finding, follow this process:
2a — Read and Understand the Source
-
Use
Readto examine the function atfinding.location.file:finding.location.line. Read at least 50 lines of context around the finding location. -
Identify:
- Function signature: name, parameters, return type
- Sensitive variable: exact name, type, size (from
finding.object) - Wipe presence: does the source contain an approved wipe call for this variable? Where?
- Error paths: for error-path findings, identify what inputs trigger the error return
- Control flow: for path-coverage findings, identify which paths lack the wipe
-
Use
Grepto find:- Callers of the target function (to understand valid argument patterns)
- Type definitions for the sensitive object (structs, typedefs)
- Include dependencies needed by the target function
2b — Determine Inclusion Strategy
- If the target function is
staticor the source file is ≤source_inclusion_thresholdlines: use#includeto include the source file directly - If the target function is
externand the file is large: compile the target source to an object file and link via the Makefile
2c — Write the PoC
Write {poc_output_dir}/poc_za_NNNN_category.c (or .rs for Rust). The PoC must:
- Include
poc_common.hfor shared helpers - Set up required context: include necessary headers, define structs/types used by the target function, declare external symbols if linking
- Initialize the sensitive buffer: fill with
SECRET_FILL_BYTEusingmemset()before calling the target function. This establishes the "secret" content that should be wiped. - Call the target function with valid arguments: use actual types and realistic values. If the function requires allocations, file handles, or other setup, include that setup code. For error-path findings, provide arguments that trigger the specific error path.
- Apply the verification technique appropriate for the finding category (see Category Techniques below)
- Exit with the correct code: use
POC_PASS()if the secret persists,POC_FAIL()if it was wiped
Critical: Each PoC must be specific to the finding. Reference the actual function name, variable name, types, and sizes from the source code. Add a comment block at the top explaining:
- Which finding this PoC demonstrates (finding ID and category)
- What function and variable are being tested
- What the PoC does and what a passing result (exit 0) means
2d — Rust PoC Generation
Enabled for MISSING_SOURCE_ZEROIZE, SECRET_COPY, and PARTIAL_WIPE only. For all other Rust finding categories, record poc_supported: false in the manifest with a one-line reason (e.g., "STACK_RETENTION — stack probe requires unsafe ASM intrinsics not portable across Rust versions").
Exit code convention for Rust PoCs (via cargo test):
assert!passes → cargo exits 0 →"exploitable"(secret persists)assert!panics / test fails → cargo exits non-zero →"not_exploitable"(secret wiped)
2d-i — Read Source Context
- Read
finding.fileatfinding.lineto extract the struct definition, field names, field types, and their sizes. - Identify how to construct the sensitive type (constructor,
from(),new(), raw struct literal, etc.). - Identify how to obtain a raw pointer to the backing buffer (
as_ref().as_ptr(),.as_ptr(),Box::into_raw, etc.).
2d-ii — Write {poc_output_dir}/ZA-NNNN_<category>.rs
Use the actual types, trait implementations, and function signatures from the Rust crate. Import the crate under test by name. Reference specific struct fields and method names. Every PoC must be in an unsafe block only where necessary (the read_volatile call).
Template:
// PoC for ZA-NNNN: <category>
// Finding: <one-line description>
// Verification: cargo test --manifest-path {poc_output_dir}/Cargo.toml --test ZA-NNNN_<category>
// Exit 0 (test pass, assert holds) = exploitable; non-zero (test fail, assert panics) = not exploitable
#![allow(unused_imports)]
use <crate_name>::<SensitiveType>;
use std::ptr;
#[test]
fn poc_<za_NNNN>_<category>() {
// Construct the sensitive type with the fill pattern
let obj = <SensitiveType>::<constructor>(<fill_args>);
// Capture a raw pointer to the heap-backed buffer BEFORE any drop
let raw: *const u8 = <ptr_expression> as *const u8;
let len: usize = <size>;
// <Perform the vulnerable operation — call function, trigger drop, etc.>
// Volatile-read: if fill pattern persists, the secret was not wiped
let secret_persists = (0..len).any(|i| unsafe {
ptr::read_volatile(raw.add(i)) == 0xAA
});
// assert! holds when secret persists → cargo exits 0 → exploitable
assert!(secret_persists, "Secret was wiped — not exploitable");
}
Per-category adaptations:
MISSING_SOURCE_ZEROIZE — Drop the type and check the backing buffer:
let obj = SensitiveKey::new([0xAAu8; 32]);
let raw = obj.as_slice().as_ptr(); // or field accessor
drop(obj); // or let scope end
// volatile-read raw...
SECRET_COPY — Perform the copy/clone/From operation, drop the original, check the copy:
let original = SensitiveKey::new([0xAAu8; 32]);
let copy = original.clone(); // or Copy assignment, or From::from()
let raw = copy.as_slice().as_ptr();
drop(original);
// Do NOT drop copy yet — read_volatile the copy
let secret_persists = (0..32).any(|i| unsafe { ptr::read_volatile(raw.add(i)) == 0xAA });
drop(copy);
assert!(secret_persists, "Copy was wiped — not exploitable");
PARTIAL_WIPE — Check only the tail bytes beyond the identified wipe region:
let obj = SensitiveStruct { key: [0xAAu8; 64] }; // full size = 64
let raw = obj.key.as_ptr();
drop(obj);
// wiped_size from finding evidence = 32; check bytes 32..64
let tail_persists = (32usize..64).any(|i| unsafe { ptr::read_volatile(raw.add(i)) == 0xAA });
assert!(tail_persists, "Tail was wiped — not exploitable");
Pointer validity note: read_volatile after drop() is only safe when the buffer is heap-allocated (e.g., Box<[u8]>, Vec<u8>, or a struct that owns heap data). If the sensitive type is stack-only (no heap fields), the raw pointer is dangling after drop — in that case, use Box::new(obj) to force heap allocation and obtain the pointer via Box::into_raw.
2d-iii — Generate or Update {poc_output_dir}/Cargo.toml
On the first Rust PoC, create the file:
[package]
name = "zeroize-audit-pocs"
version = "0.1.0"
edition = "2021"
[dev-dependencies]
<crate_name> = { path = "<absolute_path_to_cargo_manifest_dir>" }
For each Rust PoC, append a [[test]] entry:
[[test]]
name = "ZA-NNNN_<category>"
path = "ZA-NNNN_<category>.rs"
Test names must be valid Rust identifiers: replace - with _ (e.g., ZA-0001 → za_0001).
2d-iv — Record in Manifest
For enabled Rust PoCs:
{
"finding_id": "ZA-0001",
"category": "MISSING_SOURCE_ZEROIZE",
"language": "rust",
"poc_file": "ZA-0001_missing_source_zeroize.rs",
"poc_supported": true,
"compile_cmd": "cargo test --manifest-path {poc_output_dir}/Cargo.toml --no-run --test za_0001_missing_source_zeroize",
"run_cmd": "cargo test --manifest-path {poc_output_dir}/Cargo.toml --test za_0001_missing_source_zeroize",
"compile_opt": "debug",
"technique": "volatile_read_after_drop",
"target_function": "<function name>",
"target_variable": "<variable name>",
"notes": "<what the PoC does and what exit 0 means>"
}
For excluded Rust categories:
{
"finding_id": "ZA-0005",
"category": "STACK_RETENTION",
"language": "rust",
"poc_file": null,
"poc_supported": false,
"reason": "STACK_RETENTION — stack probe requires unsafe ASM intrinsics; not implemented for Rust"
}
Step 3 — Write Makefile
Generate {poc_output_dir}/Makefile that builds all PoC targets:
-
Extract per-TU compile flags using:
uv run --no-project {baseDir}/tools/extract_compile_flags.py \ --compile-db <compile_db> --src <source_file> --format lines -
For each PoC target, set:
- The correct optimization level for the finding category (see Category Techniques)
- Include paths from the target's compile flags
- Object file dependencies if using link-based inclusion
-
Add an
alltarget and individual targets for each PoC. -
Add a
run-alltarget that compiles and runs each PoC, printing the finding ID and result.
Step 4 — Write Manifest
Write {poc_output_dir}/poc_manifest.json:
{
"poc_version": "0.2.0",
"findings_count": 5,
"pocs": [
{
"finding_id": "ZA-0001",
"category": "MISSING_SOURCE_ZEROIZE",
"poc_file": "poc_za_0001_missing_source_zeroize.c",
"makefile_target": "poc_za_0001_missing_source_zeroize",
"compile_opt": "-O0",
"technique": "volatile_read_after_return",
"target_function": "handle_key",
"target_variable": "session_key",
"source_file": "src/crypto.c",
"notes": "Calls handle_key() with minimal valid arguments, then volatile-reads session_key buffer to check if the secret persists after the function returns"
}
]
}
Each entry documents the technique used, the target function and variable, and a human-readable description of what the PoC does. This manifest is consumed by the verification agent to check PoC correctness.
Step 5 — Write Notes
Write {poc_output_dir}/notes.md summarizing:
- Number of PoCs generated
- For each PoC: finding ID, category, technique used, target function/variable
- Any findings for which the PoC may be unreliable (e.g., complex error path triggers, allocator-dependent behavior)
- Source files read during PoC crafting
Category Techniques
Use the appropriate technique for each finding category. Refer to {baseDir}/references/poc-generation.md for detailed strategies and pitfalls.
| Category | Opt Level | Core Technique |
|---|---|---|
MISSING_SOURCE_ZEROIZE |
-O0 |
Fill buffer with secret pattern, call target function, volatile-read the buffer after return |
OPTIMIZED_AWAY_ZEROIZE |
Level from compiler_evidence.diff_summary |
Same as above, but compile at the optimization level where the wipe disappears |
STACK_RETENTION |
-O2 |
Call target function, then immediately call stack_probe() to detect secret bytes in the prior stack frame |
REGISTER_SPILL |
-O2 |
Target the specific stack offset from ASM evidence (-N(%rsp)) using stack_probe() |
SECRET_COPY |
-O0 |
Call the function that copies the secret, volatile-read the copy destination |
MISSING_ON_ERROR_PATH |
-O0 |
Provide inputs that trigger the error return path, then volatile-read the secret buffer |
PARTIAL_WIPE |
-O0 |
Fill entire buffer, call target, volatile-read the tail beyond the incorrectly-sized wipe region |
NOT_ON_ALL_PATHS |
-O0 |
Provide inputs that force execution down the path lacking the wipe, then volatile-read |
INSECURE_HEAP_ALLOC |
-O0 |
Use heap_residue_check() with the target's allocation size. Do NOT compile with -fsanitize=address |
LOOP_UNROLLED_INCOMPLETE |
-O2 |
Fill buffer, call target at -O2, volatile-read the tail beyond the unrolled region |
NOT_DOMINATING_EXITS |
-O0 |
Provide inputs that reach the non-dominated exit path, then volatile-read |
Output
Write to {poc_output_dir} (default {workdir}/poc/):
| File | Content |
|---|---|
poc_common.h |
Shared volatile read, stack probe, heap residue helpers (C/C++ PoCs) |
poc_*.c |
Per-finding bespoke C/C++ PoC source files |
ZA-NNNN_<category>.rs |
Per-finding Rust PoC test files (MISSING_SOURCE_ZEROIZE, SECRET_COPY, PARTIAL_WIPE only) |
Cargo.toml |
Rust PoC test harness manifest (created on first Rust PoC; updated for each subsequent one) |
Makefile |
Build and run targets for C/C++ PoCs with correct flags per finding |
poc_manifest.json |
Manifest listing all PoCs (C/C++ and Rust) with technique, target, and language details |
notes.md |
Summary of PoCs crafted, techniques used, reliability concerns |
Compilation and running of PoCs is handled by the orchestrator in Phase 5 (PoC Validation & Verification), not by this agent.
Crafting Principles
- Read before writing: Always read the actual source code before writing the PoC. Never guess function signatures or variable types.
- Minimal but complete: Include only the setup necessary to exercise the vulnerable code path. Don't import the entire project — just what the PoC needs.
- Explicit over implicit: Document in comments what the PoC is testing and why. Someone reading the PoC should understand the vulnerability without consulting the finding report.
- Match the source exactly: Use the exact variable names, types, and sizes from the source code. If the source uses
uint8_t key[32], the PoC usesuint8_t key[32]— notchar buf[256]. - Exercise the specific path: For path-coverage and error-path findings, craft inputs that force execution through the exact path identified in the finding. Comment the input choice rationale.
Error Handling
- Source file unreadable: Log the error in notes.md, write a stub PoC with a comment explaining the failure, mark in manifest.
- Function signature unclear: Best-effort PoC with documented assumptions. Note uncertainty in manifest.
- Always write
poc_manifest.jsonandnotes.md— even if some PoCs couldn't be crafted.