-
Notifications
You must be signed in to change notification settings - Fork 5.1k
http: fix ProxyTunnel use-after-free when response completes mid-handleReading #30606
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
20612b5
4454b1e
869fe68
9272117
80f7b5d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // Regression: ProxyTunnel SSLWrapper callbacks firing with a freed | ||
| // *HTTPClient after the request completed inside the same handleReading(). | ||
| // | ||
| // SSLWrapper.handleReading → triggerDataCallback → ProxyTunnel.onData → | ||
| // progressUpdate → onAsyncHTTPCallback frees the ThreadlocalAsyncHTTP | ||
| // (and the embedded HTTPClient) synchronously. If the same handleReading | ||
| // then hits SSL_ERROR_SSL, triggerCloseCallback → onClose dereferences the | ||
| // freed pointer. The proxy in the fixture appends a malformed TLS record | ||
| // right after the HTTP-response record so both land in one BIO fill. | ||
| // | ||
| // Under debug+ASAN the pre-fix binary aborts with use-after-poison at | ||
| // ProxyTunnel.onClose. Release builds read poisoned memory without | ||
| // trapping, so this test is only meaningful on sanitizer builds. | ||
|
|
||
| import { expect, test } from "bun:test"; | ||
| import { bunEnv, bunExe, tls as tlsCert } from "harness"; | ||
| import { join } from "node:path"; | ||
|
|
||
| test("ProxyTunnel onClose does not use freed HTTPClient after response completes", async () => { | ||
| await using proc = Bun.spawn({ | ||
| cmd: [bunExe(), join(import.meta.dir, "fetch-proxy-tunnel-onclose-uaf-fixture.ts")], | ||
| env: { | ||
| ...bunEnv, | ||
| TLS_CERT: tlsCert.cert, | ||
| TLS_KEY: tlsCert.key, | ||
| // bunEnv sets NO_PROXY=localhost,127.0.0.1,... which makes fetch | ||
| // bypass the explicit `proxy:` option for our 127.0.0.1 target. | ||
| NO_PROXY: "", | ||
| no_proxy: "", | ||
| }, | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
|
|
||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
|
|
||
| if (exitCode !== 0) { | ||
| console.error("Fixture stderr:", stderr); | ||
| } | ||
| expect(exitCode).toBe(0); | ||
|
|
||
| const lastLine = stdout.trim().split("\n").pop()!; | ||
| const result = JSON.parse(lastLine); | ||
| expect(result.ok).toBeGreaterThan(0); | ||
| }, 30_000); | ||
|
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. 🟡 nit: test/CLAUDE.md says "Do not set a timeout on tests. Bun already has timeouts." — though given this fixture does 4×32 proxied TLS handshakes the 30s is probably load-bearing on slow CI, so if you want to drop it consider trimming the round/batch counts instead. Extended reasoning...What this is
The new test passes Step-by-step
Why the rule isn't a clean fit hereThe fixture spawns a subprocess that performs 4 rounds × 32 = 128 proxied HTTPS requests. Each one is TCP connect → This is also far from unique in the codebase: a grep shows 150+ test files passing a numeric second argument to Why flag it anywayIt's new code being added, the guideline is explicitly marked CRITICAL in the repo's own docs, and there's a straightforward alternative that satisfies both concerns: the iteration counts in the fixture are arbitrary load-generators, not correctness-bearing constants. Suggested fixEither leave the timeout (matching neighbours) and ignore this nit, or — if you'd prefer to follow for (let round = 0; round < 2; round++) {
for (let i = 0; i < 16; i++) {32 concurrent + 2 rounds is still plenty to trip the UAF under ASAN (the original repro is a single-request race; the batching just amortises scheduling jitter).
Collaborator
Author
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. Leaving the 30s timeout — matches the neighbouring |
||
Uh oh!
There was an error while loading. Please reload this page.