Skip to content

Commit eed681b

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 a0b7f64 commit eed681b

File tree

3 files changed

+59
-18
lines changed

3 files changed

+59
-18
lines changed

Sources/NIO/ByteBuffer-core.swift

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

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

@@ -268,23 +269,28 @@ public struct ByteBuffer {
268269
private mutating func ensureAvailableCapacity(_ capacity: Capacity, at index: Index) {
269270
assert(isKnownUniquelyReferenced(&self._storage))
270271

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

290296
// 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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,39 @@ 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 as UInt8, at: 0)
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+
97130
func testReadWrite() {
98131
buf.write(string: "X")
99132
buf.write(string: "Y")

0 commit comments

Comments
 (0)