From 0e68af960d737ecc43d1d0bb55298a5d026d8a10 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Thu, 29 May 2025 17:47:58 -0700 Subject: [PATCH] break more --- src/bun.js/api/bun/h2_frame_parser.zig | 44 +++++----- .../test/parallel/test-http2-reset-flood.js | 85 +++++++++++++++++++ 2 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 test/js/node/test/parallel/test-http2-reset-flood.js diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index c3f70dca84..8d12f34376 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -108,26 +108,29 @@ const SettingsType = enum(u16) { _, // we can have more unsupported extension settings types }; +inline fn u32FromBytes(src: []const u8) u32 { + var dst: u32 = 0; + @memcpy(@as(*[4]u8, @ptrCast(&dst)), src); + return @byteSwap(dst); +} + const UInt31WithReserved = packed struct(u32) { reserved: bool = false, uint31: u31 = 0, - pub fn from(value: u32) UInt31WithReserved { - return .{ - .reserved = false, - .uint31 = @truncate(value), - }; + const log = Output.scoped(.UInt31WithReserved, false); + + pub inline fn from(value: u32) UInt31WithReserved { + return .{ .uint31 = @truncate(value & 0x7fffffff), .reserved = value & 0x80000000 != 0 }; } - pub fn toUInt32(value: UInt31WithReserved) u32 { + pub inline fn toUInt32(value: UInt31WithReserved) u32 { return @bitCast(value); } pub inline fn fromBytes(src: []const u8) UInt31WithReserved { - var dst: u32 = 0; - @memcpy(@as(*[4]u8, @ptrCast(&dst)), src); - dst = @byteSwap(dst); - return @bitCast(dst); + const value: u32 = u32FromBytes(src); + return .{ .uint31 = @truncate(value & 0x7fffffff), .reserved = value & 0x80000000 != 0 }; } pub inline fn write(this: UInt31WithReserved, comptime Writer: type, writer: Writer) bool { @@ -662,7 +665,7 @@ pub const H2FrameParser = struct { const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{}); pub const ref = RefCount.ref; pub const deref = RefCount.deref; - const ENABLE_AUTO_CORK = false; // ENABLE CORK OPTIMIZATION + const ENABLE_AUTO_CORK = true; // ENABLE CORK OPTIMIZATION const ENABLE_ALLOCATOR_POOL = true; // ENABLE HIVE ALLOCATOR OPTIMIZATION const MAX_BUFFER_SIZE = 32768; @@ -986,7 +989,7 @@ pub const H2FrameParser = struct { this.remoteUsedWindowSize += able_to_send.len; client.remoteUsedWindowSize += able_to_send.len; - log("dataFrame partial flushed {} {}", .{ able_to_send.len, frame.end_stream }); + log("dataFrame partial flushed {} {} {} {} {} {} {}", .{ able_to_send.len, frame.end_stream, client.queuedDataSize, this.remoteUsedWindowSize, client.remoteUsedWindowSize, this.remoteWindowSize, client.remoteWindowSize }); const padding = this.getPadding(able_to_send.len, MAX_PAYLOAD_SIZE_WITHOUT_FRAME - 1); const payload_size = able_to_send.len + (if (padding != 0) padding + 1 else 0); @@ -1839,6 +1842,7 @@ pub const H2FrameParser = struct { } pub fn handleWindowUpdateFrame(this: *H2FrameParser, frame: FrameHeader, data: []const u8, stream: ?*Stream) usize { + log("handleWindowUpdateFrame {}", .{frame.streamIdentifier}); // must be always 4 bytes (https://datatracker.ietf.org/doc/html/rfc7540#section-6.9) if (frame.length != 4) { this.sendGoAway(frame.streamIdentifier, ErrorCode.FRAME_SIZE_ERROR, "Invalid dataframe frame size", this.lastStreamID, true); @@ -1856,7 +1860,7 @@ pub const H2FrameParser = struct { } else { this.remoteWindowSize += windowSizeIncrement.uint31; } - log("windowSizeIncrement stream {} value {}", .{ frame.streamIdentifier, windowSizeIncrement.uint31 }); + log("windowSizeIncrement stream {} value {}", .{ frame.streamIdentifier, windowSizeIncrement }); return content.end; } // needs more data @@ -2035,7 +2039,7 @@ pub const H2FrameParser = struct { if (handleIncommingPayload(this, data, frame.streamIdentifier)) |content| { const payload = content.data; - const error_code = UInt31WithReserved.fromBytes(payload[4..8]).toUInt32(); + const error_code = u32FromBytes(payload[4..8]); const chunk = this.handlers.binary_type.toJS(payload[8..], this.handlers.globalObject); this.readBuffer.reset(); this.dispatchWith2Extra(.onGoAway, JSC.JSValue.jsNumber(error_code), JSC.JSValue.jsNumber(this.lastStreamID), chunk); @@ -2157,7 +2161,7 @@ pub const H2FrameParser = struct { if (handleIncommingPayload(this, data, frame.streamIdentifier)) |content| { const payload = content.data; - const rst_code = UInt31WithReserved.fromBytes(payload).toUInt32(); + const rst_code = u32FromBytes(payload); stream.rstCode = rst_code; this.readBuffer.reset(); stream.state = .CLOSED; @@ -2368,17 +2372,19 @@ pub const H2FrameParser = struct { this.dispatch(.onLocalSettings, this.localSettings.toJS(this.handlers.globalObject)); } else { + log("empty settings has remoteSettings? {}", .{this.remoteSettings != null}); if (this.remoteSettings == null) { + // ok empty settings so default settings const remoteSettings: FullSettingsPayload = .{}; this.remoteSettings = remoteSettings; defer this.incrementWindowSizeIfNeeded(); - if (remoteSettings.initialWindowSize >= this.remoteUsedWindowSize) { + if (remoteSettings.initialWindowSize >= this.remoteWindowSize) { defer _ = this.flushStreamQueue(); this.remoteWindowSize = remoteSettings.initialWindowSize; var it = this.streams.valueIterator(); while (it.next()) |stream| { - if (remoteSettings.initialWindowSize >= stream.remoteUsedWindowSize) { + if (remoteSettings.initialWindowSize >= stream.remoteWindowSize) { stream.remoteWindowSize = remoteSettings.initialWindowSize; } } @@ -2404,12 +2410,12 @@ pub const H2FrameParser = struct { this.remoteSettings = remoteSettings; defer this.incrementWindowSizeIfNeeded(); log("remoteSettings.initialWindowSize: {} {} {}", .{ remoteSettings.initialWindowSize, this.remoteUsedWindowSize, this.remoteWindowSize }); - if (remoteSettings.initialWindowSize >= this.remoteUsedWindowSize) { + if (remoteSettings.initialWindowSize >= this.remoteWindowSize) { defer _ = this.flushStreamQueue(); this.remoteWindowSize = remoteSettings.initialWindowSize; var it = this.streams.valueIterator(); while (it.next()) |stream| { - if (remoteSettings.initialWindowSize >= stream.remoteUsedWindowSize) { + if (remoteSettings.initialWindowSize >= stream.remoteWindowSize) { stream.remoteWindowSize = remoteSettings.initialWindowSize; } } diff --git a/test/js/node/test/parallel/test-http2-reset-flood.js b/test/js/node/test/parallel/test-http2-reset-flood.js new file mode 100644 index 0000000000..a209708620 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-reset-flood.js @@ -0,0 +1,85 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); +const net = require('net'); +const { Worker, parentPort } = require('worker_threads'); + +// Verify that creating a number of invalid HTTP/2 streams will eventually +// result in the peer closing the session. +// This test uses separate threads for client and server to avoid +// the two event loops intermixing, as we are writing in a busy loop here. + +if (process.env.HAS_STARTED_WORKER) { + const server = http2.createServer({ maxSessionInvalidFrames: 100 }); + server.on('stream', (stream) => { + stream.respond({ + 'content-type': 'text/plain', + ':status': 200 + }); + stream.end('Hello, world!\n'); + }); + server.listen(0, () => parentPort.postMessage(server.address().port)); + return; +} + +process.env.HAS_STARTED_WORKER = 1; +const worker = new Worker(__filename).on('message', common.mustCall((port) => { + const h2header = Buffer.alloc(9); + const conn = net.connect({ port, allowHalfOpen: true }); + + conn.write('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'); + + h2header[3] = 4; // Send a settings frame. + conn.write(Buffer.from(h2header)); + + let inbuf = Buffer.alloc(0); + let state = 'settingsHeader'; + let settingsFrameLength; + conn.on('data', (chunk) => { + inbuf = Buffer.concat([inbuf, chunk]); + switch (state) { + case 'settingsHeader': + if (inbuf.length < 9) return; + settingsFrameLength = inbuf.readIntBE(0, 3); + inbuf = inbuf.slice(9); + state = 'readingSettings'; + // Fallthrough + case 'readingSettings': + if (inbuf.length < settingsFrameLength) return; + inbuf = inbuf.slice(settingsFrameLength); + h2header[3] = 4; // Send a settings ACK. + h2header[4] = 1; + conn.write(Buffer.from(h2header)); + state = 'ignoreInput'; + writeRequests(); + } + }); + + let gotError = false; + let streamId = 1; + + function writeRequests() { + for (let i = 1; i < 10 && !gotError; i++) { + h2header[3] = 1; // HEADERS + h2header[4] = 0x5; // END_HEADERS|END_STREAM + h2header.writeIntBE(1, 0, 3); // Length: 1 + h2header.writeIntBE(streamId, 5, 4); // Stream ID + streamId += 2; + // 0x88 = :status: 200 + if (!conn.write(Buffer.concat([h2header, Buffer.from([0x88])]))) { + break; + } + } + if (!gotError) + setImmediate(writeRequests); + } + + conn.once('error', common.mustCall(() => { + gotError = true; + worker.terminate(); + conn.destroy(); + })); +}));