Skip to content

Commit 679b929

Browse files
committed
[JSON] Fix a potential buffer over read in floating point parsing.
The source JSON buffer is not guaranteed to be null terminated. If a number is at top-level, 'strtod'/'strtof' tries to read past the buffer which is not great
1 parent cf4f3e4 commit 679b929

File tree

2 files changed

+41
-10
lines changed

2 files changed

+41
-10
lines changed

Sources/SwiftCompilerPluginMessageHandling/JSON/JSONDecoding.swift

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -622,19 +622,40 @@ private enum _JSONNumberParser {
622622
}
623623

624624
static func parseFloatingPoint<Floating: BinaryFloatingPoint>(source: UnsafeBufferPointer<UInt8>) -> Floating? {
625-
var endPtr: UnsafeMutablePointer<CChar>? = nil
626-
let value: Floating?
627-
if Floating.self == Double.self {
628-
value = Floating(exactly: strtod(source.baseAddress!, &endPtr))
629-
} else if Floating.self == Float.self {
630-
value = Floating(exactly: strtof(source.baseAddress!, &endPtr))
625+
// Since source is not NUL terminated, we need to make a temporary storage.
626+
// Depending on the length of the source, prepare the buffer on stack or heap,
627+
// then call 'impl(_:)' (defined below) for the actual operation.
628+
if source.count + 1 <= MemoryLayout<UInt64>.size {
629+
var stash: UInt64 = 0
630+
return withUnsafeMutableBytes(of: &stash) {
631+
$0.withMemoryRebound(to: UInt8.self, impl)
632+
}
631633
} else {
632-
fatalError("unsupported floating point type")
634+
let stash = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: source.count + 1)
635+
defer { stash.deallocate() }
636+
return impl(stash)
633637
}
634-
guard endPtr! == source.baseAddress! + source.count else {
635-
return nil
638+
639+
func impl(_ stash: UnsafeMutableBufferPointer<UInt8>) -> Floating? {
640+
// Create a NUL terminated string in the stash.
641+
assert(stash.count >= source.count + 1)
642+
let end = stash.initialize(fromContentsOf: source)
643+
stash.initializeElement(at: end, to: 0)
644+
645+
var endPtr: UnsafeMutablePointer<CChar>? = nil
646+
let value: Floating?
647+
if Floating.self == Double.self {
648+
value = Floating(exactly: strtod(stash.baseAddress!, &endPtr))
649+
} else if Floating.self == Float.self {
650+
value = Floating(exactly: strtof(stash.baseAddress!, &endPtr))
651+
} else {
652+
preconditionFailure("unsupported floating point type")
653+
}
654+
guard let endPtr, endPtr == stash.baseAddress! + source.count else {
655+
return nil
656+
}
657+
return value
636658
}
637-
return value
638659
}
639660

640661
static func parseHexIntegerDigits<Integer: FixedWidthInteger>(source: UnsafeBufferPointer<UInt8>) -> Integer? {

Tests/SwiftCompilerPluginTest/JSONTests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,16 @@ final class JSONTests: XCTestCase {
172172
assertRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Double], as: [Bool].self)
173173
}
174174

175+
func testFloatingPointBufferBoundary() throws {
176+
// Make sure floating point parsing does not read past the decoding JSON buffer.
177+
var str = "0.199"
178+
try str.withUTF8 { buf in
179+
let truncated = UnsafeBufferPointer(rebasing: buf[0..<3])
180+
XCTAssertEqual(try JSON.decode(Double.self, from: truncated), 0.1)
181+
XCTAssertEqual(try JSON.decode(Float.self, from: truncated), 0.1)
182+
}
183+
}
184+
175185
private func assertRoundTrip<T: Codable & Equatable>(
176186
of value: T,
177187
expectedJSON: String,

0 commit comments

Comments
 (0)