Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
72f83e2
chore(quest): claim quest/m2/media-fuzz.md
kixelated Oct 30, 2026
85c039b
feat(fuzz): fuzz the media parsers untrusted publishers reach
kixelated Oct 30, 2026
047e8c8
style: format the fuzz changes
kixelated Oct 30, 2026
a676c94
fix(fuzz): name the catalog poll result for clippy
kixelated Oct 30, 2026
f941056
Merge origin/main into quest/m2/media-fuzz
kixelated Oct 30, 2026
c2bfb3f
docs(quest): plan the media fuzz findings
kixelated Oct 30, 2026
bd91a2d
docs(quest): record the 2026-10-30 fuzz-findings decisions
kixelated Oct 30, 2026
65647df
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 31, 2026
c03b308
docs(quest): align the mp4-atom bump with main's move to 0.17.0
kixelated Oct 31, 2026
6bcdc65
docs(quest): point wire-fuzz-coverage at the fuzz-findings epic
kixelated Oct 31, 2026
44784ee
Merge base and resolve fuzz coverage and selection conflicts
Nov 1, 2026
bb2bcbe
Merge base and preserve fuzz workspace and selection changes
Nov 1, 2026
206a5fc
Merge origin/main into quest/m2/media-fuzz
kixelated Nov 2, 2026
e29bc0d
quest: link the fuzz-findings epic from the security audit
kixelated Nov 2, 2026
da4ac94
test(rs): keep the fuzz exclusion line within ruff's length limit
kixelated Nov 2, 2026
c3eccc4
Merge origin/main into quest/m2/media-fuzz
kixelated Nov 2, 2026
c107623
Merge remote-tracking branch 'origin/main' into quest/m2/media-fuzz
kixelated Nov 3, 2026
8234e5b
Merge remote-tracking branch 'origin/main' into quest/m2/media-fuzz
kixelated Nov 3, 2026
0d5e12a
quest: move the fMP4 metadata limits to m1
kixelated Nov 3, 2026
c1b0999
Merge remote-tracking branch 'origin/main' into quest/m2/media-fuzz
kixelated Nov 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/hakari.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ platforms = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-pc-win
# exact-versions = true

[traversal-excludes]
workspace-members = ["moq-net-fuzz", "moq-quic-fuzz"]
workspace-members = ["moq-mux-fuzz", "moq-net-fuzz", "moq-quic-fuzz", "moq-rtmp-fuzz"]

