Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions RTMPHaishinKit/Sources/RTMP/RTMPMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -659,8 +659,21 @@ struct RTMPUserControlMessage: RTMPMessage {
init(_ header: RTMPChunkMessageHeader) {
streamId = header.messageStreamId
timestamp = header.timestamp
event = Event(rawValue: header.payload[1]) ?? .unknown
value = Int32(data: header.payload[2..<header.payload.count]).bigEndian
// A well-formed user control message is at least 6 bytes: a 2-byte
// event type followed by a 4-byte value. A truncated payload from the
// peer would otherwise index past the end of `payload` and trap
// (EXC_BREAKPOINT / out of bounds), crashing the whole process from
// the RTMP receive loop. Re-base to a 0-indexed copy (a Data slice
// keeps its parent's indices) and bail out to a safe default when the
// payload is too short.
let payload = Data(header.payload)
guard payload.count >= 6 else {
event = .unknown
value = 0
return
}
event = Event(rawValue: payload[1]) ?? .unknown
value = Int32(data: payload[2..<payload.count]).bigEndian
}

init(event: Event, value: Int32) {
Expand Down
47 changes: 47 additions & 0 deletions RTMPHaishinKit/Tests/RTMP/RTMPUserControlMessageTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Foundation
import Testing

@testable import RTMPHaishinKit

@Suite struct RTMPUserControlMessageTests {
// Regression: a truncated user control message (shorter than the 6-byte
// event-type + value layout) must not trap on an out-of-bounds Data
// subscript. Before the bounds guard this crashed the process from the
// RTMP receive loop (EXC_BREAKPOINT in RTMPUserControlMessage.init).
@Test func truncatedPayloadDoesNotCrash() {
// messageLength 0 -> empty payload, position 0 == count 0, so
// makeMessage() builds the message and runs the (previously
// unguarded) decode against an empty buffer.
let header = RTMPChunkMessageHeader(
timestmap: 0,
messageLength: 0,
messageTypeId: RTMPMessageType.user.rawValue,
messageStreamId: 0
)
let message = header.makeMessage() as? RTMPUserControlMessage
#expect(message?.event == .unknown)
#expect(message?.value == 0)
}

// A well-formed 6-byte user control message must still decode correctly
// (2-byte event type + 4-byte big-endian value).
@Test func wellFormedPayloadDecodes() {
let buffer = RTMPChunkBuffer()
// basic header (fmt 0, csid 2) | timestamp(3) | length(3)=6 |
// typeId(1)=4 | streamId(4) | payload: event 0x0000 (streamBegin),
// value 0x00000001.
buffer.put(Data([2, 0, 0, 0, 0, 0, 6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]))
do {
let (chunkType, chunkStreamId) = try buffer.getBasicHeader()
#expect(chunkType == .zero)
#expect(chunkStreamId == 2)
let header = RTMPChunkMessageHeader()
try buffer.getMessageHeader(chunkType, messageHeader: header)
let message = header.makeMessage() as? RTMPUserControlMessage
#expect(message?.event == .streamBegin)
#expect(message?.value == 1)
} catch {
Issue.record("unexpected error decoding user control message: \(error)")
}
}
}