diff --git a/src/jsc/bindings/c-bindings.cpp b/src/jsc/bindings/c-bindings.cpp index ea81ebd5075e..49a07c324d3a 100644 --- a/src/jsc/bindings/c-bindings.cpp +++ b/src/jsc/bindings/c-bindings.cpp @@ -1113,7 +1113,17 @@ extern "C" void Bun__signpost_emit(os_log_t log, os_signpost_type_t type, os_sig #if OS(DARWIN) || defined(__linux__) || defined(__FreeBSD__) -#define BLOB_HEADER_ALIGNMENT 16 * 1024 +#if OS(DARWIN) +// exe_format/macho.rs expands the __BUN segment in place at this alignment +// (the page size on Apple Silicon). +#define BLOB_HEADER_ALIGNMENT (16 * 1024) +#else +// ELF: the section holds only this 8-byte header; exe_format/elf.rs places the +// --compile payload at a page-aligned vaddr itself. A larger alignment raises +// the RW PT_LOAD's p_align past the page size, which makes it overlap the +// previous segment under strict-p_align loaders like UPX's stub (#40752). +#define BLOB_HEADER_ALIGNMENT 8 +#endif extern "C" { struct BlobHeader { diff --git a/test/bundler/compile-elf-segment-layout.test.ts b/test/bundler/compile-elf-segment-layout.test.ts new file mode 100644 index 000000000000..f2a72143b92a --- /dev/null +++ b/test/bundler/compile-elf-segment-layout.test.ts @@ -0,0 +1,91 @@ +// The `.bun` section (the standalone module graph header) used to be declared +// with 16KB alignment on ELF. That raised the RW PT_LOAD's p_align to 0x4000 +// while the other segments stayed at 0x1000, and lld assigned the RW p_vaddr +// without keeping round_down(p_vaddr, 0x4000) clear of the previous segment's +// pages. The kernel ignores p_align at execve, so the plain binary ran, but a +// loader that honors p_align (UPX's decompression stub) mapped the RW segment +// over the tail of the R E segment and the embedded module source read back +// corrupted: `SyntaxError: Invalid character: '\0'`. +// +// These tests assert the strict-p_align non-overlap invariant on the bun +// binary itself and on a `--compile` output (the file UPX processes): +// for each PT_LOAD pair sorted by vaddr, +// round_down(next.p_vaddr, next.align) >= round_up(prev.p_vaddr + prev.p_memsz, prev.align) +// where align = max(p_align, page size). +// +// https://github.com/oven-sh/bun/issues/40752 + +import { expect, test } from "bun:test"; +import type { Elf64ProgramHeader } from "harness"; +import { bunEnv, bunExe, isFreeBSD, isLinux, readElf64ProgramHeaders, tempDir } from "harness"; +import { join } from "node:path"; + +type LoadSegment = Pick; + +/** PT_LOAD program headers of an ELF64 file. */ +function readLoadSegments(path: string): LoadSegment[] { + return readElf64ProgramHeaders(path).filter(ph => ph.type === 1 /* PT_LOAD */); +} + +/** + * Mapped range of a PT_LOAD under strict p_align semantics: what a loader + * that honors p_align (like UPX's stub) maps for the segment. + */ +function strictRange({ vaddr, memsz, align }: LoadSegment): [bigint, bigint] { + const a = align > 0x1000n ? align : 0x1000n; // mapping granularity is at least a page + const start = vaddr & ~(a - 1n); + const end = (vaddr + memsz + a - 1n) & ~(a - 1n); + return [start, end]; +} + +function expectNoOverlap(path: string) { + const loads = readLoadSegments(path).sort((a, b) => (a.vaddr < b.vaddr ? -1 : 1)); + expect(loads.length).toBeGreaterThan(1); + for (let i = 1; i < loads.length; i++) { + const [, prevEnd] = strictRange(loads[i - 1]); + const [nextStart] = strictRange(loads[i]); + if (nextStart < prevEnd) { + const fmt = (s: LoadSegment) => + `vaddr=0x${s.vaddr.toString(16)} memsz=0x${s.memsz.toString(16)} align=0x${s.align.toString(16)}`; + throw new Error( + `PT_LOAD segments overlap under strict p_align semantics by 0x${(prevEnd - nextStart).toString(16)} bytes:\n` + + ` ${fmt(loads[i - 1])}\n ${fmt(loads[i])}`, + ); + } + } +} + +test.skipIf(!(isLinux || isFreeBSD))("bun binary has no PT_LOAD overlap under strict p_align", () => { + expectNoOverlap(bunExe()); +}); + +test.skipIf(!(isLinux || isFreeBSD))( + "compiled executable has no PT_LOAD overlap under strict p_align", + async () => { + using dir = tempDir("elf-segment-layout", { + "index.ts": `console.log("hello from compiled");`, + }); + const cwd = String(dir); + const out = join(cwd, "app"); + + await using build = Bun.spawn({ + cmd: [bunExe(), "build", "--compile", join(cwd, "index.ts"), "--outfile", out], + env: bunEnv, + cwd, + stderr: "pipe", + stdout: "pipe", + }); + const [, buildErr, buildExit] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); + expect(buildErr).not.toContain("error:"); + expect(buildExit).toBe(0); + + expectNoOverlap(out); + + await using run = Bun.spawn({ cmd: [out], env: bunEnv, cwd, stderr: "pipe", stdout: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([run.stdout.text(), run.stderr.text(), run.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe("hello from compiled\n"); + expect(exitCode).toBe(0); + }, + 180_000, +); diff --git a/test/harness.ts b/test/harness.ts index 016b40e96dad..3e4c5c8c69f0 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -2308,3 +2308,63 @@ export const rss: () => number = process.platform === "darwin" && typeof Bun.unsafe.memoryFootprint === "function" ? (Bun.unsafe.memoryFootprint as () => number) : process.memoryUsage.rss; + +/** Read exactly `len` bytes from `fd` at absolute `offset`. */ +export function preadExact(fd: number, offset: number, len: number): Buffer { + const buf = Buffer.alloc(len); + let got = 0; + while (got < len) { + const n = fs.readSync(fd, buf, got, len - got, offset + got); + if (n === 0) throw new Error(`short read at ${offset}`); + got += n; + } + return buf; +} + +/** One ELF64 program header, fields as in Elf64_Phdr. */ +export interface Elf64ProgramHeader { + type: number; + flags: number; + offset: bigint; + vaddr: bigint; + paddr: bigint; + filesz: bigint; + memsz: bigint; + align: bigint; +} + +/** Program headers of an ELF64 binary (either endianness). */ +export function readElf64ProgramHeaders(path: string): Elf64ProgramHeader[] { + const fd = openSync(path, "r"); + try { + const ehdr = preadExact(fd, 0, 64); + if (ehdr.readUInt32BE(0) !== 0x7f454c46) throw new Error("not ELF"); + if (ehdr[4] !== 2) throw new Error("only ELF64 supported"); // EI_CLASS + const le = ehdr[5] === 1; // EI_DATA + const u16 = (b: Buffer, o: number) => (le ? b.readUInt16LE(o) : b.readUInt16BE(o)); + const u32 = (b: Buffer, o: number) => (le ? b.readUInt32LE(o) : b.readUInt32BE(o)); + const u64 = (b: Buffer, o: number) => (le ? b.readBigUInt64LE(o) : b.readBigUInt64BE(o)); + + const e_phoff = Number(u64(ehdr, 32)); + const e_phentsize = u16(ehdr, 54); + const e_phnum = u16(ehdr, 56); + + const headers: Elf64ProgramHeader[] = []; + for (let i = 0; i < e_phnum; i++) { + const ph = preadExact(fd, e_phoff + i * e_phentsize, e_phentsize); + headers.push({ + type: u32(ph, 0), + flags: u32(ph, 4), + offset: u64(ph, 8), + vaddr: u64(ph, 16), + paddr: u64(ph, 24), + filesz: u64(ph, 32), + memsz: u64(ph, 40), + align: u64(ph, 48), + }); + } + return headers; + } finally { + closeSync(fd); + } +} diff --git a/test/js/bun/binary/tls-segment-size.test.ts b/test/js/bun/binary/tls-segment-size.test.ts index 39c499e2c91c..ed35e40d8387 100644 --- a/test/js/bun/binary/tls-segment-size.test.ts +++ b/test/js/bun/binary/tls-segment-size.test.ts @@ -15,20 +15,8 @@ // every platform rather than only showing up as a Windows size bump. import { describe, expect, test } from "bun:test"; -import { isFreeBSD, isLinux, isWindows } from "harness"; -import { closeSync, openSync, readSync } from "node:fs"; - -/** Read `len` bytes from `fd` at absolute `offset`. */ -function preadExact(fd: number, offset: number, len: number): Buffer { - const buf = Buffer.alloc(len); - let got = 0; - while (got < len) { - const n = readSync(fd, buf, got, len - got, offset + got); - if (n === 0) throw new Error(`short read at ${offset}`); - got += n; - } - return buf; -} +import { isFreeBSD, isLinux, isWindows, preadExact, readElf64ProgramHeaders } from "harness"; +import { closeSync, openSync } from "node:fs"; /** * ELF: size of the PT_TLS segment's in-memory template (p_memsz). This is @@ -37,30 +25,8 @@ function preadExact(fd: number, offset: number, len: number): Buffer { * the main image. */ function elfTlsMemSize(path: string): number { - const fd = openSync(path, "r"); - try { - const ehdr = preadExact(fd, 0, 64); - if (ehdr.readUInt32BE(0) !== 0x7f454c46) throw new Error("not ELF"); - if (ehdr[4] !== 2) throw new Error("only ELF64 supported"); // EI_CLASS - const le = ehdr[5] === 1; // EI_DATA - const u16 = (b: Buffer, o: number) => (le ? b.readUInt16LE(o) : b.readUInt16BE(o)); - const u64 = (b: Buffer, o: number) => (le ? b.readBigUInt64LE(o) : b.readBigUInt64BE(o)); - - const e_phoff = Number(u64(ehdr, 32)); - const e_phentsize = u16(ehdr, 54); - const e_phnum = u16(ehdr, 56); - - for (let i = 0; i < e_phnum; i++) { - const ph = preadExact(fd, e_phoff + i * e_phentsize, e_phentsize); - const p_type = le ? ph.readUInt32LE(0) : ph.readUInt32BE(0); - if (p_type === 7 /* PT_TLS */) { - return Number(u64(ph, 40)); // p_memsz - } - } - return 0; // no TLS segment - } finally { - closeSync(fd); - } + const tls = readElf64ProgramHeaders(path).find(ph => ph.type === 7 /* PT_TLS */); + return tls ? Number(tls.memsz) : 0; } /**