sc-compose Adversarial Fuzz Campaign -- 20260811-3

Session sc-compose-fuzz-20260811-3 -- 4 workers, 260 bounded iterations against a 105-file real-world corpus

Generated: 2026-08-11

Source: sc-adversarial-fuzz-probe (4 background workers)

FAIL

Summary

Adversarial fuzz campaign sc-compose-fuzz-20260811-3 ran 4 background workers (shape-probe, template-probe, boundary-probe, differential-probe) against a 105-file real-world corpus (atm-core:47, data-sourcegenerators:31, p3-documentation:5, HitlCore:22), partitioned deterministically via SHA-256(seed 157 + filepath) mod 4, for a total of 260 bounded iterations. 3 confirmed bugs (FUZZ-001, FUZZ-002, FUZZ-003) were found and promoted as RED (intentionally failing) regression tests, following this repo's test-first/fix-later pattern; no production code was modified or committed, and no PR was opened. Two additional candidates (template-probe-001, differential-probe-001) were traced to documented, intentional behavior and are not filed as bugs.

Fuzz run descriptionIterationsPassResult
Recursive JSON/YAML values, mixed arrays, and ingress parity against the corpus partition 46 45/46 FAIL
Nested loops, control flow, includes, delimiters, and output against the corpus partition 100 98/100 FAIL
Malformed inputs, stable diagnostics, and path boundaries against the corpus partition 85 84/85 FAIL
Baseline comparison, metamorphic relations, and determinism against the corpus partition 29 29/29 PASS

