mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-10-25 07:41:41 +00:00
Merge b74e92958d1e8dcd58d6c2ef5750754d68cc4464 into 9b84b361225facd8e623f25d76f889d3dc54a879
This commit is contained in:
commit
081d144f11
@ -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))
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user