From 052de38ad0c6a99a0b8f51d094bd85f77d0cd89f Mon Sep 17 00:00:00 2001 From: Nova Date: Thu, 20 Aug 2026 10:18:50 +0800 Subject: [PATCH] [Swift] Read vtable field slots as unsigned VOffset to match the verifier TableVerifier.dereference validates vtable field slots as VOffset (UInt16), but the accessors re-read the same slots as Int16. A slot of 0x8000 is +32768 for the verifier (accepted when in bounds) but -32768 for the accessors, steering reads and writes before the start of a verified buffer. Read the slot as VOffset in Table.offset and the two static Table.offset(_:vOffset:fbb:) overloads used by generated lookupByKey/sortVectorOf code, so accessors observe the same displacement the verifier validated. Slots >= 0x8000 are format-legal for tables with more than 32KB of inline field data and are handled correctly after this change; slots <= 0x7FFF are bit-identical. Adds crafted-buffer regression tests for the read, mutate, and lookupByKey paths. --- swift/Sources/FlatBuffers/Table.swift | 10 ++- .../FlatbuffersVerifierTests.swift | 81 +++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/swift/Sources/FlatBuffers/Table.swift b/swift/Sources/FlatBuffers/Table.swift index de87626549..157c0c428f 100644 --- a/swift/Sources/FlatBuffers/Table.swift +++ b/swift/Sources/FlatBuffers/Table.swift @@ -55,8 +55,10 @@ public struct Table { < bb .read(def: VOffset.self, position: Int(vtable)) ? Int32( + // Read the slot as unsigned VOffset to match TableVerifier.dereference; + // a signed re-read diverges for slots >= 0x8000. bb.read( - def: Int16.self, + def: VOffset.self, position: Int(vtable &+ Int32(o)))) : 0 } @@ -201,8 +203,9 @@ public struct Table { let vTable = Int32(fbb.capacity) &- o return vTable &+ Int32( + // Read the slot as unsigned VOffset to match TableVerifier.dereference. fbb.read( - def: Int16.self, + def: VOffset.self, position: Int( vTable &+ vOffset &- fbb.read( @@ -283,8 +286,9 @@ public struct Table { let vTable = Int32(fbb.capacity) &- o return vTable &+ Int32( + // Read the slot as unsigned VOffset to match TableVerifier.dereference. fbb.read( - def: Int16.self, + def: VOffset.self, position: Int( vTable &+ vOffset &- fbb.read( diff --git a/tests/swift/Tests/Flatbuffers/FlatbuffersVerifierTests.swift b/tests/swift/Tests/Flatbuffers/FlatbuffersVerifierTests.swift index 0b5847bcc5..c78e4461e0 100644 --- a/tests/swift/Tests/Flatbuffers/FlatbuffersVerifierTests.swift +++ b/tests/swift/Tests/Flatbuffers/FlatbuffersVerifierTests.swift @@ -561,4 +561,85 @@ final class FlatbuffersVerifierTests { let offset: UOffset = try verifier.getValue(at: value) return Int(clamping: (Int(offset) &+ 0).magnitude) } + + @Test + func testHighVTableSlotIsReadUnsignedLikeTheVerifier() throws { + // Crafted buffer whose `array` vtable slot is 0x8000. The verifier reads + // the slot as an unsigned offset and accepts the buffer (the vector at + // table + 32768 is in bounds). Table.offset must observe the same + // displacement; re-reading the slot as a signed value would send the + // accessor before the start of the buffer. + // `array` is the 3rd field in tests/vector_has_test.fbs (VT.array = 8). + // The object size of 4 is deliberate: field data may live outside the + // table's inline region; the verifier only checks target bounds. + var bytes = [UInt8](repeating: 0, count: 32792) + bytes[0..<4] = [4, 0, 0, 0] // root offset -> table at 4 + bytes[4..<8] = [0xFC, 0xFF, 0xFF, 0xFF] // soffset -4 -> vtable at 8 + bytes[8..<10] = [10, 0] // vtable size + bytes[10..<12] = [4, 0] // object size + bytes[16..<18] = [0, 0x80] // `array` slot (vtable + 8) = 0x8000 + // Field lives at table (4) + 0x8000 and holds a uoffset to the vector. + bytes[32772..<32776] = [8, 0, 0, 0] // uoffset -> vector header at 32780 + bytes[32780..<32784] = [1, 0, 0, 0] // vector length = 1 + bytes[32784..<32792] = [0x2A, 0, 0, 0, 0, 0, 0, 0] // element 42 + + var byteBuffer = ByteBuffer(bytes: bytes) + let vectors: Swift_Tests_Vectors = try getCheckedRoot(byteBuffer: &byteBuffer) + #expect(vectors.array.count == 1) + #expect(vectors.array[0] == 42) + } + + @Test + func testHighVTableSlotMutateUsesVerifiedPosition() throws { + // Same crafted-slot shape through the write path: `mutate` reuses + // Table.offset, so the patched unsigned read must also steer writes to + // the verified field position. `count` is the 3rd field of + // MyGame.Example.Stat (VT.count = 8). + var bytes = [UInt8](repeating: 0, count: 32776) + bytes[0..<4] = [4, 0, 0, 0] // root offset -> table at 4 + bytes[4..<8] = [0xFC, 0xFF, 0xFF, 0xFF] // soffset -4 -> vtable at 8 + bytes[8..<10] = [10, 0] // vtable size + bytes[10..<12] = [4, 0] // object size + bytes[16..<18] = [0, 0x80] // `count` slot (vtable + 8) = 0x8000 + bytes[32772..<32774] = [0x2A, 0] // count = 42 at table (4) + 0x8000 + + var byteBuffer = ByteBuffer(bytes: bytes) + var stat: MyGame_Example_Stat = try getCheckedRoot(byteBuffer: &byteBuffer) + #expect(stat.count == 42) + #expect(stat.mutate(count: 0x7777)) + #expect(stat.count == 0x7777) + } + + @Test + func testHighVTableSlotLookupByKeyUsesVerifiedPosition() throws { + // Generated `lookupByKey` resolves keyed-table slots through the static + // Table.offset(_:vOffset:fbb:) overload; the slot read there must match + // the verifier too. Monster carries a sorted Stat vector + // (VT.scalarKeySortedTables = 104) whose Stat `count` slot + // (VT.count = 8) is 0x8000. + var bytes = [UInt8](repeating: 0, count: 32928) + bytes[0..<4] = [4, 0, 0, 0] // root offset -> Monster table at 4 + bytes[4..<8] = [0xFC, 0xFF, 0xFF, 0xFF] // soffset -4 -> vtable at 8 + bytes[8..<10] = [106, 0] // Monster vtable size (covers slot 104) + bytes[10..<12] = [4, 0] // object size + bytes[18..<20] = [112, 0] // `name` slot (vtable + 10) -> field at 116 + bytes[112..<114] = [128, 0] // `scalarKeySortedTables` slot -> field at 132 + bytes[116..<120] = [8, 0, 0, 0] // name uoffset -> string at 124 + bytes[124..<128] = [2, 0, 0, 0] // string length + bytes[128..<130] = [0x41, 0x42] // "AB" + bytes[132..<136] = [8, 0, 0, 0] // vector-field uoffset -> header at 140 + bytes[140..<144] = [1, 0, 0, 0] // vector length = 1 + bytes[144..<148] = [8, 0, 0, 0] // element uoffset -> Stat table at 152 + bytes[152..<156] = [0xF8, 0xFF, 0xFF, 0xFF] // soffset -8 -> vtable at 160 + bytes[160..<162] = [10, 0] // Stat vtable size + bytes[162..<164] = [4, 0] // object size + bytes[168..<170] = [0, 0x80] // `count` slot (vtable + 8) = 0x8000 + bytes[32920..<32922] = [0, 0] // count = 0 at Stat (152) + 0x8000 + + var byteBuffer = ByteBuffer(bytes: bytes) + let monster: MyGame_Example_Monster = try getCheckedRoot(byteBuffer: &byteBuffer) + let stat = monster.scalarKeySortedTablesBy(key: 0) + #expect(stat != nil) + #expect(stat?.count == 0) + } }