goav1 is a pure-Go AV1 decoder intended for realtime/WebRTC and
batch decoding workloads. This document describes the security posture
of the project, the threat model it targets, the mitigations already in
place, and how to report a vulnerability.
It is meant for production integrators who must reason about the blast radius of an untrusted bitstream reaching the decoder.
If you believe you have found a security issue in goav1, please do
not open a public GitHub issue. Instead:
- Email the maintainer at
thesyncim@gmail.comwith the subject linegoav1 security reportand a description of the issue, ideally including a minimal reproducer (an.ivf,.obu, or captured RTP payload), the goav1 commit/tag, and the Go toolchain version. - If you prefer, use GitHub's private vulnerability reporting on
https://github.com/thesyncim/goav1(Security tab > "Report a vulnerability") so the disclosure stays inside GitHub.
Please give the maintainer a reasonable disclosure window before going public. As a single-maintainer open-source project, target response times are best-effort:
| Stage | Target |
|---|---|
| Acknowledgement | within 5 business days |
| Initial triage | within 10 business days |
| Fix or mitigation plan | within 30 days of triage |
| Coordinated disclosure | once a fix is available, or 90 days from triage, whichever is sooner |
If a report is declined (out-of-scope, not reproducible, expected behavior, etc.) the maintainer will explain why and, where appropriate, suggest a non-security route to land the change.
goav1 is pre-1.0 and follows a rolling-main release model. Only the
latest commit on main and the most recent tagged release receive
security fixes. Older tags will not be back-patched. Integrators are
expected to pin a recent commit and re-pin during routine dependency
updates.
| Version | Security fixes |
|---|---|
main (HEAD) |
Yes |
| Latest tag | Yes |
| Older tags | No |
The fundamental trust boundary is untrusted compressed AV1 bytes
entering the decoder from a remote or otherwise attacker-controlled
source (WebRTC peer, RTP relay, IVF/OBU file from disk, network
fetch, etc.). Everything downstream of DecoderStream and the public
transport parsers must assume that input bytes are adversarial.
Out of scope:
- Trusted-side encoder misuse once the WebRTC encoder lands, such as invalid local raw-frame buffers or unsupported control combinations supplied by the calling application.
- The host Go runtime, OS kernel, or hardware vulnerabilities.
- Calling-program misuse of caller-owned scratch buffers in a way the documented contract forbids (e.g. mutating a buffer while the decoder still holds a reference to it).
The reachable attack surface from an attacker who controls the input bytes:
- Transport parsers. IVF (
NewIVFIterator), Section 5 temporal-unit OBU streams (NewLowOverheadIterator,NewTemporalUnitIterator), Annex B (NewAnnexBIterator), and the AV1 RTP aggregation header / payload iterator / frame assembler (NewRTPPayloadIterator,AssembleRTPFrame). These walk length-prefixed and LEB128-framed byte slices and must never run off the end of a caller-owned buffer or accept lengths that exceed the payload. - OBU header parser. Forbidden-bit and reserved-bit checks,
size-field presence checks (low-overhead vs. Annex B), temporal
delimiter ordering. Errors are returned, not panicked — see
internal/av1/obu/errors.go. - Sequence / frame header parser. Profile, bit depth, chroma
subsampling, frame size, render size, tile layout, segmentation,
loop-filter, CDEF, loop-restoration, transform-mode, reference-mode,
skip-mode, warp/global-motion, and film-grain parameters. Every
syntax element has a documented numeric domain; out-of-domain values
produce an
ErrInvalid*error instead of a panic. - Tile-group parser and tile decode. Tile sizing, tile-payload
span splitting, entropy reader setup, coefficient decode. Tile-row
and tile-column counts are hard-capped at
MaxTileRows = 64andMaxTileCols = 64(seeinternal/av1/parser/tile.go). - Residual + reconstruct path. Dequantization, inverse transform dispatch (DCT/ADST/IDTX/WHT and the inverse-qmatrix path), residual add into caller-owned planes, intra/inter prediction (directional, filter-intra, CfL, OBMC, compound, warp), and the postfilter pipeline (loop filter, CDEF, super-res, loop restoration, film grain).
- Caller-owned-buffer sizing helpers. The decoder exposes
public preflight sizing helpers (
*Size,*ScratchSize,Required*accessors) so callers can compute exact buffer sizes before binding them. A malformed header that asks for a buffer larger than the caller provides is detected and returned as an error before any out-of-bounds write.
An attacker submits bytes that pass obu framing but violate AV1
syntax constraints inside a sequence header, frame header, tile group,
or coefficient stream — for example, advertising a tile count or
quantizer level outside the legal range, an unsupported transform
class, or a film-grain parameter table that contradicts the sequence
header.
Mitigation. Each parser validates syntax-element domains and returns
ErrInvalid* rather than panicking. Recent hardening for
filmgrain / quantize (commit 744ef85) and the restoration
bordered-block path (commit 7c893f5) explicitly converted previously
panicking branches into typed errors.
An attacker truncates an OBU mid-LEB128, claims a size larger than the remaining buffer, or stuffs the temporal-unit / Annex B framing with nested oversize lengths.
Mitigation. All readers operate on caller-owned byte slices with
bounds-checked indexing. LEB128 reads (internal/av1/bitstream) cap
at the AV1-defined byte count and reject overlong encodings. The OBU
iterator surfaces ErrShortHeader, ErrShortPayload,
ErrSizeMismatch, and ErrInvalidAnnexB for these conditions.
The RTP depacketizer cross-checks fragment lengths against the
remainder of the payload before reassembly. Fuzz coverage:
internal/av1/bitstream/fuzz_test.go::FuzzReadLEB128 and the
public-API fuzz harnesses listed below.
A small adversarial input could in principle trigger a disproportionate amount of allocation, compute, or descriptor churn (a "decoder bomb").
Mitigation.
- Memory. The decoder is a zero-allocation-in-steady-state
design. Buffers (
FramePool, postfilter scratch, residual scratch, loop-restoration frame buffers, side-data arenas) are caller-owned and bound up front via public sizing helpers. The decoder cannot silently allocate large buffers in response to a hostile header — if the header asks for more than the caller has bound, the call returns an error. - Compute. Tile counts are capped at 64x64. Per-frame work is
bounded by the parsed (and validated) frame dimensions. The tile
worker pool is bounded and caller-owned
(
NewTileWorkerPool). - File descriptors. The library opens no files and no sockets. All
I/O is owned by the caller; the public API consumes
[]byteor iterator state. Goroutine fan-out is bounded by the caller-owned worker pool.
goav1 does not handle secret material. There is no key, no MAC, no
session token routed through the decoder. As such, no part of the
decoder claims to be constant-time, and timing of decode operations is
necessarily data-dependent (skip blocks, transform classes, palette
modes, and entropy-coded residuals all branch on bitstream content).
Integrators who multiplex a decoder instance across mutually
distrusting tenants should not infer secrecy properties from decode
timing.
Race conditions inside the decoder are treated as correctness bugs with potential security impact (torn writes into caller planes, double-release of frame-pool surfaces).
Mitigation. The threading model is documented in ARCHITECTURE.md.
State that crosses goroutines is bound to caller-owned scratch with
explicit lifecycle helpers (BeginDecoderFrameWork,
RunDecoderFrameWorkEventWithContext, batch run/finish helpers).
Frame-pool acquire/release is checked. CI runs make test (which
includes -race coverage for the threading and tile packages).
| Layer | Rejects on |
|---|---|
bitstream (LEB128, bit reader) |
Overlong / truncated LEB128, reads past end-of-buffer. |
obu |
Short headers, forbidden/reserved bits, missing size field, size > buffer. |
rtp |
Inconsistent fragment lengths, missing aggregation header, oversize OBU spans. |
parser (sequence/frame/tile) |
Out-of-domain syntax elements, unsupported profile/bitdepth combos, tile-count overflow. |
quantize, filmgrain |
Invalid quantizer indices, malformed grain tables (commit 744ef85). |
restoration |
Bordered-block layout that would read outside the unit (commit 7c893f5). |
frame / memory (pool, scratch) |
Surface size mismatch on acquire; capacity vs. requested-size mismatch on bind. |
| Decoder lifecycle helpers | Event ordering (e.g. BeginDecoderFrameWork before run/finish), surface double-release. |
Beyond these boundaries the decoder treats bytes as already validated for the corresponding layer, so all bounds checks must happen at the boundary above.
- Panic-to-error hardening. Bitstream-driven panics in the
filmgrain,quantize, andrestorationpaths were converted into typed errors so callers can recover gracefully from hostile input. See commits744ef85and7c893f5. There are currently nopanic()calls in non-test code paths in the module. - Fuzz harnesses. The repository ships over 100
Fuzz*harnesses across the public API and internal packages, including:- Public API:
FuzzPublicDecodeAndReconstructDecoderFrameWorkJobResiduals,FuzzPublicDecodeAndRetainDecoderFrameWorkBatchResiduals,FuzzPublicDecoderFrameWorkBatchResidualRunnerSideData,FuzzPublicRunDecoderFrameWorkEventWithResidualRunner,FuzzPublicDecoderFrameWorkSideDataBinding,FuzzPublicDecoderFrameWorkPostFilterScratchContext,FuzzPublicSimpleDecoderIVF,FuzzPublicDecodeTileBlockCoefficients,FuzzPublicParseTileListOBU,FuzzPublicDecodeAndReconstructDecoderFrameWorkBlockCoefficients. - Inverse transforms: WHT, ADST, warp, film grain (commit
ff8ba63). - Inverse-qmatrix dequant path (commit
455ddac). - Palette mode and color-map decode (commit
133a448). internal/av1/bitstream/fuzz_test.go::FuzzReadLEB128.- Decoder stream dispatch across IVF, low-overhead OBU, Annex B,
temporal-unit grouping, and single-OBU input (
FuzzStreamPush). - Tile-list OBU parsing (
FuzzParseTileListOBU). - Entropy reader and CDF state harnesses.
- Prediction harnesses (intra, directional, filter-intra, CFL, static intra, intra edges, DC predictor).
- Frame plane round-trip harnesses (sample plane, bordered sample
plane).
Run the short sweep with
make fuzz-smoke.
- Public API:
- Allocation regression coverage.
make allockeeps the public hot paths zero-allocation, which limits the blast radius of adversarial input as a memory-pressure vector. Locked in across the postfilter dispatch in commite69a428. - Bounds-checked indexing throughout. Tile-count, tile-row, and tile-column counts are capped at 64. Frame and tile geometry helpers validate the requested block against the bound plane window before any write.
- Caller-owned buffers and preflight sizing. Every hot-path
allocation is replaced by a caller-supplied buffer plus a public
*Size/Required*helper. A malformed header that would require more memory than bound is rejected at bind time, before any write. - CI gates.
make ci-local(run in CI) executesfmt-check,vet,test, andalloc.make testvectors-fastruns the committed libaom conformance subset on every push.
The decoder is pure Go with no third-party module dependencies.
go.mod:
module github.com/thesyncim/goav1
go 1.26
There is no go.sum file because the module has no external
require directives. The only imports are from the Go standard
library and github.com/thesyncim/goav1/internal/.... This is a
deliberate design choice — see the "Pure Go. No CGO wrappers around C
codecs." principle in README.md — and substantially reduces the
upstream supply-chain risk.
third_party/upstream holds vendored libaom reference data /
specification artifacts pinned per UPSTREAM.md; it is not a Go
dependency and is not compiled into binaries that use this module.
The module is regularly scanned with govulncheck.
To install and run it locally:
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...Last run against main (Go 1.26.3, govulncheck v1.3.0, DB
https://vuln.go.dev updated 2026-05-22):
No vulnerabilities found.
Because the module has no external dependencies, govulncheck only
reports issues against the standard library symbols actually used by
goav1. Integrators should run govulncheck against their own
binary as well — that scan covers the toolchain version they ship
with.
No security advisories have been issued for goav1 as of the date
of this document. Future advisories will be published as GitHub
Security Advisories on https://github.com/thesyncim/goav1 and
referenced here.