Compare commits

..

1 Commits

Author SHA1 Message Date
Dylan Conway
7baf50f379 fix(http): align DeadSocket to prevent crash on Windows ARM64 stable builds (#27290)
## Summary

- `DeadSocket` only contains a `u8` field (alignment 1), so the linker
could place `DeadSocket.dead_socket` at a non-8-byte-aligned address
- When `markTaggedSocketAsDead` creates a tagged pointer embedding this
address and passes it through `bun.cast(**anyopaque, ...)`, the
`@alignCast` panics with "incorrect alignment" because the bottom bits
of the tagged value come from the unaligned address
- Fix: add `align(@alignOf(usize))` to the `dead_socket` variable
declaration

This only manifested on stable (non-canary) Windows ARM64 builds because
the binary layout differs when `ci_assert` is false, shifting the static
variable to a non-aligned address. Canary builds happened to place it at
an aligned address by coincidence.

## Test plan

- [x] Verified `fetch('https://example.com')` no longer crashes on
Windows ARM64 stable build (ENABLE_CANARY=OFF)
- [x] Verified 5 sequential HTTPS fetches complete successfully
- [x] Verified the fix is a single-line change with no behavioral side
effects

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 13:51:24 -08:00
3 changed files with 4 additions and 192 deletions

View File

@@ -480,7 +480,9 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
const DeadSocket = struct {
garbage: u8 = 0,
pub var dead_socket: DeadSocket = .{};
/// Must be aligned to `@alignOf(usize)` so that tagged pointer values
/// embedding this address pass the `@alignCast` in `bun.cast`.
pub var dead_socket: DeadSocket align(@alignOf(usize)) = .{};
};
var dead_socket = &DeadSocket.dead_socket;

View File

@@ -93,12 +93,10 @@ function ClientRequest(input, options, cb) {
callback = undefined;
}
hasExplicitWrite = true;
return write_(chunk, encoding, callback);
};
let writeCount = 0;
let hasExplicitWrite = false;
let resolveNextChunk: ((end: boolean) => void) | undefined = _end => {};
const pushChunk = chunk => {
@@ -568,22 +566,7 @@ function ClientRequest(input, options, cb) {
this[kAbortController] ??= new AbortController();
this[kAbortController].signal.addEventListener("abort", onAbort, { once: true });
var body;
// Match Node.js behavior: when req.write() was explicitly called to send body data
// without an explicit Content-Length header, use a streaming body so that the HTTP
// layer sends Transfer-Encoding: chunked instead of Content-Length.
// When only req.end(data) is used (no prior req.write()), use a concrete body with
// Content-Length, which also matches Node.js behavior.
if (hasExplicitWrite && this[kBodyChunks]?.length > 0 && !this.getHeader("content-length")) {
const chunks = this[kBodyChunks];
body = async function* () {
for (const chunk of chunks) {
yield chunk;
}
};
} else {
body = this[kBodyChunks] && this[kBodyChunks].length > 1 ? new Blob(this[kBodyChunks]) : this[kBodyChunks]?.[0];
}
var body = this[kBodyChunks] && this[kBodyChunks].length > 1 ? new Blob(this[kBodyChunks]) : this[kBodyChunks]?.[0];
try {
startFetch(body);

View File

@@ -1,173 +0,0 @@
import { expect, test } from "bun:test";
import http from "node:http";
// https://github.com/oven-sh/bun/issues/23751
// When using req.write() followed by req.end(), Bun should send
// Transfer-Encoding: chunked instead of Content-Length, matching Node.js behavior.
test("http.request with req.write() uses Transfer-Encoding: chunked", async () => {
const { promise, resolve } = Promise.withResolvers<{
te: string | null;
cl: string | null;
body: string;
}>();
using server = Bun.serve({
port: 0,
async fetch(req) {
const body = await req.text();
resolve({
te: req.headers.get("transfer-encoding"),
cl: req.headers.get("content-length"),
body,
});
return new Response("OK");
},
});
const req = http.request(
{
hostname: server.hostname,
port: server.port,
path: "/",
method: "POST",
},
res => {
res.on("data", () => {});
res.on("end", () => {});
},
);
req.write("hello");
req.end();
const result = await promise;
expect(result.te).toBe("chunked");
expect(result.cl).toBeNull();
expect(result.body).toBe("hello");
});
test("http.request with req.write() multiple chunks uses Transfer-Encoding: chunked", async () => {
const { promise, resolve } = Promise.withResolvers<{
te: string | null;
cl: string | null;
body: string;
}>();
using server = Bun.serve({
port: 0,
async fetch(req) {
const body = await req.text();
resolve({
te: req.headers.get("transfer-encoding"),
cl: req.headers.get("content-length"),
body,
});
return new Response("OK");
},
});
const req = http.request(
{
hostname: server.hostname,
port: server.port,
path: "/",
method: "POST",
},
res => {
res.on("data", () => {});
res.on("end", () => {});
},
);
req.write("hello ");
req.write("world");
req.end();
const result = await promise;
expect(result.te).toBe("chunked");
expect(result.cl).toBeNull();
expect(result.body).toBe("hello world");
});
test("http.request with explicit Content-Length preserves it", async () => {
const { promise, resolve } = Promise.withResolvers<{
te: string | null;
cl: string | null;
body: string;
}>();
using server = Bun.serve({
port: 0,
async fetch(req) {
const body = await req.text();
resolve({
te: req.headers.get("transfer-encoding"),
cl: req.headers.get("content-length"),
body,
});
return new Response("OK");
},
});
const req = http.request(
{
hostname: server.hostname,
port: server.port,
path: "/",
method: "POST",
headers: {
"Content-Length": "5",
},
},
res => {
res.on("data", () => {});
res.on("end", () => {});
},
);
req.write("hello");
req.end();
const result = await promise;
expect(result.cl).toBe("5");
expect(result.te).toBeNull();
expect(result.body).toBe("hello");
});
test("http.request with req.end(data) and no req.write() uses Content-Length", async () => {
const { promise, resolve } = Promise.withResolvers<{
te: string | null;
cl: string | null;
body: string;
}>();
using server = Bun.serve({
port: 0,
async fetch(req) {
const body = await req.text();
resolve({
te: req.headers.get("transfer-encoding"),
cl: req.headers.get("content-length"),
body,
});
return new Response("OK");
},
});
const req = http.request(
{
hostname: server.hostname,
port: server.port,
path: "/",
method: "POST",
},
res => {
res.on("data", () => {});
res.on("end", () => {});
},
);
req.end("hello");
const result = await promise;
expect(result.cl).toBe("5");
expect(result.te).toBeNull();
expect(result.body).toBe("hello");
});