Skip to content

Commit 8af47cb

Browse files
committed
security: ByteBuffer: fix heap buffer overflow on slice realloc
Motivation: ByteBuffer had a very bad (exploitable!) security vulnerability if the following conditions are all true: - user writes to a ByteBuffer which is a slice (slice.lowerBound != 0) - the slice is uniquely referenced (ie. the buffer that it was sliced from is gone) - the write triggers a re-allocation Then the slice is actually _larger_ than the overall available capacity so another write to said ByteBuffer could end up out of bounds. Modifications: - fixed slice reallocation Result: - fixed security vulnerability
1 parent 3275ff7 commit 8af47cb

File tree

3 files changed

+61
-19
lines changed

3 files changed

+61
-19
lines changed

Sources/NIO/ByteBuffer-core.swift

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,13 @@ public struct ByteBuffer {
217217
return new
218218
}
219219

220-
public func reallocStorage(capacity: Capacity) {
221-
let ptr = self.allocator.realloc(self.bytes, Int(capacity))!
220+
public func reallocStorage(capacity minimumNeededCapacity: Capacity) {
221+
let newCapacity = minimumNeededCapacity.nextPowerOf2ClampedToMax()
222+
let ptr = self.allocator.realloc(self.bytes, Int(newCapacity))!
222223
/* bind the memory so we can assume it elsewhere to be bound to UInt8 */
223-
ptr.bindMemory(to: UInt8.self, capacity: Int(capacity))
224+
ptr.bindMemory(to: UInt8.self, capacity: Int(newCapacity))
224225
self.bytes = ptr
225-
self.capacity = capacity
226+
self.capacity = newCapacity
226227
self.fullSlice = 0..<self.capacity
227228
}
228229

@@ -267,23 +268,28 @@ public struct ByteBuffer {
267268
@_versioned mutating func _ensureAvailableCapacity(_ capacity: Capacity, at index: Index) {
268269
assert(isKnownUniquelyReferenced(&self._storage))
269270

270-
if self._slice.lowerBound + index + capacity > self._slice.upperBound {
271-
// double the capacity, we may want to use different strategies depending on the actual current capacity later on.
272-
var newCapacity = max(1, _toCapacity(self.capacity))
273-
274-
// double the capacity until the requested capacity can be full-filled
275-
repeat {
276-
precondition(newCapacity != Capacity.max, "cannot make ByteBuffers larger than \(newCapacity)")
277-
if newCapacity < (Capacity.max >> 1) {
278-
newCapacity = newCapacity << 1
271+
let totalNeededCapacityWhenKeepingSlice = self._slice.lowerBound + index + capacity
272+
if totalNeededCapacityWhenKeepingSlice > self._slice.upperBound {
273+
// we need to at least adjust the slice's upper bound which we can do as we're the unique owner of the storage,
274+
// let's see if adjusting the slice's upper bound buys us enough storage
275+
if totalNeededCapacityWhenKeepingSlice > self._storage.capacity {
276+
let newStorageMinCapacity = index + capacity
277+
// nope, we need to actually re-allocate again. If our slice does not start at 0, let's also rebase
278+
if self._slice.lowerBound == 0 {
279+
self._storage.reallocStorage(capacity: newStorageMinCapacity)
279280
} else {
280-
newCapacity = Capacity.max
281+
self._storage = self._storage.reallocSlice(self._slice.lowerBound ..< self._slice.upperBound,
282+
capacity: newStorageMinCapacity)
281283
}
282-
} while newCapacity < index || newCapacity - index < capacity
283-
284-
self._storage.reallocStorage(capacity: newCapacity)
285-
self._slice = _slice.lowerBound..<_slice.lowerBound + newCapacity
284+
self._slice = self._storage.fullSlice
285+
} else {
286+
// yes, let's just extend the slice until the end of the buffer
287+
self._slice = _slice.lowerBound ..< self._storage.capacity
288+
}
286289
}
290+
assert(self._slice.lowerBound + index + capacity <= self._slice.upperBound)
291+
assert(self._slice.lowerBound >= 0, "illegal slice: negative lower bound: \(self._slice.lowerBound)")
292+
assert(self._slice.upperBound <= self._storage.capacity, "illegal slice: upper bound (\(self._slice.upperBound)) exceeds capacity: \(self._storage.capacity)")
287293
}
288294

289295
// MARK: Internal API

Tests/NIOTests/ByteBufferTest+XCTest.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ extension ByteBufferTest {
3030
("testEqualsComparesReadBuffersOnly", testEqualsComparesReadBuffersOnly),
3131
("testSimpleReadTest", testSimpleReadTest),
3232
("testSimpleWrites", testSimpleWrites),
33+
("testMakeSureUniquelyOwnedSliceDoesNotGetReallocatedOnWrite", testMakeSureUniquelyOwnedSliceDoesNotGetReallocatedOnWrite),
34+
("testWriteToUniquelyOwnedSliceWhichTriggersAReallocation", testWriteToUniquelyOwnedSliceWhichTriggersAReallocation),
3335
("testReadWrite", testReadWrite),
3436
("testStaticStringReadTests", testStaticStringReadTests),
3537
("testString", testString),

Tests/NIOTests/ByteBufferTest.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,40 @@ class ByteBufferTest: XCTestCase {
9494
XCTAssertEqual(6, buf.readableBytes)
9595
}
9696

97+
func makeSliceToBufferWhichIsDeallocated() -> ByteBuffer {
98+
var buf = self.allocator.buffer(capacity: 16)
99+
let oldCapacity = buf.capacity
100+
buf.write(bytes: 0..<16)
101+
XCTAssertEqual(oldCapacity, buf.capacity)
102+
return buf.getSlice(at: 15, length: 1)!
103+
}
104+
105+
func testMakeSureUniquelyOwnedSliceDoesNotGetReallocatedOnWrite() {
106+
var slice = self.makeSliceToBufferWhichIsDeallocated()
107+
XCTAssertEqual(1, slice.capacity)
108+
let oldStorageBegin = slice.withUnsafeReadableBytes { ptr in
109+
return UInt(bitPattern: ptr.baseAddress!)
110+
}
111+
slice.set(integer: 1, at: 0, as: UInt8.self)
112+
let newStorageBegin = slice.withUnsafeReadableBytes { ptr in
113+
return UInt(bitPattern: ptr.baseAddress!)
114+
}
115+
XCTAssertEqual(oldStorageBegin, newStorageBegin)
116+
}
117+
118+
func testWriteToUniquelyOwnedSliceWhichTriggersAReallocation() {
119+
var slice = self.makeSliceToBufferWhichIsDeallocated()
120+
XCTAssertEqual(1, slice.capacity)
121+
// this will cause a re-allocation, the whole buffer should be 32 bytes then, the slice having 17 of that.
122+
// this fills 16 bytes so will still fit
123+
slice.write(bytes: Array(16..<32))
124+
XCTAssertEqual(Array(15..<32), slice.readBytes(length: slice.readableBytes)!)
125+
126+
// and this will need another re-allocation
127+
slice.write(bytes: Array(32..<47))
128+
}
129+
130+
97131
func testReadWrite() {
98132
buf.write(string: "X")
99133
buf.write(string: "Y")
@@ -491,7 +525,7 @@ class ByteBufferTest: XCTestCase {
491525
XCTAssertEqual(16, buf.writerIndex)
492526
XCTAssertEqual(0, buf.readerIndex)
493527
buf.write(bytes: "X".data(using: .utf8)!)
494-
XCTAssertEqual(32, buf.capacity)
528+
XCTAssertGreaterThan(buf.capacity, 16)
495529
XCTAssertEqual(17, buf.writerIndex)
496530
XCTAssertEqual(0, buf.readerIndex)
497531
buf.withUnsafeReadableBytes { ptr in

0 commit comments

Comments
 (0)