Skip to content

Commit ac2a5af

Browse files
authored
Merge pull request from GHSA-q36x-r5x4-h4q6
Motivation The HTTP2FrameDecoder is a complex object that was written early in the development of swift-nio-http2. Its logical flow is complex, and it hasn't been meaningfully rewritten in quite some time, so it's difficult to work with and understand. Annoyingly, some bugs have crept in over the years. Because of the structure of the code it can be quite difficult to understand how the parser actually works, and fixing a given issue can be difficult. This patch aims to produce a substantial change to the HTTP2FrameDecoder to make it easier to understand and maintain in the long run. Modifications This patch provides a complete rewrite of HTTP2FrameDecoder. It doesn't do this by having a ground-up rewrite: instead, it's more like a renovation, with the general scaffolding kept. The rewrite was performed incrementally, keeping the existing test suite passing and writing new tests when necessary. The following major changes were made: 1. New states and edges were added to the state machine to handle padding. Prior to this change, padding was handled as part of frame payload decoding. This is not totally unreasonable, but it dispersed padding management widely and made it easy to have bugs slip in. This patch replaces this with a smaller set of locations. Padding is now handled in two distinct ways. For HEADERS and PUSH_PROMISE frames, trailing padding is still stripped as part of frame payload decode, but it's done so generically, and the padding bytes are never exposed to the individual frame parser. For DATA, there is a new state to handle trailing padding removal, which simplifies the highly complex logic around synthesised data frames. For all frames, the leading padding byte is handled by a new dedicated state which is used unconditionally, instead of attempting to opportunistically strip it. This simplifies the code flow. As a side benefit, this change means we can now accurately report the padding used on HEADERS and PUSH_PROMISE frames, even when they are part of a CONTINUATION sequence. 2. The synthesised DATA frame logic has been substantially reworked. With the removal of the padding logic from the state, we now know that so long as we have either got a byte of data to emit _or_ the DATA frame is zero length, we will always emit a frame. This has made it simpler to understand the control flow when synthesising DATA frames. 3. The monolithic state switch has been refactored into per-state methods. This helps manage the amount of state that each method can see, as well as to logically split them up. In addition, it allows us to recast state transformations as (fairly) pure functions. Additionally, this allowed the larger methods to be refactored with smaller helpers that are more obviously correct. 4. The frame payload parsers have been rewritten. The main goal here was to remove preflight length checks and unsafe code. The preflight length checks cause trouble when they disagree with the parsing code, so we now rely on the parsing code being correct with regard to length. Relatedly, we previously had two separate places where we communicated length: a frame header length and a ByteBuffer length. This was unnecessary duplication of information, so we instead use a ByteBuffer slice to manage the length. This ensures that we cannot over-parse a message. Finally, in places that used unsafe code or separate integer reads, we have refactored to stop using that unsafe code and to use combined integer reads. 5. Extraneous serialization code has been extracted. The HTTP2FrameEncoder was unnecessarily in this file, which took a large file and made it larger. I moved this out. Result The resulting parser is clearer and safer. Complex logic has been broken out into smaller methods with less access to global data. The code should be generally clearer.
1 parent 0ad7ff6 commit ac2a5af

8 files changed

