Merge b74e92958d1e8dcd58d6c2ef5750754d68cc4464 into 9b84b361225facd8e623f25d76f889d3dc54a879

This commit is contained in:
kappaone-eth 2026-08-23 15:20:18 +00:00 committed by GitHub
commit 081d144f11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 161 additions and 0 deletions

View File

@ -18,6 +18,7 @@ struct BLEInboundWriteBuffer {
case decoded(packet: BitchatPacket, metadata: BLEInboundWriteAppendMetadata)
case waiting(metadata: BLEInboundWriteAppendMetadata)
case oversized(metadata: BLEInboundWriteAppendMetadata)
case invalid(metadata: BLEInboundWriteAppendMetadata)
}
private var buffersByCentralID: [String: Data] = [:]
@ -34,10 +35,36 @@ struct BLEInboundWriteBuffer {
var combined = buffersByCentralID[centralID] ?? Data()
var appendedBytes = 0
var offsets: [Int] = []
// New chunks must not overlap bytes already buffered for this central.
var lastEnd = combined.count
for chunk in chunks where !chunk.data.isEmpty {
offsets.append(chunk.offset)
// `.withoutResponse` writes always land at offset 0, so an
// offset-0 chunk arriving on a non-empty buffer is a fresh frame
// replacing stale partial bytes, not an overlap.
if chunk.offset == 0, !combined.isEmpty {
combined.removeAll()
lastEnd = 0
}
// Reject malformed writes before touching the buffer: a negative
// offset traps `Data.replaceSubrange`, and non-monotonic or
// overlapping offsets corrupt previously written bytes.
guard chunk.offset >= 0, chunk.offset >= lastEnd else {
let metadata = BLEInboundWriteAppendMetadata(
accumulatedBytes: combined.count,
appendedBytes: appendedBytes,
offsets: offsets,
packetType: combined.count >= 2 ? combined[1] : nil
)
buffersByCentralID.removeValue(forKey: centralID)
return .invalid(metadata: metadata)
}
let end = chunk.offset + chunk.data.count
lastEnd = end
if combined.count < end {
combined.append(Data(repeating: 0, count: end - combined.count))

View File

@ -284,6 +284,11 @@ extension BLEService: CBPeripheralManagerDelegate {
logAccumulatedCentralWrite(metadata, centralUUID: centralUUID)
SecureLogger.warning("⚠️ Dropping oversized pending write buffer (\(metadata.accumulatedBytes) bytes) for central \(centralUUID.prefix(8))…", category: .session)
logFailedSingleWriteIfNeeded(hasMultiple: hasMultiple, sortedRequests: sorted)
case let .invalid(metadata):
logAccumulatedCentralWrite(metadata, centralUUID: centralUUID)
SecureLogger.warning("⚠️ Dropping malformed pending write (offsets=\(metadata.offsets)) for central \(centralUUID.prefix(8))…", category: .session)
logFailedSingleWriteIfNeeded(hasMultiple: hasMultiple, sortedRequests: sorted)
}
}
}

View File

@ -126,6 +126,135 @@ struct BLEInboundWriteBufferTests {
}
}
@Test
func appendRejectsNegativeOffsetAndClearsBuffer() throws {
var buffer = BLEInboundWriteBuffer()
_ = buffer.append(
chunks: [BLEInboundWriteChunk(offset: 0, data: Data(repeating: 0x01, count: 4))],
for: "central-1",
capBytes: 1024
)
let result = buffer.append(
chunks: [BLEInboundWriteChunk(offset: -1, data: Data([0x02]))],
for: "central-1",
capBytes: 1024
)
if case let .invalid(metadata) = result {
#expect(metadata.offsets == [-1])
} else {
Issue.record("Expected negative offset to be rejected")
}
// The poisoned buffer was cleared, so a clean full frame still decodes.
let packet = makePacket(timestamp: 0x123)
let frame = try #require(packet.toBinaryData(padding: false))
let decoded = buffer.append(
chunks: [BLEInboundWriteChunk(offset: 0, data: frame)],
for: "central-1",
capBytes: 1024
)
if case let .decoded(decodedPacket, _) = decoded {
#expect(decodedPacket.timestamp == packet.timestamp)
} else {
Issue.record("Expected clean frame to decode after invalid write reset the buffer")
}
}
@Test
func appendRejectsOverlappingChunks() {
var buffer = BLEInboundWriteBuffer()
let result = buffer.append(
chunks: [
BLEInboundWriteChunk(offset: 0, data: Data(repeating: 0x01, count: 8)),
BLEInboundWriteChunk(offset: 4, data: Data(repeating: 0x02, count: 4))
],
for: "central-1",
capBytes: 1024
)
if case let .invalid(metadata) = result {
#expect(metadata.offsets == [0, 4])
} else {
Issue.record("Expected overlapping chunks to be rejected")
}
}
@Test
func appendRejectsNonMonotonicChunks() {
var buffer = BLEInboundWriteBuffer()
let result = buffer.append(
chunks: [
BLEInboundWriteChunk(offset: 8, data: Data(repeating: 0x01, count: 4)),
BLEInboundWriteChunk(offset: 4, data: Data(repeating: 0x02, count: 4))
],
for: "central-1",
capBytes: 1024
)
if case let .invalid(metadata) = result {
#expect(metadata.offsets == [8, 4])
} else {
Issue.record("Expected non-monotonic chunks to be rejected")
}
}
@Test
func appendTreatsOffsetZeroOnStaleBufferAsRestart() throws {
var buffer = BLEInboundWriteBuffer()
let packet = makePacket(timestamp: 0x456)
let frame = try #require(packet.toBinaryData(padding: false))
let splitIndex = max(1, frame.count / 2)
// Stale partial frame left pending from an earlier write.
_ = buffer.append(
chunks: [BLEInboundWriteChunk(offset: 0, data: frame.prefix(splitIndex))],
for: "central-1",
capBytes: 1024
)
// A fresh `.withoutResponse` write lands at offset 0 and replaces it
// instead of being rejected as an overlap with the stale bytes.
let result = buffer.append(
chunks: [BLEInboundWriteChunk(offset: 0, data: frame)],
for: "central-1",
capBytes: 1024
)
if case let .decoded(decoded, metadata) = result {
#expect(decoded.timestamp == packet.timestamp)
#expect(metadata.offsets == [0])
} else {
Issue.record("Expected offset-0 write to restart the stale buffer")
}
}
@Test
func appendRejectsOverlapWithPreviouslyBufferedBytes() {
var buffer = BLEInboundWriteBuffer()
_ = buffer.append(
chunks: [BLEInboundWriteChunk(offset: 0, data: Data(repeating: 0x01, count: 8))],
for: "central-1",
capBytes: 1024
)
let result = buffer.append(
chunks: [BLEInboundWriteChunk(offset: 4, data: Data(repeating: 0x02, count: 4))],
for: "central-1",
capBytes: 1024
)
if case let .invalid(metadata) = result {
#expect(metadata.offsets == [4])
} else {
Issue.record("Expected overlap with buffered bytes to be rejected")
}
}
private func makePacket(timestamp: UInt64 = 0x0102030405) -> BitchatPacket {
BitchatPacket(
type: MessageType.message.rawValue,