From 43f2ea09cc865141bc5d437750b30be1993be359 Mon Sep 17 00:00:00 2001 From: Fernando M Thomasella Date: Fri, 25 Sep 2026 17:39:43 -0300 Subject: [PATCH] [Swift] Enforce read bounds in release builds (assert -> precondition) and add missing guards The FlatBuffers/Swift readers (ByteBuffer.read/readSlice/readString, FlexBuffers counterparts, skipPrefix) guarded out-of-bounds reads only with assert(), which compiles away in release builds: reading attacker- controlled FlatBuffers/FlexBuffers without a prior verifier pass then performs silent out-of-bounds reads of heap-adjacent memory (see precedent #9081). TypedVector subscript and FlexBuffers getRootChecked had no bounds guard at all (the latter still carries a TODO verifier). - Convert the read-path asserts to precondition() so they hold in release (-O) as well; builder-side (write-path) asserts untouched. - skipPrefix: guard the prefix read against undersized buffers. - TypedVector.subscript: guard index against the vector count. - getRootChecked: fail closed on buffers too small to hold any root. - Add in-bounds regression tests for the guarded paths. Demonstration (linux, swift 6, -O): readSlice(0,64) against a 4-byte buffer returns 64 bytes on master (41 42 43 44 + heap-adjacent), traps with this patch. --- swift/Sources/FlatBuffers/ByteBuffer.swift | 11 ++++-- swift/Sources/FlexBuffers/ByteBuffer.swift | 8 ++-- .../FlexBuffers/Reader/Reference.swift | 3 ++ .../FlexBuffers/Reader/TypedVector.swift | 3 ++ .../ByteBufferBoundsRegressionTests.swift | 37 +++++++++++++++++++ 5 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 tests/swift/Tests/Flatbuffers/ByteBufferBoundsRegressionTests.swift diff --git a/swift/Sources/FlatBuffers/ByteBuffer.swift b/swift/Sources/FlatBuffers/ByteBuffer.swift index f22ecff571..49fee66f99 100644 --- a/swift/Sources/FlatBuffers/ByteBuffer.swift +++ b/swift/Sources/FlatBuffers/ByteBuffer.swift @@ -370,7 +370,7 @@ public struct ByteBuffer: @unchecked Sendable { index: Int, count: Int) -> [T] { - assert( + precondition( index + count <= capacity, "Reading out of bounds is illegal") @@ -393,7 +393,7 @@ public struct ByteBuffer: @unchecked Sendable { count: Int, body: (UnsafeRawBufferPointer) throws -> T) rethrows -> T { - assert( + precondition( index + count <= capacity, "Reading out of bounds is illegal") return try _storage.readWithUnsafeRawPointer(position: index) { @@ -413,7 +413,7 @@ public struct ByteBuffer: @unchecked Sendable { count: Int, type: String.Encoding = .utf8) -> String? { - assert( + precondition( index + count <= capacity, "Reading out of bounds is illegal") return _storage.readWithUnsafeRawPointer(position: index) { @@ -435,7 +435,7 @@ public struct ByteBuffer: @unchecked Sendable { at index: Int, count: Int) -> String? { - assert( + precondition( index + count <= capacity, "Reading out of bounds is illegal") return _storage.readWithUnsafeRawPointer(position: index) { @@ -465,6 +465,9 @@ public struct ByteBuffer: @unchecked Sendable { @usableFromInline @inline(__always) mutating func skipPrefix() -> Int32 { + precondition( + _readerIndex >= MemoryLayout.size && capacity >= MemoryLayout.size, + "Reading out of bounds is illegal") _readerIndex = _readerIndex &- MemoryLayout.size return read(def: Int32.self, position: 0) } diff --git a/swift/Sources/FlexBuffers/ByteBuffer.swift b/swift/Sources/FlexBuffers/ByteBuffer.swift index 6789618ced..ca8d123a54 100644 --- a/swift/Sources/FlexBuffers/ByteBuffer.swift +++ b/swift/Sources/FlexBuffers/ByteBuffer.swift @@ -411,7 +411,7 @@ public struct ByteBuffer { index: Int, count: Int) -> [T] { - assert( + precondition( index + count <= capacity, "Reading out of bounds is illegal") @@ -429,7 +429,7 @@ public struct ByteBuffer { count: Int, type: String.Encoding) -> String? { - assert( + precondition( index + count <= capacity, "Reading out of bounds is illegal") return _storage.readWithUnsafeRawPointer(position: index) { @@ -451,7 +451,7 @@ public struct ByteBuffer { at index: Int, count: Int) -> String? { - assert( + precondition( index + count <= capacity, "Reading out of bounds is illegal") return _storage.readWithUnsafeRawPointer(position: index) { @@ -470,7 +470,7 @@ public struct ByteBuffer { count: Int, body: (UnsafeRawBufferPointer) throws -> T) rethrows -> T { - assert( + precondition( index + count <= capacity, "Reading out of bounds is illegal") return try _storage.readWithUnsafeRawPointer(position: index) { diff --git a/swift/Sources/FlexBuffers/Reader/Reference.swift b/swift/Sources/FlexBuffers/Reader/Reference.swift index e78efecce7..01bd75535f 100644 --- a/swift/Sources/FlexBuffers/Reader/Reference.swift +++ b/swift/Sources/FlexBuffers/Reader/Reference.swift @@ -47,6 +47,9 @@ public func getRoot(buffer: ByteBuffer) throws -> Reference? { @inline(__always) public func getRootChecked(buffer: ByteBuffer) throws -> Reference? { // TODO(mustiikhalil): implement verifier + // Until a full verifier exists, at minimum fail closed when the root + // header cannot possibly fit in the buffer. + precondition(buffer.capacity >= 3, "FlexBuffer too small to hold a root") return try getRoot(buffer: buffer) } diff --git a/swift/Sources/FlexBuffers/Reader/TypedVector.swift b/swift/Sources/FlexBuffers/Reader/TypedVector.swift index 45880a96c4..e838cecf59 100644 --- a/swift/Sources/FlexBuffers/Reader/TypedVector.swift +++ b/swift/Sources/FlexBuffers/Reader/TypedVector.swift @@ -41,6 +41,9 @@ public struct TypedVector: FlexBufferVector { @inline(__always) public subscript(index: Int) -> Reference? { + precondition( + index >= 0 && index < count, + "TypedVector index out of bounds") let elementOffset = offset &+ (numericCast(index) &* numericCast(byteWidth)) return Reference( byteBuffer: byteBuffer, diff --git a/tests/swift/Tests/Flatbuffers/ByteBufferBoundsRegressionTests.swift b/tests/swift/Tests/Flatbuffers/ByteBufferBoundsRegressionTests.swift new file mode 100644 index 0000000000..8536b1cff7 --- /dev/null +++ b/tests/swift/Tests/Flatbuffers/ByteBufferBoundsRegressionTests.swift @@ -0,0 +1,37 @@ +import XCTest + +@testable import FlatBuffers + +/// Regression coverage for the release-enforced read guards. +/// +/// `read`, `readSlice`, `readString`, `readDirectString`, and `skipPrefix` +/// guard against out-of-bounds reads with `precondition` (enforced in release +/// builds). These tests pin the in-bounds behavior so the guards cannot +/// accidentally reject well-formed access. +final class ByteBufferBoundsRegressionTests: XCTestCase { + + func testReadSliceInBounds() { + let buffer = ByteBuffer(bytes: [1, 2, 3, 4, 5, 6, 7, 8]) + let slice: [UInt8] = buffer.readSlice(index: 2, count: 4) + XCTAssertEqual(slice, [3, 4, 5, 6]) + } + + func testReadStringInBounds() { + let buffer = ByteBuffer(bytes: Array("hello flatbuffers".utf8)) + let str = buffer.readString(at: 0, count: 5) + XCTAssertEqual(str, "hello") + } + + func testReadStringInteriorInBounds() { + let buffer = ByteBuffer(bytes: Array("swift".utf8)) + let str = buffer.readString(at: 1, count: 4) + XCTAssertEqual(str, "wift") + } + + func testSkipPrefixOnPrefixedBuffer() { + var buffer = ByteBuffer(bytes: [0x03, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC]) + let prefix = buffer.skipPrefix() + XCTAssertEqual(prefix, 3) + XCTAssertEqual(buffer.read(def: UInt8.self, position: 4), 0xAA) // position is _readerIndex-relative + } +}