Compare commits

...

4 Commits

Author SHA1 Message Date
Jarred Sumner
07c75759a2 Merge branch 'main' into claude/fix-http-send-payload-oob 2026-02-20 23:32:44 -08:00
Jarred Sumner
5739c5a0df Merge branch 'main' into claude/fix-http-send-payload-oob 2026-02-20 22:14:33 -08:00
Claude
546e918072 move regression test to issue/23092 and use Buffer.alloc
- Rename test to test/regression/issue/23092.test.ts using real issue number
- Replace .repeat() with Buffer.alloc().toString() per test guidelines

https://claude.ai/code/session_01BAYXgJ7J3MSJqLU1dhp22M
2026-02-21 05:05:05 +00:00
Claude Bot
af93e5625b fix(http): match iterator arguments between FetchHeaders count and copyTo
WebCore__FetchHeaders__count used createIterator() (lowerCaseKeys=true)
while WebCore__FetchHeaders__copyTo used createIterator(false). This
means count computed buf_len using lowercased header names while copyTo
wrote original-case names into the buffer.

For pure ASCII headers this is harmless since lowercasing preserves byte
length. But for non-ASCII header names (which can enter via internal
code paths like createFromPicoHeaders or createFromUWS), Unicode
lowercasing can change UTF-8 byte lengths. For example, U+0130
LATIN CAPITAL LETTER I WITH DOT ABOVE ('İ', 2 UTF-8 bytes) lowercases
to 'i' (1 UTF-8 byte). This causes count to underestimate buf_len,
so the buffer allocated by Headers.from is too small. copyTo then
writes past the buffer end, corrupting adjacent MultiArrayList memory
containing StringPointer offset/length fields — producing the observed
panic with a garbage offset (4162794746) into a valid-length buffer
(671).

Fix by passing lowerCaseKeys=false to createIterator in count, matching
what copyTo uses.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-21 04:36:31 +00:00
2 changed files with 50 additions and 4 deletions

View File

@@ -1906,12 +1906,14 @@ void WebCore__FetchHeaders__copyTo(WebCore::FetchHeaders* headers, StringPointer
}
void WebCore__FetchHeaders__count(WebCore::FetchHeaders* headers, uint32_t* count, uint32_t* buf_len)
{
auto iter = headers->createIterator();
// Use lowerCaseKeys=false to match copyTo, which also uses false.
// This ensures the byte length computed here matches what copyTo writes.
// With lowerCaseKeys=true, Unicode lowercasing can change UTF-8 byte lengths
// (e.g. U+0130 'İ' is 2 bytes but lowercases to 'i' which is 1 byte),
// causing the buffer to be undersized and copyTo to write out of bounds.
auto iter = headers->createIterator(false);
size_t i = 0;
for (auto pair = iter.next(); pair; pair = iter.next()) {
// UTF8 byteLength is not strictly necessary here
// They should always be ASCII.
// However, we can still do this out of an abundance of caution
i += BunString::utf8ByteLength(pair->key);
i += BunString::utf8ByteLength(pair->value);
}

View File

@@ -0,0 +1,44 @@
import { expect, test } from "bun:test";
// Regression test for #23092 - panic in sendInitialRequestPayload:
// "index out of bounds: index 4162794746, len 671"
//
// Root cause: WebCore__FetchHeaders__count used createIterator()
// (lowerCaseKeys=true) while WebCore__FetchHeaders__copyTo used
// createIterator(false). The mismatch means count computed byte
// lengths on lowercased keys while copyTo wrote original-case keys.
// For non-ASCII header names where Unicode lowercasing changes UTF-8
// byte length, this caused the buffer to be undersized, and copyTo
// would write out of bounds, corrupting adjacent StringPointer data.
test("fetch with many headers does not corrupt header data", async () => {
using server = Bun.serve({
port: 0,
async fetch(req) {
const body = await req.text();
return new Response(`${req.headers.get("x-check") ?? "missing"}:${body.length}`, {
status: 200,
});
},
});
// Exercise the header copy path with varying header sizes
for (let i = 0; i < 10; i++) {
const headers = new Headers();
headers.set("X-Check", `value-${i}`);
// Add progressively more headers to stress the buffer
for (let j = 0; j < i * 5; j++) {
headers.set(`X-Header-${j}`, Buffer.alloc(j + 1, "x").toString());
}
const response = await fetch(server.url, {
method: "POST",
headers,
body: Buffer.alloc(671, "a").toString(),
});
const text = await response.text();
expect(text).toBe(`value-${i}:671`);
expect(response.status).toBe(200);
}
});