Skip to content

fix(moq-net): an IETF group fetch fill is complete or refused, from the wanted frame - #4544

Merged
kixelated merged 3 commits into
quest/m1/moxygen/READMEfrom
quest/m1/moxygen/fetch-fill
Sep 30, 2026
Merged

kixelated merged 3 commits into
quest/m1/moxygen/READMEfrom
quest/m1/moxygen/fetch-fill

Conversation

@kixelated

@kixelated kixelated commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

#4276 fills a relay cache miss from an IETF upstream with a standalone FETCH of one group. Three Codex findings merged unanswered, and the maintainer ruled each blocks the moxygen line (quest/m1/moxygen/fetch-fill.md):

  • A fetch stream that FINs short of FETCH_OK's End Location was cached as a complete group (r4113942736).
  • A first fetch object with no Group or Object ID was cached under the requested group (r4113942737).
  • The upstream FETCH always asked from object 0, ignoring group::Request::frame_start, and never start_at the producer (r4113942733).

Approach

All in rs/moq-net/src/ietf/subscriber.rs:

  • End Location. When FETCH_OK's exclusive End Location falls inside the group, it is the group's Largest Object + 1 (a live group, or End of Track), so the stream owes every object before it. Ending short of it, or running past it, aborts the group with ProtocolViolation. An End Location past the group covers it whole, and the stream's FIN ends it (see the decision below).
  • First object. decode_fetch_object refuses a first object that leaves any field to a prior object: Group ID, Object ID, Subgroup (Prior/PriorPlusOne), or Publisher Priority. This is the drafts' own rule ("If the first Object in the FETCH response uses a flag that references fields in the prior Object, the Subscriber MUST close the session with a PROTOCOL_VIOLATION", draft-15 onward), and it covers the joining-fetch fill too. An object in another group, or with an ID before the next one, is now ProtocolViolation instead of Unsupported. A skipped ID is still Unsupported: the drafts allow the hole, but the model cannot hold it.
  • Frame start. The FETCH starts at (group, frame_start). The accepted producer is start_at it, as frame_start's docs require, and the decoder numbers from it. This mirrors the lite subscriber.

Three regression tests, each failing without its fix: a_group_fetch_refuses_a_first_object_that_inherits, a_group_fetch_must_reach_its_end_location, and a_group_fetch_asks_from_the_wanted_frame (end to end through run_group_fetch, checking the FETCH on the wire).

Decision: the whole-group end signal

The quest asks for a wire signal marking a complete whole-group fetch, and says to ask if the drafts offer none. They offer no explicit one:

  • Draft-14 to 16 define it implicitly: "The final Object in a Group is the Object with Status END_OF_GROUP or the last Object sent in a FETCH that requested the entire Group" (§2.5). So the clean FIN is the signal.
  • Draft-17 onward drop that clause: "If the end of a Group is implicitly determined via a gap in a FETCH response, the final Object in the Group remains unknown." But every draft still says gaps up to FETCH_OK's end "indicate objects that do not exist... so long as the fetch stream is terminated by a FIN."
  • An END_OF_GROUP status object exists only on draft-14/15 fetch streams, and only as a MAY. Fetch objects have no status field from draft-16 on. End of Non-Existent Range (0x8C) "SHOULD NOT" be used except to split ranges.
  • moxygen's relay cache treats the FIN the same way: its FETCH_OK reports the requested end so that "the objects between the last one served and the end do not exist."

Options:

  1. Trust the clean FIN (this PR, confirmed by the maintainer). A reset is already an error. A peer that FINs early has published a malformed track by the drafts' own definition, and neither we nor moxygen can tell. Needs no new wire.
  2. Require an END_OF_GROUP marker. This refuses every whole-group fill on draft-16 and later, and from any draft-14/15 peer that skips the MAY, including our own publisher.
  3. Serve a FIN-ended whole group to the waiting reader but don't cache it. The model has no uncached group, and it contradicts "FETCH is authoritative".

