[Swift] Verify size-prefixed roots from reader position (#9196)

* [Swift] Verify size-prefixed roots from reader position

Validate the size prefix before advancing ByteBuffer.reader, then use the active reader position consistently for root, file identifier, and returned-object verification. Cover malformed decoy roots, oversized prefixes, short identifiers, and valid prefixed identifiers.

* Use wrapping arithmetic in Swift verifier

---------

Co-authored-by: Darren Carreras <283775510+carrerasdarren-cell@users.noreply.github.com>
diff --git a/swift/Sources/FlatBuffers/Root.swift b/swift/Sources/FlatBuffers/Root.swift
index e176414..807f7b4 100644
--- a/swift/Sources/FlatBuffers/Root.swift
+++ b/swift/Sources/FlatBuffers/Root.swift
@@ -16,6 +16,30 @@
 
 import Foundation
 
+@inline(__always)
+private func verifySizePrefix(
+  byteBuffer: inout ByteBuffer,
+  requireExactSize: Bool,
+  options: VerifierOptions) throws
+{
+  let verifier = try Verifier(buffer: &byteBuffer, options: options)
+  let prefixPosition = byteBuffer.reader
+  let prefix: UOffset = try verifier.getValue(at: prefixPosition)
+  let availableSize = byteBuffer.size &- UOffset(MemoryLayout<UOffset>.size)
+
+  if requireExactSize {
+    guard prefix == availableSize else {
+      throw FlatbuffersErrors.prefixedSizeNotEqualToBufferSize
+    }
+  } else if prefix > availableSize {
+    throw FlatbuffersErrors.outOfBounds(
+      position: UInt(prefixPosition)
+        &+ UInt(MemoryLayout<UOffset>.size)
+        &+ UInt(prefix),
+      end: byteBuffer.capacity)
+  }
+}
+
 /// Takes in a prefixed sized buffer, where the prefixed size would be skipped.
 /// And would verify that the buffer passed is a valid `Flatbuffers` Object.
 /// - Parameters:
@@ -31,6 +55,10 @@
   fileId: String? = nil,
   options: VerifierOptions = .init()) throws -> T
 {
+  try verifySizePrefix(
+    byteBuffer: &byteBuffer,
+    requireExactSize: false,
+    options: options)
   byteBuffer.skipPrefix()
   return try getCheckedRoot(
     byteBuffer: &byteBuffer,
@@ -53,10 +81,11 @@
   fileId: String? = nil,
   options: VerifierOptions = .init()) throws -> T
 {
-  let prefix = byteBuffer.skipPrefix()
-  if prefix != byteBuffer.size {
-    throw FlatbuffersErrors.prefixedSizeNotEqualToBufferSize
-  }
+  try verifySizePrefix(
+    byteBuffer: &byteBuffer,
+    requireExactSize: true,
+    options: options)
+  byteBuffer.skipPrefix()
   return try getCheckedRoot(
     byteBuffer: &byteBuffer,
     fileId: fileId,
@@ -95,14 +124,15 @@
   options: VerifierOptions = .init()) throws -> T
 {
   var verifier = try Verifier(buffer: &byteBuffer, options: options)
+  let rootPosition = byteBuffer.reader
   if let fileId = fileId {
-    try verifier.verify(id: fileId)
+    try verifier.verify(id: fileId, at: rootPosition)
   }
-  try ForwardOffset<T>.verify(&verifier, at: 0, of: T.self)
+  try ForwardOffset<T>.verify(&verifier, at: rootPosition, of: T.self)
   return T.init(
     byteBuffer,
-    o: Int32(byteBuffer.read(def: UOffset.self, position: byteBuffer.reader))
-      &+ Int32(byteBuffer.reader))
+    o: Int32(byteBuffer.read(def: UOffset.self, position: rootPosition))
+      &+ Int32(rootPosition))
 }
 
 /// Returns a `NON-Checked` flatbuffers object
diff --git a/swift/Sources/FlatBuffers/Verifier.swift b/swift/Sources/FlatBuffers/Verifier.swift
index 525e149..5922ddb 100644
--- a/swift/Sources/FlatBuffers/Verifier.swift
+++ b/swift/Sources/FlatBuffers/Verifier.swift
@@ -215,12 +215,16 @@
   }
 
   @inline(__always)