+1525
-887
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ let package = Package(
2121
.library(name: "NIOHTTP2", targets: ["NIOHTTP2"]),
2222
],
2323
dependencies: [
24-
.package(url: "https://github.com/apple/swift-nio.git", from: "2.32.0")
24+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.35.0")
2525
],
2626
targets: [
2727
.target(

Sources/NIOHTTP2/HTTP2ChannelHandler.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
152152
}
153153

154154
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
155-
var data = self.unwrapInboundIn(data)
156-
self.frameDecoder.append(bytes: &data)
155+
let data = self.unwrapInboundIn(data)
156+
self.frameDecoder.append(bytes: data)
157157

158158
// Before we go in here we need to deliver any pending user events. This is because
159159
// we may have been called re-entrantly.

Sources/NIOHTTP2/HTTP2Error.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,13 @@ internal enum InternalError: Error {
14381438
case attemptedToCreateStream
14391439

14401440
case codecError(code: HTTP2ErrorCode)
1441+
1442+
// Used to record that an impossible situation occured. Crashes in debug mode, errors in
1443+
// release mode.
1444+
static func impossibleSituation(file: StaticString = #file, line: UInt = #line) -> InternalError {
1445+
assertionFailure(file: file, line: line)
1446+
return .codecError(code: .internalError)
1447+
}
14411448
}
14421449

14431450
extension InternalError: Hashable { }
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
import NIOHPACK
17+
18+
struct HTTP2FrameEncoder {
19+
var headerEncoder: HPACKEncoder
20+
21+
// RFC 7540 § 6.5.2 puts the initial value of SETTINGS_MAX_FRAME_SIZE at 2**14 octets
22+
var maxFrameSize: UInt32 = 1<<14
23+
24+
init(allocator: ByteBufferAllocator) {
25+
self.headerEncoder = HPACKEncoder(allocator: allocator)
26+
}
27+
28+
/// Encodes the frame and optionally returns one or more blobs of data
29+
/// ready for the system.
30+
///
31+
/// Returned data blobs would include anything of potentially flexible
32+
/// length, such as DATA payloads, header fragments in HEADERS or PUSH_PROMISE
33+
/// frames, and so on. This is to avoid manually copying chunks of data which
34+
/// we could just enqueue separately in sequence on the channel. Generally, if
35+
/// we have a byte buffer somewhere, we will return that separately rather than
36+
/// copy it into another buffer, with the corresponding allocation overhead.
37+
///
38+
/// - Parameters:
39+
/// - frame: The frame to encode.
40+
/// - buf: Destination buffer for the encoded frame.
41+
/// - Returns: An array containing zero or more additional buffers to send, in
42+
/// order. These may contain data frames' payload bytes, encoded
43+
/// header fragments, etc.
44+
/// - Throws: Errors returned from HPACK encoder.
45+
mutating func encode(frame: HTTP2Frame, to buf: inout ByteBuffer) throws -> IOData? {
46+
// note our starting point
47+
let start = buf.writerIndex
48+
49+
// +-----------------------------------------------+
50+
// | Length (24) |
51+
// +---------------+---------------+---------------+
52+
// | Type (8) | Flags (8) |
53+
// +-+-------------+---------------+-------------------------------+
54+
// |R| Stream Identifier (31) |
55+
// +=+=============================================================+
56+
// | Frame Payload (0...) ...
57+
// +---------------------------------------------------------------+
58+
59+
// skip 24-bit length for now, we'll fill that in later
60+
buf.moveWriterIndex(forwardBy: 3)
61+
62+
// 8-bit type
63+
buf.writeInteger(frame.payload.code)
64+
65+
// skip the 8 bit flags for now, we'll fill it in later as well.
66+
let flagsIndex = buf.writerIndex
67+
var flags = FrameFlags()
68+
buf.moveWriterIndex(forwardBy: 1)
69+
70+
// 32-bit stream identifier -- ensuring the top bit is empty
71+
buf.writeInteger(Int32(frame.streamID))
72+
73+
// frame payload follows, which depends on the frame type itself
74+
let payloadStart = buf.writerIndex
75+
let extraFrameData: IOData?
76+
let payloadSize: Int
77+
78+
switch frame.payload {
79+
case .data(let dataContent):
80+
if dataContent.paddingBytes != nil {
81+
// we don't support sending padded frames just now
82+
throw NIOHTTP2Errors.unsupported(info: "Padding is not supported on sent frames at this time")
83+
}
84+
85+
if dataContent.endStream {
86+
flags.insert(.endStream)
87+
}
88+
extraFrameData = dataContent.data
89+
payloadSize = dataContent.data.readableBytes
90+
91+
case .headers(let headerData):
92+
if headerData.paddingBytes != nil {
93+
// we don't support sending padded frames just now
94+
throw NIOHTTP2Errors.unsupported(info: "Padding is not supported on sent frames at this time")
95+
}
96+
97+
flags.insert(.endHeaders)
98+
if headerData.endStream {
99+
flags.insert(.endStream)
100+
}
101+
102+
if let priority = headerData.priorityData {
103+
flags.insert(.priority)
104+
var dependencyRaw = UInt32(priority.dependency)
105+
if priority.exclusive {
106+
dependencyRaw |= 0x8000_0000
107+
}
108+
buf.writeInteger(dependencyRaw)
109+
buf.writeInteger(priority.weight)
110+
}
111+
112+
try self.headerEncoder.encode(headers: headerData.headers, to: &buf)
113+
payloadSize = buf.writerIndex - payloadStart
114+
extraFrameData = nil
115+
116+
case .priority(let priorityData):
117+
var raw = UInt32(priorityData.dependency)
118+
if priorityData.exclusive {
119+
raw |= 0x8000_0000
120+
}
121+
buf.writeInteger(raw)
122+
buf.writeInteger(priorityData.weight)
123+
124+
extraFrameData = nil
125+
payloadSize = 5
126+
127+
case .rstStream(let errcode):
128+
buf.writeInteger(UInt32(errcode.networkCode))
129+
130+
payloadSize = 4
131+
extraFrameData = nil
132+
133+
case .settings(.settings(let settings)):
134+
for setting in settings {
135+
buf.writeInteger(setting.parameter.networkRepresentation)
136+
buf.writeInteger(setting._value)
137+
}
138+
139+
payloadSize = settings.count * 6
140+
extraFrameData = nil
141+
142+
case .settings(.ack):
143+
payloadSize = 0
144+
extraFrameData = nil
145+
flags.insert(.ack)
146+
147+
case .pushPromise(let pushPromiseData):
148+
if pushPromiseData.paddingBytes != nil {
149+
// we don't support sending padded frames just now
150+
throw NIOHTTP2Errors.unsupported(info: "Padding is not supported on sent frames at this time")
151+
}
152+
153+
let streamVal: UInt32 = UInt32(pushPromiseData.pushedStreamID)
154+
buf.writeInteger(streamVal)
155+
156+
try self.headerEncoder.encode(headers: pushPromiseData.headers, to: &buf)
157+
158+
payloadSize = buf.writerIndex - payloadStart
159+
extraFrameData = nil
160+
flags.insert(.endHeaders)
161+
162+
case .ping(let pingData, let ack):
163+
withUnsafeBytes(of: pingData.bytes) { ptr -> Void in
164+
_ = buf.writeBytes(ptr)
165+
}
166+
167+
if ack {
168+
flags.insert(.ack)
169+
}
170+
171+
payloadSize = 8
172+
extraFrameData = nil
173+
174+
case .goAway(let lastStreamID, let errorCode, let opaqueData):
175+
let streamVal: UInt32 = UInt32(lastStreamID) & ~0x8000_0000
176+
buf.writeInteger(streamVal)
177+
buf.writeInteger(UInt32(errorCode.networkCode))
178+
179+
if let data = opaqueData {
180+
payloadSize = data.readableBytes + 8
181+
extraFrameData = .byteBuffer(data)
182+
} else {
183+
payloadSize = 8
184+
extraFrameData = nil
185+
}
186+
187+
case .windowUpdate(let size):
188+
buf.writeInteger(UInt32(size) & ~0x8000_0000)
189+
payloadSize = 4
190+
extraFrameData = nil
191+
192+
case .alternativeService(let origin, let field):
193+
if let org = origin {
194+
buf.moveWriterIndex(forwardBy: 2)
195+
let start = buf.writerIndex
196+
buf.writeString(org)
197+
buf.setInteger(UInt16(buf.writerIndex - start), at: payloadStart)
198+
} else {
199+
buf.writeInteger(UInt16(0))
200+
}
201+
202+
if let value = field {
203+
payloadSize = buf.writerIndex - payloadStart + value.readableBytes
204+
extraFrameData = .byteBuffer(value)
205+
} else {
206+
payloadSize = buf.writerIndex - payloadStart
207+
extraFrameData = nil
208+
}
209+
210+
case .origin(let origins):
211+
for origin in origins {
212+
let sizeLoc = buf.writerIndex
213+
buf.moveWriterIndex(forwardBy: 2)
214+
215+
let start = buf.writerIndex
216+
buf.writeString(origin)
217+
buf.setInteger(UInt16(buf.writerIndex - start), at: sizeLoc)
218+
}
219+
220+
payloadSize = buf.writerIndex - payloadStart
221+
extraFrameData = nil
222+
}
223+
224+
// Confirm we're not about to violate SETTINGS_MAX_FRAME_SIZE.
225+
guard payloadSize <= Int(self.maxFrameSize) else {
226+
throw InternalError.codecError(code: .frameSizeError)
227+
}
228+
229+
// Write the frame data. This is the payload size and the flags byte.
230+
buf.writePayloadSize(payloadSize, at: start)
231+
buf.setInteger(flags.rawValue, at: flagsIndex)
232+
233+
// all bytes to write are in the provided buffer now
234+
return extraFrameData
235+
}
236+
}
237+
238+
extension ByteBuffer {
239+
fileprivate mutating func writePayloadSize(_ size: Int, at location: Int) {
240+
// Yes, this performs better than running a UInt8 through the generic write(integer:) three times.
241+
var bytes: (UInt8, UInt8, UInt8)
242+
bytes.0 = UInt8((size & 0xff_00_00) >> 16)
243+
bytes.1 = UInt8((size & 0x00_ff_00) >> 8)
244+
bytes.2 = UInt8( size & 0x00_00_ff)
245+
withUnsafeBytes(of: bytes) { ptr in
246+
_ = self.setBytes(ptr, at: location)
247+
}
248+
}
249+
}
250+

0 commit comments

Comments
 (0)