All 3 confirmed bugs remain unresolved pending a production fix: FUZZ-001 (template-init/render JSON round-trip in crates/sc-compose/src/commands/template_init.rs and crates/sc-composer/src/renderer.rs), and FUZZ-002/FUZZ-003 (clap error paths bypassing the --json DiagnosticEnvelope contract per FR-8a, shared fix target in crates/sc-compose/src/main.rs / crates/sc-compose/src/cli/mod.rs). A duplicate check against GitHub issues (#166-170, #240-254, #268-273, #293, #370-375) found no existing coverage; all three are new findings.

Adversarial fuzz worker

shape-probe

Session sc-compose-fuzz-20260811-3 · Worker shape-probe

FAIL

Fuzz run descriptionIterationsPassResult
Recursive JSON/YAML values, mixed arrays, and ingress parity against the corpus partition 46 45/46 FAIL

Inputs exercised

CaseTemplate / inputOutcome
FUZZ-001template-init on a real JSON file with a quoted string value, then render the generated templateFAIL: render output is {"worktree_path": "\"/tmp/wt\""}, not valid round-trip JSON, exit 0, no diagnostic
shape-baseline-recursive-yamlDeeply nested YAML frontmatter with mixed arrays/maps from atm-core corpus filesPASS: recursive structures parse and round-trip through validate --json without panics; only expected missing-required-variable diagnostics observed

Findings

FUZZ-001

Minimal template / frontmatter
payload.json.j2: {"worktree_path": "{{ worktree_path }}"}
Input
--pass 1 --var worktree_path=/tmp/wt (render); template-init used --pass 1 --var worktree_path=/tmp/wt against a concrete file containing "worktree_path": "/tmp/wt"
Expected
Running template-init on a concrete JSON file and then rendering the generated template with the same variable value should reproduce the original document byte-for-byte (a round-trip guarantee), or fail loudly with a diagnostic if that guarantee cannot hold for the substituted value's type.
Observed
template-init substitutes value text for a {{ var }} token while preserving surrounding literal quote characters verbatim, producing "worktree_path": "{{ worktree_path }}". crates/sc-composer/src/renderer.rs's legacy_auto_escape_callback applies AutoEscape::Json to any template whose name (after stripping .j2/.jinja2/.jinja) ends in .json, which unconditionally wraps every substituted string value in its own quotes. Re-rendering therefore produces {"worktree_path": "\"/tmp/wt\""} -- invalid round-trip JSON -- with exit 0 and zero diagnostics. Reproduced deterministically 3/3 runs via the release binary.
Requirement / ADR
docs/requirements.md FR-8a documents the `render --json` payload schema but does not state a round-trip invariant between template-init output and a subsequent render. No requirement or ADR currently covers a template-init-then-render round-trip guarantee for *.json templates; the only documented contract for JSON auto-escaping is internal (renderer.rs unit tests asserting bare/unquoted placeholders are the supported input shape for *.json templates).
Requirement / ADR follow-up
No requirement or ADR currently covers this behavior. This is a genuine contract gap between two first-class CLI commands (template-init and render) rather than an intentional boundary: template-init has no awareness of the JSON auto-escape rule it is about to violate, and a user following the documented template-init workflow on any JSON file with a string value will silently corrupt output. Recommend either teaching template-init to strip the surrounding literal quotes when the target file is *.json (so the substituted token is bare, matching the auto-escape contract), or documenting the round-trip hazard explicitly in template-init --help and README.md. Owner: sc-compose CLI maintainer (template_init.rs), since the fix is command-specific, not renderer-wide.
Root cause
crates/sc-compose/src/commands/template_init.rs performs literal substring replacement, preserving the value's original surrounding quote characters verbatim. crates/sc-composer/src/renderer.rs legacy_auto_escape_callback applies AutoEscape::Json to any *.json-named template, which re-quotes every substituted string value. The two commands' contracts are inconsistent for the *.json case: template-init assumes pre-quoted placeholders, render's JSON auto-escape assumes bare placeholders.
Recommended fix
In template_init.rs, when the target file's stripped extension is json, detect and consume the surrounding quote characters as part of the replacement span (so the generated token is bare, e.g. "worktree_path": {{ worktree_path }}), matching the auto-escape contract already exercised by renderer.rs's existing unit tests. Add a regression test asserting template-init followed by render round-trips a JSON value byte-for-byte.

Adversarial fuzz worker

template-probe

Session sc-compose-fuzz-20260811-3 · Worker template-probe

FAIL

Fuzz run descriptionIterationsPassResult
Nested loops, control flow, includes, delimiters, and output against the corpus partition 100 98/100 FAIL

Inputs exercised

CaseTemplate / inputOutcome
FUZZ-003render --json --all --brace-count 3 --file t.j2 --root <root>FAIL: --all conflicts_with_all --brace-count triggers clap's own error path; empty stdout, plain-text stderr, --json ignored
template-probe-001--pass N --var/--var-file scoped inputs do not appear in `sc-compose render --help`INTENTIONAL BOUNDARY: --pass is parsed from raw argv via parse_pass_inputs and stripped before clap ever sees it (filtered_args_for_clap), by design; documented in README.md's --pass N --var table row, just help-text-omitted -- not filed as a bug

Findings

FUZZ-003

Minimal template / frontmatter
t.j2: "Hello {{ name }}\n"
Input
sc-compose render --json --all --brace-count 3 --file t.j2 --root <fixture>
Expected
Per FR-8a, all CLI --json output, including CLI-usage/argument errors, must be delivered as the versioned DiagnosticEnvelope on stdout.
Observed
--all is declared conflicts_with_all against --brace-count and --variable-delimiters in crates/sc-compose/src/cli/schema.rs. clap enforces that conflict before sc-compose's application layer runs, printing plain-text usage text to stderr and leaving stdout completely empty, even though --json was explicitly requested. Reproduced deterministically 3/3 runs.
Requirement / ADR
docs/requirements.md FR-8a: "CLI --json output must use the versioned DiagnosticEnvelope as the canonical transport format" -- stated without a carve-out for CLI-usage/argument-parsing errors.
Requirement / ADR follow-up
No new requirement or ADR is needed; FR-8a already covers this case as written. The gap is a missing test/implementation guard, not a documentation gap.
Root cause
clap's own error-printing path (triggered by a conflicts_with_all argument-group violation, declared in crates/sc-compose/src/cli/schema.rs on RenderArgs.brace_count/variable_delimiters) runs and exits before sc-compose's application-layer --json rendering logic in main.rs ever executes, so the error never passes through the DiagnosticEnvelope wrapper.
Recommended fix
Detect --json early (from raw argv, before/alongside the existing --pass pre-scan) and, on any clap::Error, wrap the rendered clap usage text inside a DiagnosticEnvelope with an appropriate ErrConfigParse-family diagnostic code instead of letting clap print and exit directly. Shared fix target with FUZZ-002 (same root-cause family: clap error path bypasses --json).

Adversarial fuzz worker

boundary-probe

Session sc-compose-fuzz-20260811-3 · Worker boundary-probe

FAIL

Fuzz run descriptionIterationsPassResult
Malformed inputs, stable diagnostics, and path boundaries against the corpus partition 85 84/85 FAIL

Inputs exercised

CaseTemplate / inputOutcome
FUZZ-002validate --json --var novalue (missing key=value separator)FAIL: clap's custom value_parser (parse_var) error path bypasses --json; empty stdout, plain-text stderr
boundary-baseline-path-confinementSymlink and ../ escape attempts against corpus rootsPASS: all escape attempts rejected with ERR_INCLUDE_ESCAPE-family diagnostics, no existence-oracle leak

Findings

FUZZ-002

Minimal template / frontmatter
n/a (no template needed; CLI argument parsing only)
Input
sc-compose validate --json --var novalue
Expected
Per FR-8a, all CLI --json output, including CLI-usage/argument errors, must be delivered as the versioned DiagnosticEnvelope on stdout.
Observed
crates/sc-compose/src/cli/pass_input.rs's parse_var (registered as CommonArgs.vars's clap value_parser) rejects novalue with a plain "expected key=value" error. This is clap's own custom-value_parser error-printing path, which runs and exits before the application's --json layer, so stdout is completely empty and stderr carries plain clap usage text. Reproduced deterministically 3/3 runs.
Requirement / ADR
docs/requirements.md FR-8a: "CLI --json output must use the versioned DiagnosticEnvelope as the canonical transport format" -- stated without a carve-out for CLI-usage/argument-parsing errors.
Requirement / ADR follow-up
No new requirement or ADR is needed; FR-8a already covers this case as written. The gap is a missing test/implementation guard, not a documentation gap.
Root cause
clap's own value_parser error path (crates/sc-compose/src/cli/pass_input.rs::parse_var, invoked directly by clap during argument parsing for --var) runs and exits before sc-compose's application-layer --json rendering logic ever executes, so the error never passes through the DiagnosticEnvelope wrapper. Same root-cause family as FUZZ-003 (clap error path bypasses --json), triggered via a different clap mechanism (custom value_parser vs. conflicts_with_all).
Recommended fix
Detect --json early from raw argv and, on any clap::Error (covering both custom value_parser failures and argument-group conflicts), wrap the rendered clap usage text inside a DiagnosticEnvelope with an appropriate ErrConfigParse-family diagnostic code instead of letting clap print and exit directly. Shared fix target with FUZZ-003.

Adversarial fuzz worker

differential-probe

Session sc-compose-fuzz-20260811-3 · Worker differential-probe

FAIL

Fuzz run descriptionIterationsPassResult
Baseline comparison, metamorphic relations, and determinism against the corpus partition 29 29/29 PASS

Inputs exercised

CaseTemplate / inputOutcome
differential-probe-001validate --json vs render --json divergence on the same real corpus file/varsINTENTIONAL BOUNDARY: validate and render are documented as distinct commands with different scopes (validate --help: "Validate templates without rendering output"); no metamorphic/parity requirement is stated or implied between them
differential-baseline-determinismRepeat render --json runs of identical corpus fixture across fresh processesPASS: byte-identical output and diagnostics across repeated fresh-process runs

Findings

Recommendations

Metadata

Session IDsc-compose-fuzz-20260811-3
Worktree/Users/randlee/Documents/github/sc-compose
Targetfull (var-file, frontmatter, resolver, renderer, includes, cli)
Baseline reforigin/develop
Seed157
Max workers4
Cases per worker (requested)100
Per-worker timeout (s)120
Execution mode4 background sc-adversarial-fuzz-probe workers (shape-probe, template-probe, boundary-probe, differential-probe) spawned via Agent tool run_in_background:true, aggregated by correlation ID
Corpus105 real-world .j2 templates from four external read-only repos (atm-core:47, data-sourcegenerators:31, p3-documentation:5, HitlCore:22), partitioned deterministically across the 4 workers via SHA-256(seed 157 + filepath) mod 4
Total iterations260
Confirmed bugs3
Intentional boundaries2
Inconclusive0
Promoted regression tests3
Promote regressions requestedTrue
No PR / no pushTrue
Regression promotion notepromote_regressions requested as enabled. Three confirmed bugs (FUZZ-001, FUZZ-002, FUZZ-003) were promoted as RED (intentionally failing) regression tests, following this repo's established test-first/fix-later pattern for fuzz-discovered bugs. No production code was modified or committed.
Duplicate-check methodgh issue list --repo randlee/sc-compose --search "fuzz OR json envelope OR template-init round-trip" --state all --limit 30
Duplicate-check resultNo existing open or closed GitHub issue matches FUZZ-001 (template-init/render JSON round-trip), FUZZ-002 (--var value-parser bypasses --json), or FUZZ-003 (--all/--brace-count conflict bypasses --json). Prior fuzz-derived issues (#166-170, #240-254, #268-273, #293, #370-375) cover distinct subsystems (include resolver, var-file ingress, format-aware escaping, frontmatter). All three are new findings.