Skip to content

fix(vmafx-tune-go): deep bug audit — 5 fixes (JSON NaN, subprocess hang, codec cache) - #505

Merged
lusoris merged 1 commit into
masterfrom
fix/vmafx-tune-go-audit-20260531
May 31, 2026
Merged

lusoris merged 1 commit into
masterfrom
fix/vmafx-tune-go-audit-20260531

Conversation

@lusoris

@lusoris lusoris commented May 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Deep audit of cmd/vmafx-tune/ (Stage-1 Go CLI per ADR-0705 / ADR-0713) and its
pkg/{report,bisect,encoder,ladder} dependencies. Five distinct bugs bundled
into one PR, every fix shipping with a focused regression test.

  • Bug 1 — JSON NaN propagation in bisect_samples. report.EmitJSON and
    cmd/vmafx-tune/cmd.emitSweepJSON sanitised only the top-level row floats,
    leaving []bisect.Sample declared as raw float64 in the wire shape. A
    single non-finite VMAF / bitrate / encode-time sample (e.g. propagated from
    a corrupt vmaf XML mean) crashed json.MarshalIndent with "unsupported value: NaN" and broke the Python ↔ Go parser-parity invariant (AGENTS.md
    rebase-sensitive invariant chore(docs): update mkdocs site_url to vmafx.github.io/vmafx #2). New public helper
    report.SanitizeBisectSamples walks the nested floats; mirrored coverage
    added in emitLadderJSON for Cloud + Hull + Renditions across
    BitratekBps, VMAF, TargetVMAF.

  • Bug 2 — parseVMAFXMLMean accepted the literal tokens "NaN" / "+Inf"
    / "-Inf"
    . Go strconv.ParseFloat returns those silently. The parser now
    rejects non-finite means at the source so the bisect step records a score
    failure rather than seeding bug 1 failure mode.

  • Bugs 3 / 4 / 5 — Subprocess hang risk (ffmpeg, vmaf, ffprobe / codec
    discovery). Every exec.Command in pkg/encoder and pkg/bisect ran with
    no context and no timeout, so a hung child pinned the sweep forever. All
    switched to exec.CommandContext with per-stage upper bounds overridable
    via VMAFX_TUNE_ENCODE_TIMEOUT (default 60m), VMAFX_TUNE_SCORE_TIMEOUT
    (default 30m), VMAFX_TUNE_PROBE_TIMEOUT (default 30s).

  • Bug 6 — Codec-discovery cache stale-key. The prior sync.Once gate
    locked in whichever ffmpeg binary path was probed first, with
    _ = ffmpegBin masquerading as cache invalidation. Cache key is now the
    binary path; calling with a different path triggers a re-probe and replaces
    the cache.

Plus a pkg/encoder/discover_test.go update to the new per-binary cache
shape (removed the sync.Once references) and minor gofmt alignment of
the timeout helpers.

No ADR — bug fixes per CLAUDE §12 r8.

Test plan

  • go test -race -count=1 -timeout=180s ./cmd/vmafx-tune/... ./pkg/bisect/... ./pkg/encoder/... ./pkg/ladder/... ./pkg/report/... — all packages pass.
  • go vet ./... clean.
  • gofmt -l ... clean.
  • pre-commit run --files <all touched files> clean.
  • Python parser parity: pytest tools/vmaf-tune/tests/test_bisect.py tools/vmaf-tune/tests/test_compare.py tools/vmaf-tune/tests/test_compare_rate_quality_sweep.py tools/vmaf-tune/tests/test_compare_no_bisect.py — 88/88 pass.
  • Reproducer for the JSON NaN bug — see TestEmitJSON_NaNInBisectSamplesDoesNotCrash (pkg/report/sanitize_test.go); pre-fix it would crash with "json: unsupported value: NaN", post-fix it round-trips cleanly through json.Unmarshal.
  • Reproducer for the codec-cache bug — see TestProbeAvailableCodecs_CacheRespectsBinaryPath (pkg/encoder/discover_cache_test.go); pre-fix the second call returns the stale first-binary result, post-fix it re-probes.

Per-surface notes (CLAUDE §12 r10)

  • cmd/vmafx-tune/cmd/{compare,ladder}.go — public-CLI emitter behaviour
    (NaN sanitisation) hardened; no user-visible CLI flag change, so the
    doc-substance rule has nothing to update.
  • pkg/report.SanitizeBisectSamples — new exported helper; internal to the
    Go binary (not a libvmaf C-API surface), no docs/api/ update needed.
  • New env-var knobs VMAFX_TUNE_{ENCODE,SCORE,PROBE}_TIMEOUT — surfaced in
    the changelog fragment and rebase-notes for operator discoverability.

Deep-dive deliverables (CLAUDE §12 r11)

  1. Research digest: no digest needed (bug fix in fork-local Go package).
  2. Decision matrix: no alternatives — every fix is the only-correct shape
    (NaN→null, ctx with timeout, cache-key on binary path).
  3. AGENTS.md invariant note: rebase invariants in
    cmd/vmafx-tune/AGENTS.md already cover NaN coercion (chore(docs): update mkdocs site_url to vmafx.github.io/vmafx #2) and the
    bisect midpoint (chore(deps): Pin dependencies #3); no edits needed.
  4. Reproducer / smoke command: see Test plan above.
  5. Changelog fragment: changelog.d/fixed/0979-vmafx-tune-go-deep-bug-audit.md.
  6. Rebase notes: docs/rebase-notes.md entry added.

Scope guardrails

  • Draft-only; no admin bypass; standard pre-commit hooks all run.
  • Pre-push: go test -race, go vet, gofmt, pre-commit run all clean.
  • Python parser-parity test sweep run locally; 88/88 green.
  • AHEAD safety: rebased onto current origin/master (1 ahead, 0 behind at push time).
  • docs/state.md row added per CLAUDE §12 r13.

🤖 Generated with Claude Code

@lusoris
lusoris marked this pull request as ready for review May 31, 2026 21:57
…ng, codec cache)

Deep audit of cmd/vmafx-tune/ (Stage-1 Go CLI per ADR-0705 / ADR-0713) and its
pkg/{report,bisect,encoder} dependencies, closing five distinct defects in one
PR — every fix ships with a regression test.

1. JSON NaN propagation in bisect_samples
   report.EmitJSON and cmd/vmafx-tune/cmd.emitSweepJSON sanitised only the
   top-level row floats, leaving []bisect.Sample declared as raw float64 in the
   wire shape. One non-finite sample (e.g. from a corrupt vmaf XML mean)
   crashed json.MarshalIndent with "unsupported value: NaN" and broke AGENTS.md
   rebase-sensitive invariant #2 (Python ↔ Go parser parity). New public
   report.SanitizeBisectSamples walks the nested floats; mirrored in
   emitLadderJSON for Cloud + Hull + Renditions across BitratekBps, VMAF,
   TargetVMAF.

2. parseVMAFXMLMean accepted "NaN" / "+Inf" / "-Inf"
   Go strconv.ParseFloat returns those tokens without error, so a corrupt vmaf
   XML mean fed non-finite scores into bisect.Sample. Parser now rejects
   non-finite means at the source so the bisect step records a score failure
   rather than propagating a corrupt value.

3,4,5. Subprocess hang risk (ffmpeg, vmaf, ffprobe)
   Every exec.Command in pkg/encoder (ffmpeg encode, ffprobe bitrate probe,
   codec discovery) and pkg/bisect (vmaf scoring) ran with no context and no
   timeout. A hung child pinned the sweep forever. Switched to
   exec.CommandContext with per-stage upper bounds overridable via
   VMAFX_TUNE_ENCODE_TIMEOUT (default 60m), VMAFX_TUNE_SCORE_TIMEOUT
   (default 30m), VMAFX_TUNE_PROBE_TIMEOUT (default 30s).

6. Codec-discovery cache stale-key
   The prior sync.Once gate locked in whichever ffmpeg binary path was probed
   first, with "_ = ffmpegBin" masquerading as cache invalidation. Cache key
   is now the binary path; calling with a different path triggers a re-probe.
   RefreshCodecCache updates the cache key alongside the map.

Tests
- 8 new Go regression tests across pkg/{report,bisect,encoder} and
  cmd/vmafx-tune/cmd/ — every fix has a focused test that fails without
  the change.
- Existing pkg/encoder/discover_test.go updated to the new per-binary cache
  shape (sync.Once references removed).
- go test -race -count=1 ./cmd/vmafx-tune/... ./pkg/{bisect,encoder,ladder,
  report}/... — all pass.
- go vet ./... clean.
- Python compare-parser tests (88 across tools/vmaf-tune/tests/test_{bisect,
  compare,compare_rate_quality_sweep,compare_no_bisect}.py) still pass —
  verifies the JSON schema-v1/v2 parser parity invariant.

Docs
- changelog.d/fixed/0979-vmafx-tune-go-deep-bug-audit.md
- docs/rebase-notes.md (fork-local; no Netflix upstream counterpart)
- docs/state.md (T-VMAFX-TUNE-GO-DEEP-BUG-AUDIT-2026-05-31 closed)

Bug fixes; no ADR per CLAUDE §12 r8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@lusoris
lusoris force-pushed the fix/vmafx-tune-go-audit-20260531 branch from 9df521d to 029ec5f Compare May 31, 2026 21:57
@lusoris
lusoris merged commit 4cdcd69 into master May 31, 2026
33 of 56 checks passed
@lusoris
lusoris deleted the fix/vmafx-tune-go-audit-20260531 branch May 31, 2026 21:57
lusoris added a commit that referenced this pull request May 31, 2026
…510)

Re-run of the gosec static-security scan against the post-#505 / post-#509-close
master tip. PR #509 had source conflicts with PR #505 on pkg/bisect/bisect.go
and pkg/encoder/encoder.go; this v2 sweep applies the same security-hardening
fixes while preserving the ctx + per-stage timeout that #505 added.

Findings: 38 raw, 10 in generated/cgo trampoline code (excluded via
-exclude-generated), 26 fork-original. All 26 source findings now discharged.
Post-fix `gosec -exclude-generated ./...` returns 0 issues.

Real bug fixed:
  - G304 in cmd/vmafx-mcp/impl.go::describeModel — joined caller-supplied
    `name` onto repo root and os.Stat-ed the result, allowing
    {"name": "../../../etc/passwd"} to escape libvmaf.AllowedRoots().
    Routed through libvmaf.ValidatePath; regression test at
    cmd/vmafx-mcp/impl_gosec_test.go::TestDescribeModelRejectsTraversal.

Hardening:
  - G306/G301 in cmd/vmafx-tune/cmd/compare.go::writeOutput — tightened to
    0o600 (file) / 0o750 (directory).
  - G104 in cmd/vmafx-mcp/impl.go::runVmafScore — outFile.Close() and
    os.Remove(outPath) return values now checked.
  - G204/G304 ×22 — `//nolint:gosec` comments rewritten to
    `// #nosec G<rule> -- <citation>` (gosec ignores nolint:gosec).
  - G202 in cmd/vmafx-controller/queue/queue.go — verified safe (only
    repeatCommaQ output concatenated; status values bind via placeholders).

CI gate: gosec step in .github/workflows/go-ci.yml + make lint-go target.

Differs from PR #509:
  - pkg/bisect/bisect.go: keeps PR #505's exec.CommandContext +
    scoreTimeout() — just adds the #nosec G204 citation.
  - pkg/encoder/encoder.go: keeps PR #505's exec.CommandContext +
    encodeTimeout()/probeTimeout() — same.

Verification:
  - gosec -exclude-generated -quiet ./... → 0 findings (was 26)
  - go vet ./... clean
  - go build ./... clean
  - go test -race ./cmd/vmafx-mcp/ -run TestDescribeModel → 5/5 pass
  - go test -race ./... (skip vmafx-operator envtest) → all packages OK
    except TestHandleVmafScore_RoutesToDirect + TestHandleDescribeModel_DirectAugments
    in cmd/vmafx-mcp/ which require libvmaf.so and fail identically on master
    (verified via stash + re-run).
  - bash scripts/ci/check-adr-numbering.sh → PASS
  - bash scripts/ci/check-copyright.sh → PASS
  - pre-commit run --files → PASS

Refs ADR-0983.

Co-authored-by: Lusoris <lusoris@pm.me>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@lusoris lusoris added this to the 1.0.0 — First release milestone Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant