Skip to content

RTMP: Fix chunk timestamp/basic-header decoding and harden packet unmarshal. v8.0.3#4680

Merged
winlinvip merged 5 commits into
ossrs:developfrom
winlinvip:develop
May 29, 2026
Merged

RTMP: Fix chunk timestamp/basic-header decoding and harden packet unmarshal. v8.0.3#4680
winlinvip merged 5 commits into
ossrs:developfrom
winlinvip:develop

Conversation

@winlinvip

@winlinvip winlinvip commented May 29, 2026

Copy link
Copy Markdown
Member

Fixes three RTMP chunk-stream decoding bugs in the proxy and hardens AMF0 command-packet unmarshalling against malformed input, backed by a new protocol unit-test suite.

All changes are confined to the internal/rtmp package. No public API, log format, or emitted wire format changes — these are decode-correctness and robustness fixes only.

**3-byte chunk basic header decode (readBasicHeader) **

The 3-byte basic-header form (cid 64–65599) was selected by testing cid == 1 after cid had already been overwritten with 64 + t, so it was never detected. Capture the original marker before overwriting and test that instead.

Extended-timestamp handling (chunkStream, readMessageHeader)

  • Use the extended timestamp as a delta for fmt=1/2 chunks (and a fmt=3 first chunk continuing them), required when the delta is ≥ 0xffffff. Timestamp computation is unified into a single post-step: extended timestamp when present, otherwise the 3-byte header delta; fmt=0 absolute, fmt=1/2 accumulated.
  • Detect Type-3 chunks that omit the extended timestamp. FMLE/FMS/Flash follow the RTMP 2012 spec and always send it on Type-3 chunks; librtmp/ffmpeg may not. Switched from an unconditional 4-byte read to Peek + conditional Discard: if the peeked value differs from the stored one on a non-first chunk, those 4 bytes are payload and are left in the reader.
  • Split the single extendedTimestamp bool into hasExtendedTimestamp (bool) and extendedTimestamp (the last raw value, used for the detection above).

Packet unmarshal hardening

  • Add an advanceBytes(p, n) helper that bounds-checks each p = p[field.Size():] advance, turning a slice-out-of-range panic into a clean error on truncated/untrusted input. Applied in CallPacket, CreateStreamResPacket, PublishPacket, and PlayPacket.
  • Reset the optional CommandObject / Args to nil before probing for their presence, so a stale constructor default (e.g. Null) isn't counted by Size() and can't overflow a later advance.

winlinvip and others added 5 commits May 24, 2026 18:10
…oxy.

Port the C++ srs_protocol_rtmp_stack.cpp fix (ossrs#4356) to the Go proxy's RTMP parser in internal/rtmp.

For chunk fmt=1/2 the extended timestamp encodes a timestamp delta, not an absolute timestamp. The parser previously assigned the extended value to the message timestamp unconditionally, so once a delta reached 0xffffff the DTS was miscomputed, and since audio and video deltas differ this could cause A/V desync.

Changes:
- Split chunkStream's single ext-ts bool into hasExtendedTimestamp (presence) plus a raw extendedTimestamp uint32, mirroring the C++ has_extended_timestamp_ / extended_timestamp_ fields.
- Compute the message timestamp once: extended value when present, else the 3-byte header delta; assign it as absolute for fmt=0 and accumulate it as a delta for fmt=1/2 (and a fmt=3 first chunk).
- Resolve the 'detect the extended timestamp' TODO: peek the 4 bytes and leave them as payload when a librtmp/ffmpeg-style sender omits the ext-ts on a Type-3 chunk (Go equivalent of the C++ skip(-4)).
- Add unit tests for the fmt=1 delta-crossing-0xffffff case and the Type-3 omitted-ext-ts case.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
readBasicHeader overwrote cid with the 2-byte form (64 + byte2) before testing
whether the 3-byte form was in use, so the `cid == 1` check could never be true
and the 3-byte branch was dead code. Chunk basic headers with marker == 1 (chunk
stream IDs 320-65599) consumed only one of the two trailing bytes, leaving the
high-order byte in the stream and desyncing the chunk parser.

Keep the original marker before cid is overwritten and branch on it, matching the
C++ reference (srs_protocol_rtmp_stack.cpp, read_basic_header). The arithmetic
inside the branch was already correct.

Also correct the unit test, which had encoded the buggy result (expected cid=65
instead of 577, leaving a byte unread); it now guards the 3-byte path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the Go RTMP stack unit-test plan (internal/rtmp), cross-referenced
to the C++ srs_utest_manual_protocol/protocol2/rtmp suites, and fixes the one
panic class the fuzz targets exposed.

Tests added (rtmp_test.go):
- TestReadMessageInterleavedMultiStream    interleaved multi-cid reassembly
- TestReadMessageLargeChunkStreamID        2-/3-byte cid full-message read
- TestProtocolWritePacketReadMessageRoundTrip  encoder<->decoder, 14 pkts x 4 sizes
- TestReadMessageTimestampDiscontinuity    backward/forward jump, 31-bit wrap
- TestReadWriteLargePayloadChunkBoundaries chunk-boundary stress
- TestGoldenWireBytes                       golden bytes for headers + controls
- FuzzReadMessage / FuzzDecodeMessage / FuzzPacketUnmarshal  untrusted-input fuzz
- TestPacketUnmarshalAdversarialInputs      resource-safety / truncation
- TestProtocolTransactionMapConcurrency     -race on the transaction map

Hardening (rtmp.go): the fuzz targets found that the variantCallPacket family
counted a stale optional-field default (CommandObject/Args pre-set to Null by a
New*Packet constructor) when the wire data was exhausted, so Size() overran the
caller's p = p[Size():] advance and panicked on truncated, untrusted input.
Reset those fields to nil before the presence check, and route the embedded
advances in CallPacket/CreateStreamResPacket/PublishPacket/PlayPacket through a
bounds-checked advanceBytes helper so any future Size()/consumed mismatch becomes
a clean error instead of a slice-out-of-range panic.

Verified: full proxy unit suite passes; TestProtocolTransactionMapConcurrency
clean under go test -race -count=3; all proxy E2E scripts pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Run the proxy E2E tests:" wording read as if running the first
script was enough. Make explicit that every E2E script must run, one at
a time (fixed ports prevent parallelism), and not to stop early since a
later test can fail even when earlier ones pass.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@winlinvip winlinvip changed the title RTMP: Fix chunk timestamp/basic-header decoding and harden packet unmarshal RTMP: Fix chunk timestamp/basic-header decoding and harden packet unmarshal. v8.0.3 May 29, 2026
@winlinvip winlinvip merged commit 0f980d4 into ossrs:develop May 29, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant