mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 15:05:20 +00:00
Fix crash in BinaryProtocol.decode due to out-of-bounds data access
- Add comprehensive bounds checking before all data access operations - Validate payload lengths don't exceed available data - Protect against integer overflow in size calculations - Handle malformed compressed packets gracefully - Add extensive test coverage for edge cases This fixes crashes when receiving malformed Bluetooth packets that claim payload sizes larger than the actual data available.
This commit is contained in:
@@ -121,8 +121,9 @@ final class BinaryProtocolTests: XCTestCase {
|
||||
func testMessagePadding() throws {
|
||||
let payloads = [
|
||||
"Short",
|
||||
"This is a medium length message for testing",
|
||||
TestConstants.testLongMessage
|
||||
String(repeating: "Medium length message content ", count: 10), // ~300 bytes
|
||||
String(repeating: "Long message content that should exceed the 512 byte limit ", count: 20), // ~1200+ bytes
|
||||
String(repeating: "Very long message content that should definitely exceed the 2048 byte limit for sure ", count: 30) // ~2700+ bytes
|
||||
]
|
||||
|
||||
var encodedSizes = Set<Int>()
|
||||
@@ -150,8 +151,8 @@ final class BinaryProtocolTests: XCTestCase {
|
||||
XCTAssertEqual(String(data: decodedPacket.payload, encoding: .utf8), payload)
|
||||
}
|
||||
|
||||
// Different payload sizes should result in different padded sizes
|
||||
XCTAssertGreaterThan(encodedSizes.count, 1)
|
||||
// Different payload sizes should result in at least 2 different padded sizes
|
||||
XCTAssertGreaterThanOrEqual(encodedSizes.count, 2, "Expected at least 2 different padded sizes, got \(encodedSizes)")
|
||||
}
|
||||
|
||||
// MARK: - Message Encoding/Decoding Tests
|
||||
@@ -312,4 +313,259 @@ final class BinaryProtocolTests: XCTestCase {
|
||||
// Should fail to decode
|
||||
XCTAssertNil(BinaryProtocol.decode(encoded))
|
||||
}
|
||||
|
||||
// MARK: - Bounds Checking Tests (Crash Prevention)
|
||||
|
||||
func testMalformedPacketWithInvalidPayloadLength() throws {
|
||||
// Test the specific crash scenario: payloadLength = 193 (0xc1) but only 30 bytes available
|
||||
var malformedData = Data()
|
||||
|
||||
// Valid header (13 bytes)
|
||||
malformedData.append(1) // version
|
||||
malformedData.append(1) // type
|
||||
malformedData.append(10) // ttl
|
||||
|
||||
// Timestamp (8 bytes)
|
||||
for _ in 0..<8 {
|
||||
malformedData.append(0)
|
||||
}
|
||||
|
||||
malformedData.append(0) // flags (no recipient, no signature, not compressed)
|
||||
|
||||
// Invalid payload length: 193 (0x00c1) but we'll only provide 8 bytes total data
|
||||
malformedData.append(0x00) // high byte
|
||||
malformedData.append(0xc1) // low byte (193)
|
||||
|
||||
// SenderID (8 bytes) - this brings us to 21 bytes total
|
||||
for _ in 0..<8 {
|
||||
malformedData.append(0x01)
|
||||
}
|
||||
|
||||
// Only provide 8 more bytes instead of the claimed 193
|
||||
for _ in 0..<8 {
|
||||
malformedData.append(0x02)
|
||||
}
|
||||
|
||||
// Total data is now 30 bytes, but payloadLength claims 193
|
||||
XCTAssertEqual(malformedData.count, 30)
|
||||
|
||||
// This should not crash - should return nil gracefully
|
||||
let result = BinaryProtocol.decode(malformedData)
|
||||
XCTAssertNil(result, "Malformed packet with invalid payload length should return nil, not crash")
|
||||
}
|
||||
|
||||
func testTruncatedPacketHandling() throws {
|
||||
// Test various truncation scenarios
|
||||
let packet = TestHelpers.createTestPacket()
|
||||
guard let validEncoded = BinaryProtocol.encode(packet) else {
|
||||
XCTFail("Failed to encode test packet")
|
||||
return
|
||||
}
|
||||
|
||||
// Test truncation at various points
|
||||
let truncationPoints = [0, 5, 10, 15, 20, 25]
|
||||
|
||||
for point in truncationPoints {
|
||||
let truncated = validEncoded.prefix(point)
|
||||
let result = BinaryProtocol.decode(truncated)
|
||||
XCTAssertNil(result, "Truncated packet at \(point) bytes should return nil, not crash")
|
||||
}
|
||||
}
|
||||
|
||||
func testMalformedCompressedPacket() throws {
|
||||
// Test compressed packet with invalid original size
|
||||
var malformedData = Data()
|
||||
|
||||
// Valid header
|
||||
malformedData.append(1) // version
|
||||
malformedData.append(1) // type
|
||||
malformedData.append(10) // ttl
|
||||
|
||||
// Timestamp (8 bytes)
|
||||
for _ in 0..<8 {
|
||||
malformedData.append(0)
|
||||
}
|
||||
|
||||
malformedData.append(0x04) // flags: isCompressed = true
|
||||
|
||||
// Small payload length that's insufficient for compression
|
||||
malformedData.append(0x00) // high byte
|
||||
malformedData.append(0x01) // low byte (1 byte - insufficient for 2-byte original size)
|
||||
|
||||
// SenderID (8 bytes)
|
||||
for _ in 0..<8 {
|
||||
malformedData.append(0x01)
|
||||
}
|
||||
|
||||
// Only 1 byte of "compressed" data (should need at least 2 for original size)
|
||||
malformedData.append(0x99)
|
||||
|
||||
// Should handle this gracefully
|
||||
let result = BinaryProtocol.decode(malformedData)
|
||||
XCTAssertNil(result, "Malformed compressed packet should return nil, not crash")
|
||||
}
|
||||
|
||||
func testExcessivelyLargePayloadLength() throws {
|
||||
// Test packet claiming extremely large payload
|
||||
var malformedData = Data()
|
||||
|
||||
// Valid header
|
||||
malformedData.append(1) // version
|
||||
malformedData.append(1) // type
|
||||
malformedData.append(10) // ttl
|
||||
|
||||
// Timestamp (8 bytes)
|
||||
for _ in 0..<8 {
|
||||
malformedData.append(0)
|
||||
}
|
||||
|
||||
malformedData.append(0) // flags
|
||||
|
||||
// Maximum payload length (65535)
|
||||
malformedData.append(0xFF) // high byte
|
||||
malformedData.append(0xFF) // low byte
|
||||
|
||||
// SenderID (8 bytes)
|
||||
for _ in 0..<8 {
|
||||
malformedData.append(0x01)
|
||||
}
|
||||
|
||||
// Provide only a tiny amount of actual data
|
||||
malformedData.append(contentsOf: [0x01, 0x02, 0x03])
|
||||
|
||||
// Should handle this gracefully without trying to allocate massive amounts of memory
|
||||
let result = BinaryProtocol.decode(malformedData)
|
||||
XCTAssertNil(result, "Packet with excessive payload length should return nil, not crash")
|
||||
}
|
||||
|
||||
func testCompressedPacketWithInvalidOriginalSize() throws {
|
||||
// Test compressed packet with unreasonable original size
|
||||
var malformedData = Data()
|
||||
|
||||
// Valid header
|
||||
malformedData.append(1) // version
|
||||
malformedData.append(1) // type
|
||||
malformedData.append(10) // ttl
|
||||
|
||||
// Timestamp (8 bytes)
|
||||
for _ in 0..<8 {
|
||||
malformedData.append(0)
|
||||
}
|
||||
|
||||
malformedData.append(0x04) // flags: isCompressed = true
|
||||
|
||||
// Reasonable payload length
|
||||
malformedData.append(0x00) // high byte
|
||||
malformedData.append(0x10) // low byte (16 bytes)
|
||||
|
||||
// SenderID (8 bytes)
|
||||
for _ in 0..<8 {
|
||||
malformedData.append(0x01)
|
||||
}
|
||||
|
||||
// Original size claiming to be extremely large (2MB)
|
||||
malformedData.append(0x20) // high byte of original size
|
||||
malformedData.append(0x00) // low byte of original size (0x2000 = 8192, but let's make it larger with more bytes)
|
||||
|
||||
// Add more bytes to make it claim larger size - but this will be invalid
|
||||
// because our validation should catch unreasonable sizes
|
||||
malformedData.append(contentsOf: [0x01, 0x02, 0x03, 0x04]) // Some compressed data
|
||||
|
||||
// Pad to match payload length
|
||||
while malformedData.count < 21 + 16 { // header + senderID + payload
|
||||
malformedData.append(0x00)
|
||||
}
|
||||
|
||||
let result = BinaryProtocol.decode(malformedData)
|
||||
XCTAssertNil(result, "Compressed packet with invalid original size should return nil, not crash")
|
||||
}
|
||||
|
||||
func testMaliciousPacketWithIntegerOverflow() throws {
|
||||
// Test packet designed to cause integer overflow
|
||||
var maliciousData = Data()
|
||||
|
||||
// Valid header
|
||||
maliciousData.append(1) // version
|
||||
maliciousData.append(1) // type
|
||||
maliciousData.append(10) // ttl
|
||||
|
||||
// Timestamp (8 bytes)
|
||||
for _ in 0..<8 {
|
||||
maliciousData.append(0)
|
||||
}
|
||||
|
||||
// Set flags to have recipient and signature (increase expected size)
|
||||
maliciousData.append(0x03) // hasRecipient | hasSignature
|
||||
|
||||
// Very large payload length
|
||||
maliciousData.append(0xFF) // high byte
|
||||
maliciousData.append(0xFE) // low byte (65534)
|
||||
|
||||
// SenderID (8 bytes)
|
||||
for _ in 0..<8 {
|
||||
maliciousData.append(0x01)
|
||||
}
|
||||
|
||||
// RecipientID (8 bytes - required due to flag)
|
||||
for _ in 0..<8 {
|
||||
maliciousData.append(0x02)
|
||||
}
|
||||
|
||||
// Provide minimal payload data - should trigger bounds check failure
|
||||
maliciousData.append(contentsOf: [0x01, 0x02])
|
||||
|
||||
// Should handle gracefully without integer overflow issues
|
||||
let result = BinaryProtocol.decode(maliciousData)
|
||||
XCTAssertNil(result, "Malicious packet designed for integer overflow should return nil, not crash")
|
||||
}
|
||||
|
||||
func testPartialHeaderData() throws {
|
||||
// Test packets with incomplete headers
|
||||
let headerSizes = [0, 1, 5, 10, 12] // Various incomplete header sizes
|
||||
|
||||
for size in headerSizes {
|
||||
let partialData = Data(repeating: 0x01, count: size)
|
||||
let result = BinaryProtocol.decode(partialData)
|
||||
XCTAssertNil(result, "Partial header data (\(size) bytes) should return nil, not crash")
|
||||
}
|
||||
}
|
||||
|
||||
func testBoundaryConditions() throws {
|
||||
// Test exact boundary conditions
|
||||
let packet = TestHelpers.createTestPacket()
|
||||
guard let validEncoded = BinaryProtocol.encode(packet) else {
|
||||
XCTFail("Failed to encode test packet")
|
||||
return
|
||||
}
|
||||
|
||||
// Remove several bytes to ensure it fails - should fail gracefully
|
||||
let truncated = validEncoded.dropLast(10)
|
||||
let result = BinaryProtocol.decode(truncated)
|
||||
XCTAssertNil(result, "Truncated packet should return nil, not crash")
|
||||
|
||||
// Test minimum valid size - create a valid minimal packet
|
||||
var minData = Data()
|
||||
minData.append(1) // version
|
||||
minData.append(1) // type
|
||||
minData.append(10) // ttl
|
||||
|
||||
// Timestamp (8 bytes)
|
||||
for _ in 0..<8 {
|
||||
minData.append(0)
|
||||
}
|
||||
|
||||
minData.append(0) // flags (no optional fields)
|
||||
minData.append(0) // payload length high byte
|
||||
minData.append(0) // payload length low byte (0 payload)
|
||||
|
||||
// SenderID (8 bytes)
|
||||
for _ in 0..<8 {
|
||||
minData.append(0x01)
|
||||
}
|
||||
|
||||
// This should be exactly the minimum size and should decode without crashing
|
||||
let minResult = BinaryProtocol.decode(minData)
|
||||
// The important thing is no crash occurs - result might be nil or valid
|
||||
// We don't assert the result, just that no crash happens
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user