-
Notifications
You must be signed in to change notification settings - Fork 4.7k
http_jsc: narrow unsafe surface of WebSocketUpgradeClient::connect #30785
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -379,3 +379,60 @@ | |
| ws.close(); | ||
| }); | ||
| }); | ||
|
|
||
| // Regression for #30777: the connect() path was refactored to take | ||
| // `&[BunString]` slices (with the `(ptr, len)` → slice conversion lifted | ||
| // into the extern-C shim). Cover both ends of that slice parameter: | ||
| // the empty-headers case (`(ptr, 0)`) and the many-headers case | ||
| // (drives the `Headers8Bit::init` iteration). | ||
| describe("WebSocket connect() header slice boundaries (#30777)", () => { | ||
| async function openAndEchoHeaders(opts: { headers?: Record<string, string> }): Promise<Record<string, string>> { | ||
| const { promise: headersSeen, resolve: resolveHeaders } = Promise.withResolvers<Record<string, string>>(); | ||
| await using server = Bun.serve({ | ||
| hostname: "127.0.0.1", | ||
| port: 0, | ||
| websocket: { | ||
| open(_ws) {}, | ||
| message(_ws, _msg) {}, | ||
| }, | ||
| fetch(req, server) { | ||
| const headers: Record<string, string> = {}; | ||
| for (const [k, v] of req.headers.entries()) headers[k.toLowerCase()] = v; | ||
| resolveHeaders(headers); | ||
| if (server.upgrade(req)) return; | ||
| return new Response("no upgrade", { status: 400 }); | ||
| }, | ||
| }); | ||
| const { promise: opened, resolve: resolveOpen, reject: rejectOpen } = Promise.withResolvers<void>(); | ||
| const ws = new WebSocket(`ws://127.0.0.1:${server.port}/`, opts); | ||
| ws.onopen = () => resolveOpen(); | ||
| ws.onerror = e => rejectOpen((e as any).error ?? new Error((e as any).message ?? "ws error")); | ||
| const [, headers] = await Promise.all([opened, headersSeen]); | ||
| ws.close(); | ||
|
Check warning on line 411 in test/js/web/websocket/websocket-custom-headers.test.ts
|
||
|
Comment on lines
+407
to
+411
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 The new Extended reasoning...What this isThe file declares module-level Code path
Why this is only a nitThe refutation is right that practical leakage is bounded: It is, however, an inconsistency with both (a) the explicit pattern every other test in this file follows, and (b) the documented test guideline in Step-by-step example
Fixconst ws = new WebSocket(`ws://127.0.0.1:${server.port}/`, opts);
clients.push(ws);One line, brings the new tests in line with the rest of the file and with |
||
| return headers; | ||
| } | ||
|
|
||
| it("connects with zero user headers (ffi::slice(ptr, 0) path)", async () => { | ||
| const seen = await openAndEchoHeaders({}); | ||
| // Core upgrade headers still arrive — the connect path must not skip | ||
| // them when the user-headers slice is empty. | ||
| expect(seen["connection"]?.toLowerCase()).toContain("upgrade"); | ||
| expect(seen["upgrade"]?.toLowerCase()).toBe("websocket"); | ||
| expect(seen["sec-websocket-key"]).toBeString(); | ||
| expect(seen["sec-websocket-version"]).toBe("13"); | ||
| }); | ||
|
|
||
| it("connects with many user headers (Headers8Bit slice iteration)", async () => { | ||
| const custom: Record<string, string> = {}; | ||
| for (let i = 0; i < 12; i++) custom[`X-Custom-${i}`] = `value-${i}`; | ||
| const seen = await openAndEchoHeaders({ headers: custom }); | ||
| // Every custom header must round-trip: the refactor interleaves | ||
| // names/values via `chunks_exact(2)`, so a drop-one or pair-off-by-one | ||
| // bug would show up as a missing or swapped value here. | ||
| for (let i = 0; i < 12; i++) { | ||
| expect(seen[`x-custom-${i}`]).toBe(`value-${i}`); | ||
| } | ||
| expect(seen["connection"]?.toLowerCase()).toContain("upgrade"); | ||
| expect(seen["upgrade"]?.toLowerCase()).toBe("websocket"); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Now that
OVERLAY_CSSis wrapped inJSON.stringify(), two existing comments that cite bake-codegen.ts as a live example of passing raw*{…}CSS todefine:are stale:src/parsers/json_lexer.rs:1304("bake-codegen.ts'sOVERLAY_CSS") andtest/bundler/bun-build-api.test.ts:76("src/codegen/bake-codegen.ts passes verbatim asOVERLAY_CSS"). The auto-quote codepath and its test remain valid for the general case; only the bake-codegen.ts attribution should be reworded (e.g. "previously passed") or dropped.Extended reasoning...
This PR changes
src/codegen/bake-codegen.tsso thatOVERLAY_CSSis now passed asJSON.stringify(css(...))instead of the raw minified CSS string. The new in-file comment is explicit about the intent: "Quoting here keeps bake-codegen.ts forward- and backward-compatible with the lexer change" — i.e., bake-codegen.ts deliberately stops relying on the JSON lexer's auto-quote rescue path.However, two other places in the tree still describe bake-codegen.ts as a present-tense consumer of that rescue path:
src/parsers/json_lexer.rs:1302-1304— the comment explaining why*/?/(/)are tokenised rather than errored says: "e.g. aBun.builddefine:whose value is a raw minified CSS string starting with*{...}(bake-codegen.ts'sOVERLAY_CSS)".test/bundler/bun-build-api.test.ts:76— the comment above the auto-quote test table says: "a raw minified CSS string starts with*{...}, which src/codegen/bake-codegen.ts passes verbatim asOVERLAY_CSS".After this PR, both statements are factually wrong: bake-codegen.ts now passes a JSON-quoted string (e.g.
"\"*{box-sizing:...}\""), which the lexer parses as a normalTStringLiteraland never reaches theTAsterisk→ auto-quote path. A reader following the cross-reference fromjson_lexer.rsto bake-codegen.ts will findJSON.stringify(...)and conclude either that the lexer comment is wrong or that the auto-quote codepath is now dead — neither of which is true.Step-by-step:
define: { OVERLAY_CSS: "*{box-sizing:border-box}..." }→ JSON lexer sees*at offset 0 → tokenisesTAsterisk(per the json_lexer.rs:1299-1306 arm) →parse_env_jsonfails to parse as expression → auto-quotes the whole value. The json_lexer.rs comment correctly named this as the live example.define: { OVERLAY_CSS: "\"*{box-sizing:border-box}...\"" }→ JSON lexer sees"at offset 0 → tokenisesTStringLiteral→ parses cleanly. The auto-quote path is never entered for this value, so citing it as "bake-codegen.ts'sOVERLAY_CSS" is now a dangling reference.Impact: None at runtime. The auto-quote codepath in
json_lexer.rsand the.each(["*{box-sizing:...}", ...])test inbun-build-api.test.tsare still correct and still needed for the general case (any user can pass raw CSS todefine:). Only the parenthetical/clause attributing the behavior to bake-codegen.ts has gone stale.Fix: Reword both comments to drop or past-tense the bake-codegen.ts citation — e.g. in json_lexer.rs change "(
bake-codegen.ts'sOVERLAY_CSS)" to "(asbake-codegen.ts'sOVERLAY_CSSdid before it was pre-quoted)" or simply delete the parenthetical; in bun-build-api.test.ts change "passes verbatim" to "used to pass verbatim" or drop the file reference. This is a comment-only doc-sync nit, not a behavioral bug.