Skip to content

fix(moq-transport): reject present-but-zero subgroup history window - #67

Open
allyblockcast[bot] wants to merge 1 commit into
mainfrom
blo-22245-reject-zero-history-window
Open

fix(moq-transport): reject present-but-zero subgroup history window#67
allyblockcast[bot] wants to merge 1 commit into
mainfrom
blo-22245-reject-zero-history-window

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Aug 6, 2026

Copy link
Copy Markdown

Closes the separable half of MRS-02 per the CTO ruling on BLO-21206. Tracked as BLO-22245.

The bug

recv_subscribe_ok ended with .and_then(NonZeroU64::new), which collapsed two different wire conditions to the same result:

wire old behaviour correct?
key 0x40 absent None → unbounded yes — a conforming generic peer never implemented this private Blockcast extension, and omission is exactly how it signals that
key 0x40 present, 0 None → unbounded no — a silent fallback

The second case is the problem. The peer did implement the extension and sent a meaningless value — a window of 0 groups retains nothing, so it cannot be what any publisher meant. Reinterpreting it as "retain everything" widens retention on the strength of a frame we have just established is broken, on our own fork-added code where no conforming-peer defence applies. That is precisely the silent fallback the engineering principles ban.

What changed

Present-and-zero is now SessionError::ProtocolViolation (MoQT error code 0x3), propagating out of the control-message loop and terminating the session — the same teardown path as the sibling ProtocolViolation returns already in that loop.

The absent path is untouched. It still yields unbounded with no error, and it stays profile-independent: requiring the key is BLO-22244's negotiated wire profile, and this PR deliberately introduces no mandatory-presence enforcement.

Gate: the sender inventory

This change was not allowed to land until a written sender inventory proved no deployed producer emits zero — the G0 record's own precondition. That inventory is pim-multicast-gateway#2079 (docs/g0/blo-22245-sender-inventory.md). Summary:

  • Key 0x40 has exactly one emission site in the whole fork, and at every commit that has ever touched it the value is NonZeroU64::get() — so no build of this fork, past or present or from any branch, can emit 0. Guarded twice more upstream: TrackWriter::set_history_window takes NonZeroU64, and the remaining u64 setter rejects 0 with a pinned test.
  • moq-pub-mmtp uses a fixed PUBLISHER_HISTORY_WINDOW = 32, no override.
  • The other deployed MoQ implementations — moq-lite (in cast) and the hang-mmt-fec JS publisher — hardcode their SUBSCRIBE_OK param sets and cannot emit 0x40 at all.
  • Corroborated by a resolved-digest census of every deployed MoQ producer in prod and staging.

Worth knowing if you re-derive this: the workspace moq-rs clone is shallow (depth 5), and git log -S over it names the wrong introducing commit. The inventory records that trap and one other; both failed in the permissive direction.

Verification

cargo test -p moq-transport recv_subscribe_ok_
  recv_subscribe_ok_zero_window_is_rejected               ... ok   <- new, replaces …_is_unbounded
  recv_subscribe_ok_absent_window_is_unbounded            ... ok   <- UNCHANGED
  recv_subscribe_ok_applies_history_window_to_mirror      ... ok
  recv_subscribe_ok_readvertises_history_window_to_downstream ... ok
  test result: ok. 4 passed; 0 failed

cargo test -p moq-transport      -> 277 + 18 + 1 passed; 0 failed
cargo fmt -p moq-transport --check -> clean

recv_subscribe_ok_absent_window_is_unbounded passing unchanged is the assertion that the generic interop path was not disturbed — that is the point of the test, not an incidental pass.

