Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/jsc/bindings/c-bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Comment thread
robobun marked this conversation as resolved.
#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).
Comment thread
robobun marked this conversation as resolved.
#define BLOB_HEADER_ALIGNMENT 8
#endif

extern "C" {
struct BlobHeader {
Expand Down
91 changes: 91 additions & 0 deletions test/bundler/compile-elf-segment-layout.test.ts
Original file line number Diff line number Diff line change
@@ -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<Elf64ProgramHeader, "vaddr" | "memsz" | "align">;

/** 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,
);
Comment thread
robobun marked this conversation as resolved.
60 changes: 60 additions & 0 deletions test/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
42 changes: 4 additions & 38 deletions test/js/bun/binary/tls-segment-size.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

/**
Expand Down