This is an automated email from the ASF dual-hosted git repository.

kou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-swift.git


The following commit(s) were added to refs/heads/main by this push:
     new 20dac0a  fix: Guard against force unwraps on malformed ArrowReader 
input (#183)
20dac0a is described below

commit 20dac0a2c4049915eeed09aab13a617763788a55
Author: demetrius albuquerque <[email protected]>
AuthorDate: Thu Sep 3 02:04:02 2026 +0200

    fix: Guard against force unwraps on malformed ArrowReader input (#183)
    
    ## What's Changed
    
    `readStreaming`, `readFile`, and `fromMessage` force-unwrapped optional
    schema and message header values, causing crashes on malformed or
    out-of-order input instead of returning an `ArrowError`. For example, a
    RecordBatch message arriving with no preceding Schema message would
    crash the process rather than fail gracefully.
    
    Replaced the force unwraps with guard statements that return `.failure`
    instead:
    - `readStreaming`: guards on the RecordBatch header, `schemaMessage`,
    and `result.schema` before use
    - `readFile`: guards on `footer.schema`, the RecordBatch header, and
    `result.schema` before use
    - `fromMessage`: guards on the Schema header, the RecordBatch header,
    `result.messageSchema`, and `result.schema` before use
    
    While writing a regression test for the above, found that
    `readStreaming` also assumed zero-based `Data` indices internally.
    Slicing a `Data` (`data[offset...]`) preserves the original absolute
    indices rather than resetting to zero, so a caller passing a slice —
    rather than a freshly allocated `Data` — could desync the reader's
    internal offset tracking and crash on a completely valid, well-formed
    input. Fixed by offsetting from `input.startIndex` instead of assuming
    0.
    
    ## Testing
    
    Added a regression test (`testReadStreamingRecordBatchBeforeSchema`)
    that builds a valid streaming payload, strips the Schema message via
    slicing, and confirms `readStreaming` now returns `.failure` instead of
    crashing. This test only passes with both fixes in place — reverting
    either one reproduces a crash.
    
    Full test suite passes (48 tests, 0 failures).
    
    Closes #182
---
 Sources/Arrow/ArrowReader.swift | 48 +++++++++++++++++++++++++++++++----------
 Tests/ArrowTests/IPCTests.swift | 45 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 82 insertions(+), 11 deletions(-)

diff --git a/Sources/Arrow/ArrowReader.swift b/Sources/Arrow/ArrowReader.swift
index d1c0853..ee91eba 100644
--- a/Sources/Arrow/ArrowReader.swift
+++ b/Sources/Arrow/ArrowReader.swift
@@ -274,7 +274,7 @@ public class ArrowReader { // swiftlint:disable:this 
type_body_length
             }
 
             offset += Int(MemoryLayout<UInt32>.size)
-            streamData = input[offset...]
+            streamData = input[(input.startIndex + offset)...]
             var dataBuffer = ByteBuffer(
                 data: streamData,
                 allowReadingUnalignedBuffers: useUnalignedBuffers
@@ -282,11 +282,19 @@ public class ArrowReader { // swiftlint:disable:this 
type_body_length
             let message: org_apache_arrow_flatbuf_Message = 
getRoot(byteBuffer: &dataBuffer)
             switch message.headerType {
             case .recordbatch:
-                let rbMessage = message.header(type: 
org_apache_arrow_flatbuf_RecordBatch.self)!
+                guard let rbMessage = message.header(type: 
org_apache_arrow_flatbuf_RecordBatch.self) else {
+                    return .failure(.invalid("RecordBatch header not found"))
+                }
+                guard let schemaMsg = schemaMessage else {
+                    return .failure(.invalid("Schema must be defined before 
RecordBatch"))
+                }
+                guard let schema = result.schema else {
+                    return .failure(.invalid("Schema not loaded"))
+                }
                 let recordBatchResult = loadRecordBatch(
                     rbMessage,
-                    schema: schemaMessage!,
-                    arrowSchema: result.schema!,
+                    schema: schemaMsg,
+                    arrowSchema: schema,
                     data: input,
                     messageEndOffset: (Int64(offset) + Int64(length)))
                 switch recordBatchResult {
@@ -335,7 +343,10 @@ public class ArrowReader { // swiftlint:disable:this 
type_body_length
             data: footerData,
             allowReadingUnalignedBuffers: useUnalignedBuffers)
         let footer: org_apache_arrow_flatbuf_Footer = getRoot(byteBuffer: 
&footerBuffer)
-        let schemaResult = loadSchema(footer.schema!)
+        guard let footerSchema = footer.schema else {
+            return .failure(.invalid("Footer schema not found"))
+        }
+        let schemaResult = loadSchema(footerSchema)
         switch schemaResult {
         case .success(let schema):
             result.schema = schema
@@ -368,11 +379,16 @@ public class ArrowReader { // swiftlint:disable:this 
type_body_length
             let message: org_apache_arrow_flatbuf_Message = 
getRoot(byteBuffer: &mbb)
             switch message.headerType {
             case .recordbatch:
-                let rbMessage = message.header(type: 
org_apache_arrow_flatbuf_RecordBatch.self)!
+                guard let rbMessage = message.header(type: 
org_apache_arrow_flatbuf_RecordBatch.self) else {
+                    return .failure(.invalid("RecordBatch header not found"))
+                }
+                guard let schema = result.schema else {
+                    return .failure(.invalid("Schema not loaded"))
+                }
                 let recordBatchResult = loadRecordBatch(
                     rbMessage,
-                    schema: footer.schema!,
-                    arrowSchema: result.schema!,
+                    schema: footerSchema,
+                    arrowSchema: schema,
                     data: fileData,
                     messageEndOffset: messageEndOffset)
                 switch recordBatchResult {
@@ -421,7 +437,9 @@ public class ArrowReader { // swiftlint:disable:this 
type_body_length
         let message: org_apache_arrow_flatbuf_Message = getRoot(byteBuffer: 
&mbb)
         switch message.headerType {
         case .schema:
-            let sMessage = message.header(type: 
org_apache_arrow_flatbuf_Schema.self)!
+            guard let sMessage = message.header(type: 
org_apache_arrow_flatbuf_Schema.self) else {
+                return .failure(.invalid("Schema header not found"))
+            }
             switch loadSchema(sMessage) {
             case .success(let schema):
                 result.schema = schema
@@ -431,9 +449,17 @@ public class ArrowReader { // swiftlint:disable:this 
type_body_length
                 return .failure(error)
             }
         case .recordbatch:
-            let rbMessage = message.header(type: 
org_apache_arrow_flatbuf_RecordBatch.self)!
+            guard let rbMessage = message.header(type: 
org_apache_arrow_flatbuf_RecordBatch.self) else {
+                return .failure(.invalid("RecordBatch header not found"))
+            }
+            guard let messageSchema = result.messageSchema else {
+                return .failure(.invalid("Schema must be defined before 
RecordBatch"))
+            }
+            guard let schema = result.schema else {
+                return .failure(.invalid("Schema not loaded"))
+            }
             let recordBatchResult = loadRecordBatch(
-                rbMessage, schema: result.messageSchema!, arrowSchema: 
result.schema!,
+                rbMessage, schema: messageSchema, arrowSchema: schema,
                 data: dataBody, messageEndOffset: 0)
             switch recordBatchResult {
             case .success(let recordBatch):
diff --git a/Tests/ArrowTests/IPCTests.swift b/Tests/ArrowTests/IPCTests.swift
index 58f973e..7d7e972 100644
--- a/Tests/ArrowTests/IPCTests.swift
+++ b/Tests/ArrowTests/IPCTests.swift
@@ -262,6 +262,50 @@ final class IPCStreamReaderTests: XCTestCase {
             throw error
         }
     }
+
+    func testReadStreamingRecordBatchBeforeSchema() throws {
+        // Build a minimal streaming message: a RecordBatch header with no
+        // preceding Schema message. This should fail gracefully instead
+        // of crashing on a force unwrap.
+        let schema = makeSchema()
+        let recordBatch = try makeRecordBatch()
+        let arrowWriter = ArrowWriter()
+        let writerInfo = ArrowWriter.Info(.recordbatch, schema: schema, 
batches: [recordBatch])
+
+        switch arrowWriter.writeStreaming(writerInfo) {
+        case .success(let writeData):
+            // Mirror the parsing logic in ArrowReader.readStreaming to advance
+            // past exactly one message (the Schema message), leaving the
+            // RecordBatch message intact and correctly positioned for
+            // readStreaming to parse on its own.
+            var offset = 0
+            var length = getUInt32(writeData, offset: offset)
+            if length == CONTINUATIONMARKER {
+                offset += Int(MemoryLayout<UInt32>.size)
+                length = getUInt32(writeData, offset: offset)
+            }
+            offset += Int(MemoryLayout<UInt32>.size)
+
+            var dataBuffer = ByteBuffer(
+                data: writeData[offset...],
+                allowReadingUnalignedBuffers: false)
+            let message: org_apache_arrow_flatbuf_Message = 
getRoot(byteBuffer: &dataBuffer)
+            XCTAssertEqual(message.headerType, .schema)
+
+            offset += Int(message.bodyLength + Int64(length))
+            let truncatedData = writeData[offset...]
+
+            let arrowReader = ArrowReader()
+            switch arrowReader.readStreaming(truncatedData) {
+            case .success:
+                XCTFail("Expected failure when RecordBatch precedes Schema")
+            case .failure:
+                break // Correct: should fail gracefully, not crash
+            }
+        case .failure(let error):
+            throw error
+        }
+    }
 }
 
 final class IPCFileReaderTests: XCTestCase { // swiftlint:disable:this 
type_body_length
@@ -671,5 +715,6 @@ final class IPCFileReaderTests: XCTestCase { // 
swiftlint:disable:this type_body
             throw error
         }
     }
+
 }
 // swiftlint:disable:this file_length

Reply via email to