-  func verify(id: String) throws {
+  func verify(id: String, at position: Int) throws {
     let size = MemoryLayout<Int32>.size
-    guard storage.capacity >= (size &* 2) else {
+    guard
+      position >= 0,
+      position <= storage.capacity,
+      storage.capacity - position >= size &* 2
+    else {
       throw FlatbuffersErrors.bufferDoesntContainID
     }
-    let str = _buffer.readString(at: size, count: size)
+    let str = _buffer.readString(at: position &+ size, count: size)
     if id == str {
       return
     }
diff --git a/tests/swift/Tests/Flatbuffers/FlatbuffersVerifierTests.swift b/tests/swift/Tests/Flatbuffers/FlatbuffersVerifierTests.swift
index b4c94ca..0b5847b 100644
--- a/tests/swift/Tests/Flatbuffers/FlatbuffersVerifierTests.swift
+++ b/tests/swift/Tests/Flatbuffers/FlatbuffersVerifierTests.swift
@@ -374,6 +374,58 @@
   }
 
   @Test
+  func testSizePrefixedVerifierUsesPostPrefixRoot() throws {
+    // The size prefix points to a valid empty decoy table at byte 44. The real
+    // root at byte 4 points to a truncated UInt64 vector that must be rejected.
+    let bytes: [UInt8] = [
+      44, 0, 0, 0,
+      16, 0, 0, 0,
+      6, 0, 8, 0, 4, 0,
+      0, 0, 0, 0, 0, 0,
+      12, 0, 0, 0,
+      8, 0, 0, 0, 0, 0, 0, 0,
+      2, 0, 0, 0, 65, 66,
+      0, 0,
+      4, 0, 4, 0,
+      4, 0, 0, 0,
+    ]
+
+    var strictBuffer = ByteBuffer(bytes: bytes)
+    #expect(throws: FlatbuffersErrors.self) {
+      try getCheckedPrefixedSizeRoot(
+        byteBuffer: &strictBuffer) as Swift_Tests_Vectors
+    }
+
+    var prefixedBuffer = ByteBuffer(bytes: bytes)
+    #expect(throws: FlatbuffersErrors.self) {
+      try getPrefixedSizeCheckedRoot(
+        byteBuffer: &prefixedBuffer) as Swift_Tests_Vectors
+    }
+
+    var builder = FlatBufferBuilder()
+    let movie = Movie.createMovie(&builder)
+    Movie.finish(&builder, end: movie, prefix: true)
+    var validBuffer = builder.sizedBuffer
+    let _: Movie = try getCheckedPrefixedSizeRoot(
+      byteBuffer: &validBuffer,
+      fileId: Movie.id)
+
+    var shortBuffer = ByteBuffer(bytes: [0, 0, 0, 0])
+    #expect(throws: FlatbuffersErrors.bufferDoesntContainID) {
+      try getCheckedRoot(
+        byteBuffer: &shortBuffer,
+        fileId: Movie.id) as Movie
+    }
+
+    var oversizedPrefix = ByteBuffer(bytes: [5, 0, 0, 0, 0, 0, 0, 0])
+    #expect(throws: FlatbuffersErrors.outOfBounds(position: 9, end: 8)) {
+      try getPrefixedSizeCheckedRoot(
+        byteBuffer: &oversizedPrefix) as Movie
+    }
+    #expect(oversizedPrefix.reader == 0)
+  }
+
+  @Test
   func testFullVerifier() throws {
     _ =
       try getCheckedRoot(