[final-excludes]
workspace-members = ["kio-macros"]
2 changes: 1 addition & 1 deletion .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ jobs:
strategy:
fail-fast: false
matrix:
target: [lite, announce, ietf, varint, path, pattern, ingest, request, packet, params, streamid, streams, frame, connection]
target: [lite, announce, ietf, varint, path, pattern, ingest, request, packet, params, streamid, streams, frame, connection, fmp4, mkv, ts, flv, cmaf, annexb, catalog, amf0]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[workspace]
# Keep source-only members discoverable in Maturin's dependency-pruned archive,
# including the separately packaged UniFFI generator.
members = ["demo/carla/bridge", "rs/*", "rs/kio/macros", "rs/moq-net/fuzz", "rs/moq-quic/fuzz"]
members = ["demo/carla/bridge", "rs/*", "rs/kio/macros", "rs/moq-mux/fuzz", "rs/moq-net/fuzz", "rs/moq-quic/fuzz", "rs/moq-rtmp/fuzz"]
default-members = [
"rs/bench-harness",
"rs/kio",
Expand Down
2 changes: 2 additions & 0 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ per-frame CPU growth since #5926.
- [@moq/net group IDs past 2^53](/quest/m1/js-group-u62.md) - JS carries every 62-bit group ID instead of refusing those past 2^53
- [Pre-cut audit findings](/quest/m1/cut-audit/README.md) - fix the crashes, hostile-input costs, and correctness bugs the 2026-10-24 audit of main found
- [Vendored code has every upstream fix it needs](/quest/m1/vendor-sync/README.md) - quinn, libv4l-rs, nvidia-video-codec-sdk, rust-media-libs, cros-codecs, and UniFFI generator fixes are ported or planned, once per release cycle
- [Media fuzz findings](/quest/m1/fuzz-findings/README.md) - the nightly media fuzz targets run clean: mp4-atom, MKV block, TS step, SPS overflow, and catalog encoder bugs they found are fixed
- [fMP4 sample plans, trun counts, and trak counts are bounded](/quest/m1/fmp4-metadata-limits.md) - a `moof` or `moov` within the importer's bounds cannot plan hundreds of megabytes or take quadratic time; 116 bytes cost 584 MB today
- [Mesh dials recover from a stuck flow](/quest/m1/mesh-fresh-port.md) - a relay redials a failing cluster peer from a fresh UDP port, so one dropped 5-tuple can't strand a link
- [Cluster routing](/quest/m1/cluster-routing/README.md) - prove shared routing contracts, implement per-origin routes, and connect outbound local clusters
- [Routing guide](/quest/m1/routing-guide.md) - concepts and practical outbound federation setup
Expand Down
1 change: 0 additions & 1 deletion quest/m1/cut-audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ leaving JS-only redesigns of `js/net` to [rs2ts](/quest/m2/rs2ts-js.md).

## Related

- [Untrusted media parsers have fuzz targets](/quest/m2/media-fuzz.md) - would have caught the media parser findings here
- [Announce interest streams share one route table](/quest/m2/announce-interest-memory.md) - the audit's inherited announce memory finding
- [A parked read of a resumed group reads its budget without re-syncing](/quest/m2/recover-budget.md) - measure-first resume cost
- [Transcode rungs scale off the runtime](/quest/m2/transcode-rungs.md) - the audit's transcode findings
66 changes: 66 additions & 0 deletions quest/m1/fmp4-metadata-limits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# [M] fMP4 sample plans, trun counts, and trak counts are bounded

## Goal

An fMP4 `moov` or `moof` that fits the importer's bounds
(`fmp4::MAX_HELD_BYTES`, `fmp4::MAX_FRAGMENT_SAMPLES`) parses and plans in
memory near `MAX_HELD_BYTES` and in time linear in its size, rather than
hundreds of megabytes or minutes of CPU: a `moof`'s sample plan is bounded
in bytes, a `trun`'s sample count is refused before mp4-atom expands it, and
a `moov`'s tracks are bounded and cost linear time. Each case below has a
test that asserts its bound.

## Plan

Decided 2026-11-03 (maintainer, on
[moq-dev/moq#7313](https://github.com/moq-dev/moq/pull/7313)): the fixes land
here, in m1, not in #7313, because 116 bytes cost 584 MB on any fMP4 or HLS
ingest.

- ✅ Land #7313 as-is, with the fixes in an m1 quest
- Land #7313 as-is, with the fixes staying in m2
- Fix all three in #7313
- Accept the numbers

Earlier, decided 2026-10-31 (maintainer, from
[moq-dev/moq#7232](https://github.com/moq-dev/moq/pull/7232)): measure the
parse amplification #7232's bounds allow, and fix each case or accept it with
the number. [moq-dev/moq#7313](https://github.com/moq-dev/moq/pull/7313)
added the media fuzz targets but cannot measure this: libFuzzer inputs stay a
few KiB, far below the 32 MiB bound. It measured the cases below by hand
instead, through `Import::decode` with moq-mux's counting allocator, as peak
live heap. Every one needs a fix:

| Input | Size | Peak | Note |
|---|---|---|---|
| `moof` with field-less `trun`s declaring 2^21 samples (`MAX_FRAGMENT_SAMPLES`), then an `mdat` header | 116 B | 584 MB | the plan alone, before any `mdat` byte |
| `moof` of 2^21 4-byte `trun` entries (`sample_size` only), then an `mdat` header | 8 MiB | 667 MB (80x) | 684 MB with passthrough |
| `moof` of 4-byte entries filling `MAX_HELD_BYTES` (8.4M entries) | 32 MiB | 369 MB (11x) | refused as `TooManySamples` only after mp4-atom parses it |
| `moov` of repeated audio `trak`s (bbb.mp4's, 477 B each) | 0.24 / 0.48 / 0.95 MB | 35x | CPU quadratic: 0.25 s, 0.9 s, 3.9 s; allocations 96 MB, 360 MB, 1.4 GB |

- Planning costs about 280 B per sample: a `Sample` in each `Run`, a
`Chunk` in the `mdat`'s queue, and the growth of both unsized collections.
Bound the plan's memory, not just the sample count: for example lower
`MAX_FRAGMENT_SAMPLES` until the plan fits near `MAX_HELD_BYTES`,
presize the collections, and shrink `Chunk` (its `Text` variant sets its
size).
- mp4-atom expands each explicit `trun` entry to a 40-byte `TrunEntry`
before the sample count is checked. Refuse the count before parsing it:
read the `trun` sample counts from the held body, or bound a `moof` body
separately from a `moov` one.
- Each `trak` added to a `moov` costs time and allocation in proportion to
the tracks already added. Extrapolated to 32 MiB (about 70,000 `trak`s),
that is over an hour of CPU and a peak above 1 GB. Find the step that
revisits every earlier track (catalog renditions, track creation, or
enrollment), make it linear, and bound the tracks a `moov` declares.

The bounds are moq-mux's public consts. This quest sets their values;
[one policy for oversized container metadata](/quest/m2/import-metadata-policy.md)
documents them later with MKV's.

Public API: possibly new or changed `pub const` bounds in moq-mux. Wire: none.

## Related

- [One policy for oversized container metadata](/quest/m2/import-metadata-policy.md) - documents the importer bounds this quest sets
- [Media fuzz findings](/quest/m1/fuzz-findings/README.md) - the bugs the media fuzz targets found
43 changes: 43 additions & 0 deletions quest/m1/fuzz-findings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Media fuzz findings

## Goal

Every media fuzz target in `nightly.yml` (`rs/moq-mux/fuzz` and
`rs/moq-rtmp/fuzz`) runs a full nightly session without a crash or a
bounded-work violation, so the jobs find new bugs instead of the same ones.
Each fixed finding's minimized input is committed under the target's
`regressions/`, which the merge gate replays.

## Plan

[moq-dev/moq#7313](https://github.com/moq-dev/moq/pull/7313) added the
targets and landed with six nightly jobs red (decided 2026-10-30,
maintainer). Its short local runs found the bugs the children fix; the
minimized inputs are in [its PR comment](https://github.com/moq-dev/moq/pull/7313#issuecomment-6520535160).

Each child commits its input under `regressions/` with its fix, since an
input committed before the fix would fail the merge gate. After the last
child, run every target nightly for a few sessions; a new finding becomes a
child here.

For the crates outside the `kixelated` and `moq-dev` orgs, moq-mux fixes
the bug on its own side and the bug is reported upstream (decided
2026-10-30, maintainer): [austinleroy/webm-iterable#11](https://github.com/austinleroy/webm-iterable/issues/11),
[therishidesai/h264-parser#9](https://github.com/therishidesai/h264-parser/issues/9),
[ScuffleCloud/scuffle#653](https://github.com/ScuffleCloud/scuffle/issues/653)
(scuffle-h265), and [ScuffleCloud/scuffle#654](https://github.com/ScuffleCloud/scuffle/issues/654)
(scuffle-expgolomb). Posting anything further on those repos still needs the
maintainer's approval.

[moq-dev/moq#7234](https://github.com/moq-dev/moq/pull/7234) replaced
webm-iterable's block parsing with moq-mux's own block-header reader, which
fixed the MKV block-header panic; its input is
`rs/moq-mux/fuzz/regressions/mkv/short-block-header`.

## Required

- [moq-mux on a fixed mp4-atom](/quest/m1/fuzz-findings/mp4-atom-bump.md) - waiting on a 0.17.x release containing kixelated/mp4-atom#264, then moq-mux bumps to it and commits the `sgpd` and `iref` regressions
- [TS step scan stays ordered](/quest/m1/fuzz-findings/ts-step-scan.md) - the step detector's look-ahead never subtracts past its start
- [SPS fields cannot overflow](/quest/m1/fuzz-findings/sps-overflow.md) - hostile H.264 and H.265 SPS crop and size fields are refused instead of wrapping
- [mp4-atom iloc is bounded](/quest/m1/fuzz-findings/mp4-atom-iloc.md) - zero-size `iloc` extents cannot allocate megabytes from a few bytes
- [A catalog update reuses its encoder](/quest/m1/fuzz-findings/catalog-encoder.md) - republishing a compressed catalog or HLS group stops allocating a fresh DEFLATE encoder (about 200 KB) each time
30 changes: 30 additions & 0 deletions quest/m1/fuzz-findings/catalog-encoder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# [M] A catalog update reuses its encoder

## Goal

Republishing a compressed track's latest value, as every hang catalog and
HLS update does, no longer allocates a fresh DEFLATE encoder (about 200 KB) per
group. In the `annexb` target, the 202-byte input that caused 24 encoders
and about 4.9 MB of allocation stays inside the fuzz bound, and is committed
under `rs/moq-mux/fuzz/regressions/`.

## Plan

Decided 2026-10-30 (maintainer): the encoder per update is a bug, not an
accepted cost, so the fuzz bound gets no per-update allowance.

Each compressed group is its own DEFLATE stream (draft-lcurley-moq-flate),
so `Flate::deflate` in `rs/moq-net/src/model/group.rs` creates a
`flate::Encoder` on a group's first write and drops it at `finish`. A
publisher that writes one frame per group, as a catalog does, pays the
allocation on every update.

Recommended: keep a finished group's encoder, reset with `Compress::reset`,
on the track's cache account (`cache::Track`) and hand it to the next group
the same track opens, billed to the pool while parked so the cache budget
still counts it. At most one spare per track, so concurrent groups still
allocate their own. The spare lives as long as the track; say in the PR
whether idle compressed tracks holding it is acceptable. The wire format
does not change: a reset encoder starts a cold window, as a new one does.

Public API: none. Wire: none.
28 changes: 28 additions & 0 deletions quest/m1/fuzz-findings/mp4-atom-bump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# [XS] moq-mux on a fixed mp4-atom

## Goal

moq-mux depends on an mp4-atom release with the `sgpd` entry-count bound
from [kixelated/mp4-atom#264](https://github.com/kixelated/mp4-atom/pull/264),
so the 22-byte init that grows a Vec past 3 GB in 0.17.0 fails to decode.
The `cmaf` and `fmp4` targets' `sgpd` and `iref` inputs are committed under
`rs/moq-mux/fuzz/regressions/`.

## Plan

#264 merged on 2026-10-30 as
kixelated/mp4-atom@06a532aabe47ea108343557fec4cf6c646c2a980, after 0.17.0
was published, so 0.17.0 lacks it.

moq-mux is already on 0.17.0
([moq-dev/moq#7308](https://github.com/moq-dev/moq/pull/7308)), which fixes
the `meta/iref.rs` subtract overflow (kixelated/mp4-atom#219), so what remains
is a move within 0.17 and the regression inputs. If the
[iloc fix](/quest/m1/fuzz-findings/mp4-atom-iloc.md) merges before the
release, the same bump covers it.

Public API: none expected in moq-mux. Wire: none.

## Waiting

- Maintainer: cut an mp4-atom 0.17.x release that contains kixelated/mp4-atom@06a532aabe47ea108343557fec4cf6c646c2a980 (agents do not cut it); check that crates.io's latest mp4-atom is above 0.17.0 and that its tag contains that commit
27 changes: 27 additions & 0 deletions quest/m1/fuzz-findings/mp4-atom-iloc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# [S] mp4-atom iloc is bounded

## Goal

An `iloc` box costs mp4-atom heap proportional to its size: in the `cmaf`
target, 31 bytes of `iloc` no longer allocate about 6 MB.

## Plan

`src/meta/iloc.rs` decodes `extent_count` extents per item. When the
offset, length and index sizes are all 0, each extent consumes no input, so
every 6-byte item can push 65,535 extents (about 1.5 MB), and the item count
repeats that.

Fix it in [kixelated/mp4-atom](https://github.com/kixelated/mp4-atom), as
[#264](https://github.com/kixelated/mp4-atom/pull/264) did for `sgpd`. A
plain count-versus-remaining-bytes check, which `sgpd` and `evte` use, would
refuse a valid single zero-size extent at the end of the box. Instead, decode
fails on a zero-size extent (one that takes no bytes of the box) after an
item's first extent, so a valid single zero-size extent still decodes
(decided 2026-10-30, maintainer). The fix does not cut a release.

Then bump moq-mux with the
[mp4-atom bump](/quest/m1/fuzz-findings/mp4-atom-bump.md), or its own bump if
that has landed, and commit the input under `rs/moq-mux/fuzz/regressions/`.

Public API: none expected in moq-mux. Wire: none.
45 changes: 45 additions & 0 deletions quest/m1/fuzz-findings/sps-overflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# [S] SPS fields cannot overflow

## Goal

A hostile H.264 or H.265 SPS reaching moq-mux (fMP4 `avcC`/`hvcC`, Annex-B,
or MPEG-TS) is refused or ignored without integer overflow, so debug builds
do not panic and release builds do not wrap into wrong dimensions. The
`fmp4`, `annexb`, and `ts` targets' SPS inputs are committed under
`rs/moq-mux/fuzz/regressions/` and pass.

## Plan

The fuzzers found:

- h264-parser 0.4.2 `sps.rs:186,197,204`: `(pic_width_in_mbs_minus1 + 1) * 16`
and the crop subtraction overflow on large exp-Golomb values. Since
[moq-dev/moq#7398](https://github.com/moq-dev/moq/pull/7398), moq-mux parses
the H.264 SPS with `h264-reader` and no longer uses h264-parser;
[rechecking the SPS target on h264-reader](/quest/m2/sps-fuzz-h264-reader.md)
decides whether the H.264 half of this quest remains.
- scuffle-h265 0.2.2 `sps/mod.rs:303,305` (`ctb_log2_size_y - min_tb_log2_size_y`
in a `range_check!`) and `sps/scaling_list.rs:90`
(`matrix_id - scaling_list_pred_matrix_id_delta`): subtract overflow. The
scaling-list one also panics in release builds, on the wrapped index at
`:92`.
- scuffle-expgolomb 0.1.5 `read_exp_golomb`: 64 or more leading zeros
underflow `result - 1` (release returns `u64::MAX`).

Validate the SPS fields in moq-mux before calling the parser, refusing an
SPS that would reach those lines (decided 2026-10-30, maintainer). moq-mux
already reads SPS bits itself with `h264_reader::rbsp::BitReader` (the H.265
head in `codec/h265/sps.rs`, and `codec/annexb.rs`). SPS parsing stays on the
crates moq-mux uses now, h264-reader for H.264 and scuffle-h265 plus
`codec/h265/sps.rs` for H.265; it does not move to mp4-atom, which stays
box-level.

Reported upstream, without waiting on a fix:
[therishidesai/h264-parser#9](https://github.com/therishidesai/h264-parser/issues/9)
(filed 2026-10-25 by another moq-mux user, covering the crop and width
overflows), [ScuffleCloud/scuffle#653](https://github.com/ScuffleCloud/scuffle/issues/653)
(scuffle-h265), and [ScuffleCloud/scuffle#654](https://github.com/ScuffleCloud/scuffle/issues/654)
(scuffle-expgolomb). Each has a minimal reproducer. scuffle-h265 was last
released in 2025-05.

Public API: none. Wire: none.
19 changes: 19 additions & 0 deletions quest/m1/fuzz-findings/ts-step-scan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# [S] TS step scan stays ordered

## Goal

The MPEG-TS step detector's look-ahead never reads with `scan < at`, so its
`self.scan - self.at` staging check (`rs/moq-mux/src/container/ts/step.rs`,
`Ahead::scan`) neither overflows in debug nor wraps in release, which cut
the look-ahead short. The `ts` target's 567-byte input is committed under
`rs/moq-mux/fuzz/regressions/` and passes.

## Plan

Find how an `Ahead` gets a `scan` cursor behind its `at` (the input offset of
the packet that showed the step) from the input in the
[findings comment](https://github.com/moq-dev/moq/pull/7313#issuecomment-6520535160),
and fix the cause rather than saturating the subtraction. A unit test in
`step.rs` reproduces it with the module's `pes` helper.

Public API: none. Wire: none.
1 change: 0 additions & 1 deletion quest/m2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ lite-07; bitrate enforcement is not part of this release.
- [Mesh libraries](/quest/m2/mesh/README.md) - an `Endpoint` reaches any enrolled node by endpoint ID over relays or a sparse direct overlay, with address hints and a `.status` track; the mesh binary and UI live in Kixel
- [Byte streams](/quest/m2/stream/README.md) - TCP connections ride MoQ losslessly through relays, proxied per hop rather than as IP packets
- [Release execution](/quest/m2/release/README.md) - publication, installed-artifact proof, cleanup, and downstream adoption after m1
- [Untrusted media parsers have fuzz targets](/quest/m2/media-fuzz.md) - fMP4, MKV, FLV, TS, Annex-B, catalog, and AMF0 parsing fuzzed with a bounded-work invariant
- [One policy for oversized container metadata](/quest/m2/import-metadata-policy.md) - fMP4 and MKV refuse or stream huge metadata by one documented rule
- [Wire fuzz coverage](/quest/m2/wire-fuzz-coverage.md) - the cmsg walks in moq-sock and moq-uring have invariant fuzz targets, and the unsafe peer input reaches runs under Miri
- [VA-API renderer tests](/quest/m2/vaapi-render-tests.md) - the VA-API renderer, decode, and transcode tests run in `just rs gpu` on AMD and Intel and fail instead of skipping, and hosted CI reports them ignored
Expand Down
2 changes: 1 addition & 1 deletion quest/m2/import-metadata-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ Public API: possibly new `pub const` bounds and a shared refusal error in moq-mu

## Related

- [Untrusted media parsers have fuzz targets](/quest/m2/media-fuzz.md) - would measure parsed-state amplification under either policy
- [fMP4 sample plans, trun counts, and trak counts are bounded](/quest/m1/fmp4-metadata-limits.md) - measured what fMP4's bounds let a `moof` or `moov` build, and sets the bounds this policy documents
Loading
Loading