Skip to content

fix(moq-mux): confirm TS sync lock before trusting a candidate sync byte#1697

Merged
kixelated merged 9 commits into
mainfrom
claude/ts-sync-lock-confirm
Jun 12, 2026
Merged

fix(moq-mux): confirm TS sync lock before trusting a candidate sync byte#1697
kixelated merged 9 commits into
mainfrom
claude/ts-sync-lock-confirm

Conversation

@kixelated

@kixelated kixelated commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #1695. While reviewing the MPEG-TS sync logic we noticed a latent correctness gap: the demuxer trusted any single 0x47 at the expected offset as a packet boundary.

But 0x47 is only the sync byte at byte 0 of each 188-byte packet. The other 187 bytes (header continuation, adaptation field, payload, raw codec bitstream) are arbitrary, and TS has no byte stuffing or escaping to keep 0x47 out of them (unlike HDLC byte-stuffing or H.264 NAL emulation-prevention). So 0x47 appears in payload roughly once per packet on average. The format relies on the fixed 188-byte periodicity, not on the sync byte being unique.

Consequence: during sync acquisition (a misaligned start, mid-stream join, or post-corruption resync), a stray payload 0x47 could be mistaken for a boundary and route one bogus 188-byte "packet" before the next packet revealed the misalignment.

Fix

Add a sync lock, a persisted Import::synced flag:

  • Until synced (start, or after a missed sync byte), scan (SIMD memchr) for a candidate 0x47 whose next packet also begins with 0x47, skipping payload 0x47s, before locking onto it. If no sync byte remains, drop the buffer. If the candidate sits within 188 bytes of the buffer tail (its +188 neighbor isn't buffered yet), stay unsynced and retain it, so it's re-confirmed on the next decode call with the trailing bytes rather than trusted blindly.
  • Once synced, trust the 0x47 sync byte and stride 188 at a time; the per-packet check stays the tripwire that drops the lock the instant a stride misses.

The flag persists across decode calls, which closes the cross-call hole: a candidate pending confirmation at a buffer tail is confirmed next call, never trusted on faith.

Test plan

  • resyncs_past_false_sync_byte: enters resync, then a stray 0x47 whose +188 byte isn't a sync byte, proving the confirmation rejects it and scans on.
  • resyncs_across_chunk_boundaries: a misaligned stream fed in 100-byte chunks, exercising resync candidates carried (pending confirmation) across decode calls.
  • cargo test -p moq-mux (all 38 container::ts tests pass, including import_resyncs_after_byte_misalignment, survives_midstream_join, import_handles_unaligned_chunks, kyrion_dirtystart_extracts_real_cues)
  • cargo clippy -p moq-mux clean

(Written by Claude)

claude added 4 commits June 12, 2026 18:03
The TS demuxer scanned forward one byte at a time to find the next 0x47
sync byte after a misalignment. Replace the byte-by-byte loop with
memchr::memchr, which is SIMD-accelerated and already a dependency. The
end state is identical (jump to the next sync byte, or drop the buffer if
none remains), so resync semantics are unchanged.
Match repo Rust convention preferring if let over an unwrapping match.
The MPEG-TS demuxer trusted any single 0x47 at the expected offset as a
packet boundary. But 0x47 also occurs freely in packet payloads (TS has no
byte stuffing), so a stray payload 0x47 during sync acquisition would be
mistaken for a boundary and route one bogus 188-byte packet before the next
packet revealed the misalignment.

Add a sync lock: while unaligned, confirm a candidate 0x47 against the next
packet's sync byte (offset + 188) before trusting it. A non-sync byte there
means the candidate was payload, so keep scanning. If the buffer doesn't yet
reach offset + 188, retain the candidate and re-check on the next decode call.
Once locked we stride 188 at a time and drop the lock the instant a stride
misses a sync byte, so the per-packet check stays the tripwire and the final
packet of a buffer still emits without a follower to confirm against.
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5c339412-25e8-4fbc-a9eb-d33a78927927

📥 Commits

Reviewing files that changed from the base of the PR and between 2a7ec47 and e127b49.

📒 Files selected for processing (1)
  • rs/moq-mux/src/container/ts/import_test.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rs/moq-mux/src/container/ts/import_test.rs

Walkthrough

Import::decode now keeps a persistent synced flag and, when misaligned, uses memchr to find a candidate 0x47 and confirms it by checking the byte at candidate_offset + 188 before accepting alignment; if confirmation is impossible due to insufficient bytes the decoder remains unsynced and retains the partial tail for the next call. Two tests were added: one rejects a false stray 0x47, the other verifies resyncing across 100-byte chunked decode calls; both expect exactly one H.264 video and one AAC audio rendition.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding confirmation logic for TS sync byte candidates before trusting them.
Description check ✅ Passed The description clearly explains the correctness gap (single 0x47 trusted as boundary), the technical rationale (TS lacks byte stuffing), the fix (synced flag with double-check), and test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/ts-sync-lock-confirm

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Drop the Import::aligned field. Confirmation only matters while re-acquiring
sync, so do it inline in the resync branch: scan for a candidate 0x47 whose
next packet also starts with 0x47, skipping payload 0x47s, before trusting it.
A trusted stride keeps the cheap single-byte check, so no per-call lock state
is needed and the original aligned-stream behavior (and the final packet of a
buffer) is preserved. Use a let-else break when no sync byte remains.

Update resyncs_past_false_sync_byte to enter via a resync so it exercises the
confirmation path.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rs/moq-mux/src/container/ts/import.rs`:
- Around line 140-167: The code currently trusts any 0x47 at self.scratch[off]
without persisting a locked/unlocked state, which lets unconfirmed candidates be
parsed; add a boolean lock field on the decoder (e.g., self.locked) and use it
so that when self.locked is false you DO NOT immediately accept a sync at
self.scratch[off] but instead run the existing memchr::memchr confirmation logic
(checking self.scratch.get(off + TsPacket::SIZE) == Some(&0x47)) and only set
self.locked = true when the +188 confirmation succeeds; when confirmation cannot
be performed because the candidate is near the buffer tail, retain the candidate
(break/return without parsing) so it will be rechecked on the next decode call,
and when you detect a missed sync (current code path where self.scratch[off] !=
0x47 and you stride incorrectly) ensure you set self.locked = false so
subsequent bytes are re-scanned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77f5aa37-f429-4364-beb3-59aea00dbd6a

📥 Commits

Reviewing files that changed from the base of the PR and between 742ca41 and 0e64933.

📒 Files selected for processing (2)
  • rs/moq-mux/src/container/ts/import.rs
  • rs/moq-mux/src/container/ts/import_test.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rs/moq-mux/src/container/ts/import_test.rs

Comment thread rs/moq-mux/src/container/ts/import.rs Outdated
claude added 2 commits June 12, 2026 20:04
…onfirm

# Conflicts:
#	rs/moq-mux/src/container/ts/import.rs
Gate the resync confirmation on a persisted Import::synced flag. Without it, a
candidate found during resync that lands within 188 bytes of a buffer tail (so
its next-packet sync byte isn't buffered yet) was retained and then trusted on
the next decode call without ever being confirmed, reintroducing the bogus-packet
routing this change set out to prevent.

Now an unconfirmed candidate keeps synced=false, so the next call re-runs the
confirmation against the trailing bytes instead of trusting it. Once a candidate
is confirmed we stride 188 at a time, dropping the lock the instant a stride
misses a sync byte.

Add resyncs_across_chunk_boundaries: a misaligned stream fed in small chunks,
exercising tail candidates carried across decode calls.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
rs/moq-mux/src/container/ts/import_test.rs (1)

93-94: ⚡ Quick win

Add descriptive assertion messages for consistency.

The assertions lack failure messages, unlike similar assertions throughout this test file (e.g., lines 71-72, 57-58). Descriptive messages help diagnose which resync scenario failed.

📝 Suggested assertion messages
-	assert_eq!(snapshot.video.renditions.len(), 1);
-	assert_eq!(snapshot.audio.renditions.len(), 1);
+	assert_eq!(snapshot.video.renditions.len(), 1, "chunked resync failed: no video track");
+	assert_eq!(snapshot.audio.renditions.len(), 1, "chunked resync failed: no audio track");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rs/moq-mux/src/container/ts/import_test.rs` around lines 93 - 94, The two
assertions assert_eq!(snapshot.video.renditions.len(), 1) and
assert_eq!(snapshot.audio.renditions.len(), 1) need descriptive failure messages
like the other assertions in this test; update each assert_eq! to include a
third argument string describing the expectation (e.g., "expected exactly 1
video rendition after resync" and "expected exactly 1 audio rendition after
resync") so failures indicate which resync scenario failed and mirror the style
used in the nearby assertions (lines with similar checks).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@rs/moq-mux/src/container/ts/import_test.rs`:
- Around line 93-94: The two assertions
assert_eq!(snapshot.video.renditions.len(), 1) and
assert_eq!(snapshot.audio.renditions.len(), 1) need descriptive failure messages
like the other assertions in this test; update each assert_eq! to include a
third argument string describing the expectation (e.g., "expected exactly 1
video rendition after resync" and "expected exactly 1 audio rendition after
resync") so failures indicate which resync scenario failed and mirror the style
used in the nearby assertions (lines with similar checks).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c1b3d79-0276-4e37-853e-a6b63e706019

📥 Commits

Reviewing files that changed from the base of the PR and between 0e64933 and 3548b3e.

📒 Files selected for processing (2)
  • rs/moq-mux/src/container/ts/import.rs
  • rs/moq-mux/src/container/ts/import_test.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rs/moq-mux/src/container/ts/import.rs

@kixelated
kixelated merged commit e8346b4 into main Jun 12, 2026
1 check passed
@kixelated
kixelated deleted the claude/ts-sync-lock-confirm branch June 12, 2026 22:31
This was referenced Jun 12, 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.

2 participants