Impact

  • Public API: none. Everything touched is private to moq-net.
  • Wire (IETF, send side): a relay's upstream group FETCH now carries Start Location (group, frame_start) instead of (group, 0). The End Location is unchanged (whole group).
  • Wire (IETF, receive side): stricter validation of fetch streams. A first object that inherits is ProtocolViolation. So is an object outside the requested group or start, or a stream that doesn't match a concrete FETCH_OK End Location. As before, these reset only the stream, not the session.
  • No IETF draft or js/net change: the encoding is unchanged.

Alternatives

  • Validating against FETCH_OK alone. Our publisher, like moxygen's cache, answers a covered whole group with the exclusive requested end (group + 1, 0), which names no object count (the accepted decline in r4114050992).

Follow-ups

  • Datagram-published groups can't be filled: a fetch object with the DATAGRAM flag (0x40, a MUST from draft-16 on) maps to subgroup_ok = false and is refused as Unsupported. So a cache miss on a one-datagram-per-group track fails through the relay, which the line promises to carry. Suggest a quest.
  • A fetch-stream ProtocolViolation resets only that stream. The drafts say to close the session. Low priority, and it may be deliberate.

just test interop --all was not run. The matrix speaks moq-lite media and never issues an IETF FETCH, so it cannot exercise this change.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 3 commits September 29, 2026 13:30
…he wanted frame

- FETCH_OK's End Location inside the group is a promise: a stream short of
  it, or past it, fails the group instead of caching it.
- The first fetch object must spell out every field it could inherit.
- The upstream FETCH asks from group::Request::frame_start and numbers the
  fill from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review September 29, 2026 22:50
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-29T22:54:49.843966Z df72c33 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df72c338c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
// An exclusive End Location inside the group is its Largest Object plus one, so
// the stream owes every object up to it. One past the group covers it whole.
let end = (end.group == sequence).then_some(end.object);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate whole-group responses before caching them

When the normal whole-group FETCH_OK reports (sequence + 1, 0), as this repository's publisher does, this line converts the boundary to None. A stream carrying only an initial prefix followed by a clean FIN therefore bypasses both end checks, and recv_group_fetch finishes and caches that prefix as a complete group. This leaves the principal truncation case unfixed, so responses without a concrete object boundary need another verifiable completion condition before the producer is finished.

AGENTS.md reference: rs/moq-net/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this: the maintainer confirmed that a clean FIN marks a complete whole-group fetch (option 1 under "Open decision" in the description). No draft carries an explicit end-of-group signal on fetch streams from draft-16 on, draft-14 to 16 define the last object of a whole-group FETCH as the group's final object, and every draft treats gaps before FETCH_OK's end as nonexistent objects once the stream FINs. A peer that FINs early has published a malformed track, which neither we nor moxygen can detect. A reset is still an error, and a concrete End Location inside the group is still enforced.

(Written by Claude Opus 5.5)

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed head: df72c33

Direction: a useful correctness improvement. Requesting from the wanted frame, preserving object IDs, and refusing holes or a violated concrete end boundary is preferable to caching a knowingly incomplete answer.

No additional actionable defect found in the changed decoder/fill paths. I reviewed the existing r4139139293 discussion: a clean FIN is deliberately the completion proof for a whole-group response with no concrete object boundary. I am not duplicating that accepted protocol limitation as a defect; resets and concrete End Location mismatches remain errors. The first-object inheritance check also prevents fabricating a prior object from request context.

Verification: static review of the changed paths and scripted regressions, including the requested-tail case; no Rust tests executed.

(Written by OpenAI)

@kixelated
kixelated merged commit 3c83a08 into quest/m1/moxygen/README Sep 30, 2026
9 checks passed
@kixelated
kixelated deleted the quest/m1/moxygen/fetch-fill branch September 30, 2026 02:30
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