Two things a reviewer should push on

  1. A non-integer value for key 0x40 is deliberately left as None. It is malformed too, and leaving it is arguably incoherent next to rejecting zero. I kept it out because the inventory gating this change covers the zero case, and quietly widening scope past the evidence is the habit this issue exists to break. Happy to take it in a follow-up with its own reasoning — say the word and I will file it.
  2. A rejected SUBSCRIBE_OK still leaves its track alias bound in track_alias_map, since the alias is inserted before the window is parsed. Harmless today because the session is torn down on ProtocolViolation, but it is real state left behind and I would rather flag it than have it found later.

Refs BLO-22245, BLO-21206 (MRS-02).

🤖 Generated with Claude Code

A SUBSCRIBE_OK carrying key 0x40 with value 0 was mapped to None by
`.and_then(NonZeroU64::new)` — the same result as the key being absent —
so a malformed frame silently widened the mirror's retention to
unbounded. That is a silent fallback on fork-added code, which the
engineering principles ban.

Reject it as a protocol violation instead. The two cases are not the
same and only one has an interop defence:

- absent 0x40  → a conforming generic peer that never implemented this
  private Blockcast extension. Omission is exactly how such a peer
  signals "unsupported", so leniency here is required and is UNCHANGED
  by this commit. Mandatory presence is negotiated by the Blockcast wire
  profile (BLO-22244), not enforced here.
- present 0x40 = 0 → the peer implemented the extension and sent a
  meaningless value. A window of 0 groups retains nothing, so it cannot
  be what any publisher meant, and no conforming-peer reading applies.

Gated on the sender inventory required by the MRS-02 G0 record
(pim-multicast-gateway docs/g0/blo-22245-sender-inventory.md): key 0x40
has exactly one emission site in fork history, and at every commit that
has ever touched it the value is `NonZeroU64::get()` — so no build of
this fork, past or present, can emit 0. The other MoQ implementations we
deploy (moq-lite, the hang-mmt-fec JS publisher) hardcode their param
sets and cannot emit 0x40 at all.

`recv_subscribe_ok_zero_window_is_unbounded` is replaced by
`recv_subscribe_ok_zero_window_is_rejected`, asserting the protocol
error. `recv_subscribe_ok_absent_window_is_unbounded` passes UNCHANGED —
that is the assertion that the generic interop path was not disturbed.

A non-integer value for this key is left as-is: it is malformed too, but
the inventory gating this change covers the zero case, and widening
scope silently is the habit this issue exists to break.

Refs BLO-22245, BLO-21206 (MRS-02)

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Aug 6, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-21206
🔗 Paperclip issue: BLO-22245
🔗 Paperclip issue: BLO-22244

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 6, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-21206
🔗 Paperclip issue: BLO-22245
🔗 Paperclip issue: BLO-22244

@allyblockcast

allyblockcast Bot commented Aug 6, 2026

Copy link
Copy Markdown
Author

@ally please review at head 604a749 — this rejects a present-but-zero subgroup history window (key 0x40) in recv_subscribe_ok as a protocol violation, replacing a silent fallback to unbounded retention.

Review focus, in priority order:

  1. Did I actually leave the absent path alone? The whole safety case rests on absent-0x40 still yielding unbounded with no error, under every profile. recv_subscribe_ok_absent_window_is_unbounded passes unchanged — please check the code path, not just the test name, and confirm nothing profile-dependent leaked into that arm.
  2. Is SessionError::ProtocolViolation the right severity? It tears down the whole session (error code 0x3). Argue the other side: is a per-subscription rejection more proportionate for a malformed optional parameter?
  3. The two things I flagged in the PR body — the non-integer-value arm left as None, and the track alias staying bound in track_alias_map after a rejected SUBSCRIBE_OK. I chose to scope both out and say so rather than fix them silently. Tell me if either should be in this PR.
  4. The gate. This only lands because a sender inventory (Blockcast/pim-multicast-gateway#2079) claims no deployed producer can emit 0. The load-bearing claim is that key 0x40 has exactly one emission site and it is NonZeroU64-typed at every commit in fork history. If you can find a second emitter, or a build that could emit 0, this PR should not merge.

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.

0 participants