Skip to content

Commit 56c60a2

Browse files
authored
Merge pull request from GHSA-w3f6-pc54-gfw7
* Refactor HPACK integer decoding Motivation: The HPACK integer decoding used a number of unchecked operations which can trap on some inputs. Since it is network reachable code, it should throw errors if the operations overflow. Modifications: - Add a failure case to the fuzz testing - Refactor the integer decoding to check for overflow on the arithmetic - This throws a new 'HPACKErrors.UnrepresentableInteger' error on overflow - Add a missing bounds check - Remove an unnecessary and incorrect path - Remove the default argument from the function driving the decoding, the default was not valid and would cause an assertion to fail if used - Return the decoded value as an `Int` rather than a `UInt` - More tests Result: Integer decoding is safer. * Use unchecked shifting * Use truncatingIfNeeded * make error internal
1 parent 9321577 commit 56c60a2

File tree

5 files changed

+148
-62
lines changed

5 files changed

+148
-62
lines changed

Sources/NIOHPACK/HPACKErrors.swift

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,83 +23,83 @@ public enum NIOHPACKErrors {
2323
public struct InvalidHeaderIndex : NIOHPACKError {
2424
/// The offending index.
2525
public let suppliedIndex: Int
26-
26+
2727
/// The highest index we have available.
2828
public let availableIndex: Int
2929
}
30-
30+
3131
/// A header block indicated an indexed header with no accompanying
3232
/// value, but the index referenced an entry with no value of its own
3333
/// e.g. one of the many valueless items in the static header table.
3434
public struct IndexedHeaderWithNoValue : NIOHPACKError {
3535
/// The offending index.
3636
public let index: Int
3737
}
38-
38+
3939
/// An encoded string contained an invalid length that extended
4040
/// beyond its frame's payload size.
4141
public struct StringLengthBeyondPayloadSize : NIOHPACKError {
4242
/// The length supplied.
4343
public let length: Int
44-
44+
4545
/// The available number of bytes.
4646
public let available: Int
4747
}
48-
48+
4949
/// Decoded string data could not be parsed as valid UTF-8.
5050
public struct InvalidUTF8Data : NIOHPACKError {
5151
/// The offending bytes.
5252
public let bytes: ByteBuffer
5353
}
54-
54+
5555
/// The start byte of a header did not match any format allowed by
5656
/// the HPACK specification.
5757
public struct InvalidHeaderStartByte : NIOHPACKError {
5858
/// The offending byte.
5959
public let byte: UInt8
6060
}
61-
61+
6262
/// A dynamic table size update specified an invalid size.
6363
public struct InvalidDynamicTableSize : NIOHPACKError {
6464
/// The offending size.
6565
public let requestedSize: Int
66-
66+
6767
/// The actual maximum size that was set by the protocol.
6868
public let allowedSize: Int
6969
}
70-
70+
7171
/// A dynamic table size update was found outside its allowed place.
7272
/// They may only be included at the start of a header block.
7373
public struct IllegalDynamicTableSizeChange : NIOHPACKError {}
74-
74+
7575
/// A new header could not be added to the dynamic table. Usually
7676
/// this means the header itself is larger than the current
7777
/// dynamic table size.
7878
public struct FailedToAddIndexedHeader<Name: Collection, Value: Collection> : NIOHPACKError where Name.Element == UInt8, Value.Element == UInt8 {
7979
/// The table size required to be able to add this header to the table.
8080
public let bytesNeeded: Int
81-
81+
8282
/// The name of the header that could not be written.
8383
public let name: Name
84-
84+
8585
/// The value of the header that could not be written.
8686
public let value: Value
87-
87+
8888
public static func == (lhs: NIOHPACKErrors.FailedToAddIndexedHeader<Name, Value>, rhs: NIOHPACKErrors.FailedToAddIndexedHeader<Name, Value>) -> Bool {
8989
guard lhs.bytesNeeded == rhs.bytesNeeded else {
9090
return false
9191
}
9292
return lhs.name.elementsEqual(rhs.name) && lhs.value.elementsEqual(rhs.value)
9393
}
9494
}
95-
95+
9696
/// Ran out of input bytes while decoding.
9797
public struct InsufficientInput : NIOHPACKError {}
98-
98+
9999
/// HPACK encoder asked to begin a new header block while partway through encoding
100100
/// another block.
101101
public struct EncoderAlreadyActive : NIOHPACKError {}
102-
102+
103103
/// HPACK encoder asked to append a header without first calling `beginEncoding(allocator:)`.
104104
public struct EncoderNotStarted : NIOHPACKError {}
105105

@@ -118,4 +118,9 @@ public enum NIOHPACKErrors {
118118
public struct EmptyLiteralHeaderFieldName: NIOHPACKError {
119119
public init() { }
120120
}
121+
122+
/// The integer being decoded is not representable by this implementation.
123+
internal struct UnrepresentableInteger: NIOHPACKError {
124+
public init() {}
125+
}
121126
}

Sources/NIOHPACK/IntegerCoding.swift

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,24 @@ func encodeInteger(_ value: UInt, to buffer: inout ByteBuffer,
2828
prefix: Int, prefixBits: UInt8 = 0) -> Int {
2929
assert(prefix <= 8)
3030
assert(prefix >= 1)
31-
31+
3232
let start = buffer.writerIndex
33-
33+
3434
let k = (1 << prefix) - 1
3535
var initialByte = prefixBits
36-
36+
3737
if value < k {
3838
// it fits already!
3939
initialByte |= UInt8(truncatingIfNeeded: value)
4040
buffer.writeInteger(initialByte)
4141
return 1
4242
}
43-
43+
4444
// if it won't fit in this byte altogether, fill in all the remaining bits and move
4545
// to the next byte.
4646
initialByte |= UInt8(truncatingIfNeeded: k)
4747
buffer.writeInteger(initialByte)
48-
48+
4949
// deduct the initial [prefix] bits from the value, then encode it seven bits at a time into
5050
// the remaining bytes.
5151
var n = value - UInt(k)
@@ -54,55 +54,88 @@ func encodeInteger(_ value: UInt, to buffer: inout ByteBuffer,
5454
buffer.writeInteger(nextByte)
5555
n >>= 7
5656
}
57-
57+
5858
buffer.writeInteger(UInt8(n))
5959
return buffer.writerIndex - start
6060
}
6161

62+
fileprivate let valueMask: UInt8 = 127
63+
fileprivate let continuationMask: UInt8 = 128
64+
6265
/* private but tests */
63-
func decodeInteger(from bytes: ByteBufferView, prefix: Int) throws -> (UInt, Int) {
64-
assert(prefix <= 8)
65-
assert(prefix >= 1)
66-
67-
let mask = (1 << prefix) - 1
68-
var accumulator: UInt = 0
69-
var index = bytes.startIndex
66+
struct DecodedInteger {
67+
var value: Int
68+
var bytesRead: Int
69+
}
7070

71-
// if the available bits aren't all set, the entire value consists of those bits
72-
if bytes[index] & UInt8(mask) != mask {
73-
return (UInt(bytes[index] & UInt8(mask)), 1)
71+
/* private but tests */
72+
func decodeInteger(from bytes: ByteBufferView, prefix: Int) throws -> DecodedInteger {
73+
precondition((1...8).contains(prefix))
74+
if bytes.isEmpty {
75+
throw NIOHPACKErrors.InsufficientInput()
7476
}
7577

76-
accumulator = UInt(mask)
77-
index = bytes.index(after: index)
78-
if index == bytes.endIndex {
79-
return (accumulator, bytes.distance(from: bytes.startIndex, to: index))
78+
// See RFC 7541 § 5.1 for details of the encoding/decoding.
79+
80+
var index = bytes.startIndex
81+
// The shifting and arithmetic operate on 'Int' and prefix is 1...8, so these unchecked operations are
82+
// fine and the result must fit in a UInt8.
83+
let prefixMask = UInt8(truncatingIfNeeded: (1 &<< prefix) &- 1)
84+
let prefixBits = bytes[index] & prefixMask
85+
86+
if prefixBits != prefixMask {
87+
// The prefix bits aren't all '1', so they represent the whole value, we're done.
88+
return DecodedInteger(value: Int(prefixBits), bytesRead: 1)
8089
}
81-
90+
91+
var accumulator = Int(prefixMask)
92+
bytes.formIndex(after: &index)
93+
8294
// for the remaining bytes, as long as the top bit is set, consume the low seven bits.
83-
var shift: UInt = 0
95+
var shift: Int = 0
8496
var byte: UInt8 = 0
97+
8598
repeat {
8699
if index == bytes.endIndex {
87100
throw NIOHPACKErrors.InsufficientInput()
88101
}
89-
102+
90103
byte = bytes[index]
91-
accumulator += UInt(byte & 127) * (1 << shift)
92-
shift += 7
93-
index = bytes.index(after: index)
94-
} while byte & 128 == 128
95-
96-
return (accumulator, bytes.distance(from: bytes.startIndex, to: index))
104+
105+
let value = Int(byte & valueMask)
106+
107+
// The shift cannot overflow: the value of 'shift' is strictly less than 'Int.bitWidth'.
108+
let (multiplicationResult, multiplicationOverflowed) = value.multipliedReportingOverflow(by: (1 &<< shift))
109+
if multiplicationOverflowed {
110+
throw NIOHPACKErrors.UnrepresentableInteger()
111+
}
112+
113+
let (additionResult, additionOverflowed) = accumulator.addingReportingOverflow(multiplicationResult)
114+
if additionOverflowed {
115+
throw NIOHPACKErrors.UnrepresentableInteger()
116+
}
117+
118+
accumulator = additionResult
119+
120+
// Unchecked is fine, there's no chance of it overflowing given the possible values of 'Int.bitWidth'.
121+
shift &+= 7
122+
if shift >= Int.bitWidth {
123+
throw NIOHPACKErrors.UnrepresentableInteger()
124+
}
125+
126+
bytes.formIndex(after: &index)
127+
} while byte & continuationMask == continuationMask
128+
129+
return DecodedInteger(value: accumulator, bytesRead: bytes.distance(from: bytes.startIndex, to: index))
97130
}
98131

99132
extension ByteBuffer {
100-
mutating func readEncodedInteger(withPrefix prefix: Int = 0) throws -> Int {
101-
let (result, nread) = try decodeInteger(from: self.readableBytesView, prefix: prefix)
102-
self.moveReaderIndex(forwardBy: nread)
103-
return Int(result)
133+
mutating func readEncodedInteger(withPrefix prefix: Int) throws -> Int {
134+
let result = try decodeInteger(from: self.readableBytesView, prefix: prefix)
135+
self.moveReaderIndex(forwardBy: result.bytesRead)
136+
return result.value
104137
}
105-
138+
106139
mutating func write(encodedInteger value: UInt, prefix: Int = 0, prefixBits: UInt8 = 0) {
107140
encodeInteger(value, to: &self, prefix: prefix, prefixBits: prefixBits)
108141
}

Tests/NIOHPACKTests/IntegerCodingTests+XCTest.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ extension IntegerCodingTests {
2929
return [
3030
("testIntegerEncoding", testIntegerEncoding),
3131
("testIntegerDecoding", testIntegerDecoding),
32+
("testIntegerDecodingMultiplicationDoesNotOverflow", testIntegerDecodingMultiplicationDoesNotOverflow),
33+
("testIntegerDecodingAdditionDoesNotOverflow", testIntegerDecodingAdditionDoesNotOverflow),
34+
("testIntegerDecodingShiftDoesNotOverflow", testIntegerDecodingShiftDoesNotOverflow),
35+
("testIntegerDecodingEmptyInput", testIntegerDecodingEmptyInput),
36+
("testIntegerDecodingNotEnoughBytes", testIntegerDecodingNotEnoughBytes),
3237
]
3338
}
3439
}

Tests/NIOHPACKTests/IntegerCodingTests.swift

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ class IntegerCodingTests : XCTestCase {
3030
return data
3131
}
3232

33-
private func decodeInteger(from array: [UInt8], prefix: Int) throws -> UInt {
33+
private func decodeInteger(from array: [UInt8], prefix: Int) throws -> Int {
3434
scratchBuffer.clear()
3535
scratchBuffer.writeBytes(array)
36-
let (r, _) = try NIOHPACK.decodeInteger(from: scratchBuffer.readableBytesView, prefix: prefix)
37-
return r
36+
let result = try NIOHPACK.decodeInteger(from: scratchBuffer.readableBytesView, prefix: prefix)
37+
return result.value
3838
}
3939

4040
// MARK: - Tests
@@ -108,7 +108,7 @@ class IntegerCodingTests : XCTestCase {
108108
// something carefully crafted to produce maximum number of output bytes with minimum number of
109109
// nonzero bits:
110110
data = encodeIntegerToArray(9223372036854775809, prefix: 1)
111-
111+
112112
// calculations:
113113
// subtract prefix:
114114
// 9223372036854775809 - 1 = 9223372036854775808 or 1000 (0000 x15)
@@ -145,21 +145,64 @@ class IntegerCodingTests : XCTestCase {
145145

146146
XCTAssertEqual(try decodeInteger(from: [0b00011111, 154, 10], prefix: 5), 1337)
147147
XCTAssertEqual(try decodeInteger(from: [0b11111111, 154, 10], prefix: 5), 1337)
148-
148+
149149
XCTAssertEqual(try decodeInteger(from: [0b00101010], prefix: 8), 42)
150-
150+
151151
// Now some larger numbers:
152152
XCTAssertEqual(try decodeInteger(from: [255, 129, 254, 255, 255, 255, 255, 255, 255, 33], prefix: 8), 2449958197289549824)
153153
XCTAssertEqual(try decodeInteger(from: [1, 255, 255, 255, 255, 255, 255, 255, 255, 33], prefix: 1), 2449958197289549824)
154-
XCTAssertEqual(try decodeInteger(from: [1, 254, 255, 255, 255, 255, 255, 255, 255, 255, 1], prefix: 1), UInt(UInt64.max))
155-
154+
XCTAssertEqual(try decodeInteger(from: [1, 254, 255, 255, 255, 255, 255, 255, 255, 127, 1], prefix: 1), Int.max)
155+
156156
// lots of zeroes: each 128 yields zero
157-
XCTAssertEqual(try decodeInteger(from: [1, 128, 128, 128, 128, 128, 128, 128, 128, 128, 1], prefix: 1), 9223372036854775809)
158-
157+
XCTAssertEqual(try decodeInteger(from: [1, 128, 128, 128, 128, 128, 128, 128, 128, 127, 1], prefix: 1), 9151314442816847873)
158+
159159
// almost the same bytes, but a different prefix:
160-
XCTAssertEqual(try decodeInteger(from: [255, 128, 128, 128, 128, 128, 128, 128, 128, 128, 1], prefix: 8), 9223372036854776063)
161-
160+
XCTAssertEqual(try decodeInteger(from: [255, 128, 128, 128, 128, 128, 128, 128, 128, 127, 1], prefix: 8), 9151314442816848127)
161+
162162
// now a silly version which should never have been encoded in so many bytes
163163
XCTAssertEqual(try decodeInteger(from: [255, 129, 128, 128, 128, 128, 128, 128, 128, 0], prefix: 8), 256)
164164
}
165+
166+
func testIntegerDecodingMultiplicationDoesNotOverflow() throws {
167+
// Zeros with continuation bits (e.g. 128) to increase the shift value (to 9 * 7 = 63), and then multiply by 127.
168+
for `prefix` in 1...8 {
169+
XCTAssertThrowsError(try decodeInteger(from: [255, 128, 128, 128, 128, 128, 128, 128, 128, 128, 127], prefix: prefix)) { error in
170+
XCTAssert(error is NIOHPACKErrors.UnrepresentableInteger)
171+
}
172+
}
173+
}
174+
175+
func testIntegerDecodingAdditionDoesNotOverflow() throws {
176+
// Zeros with continuation bits (e.g. 128) to increase the shift value (to 9 * 7 = 63), and then multiply by 127.
177+
for `prefix` in 1...8 {
178+
XCTAssertThrowsError(try decodeInteger(from: [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127], prefix: prefix)) { error in
179+
XCTAssert(error is NIOHPACKErrors.UnrepresentableInteger)
180+
}
181+
}
182+
}
183+
184+
func testIntegerDecodingShiftDoesNotOverflow() throws {
185+
// With enough iterations we expect the shift to become greater >= 64.
186+
for `prefix` in 1...8 {
187+
XCTAssertThrowsError(try decodeInteger(from: [255, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], prefix: prefix)) { error in
188+
XCTAssert(error is NIOHPACKErrors.UnrepresentableInteger)
189+
}
190+
}
191+
}
192+
193+
func testIntegerDecodingEmptyInput() throws {
194+
for `prefix` in 1...8 {
195+
XCTAssertThrowsError(try decodeInteger(from: [], prefix: prefix)) { error in
196+
XCTAssert(error is NIOHPACKErrors.InsufficientInput)
197+
}
198+
}
199+
}
200+
201+
func testIntegerDecodingNotEnoughBytes() throws {
202+
for `prefix` in 1...8 {
203+
XCTAssertThrowsError(try decodeInteger(from: [255, 128], prefix: prefix)) { error in
204+
XCTAssert(error is NIOHPACKErrors.InsufficientInput)
205+
}
206+
}
207+
}
165208
}

0 commit comments

Comments
 (0)