Files
bun.sh/src/allocators/BufferFallbackAllocator.zig
Ciro Spaciari 2582e6f98e fix(http2): fix settings, window size handling, and dynamic header buffer allocation (#26119)
## Summary

This PR fixes multiple HTTP/2 protocol compliance issues that were
causing stream errors with various HTTP/2 clients (Fauna, gRPC/Connect,
etc.).
fixes https://github.com/oven-sh/bun/issues/12544
fixes https://github.com/oven-sh/bun/issues/25589
### Key Fixes

**Window Size and Settings Handling**
- Fix initial stream window size to use `DEFAULT_WINDOW_SIZE` until
`SETTINGS_ACK` is received
- Per RFC 7540 Section 6.5.1: The sender can only rely on settings being
applied AFTER receiving `SETTINGS_ACK`
- Properly adjust existing stream windows when `INITIAL_WINDOW_SIZE`
setting changes (RFC 7540 Section 6.9.2)

**Header List Size Enforcement**  
- Implement `maxHeaderListSize` checking per RFC 7540 Section 6.5.2
- Track cumulative header list size using HPACK entry overhead (32 bytes
per RFC 7541 Section 4.1)
- Reject streams with `ENHANCE_YOUR_CALM` when header list exceeds
configured limit

**Custom Settings Support**
- Add validation for `customSettings` option (up to 10 custom settings,
matching Node.js `MAX_ADDITIONAL_SETTINGS`)
- Validate setting IDs are in range `[0, 0xFFFF]` per RFC 7540
- Validate setting values are in range `[0, 2^32-1]`

**Settings Validation Improvements**
- Use float comparison for settings validation to handle large values
correctly (was using `toInt32()` which truncates)
- Use proper `HTTP2_INVALID_SETTING_VALUE_RangeError` error codes for
Node.js compatibility

**BufferFallbackAllocator** - New allocator that tries a provided buffer
first, falls back to heap:
- Similar to `std.heap.stackFallback` but accepts external buffer slice
- Used with `shared_request_buffer` (16KB threadlocal) for common cases
- Falls back to `bun.default_allocator` for large headers

## Test Plan

- [x] `bun bd` compiles successfully
- [x] Node.js HTTP/2 tests pass: `bun bd
test/js/node/test/parallel/test-http2-connect.js`
- [x] New regression tests for frame size issues: `bun bd test
test/regression/issue/25589.test.ts`
- [x] HTTP/2 continuation tests: `bun bd test
test/js/node/http2/node-http2-continuation.test.ts`

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-01-22 14:35:18 -08:00

86 lines
2.8 KiB
Zig

/// An allocator that attempts to allocate from a provided buffer first,
/// falling back to another allocator when the buffer is exhausted.
/// Unlike `std.heap.StackFallbackAllocator`, this does not own the buffer.
const BufferFallbackAllocator = @This();
#fallback_allocator: Allocator,
#fixed_buffer_allocator: FixedBufferAllocator,
pub fn init(buffer: []u8, fallback_allocator: Allocator) BufferFallbackAllocator {
return .{
.#fallback_allocator = fallback_allocator,
.#fixed_buffer_allocator = FixedBufferAllocator.init(buffer),
};
}
pub fn allocator(self: *BufferFallbackAllocator) Allocator {
return .{
.ptr = self,
.vtable = &.{
.alloc = alloc,
.resize = resize,
.remap = remap,
.free = free,
},
};
}
fn alloc(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ra: usize) ?[*]u8 {
const self: *BufferFallbackAllocator = @ptrCast(@alignCast(ctx));
return FixedBufferAllocator.alloc(
&self.#fixed_buffer_allocator,
len,
alignment,
ra,
) orelse self.#fallback_allocator.rawAlloc(len, alignment, ra);
}
fn resize(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, new_len: usize, ra: usize) bool {
const self: *BufferFallbackAllocator = @ptrCast(@alignCast(ctx));
if (self.#fixed_buffer_allocator.ownsPtr(buf.ptr)) {
return FixedBufferAllocator.resize(
&self.#fixed_buffer_allocator,
buf,
alignment,
new_len,
ra,
);
}
return self.#fallback_allocator.rawResize(buf, alignment, new_len, ra);
}
fn remap(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ra: usize) ?[*]u8 {
const self: *BufferFallbackAllocator = @ptrCast(@alignCast(ctx));
if (self.#fixed_buffer_allocator.ownsPtr(memory.ptr)) {
return FixedBufferAllocator.remap(
&self.#fixed_buffer_allocator,
memory,
alignment,
new_len,
ra,
);
}
return self.#fallback_allocator.rawRemap(memory, alignment, new_len, ra);
}
fn free(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, ra: usize) void {
const self: *BufferFallbackAllocator = @ptrCast(@alignCast(ctx));
if (self.#fixed_buffer_allocator.ownsPtr(buf.ptr)) {
return FixedBufferAllocator.free(
&self.#fixed_buffer_allocator,
buf,
alignment,
ra,
);
}
return self.#fallback_allocator.rawFree(buf, alignment, ra);
}
pub fn reset(self: *BufferFallbackAllocator) void {
self.#fixed_buffer_allocator.reset();
}
const std = @import("std");
const Allocator = std.mem.Allocator;
const FixedBufferAllocator = std.heap.FixedBufferAllocator;