Skip to content

Commit 052de38

Browse files
committed
[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.
1 parent 5761d6e commit 052de38

2 files changed

Lines changed: 88 additions & 3 deletions

File tree

swift/Sources/FlatBuffers/Table.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ public struct Table {
5555
< bb
5656
.read(def: VOffset.self, position: Int(vtable))
5757
? Int32(
58+
// Read the slot as unsigned VOffset to match TableVerifier.dereference;
59+
// a signed re-read diverges for slots >= 0x8000.
5860
bb.read(
59-
def: Int16.self,
61+
def: VOffset.self,
6062
position: Int(vtable &+ Int32(o)))) : 0
6163
}
6264

@@ -201,8 +203,9 @@ public struct Table {
201203
let vTable = Int32(fbb.capacity) &- o
202204
return vTable
203205
&+ Int32(
206+
// Read the slot as unsigned VOffset to match TableVerifier.dereference.
204207
fbb.read(
205-
def: Int16.self,
208+
def: VOffset.self,
206209
position: Int(
207210
vTable &+ vOffset
208211
&- fbb.read(
@@ -283,8 +286,9 @@ public struct Table {
283286
let vTable = Int32(fbb.capacity) &- o
284287
return vTable
285288
&+ Int32(
289+
// Read the slot as unsigned VOffset to match TableVerifier.dereference.
286290
fbb.read(
287-
def: Int16.self,
291+
def: VOffset.self,
288292
position: Int(
289293
vTable &+ vOffset
290294
&- fbb.read(

tests/swift/Tests/Flatbuffers/FlatbuffersVerifierTests.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,4 +561,85 @@ final class FlatbuffersVerifierTests {
561561
let offset: UOffset = try verifier.getValue(at: value)
562562
return Int(clamping: (Int(offset) &+ 0).magnitude)
563563
}
564+
565+
@Test
566+
func testHighVTableSlotIsReadUnsignedLikeTheVerifier() throws {
567+
// Crafted buffer whose `array` vtable slot is 0x8000. The verifier reads
568+
// the slot as an unsigned offset and accepts the buffer (the vector at
569+
// table + 32768 is in bounds). Table.offset must observe the same
570+
// displacement; re-reading the slot as a signed value would send the
571+
// accessor before the start of the buffer.
572+
// `array` is the 3rd field in tests/vector_has_test.fbs (VT.array = 8).
573+
// The object size of 4 is deliberate: field data may live outside the
574+
// table's inline region; the verifier only checks target bounds.
575+
var bytes = [UInt8](repeating: 0, count: 32792)
576+
bytes[0..<4] = [4, 0, 0, 0] // root offset -> table at 4
577+
bytes[4..<8] = [0xFC, 0xFF, 0xFF, 0xFF] // soffset -4 -> vtable at 8
578+
bytes[8..<10] = [10, 0] // vtable size
579+
bytes[10..<12] = [4, 0] // object size
580+
bytes[16..<18] = [0, 0x80] // `array` slot (vtable + 8) = 0x8000
581+
// Field lives at table (4) + 0x8000 and holds a uoffset to the vector.
582+
bytes[32772..<32776] = [8, 0, 0, 0] // uoffset -> vector header at 32780
583+
bytes[32780..<32784] = [1, 0, 0, 0] // vector length = 1
584+
bytes[32784..<32792] = [0x2A, 0, 0, 0, 0, 0, 0, 0] // element 42
585+
586+
var byteBuffer = ByteBuffer(bytes: bytes)
587+
let vectors: Swift_Tests_Vectors = try getCheckedRoot(byteBuffer: &byteBuffer)
588+
#expect(vectors.array.count == 1)
589+
#expect(vectors.array[0] == 42)
590+
}
591+
592+
@Test
593+
func testHighVTableSlotMutateUsesVerifiedPosition() throws {
594+
// Same crafted-slot shape through the write path: `mutate` reuses
595+
// Table.offset, so the patched unsigned read must also steer writes to
596+
// the verified field position. `count` is the 3rd field of
597+
// MyGame.Example.Stat (VT.count = 8).
598+
var bytes = [UInt8](repeating: 0, count: 32776)
599+
bytes[0..<4] = [4, 0, 0, 0] // root offset -> table at 4
600+
bytes[4..<8] = [0xFC, 0xFF, 0xFF, 0xFF] // soffset -4 -> vtable at 8
601+
bytes[8..<10] = [10, 0] // vtable size
602+
bytes[10..<12] = [4, 0] // object size
603+
bytes[16..<18] = [0, 0x80] // `count` slot (vtable + 8) = 0x8000
604+
bytes[32772..<32774] = [0x2A, 0] // count = 42 at table (4) + 0x8000
605+
606+
var byteBuffer = ByteBuffer(bytes: bytes)
607+
var stat: MyGame_Example_Stat = try getCheckedRoot(byteBuffer: &byteBuffer)
608+
#expect(stat.count == 42)
609+
#expect(stat.mutate(count: 0x7777))
610+
#expect(stat.count == 0x7777)
611+
}
612+
613+
@Test
614+
func testHighVTableSlotLookupByKeyUsesVerifiedPosition() throws {
615+
// Generated `lookupByKey` resolves keyed-table slots through the static
616+
// Table.offset(_:vOffset:fbb:) overload; the slot read there must match
617+
// the verifier too. Monster carries a sorted Stat vector
618+
// (VT.scalarKeySortedTables = 104) whose Stat `count` slot
619+
// (VT.count = 8) is 0x8000.
620+
var bytes = [UInt8](repeating: 0, count: 32928)
621+
bytes[0..<4] = [4, 0, 0, 0] // root offset -> Monster table at 4
622+
bytes[4..<8] = [0xFC, 0xFF, 0xFF, 0xFF] // soffset -4 -> vtable at 8
623+
bytes[8..<10] = [106, 0] // Monster vtable size (covers slot 104)
624+
bytes[10..<12] = [4, 0] // object size
625+
bytes[18..<20] = [112, 0] // `name` slot (vtable + 10) -> field at 116
626+
bytes[112..<114] = [128, 0] // `scalarKeySortedTables` slot -> field at 132
627+
bytes[116..<120] = [8, 0, 0, 0] // name uoffset -> string at 124
628+
bytes[124..<128] = [2, 0, 0, 0] // string length
629+
bytes[128..<130] = [0x41, 0x42] // "AB"
630+
bytes[132..<136] = [8, 0, 0, 0] // vector-field uoffset -> header at 140
631+
bytes[140..<144] = [1, 0, 0, 0] // vector length = 1
632+
bytes[144..<148] = [8, 0, 0, 0] // element uoffset -> Stat table at 152
633+
bytes[152..<156] = [0xF8, 0xFF, 0xFF, 0xFF] // soffset -8 -> vtable at 160
634+
bytes[160..<162] = [10, 0] // Stat vtable size
635+
bytes[162..<164] = [4, 0] // object size
636+
bytes[168..<170] = [0, 0x80] // `count` slot (vtable + 8) = 0x8000
637+
bytes[32920..<32922] = [0, 0] // count = 0 at Stat (152) + 0x8000
638+
639+
var byteBuffer = ByteBuffer(bytes: bytes)
640+
let monster: MyGame_Example_Monster = try getCheckedRoot(byteBuffer: &byteBuffer)
641+
let stat = monster.scalarKeySortedTablesBy(key: 0)
642+
#expect(stat != nil)
643+
#expect(stat?.count == 0)
644+
}
564645
}

0 commit comments

Comments
 (0)