From db85aba08dcd5f571c2ef1646899a2455914cc1c Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Wed, 1 Jul 2026 23:01:46 +0100 Subject: [PATCH] fix(freebsd): translate MAP_ANONYMOUS in the mmap leaf; read phdrs from ELF header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FreeBSD cross-compile emulation lane (Step 8) booted real FreeBSD 14.x and every cross-compiled binary — even a bare WriteLn — SIGSEGV'd. truss pinned it: mmap(0, 65536, RW, MAP_PRIVATE|0x20, -1, 0) ERR#9 'Bad file descriptor' The shared allocator (runtime.mem) hardcodes the LINUX MAP_ANONYMOUS bit (0x20); FreeBSD's MAP_ANON is 0x1000. Without a recognised anon flag FreeBSD treated the first heap arena as a file mapping on fd -1, returned EBADF, and the allocator dereferenced the -errno result -> SIGSEGV. This is exactly the runtime-only, struct/syscall-flag class of bug host-side static ELF checks cannot see, and the emulation lane caught it on its first run. Fix: the FreeBSD mmap leaf translates the flag — if the Linux MAP_ANONYMOUS bit (0x20) is set, clear it and set FreeBSD MAP_ANON (0x1000). This keeps runtime.mem OS-agnostic (one canonical, Linux-shaped flag set) and confines the divergence to the per-OS syscall leaf, matching the Strategy-B link-time-swap design. A flag already carrying 0x1000 (e.g. runtime.start.static.freebsd's TLS mmap) passes through untouched. Also harden SetupTLS: read the program-header table directly from this binary's own ELF header at the fixed non-PIE load base ($400000) instead of walking the kernel auxv for AT_PHDR. A non-PIE ET_EXEC always maps its header there, so this is exact and self-contained (FreeBSD does supply AT_PHDR, but relying on it coupled startup to the auxv layout for no benefit). Verified on real FreeBSD 14.3: hello prints 'Hello' (exit 0); fileio round-trips a file — write/read/stat/unlink — printing file-io-ok/exists-ok/done (exit 0), validating the struct-stat offsets, the file syscall leaf AND the allocator on a real kernel. Linux unaffected (FreeBSD units are standalone): all four fixpoints green, full suite 4029 tests under QBE-built and native-built runner. The freebsd-crosscheck emulation job (fileio fixture) is the standing regression guard for the mmap path. --- compiler/src/it/freebsd/fileio.pas | 6 +- .../pascal/runtime.start.static.freebsd.pas | 89 ++++++++++--------- .../main/pascal/runtime.syscall.freebsd.pas | 16 +++- 3 files changed, 69 insertions(+), 42 deletions(-) diff --git a/compiler/src/it/freebsd/fileio.pas b/compiler/src/it/freebsd/fileio.pas index 50f6b6e..ab8e424 100644 --- a/compiler/src/it/freebsd/fileio.pas +++ b/compiler/src/it/freebsd/fileio.pas @@ -9,7 +9,11 @@ done A wrong FreeBSD st_size / st_mode offset (they differ from Linux — see - rtl.platform.layout.freebsd) would make ReadFile or FileExists misbehave. } + rtl.platform.layout.freebsd) would make ReadFile or FileExists misbehave. + This also guards the memory manager's mmap path: WriteFile/ReadFile allocate, + and runtime.mem passes the Linux MAP_ANONYMOUS bit which the FreeBSD mmap leaf + must translate to MAP_ANON — without the translation the first allocation + fails with EBADF and the program SIGSEGVs (regression caught here). } program fileio; uses sysutils; var diff --git a/compiler/src/main/pascal/runtime.start.static.freebsd.pas b/compiler/src/main/pascal/runtime.start.static.freebsd.pas index b5902e4..1b393d7 100644 --- a/compiler/src/main/pascal/runtime.start.static.freebsd.pas +++ b/compiler/src/main/pascal/runtime.start.static.freebsd.pas @@ -14,7 +14,7 @@ unit runtime.start.static.freebsd; // The FreeBSD sibling of runtime.start.static.linux. A tiny asm trampoline // captures the initial stack pointer and tail-calls the Pascal _BlaiseStartC, // which: -// 1. parses argc / argv / envp / the auxv (the kernel's initial stack); +// 1. parses argc / argv / envp (the kernel's initial stack); // 2. sets up the static TLS block and the thread pointer (%fs) — required // before any threadvar (%fs-relative) access, which a static binary's // kernel does NOT do for us; @@ -28,12 +28,14 @@ unit runtime.start.static.freebsd; // (FreeBSD additionally passes an rtld cleanup pointer in %rdx for dynamic // binaries; it is 0 for a static ET_EXEC and we ignore it.) // -// The ELF auxv PT_TLS walk is PORTABLE: the AT_* tags (AT_PHDR=3, AT_PHENT=4, -// AT_PHNUM=5, AT_NULL=0) and PT_TLS=7 are identical on FreeBSD 14 (elf_common.h) -// and Linux, and x86-64 TLS is variant II on both. The ONE FreeBSD difference -// is how the %fs base is installed: Linux uses arch_prctl(ARCH_SET_FS, tp); -// FreeBSD uses sysarch(AMD64_SET_FSBASE, &tp) — note it takes a POINTER to the -// base value, not the value itself. +// TLS setup locates PT_TLS via the program headers, which SetupTLS reads +// DIRECTLY from this binary's own ELF header at the fixed non-PIE load base +// ($400000) rather than via the kernel auxv AT_PHDR entry — a non-PIE ET_EXEC +// always maps its header there, so this is exact and needs no auxv (identical +// on Linux and FreeBSD). x86-64 TLS is variant II on both. The ONE FreeBSD +// difference is how the %fs base is installed: Linux uses arch_prctl( +// ARCH_SET_FS, tp); FreeBSD uses sysarch(AMD64_SET_FSBASE, &tp) — note it takes +// a POINTER to the base value, not the value itself. // // x86-64 TLS (variant II): the thread pointer points at the TCB, which sits // ABOVE the TLS block; threadvars are at negative offsets from the TP. %fs:0 @@ -85,12 +87,17 @@ const FreeBSD 14 x86/include/sysarch.h) } PROT_RW = 3; { PROT_READ or PROT_WRITE } MAP_PRIVANON = $1002; { FreeBSD MAP_PRIVATE ($0002) or MAP_ANON ($1000) } - AT_NULL = 0; - AT_PHDR = 3; - AT_PHENT = 4; - AT_PHNUM = 5; PT_TLS = 7; +{ Our own ELF image. A non-PIE ET_EXEC is mapped at this fixed base (the + linker's TLinkTarget.BaseAddr), so the ELF header — and through e_phoff the + program-header table — is readable at compile-time-known addresses without + the kernel auxv. } + ELF_LOAD_BASE = $400000; + EH_PHOFF = $20; { e_phoff (u64) — file offset of the phdr table } + EH_PHENTSIZE = $36; { e_phentsize (u16) } + EH_PHNUM = $38; { e_phnum (u16) } + { ELF64 Phdr field offsets. } PH_TYPE = 0; { p_type (u32) } PH_OFFSET = 8; { p_offset (u64) - file offset; == vaddr-base in our images } @@ -116,14 +123,23 @@ begin sysarch(AMD64_SET_FSBASE, @Base); end; -{ Walk the auxv for PT_TLS via AT_PHDR/AT_PHENT/AT_PHNUM, then build the static - TLS block and set the thread pointer. Auxv is an array of (Int64 tag, Int64 - val) pairs terminated by AT_NULL. No-op when the program has no TLS. } -procedure SetupTLS(Auxv: Pointer); +{ Locate the program headers, find PT_TLS, then build the static TLS block and + set the thread pointer. No-op when the program has no TLS. + + We read the program-header table DIRECTLY from this binary's own ELF header, + which sits at the fixed non-PIE load base (ELF_LOAD_BASE = $400000, the + linker's TLinkTarget.BaseAddr): e_phoff at header offset 0x20, e_phentsize at + 0x36, e_phnum at 0x38. A non-PIE ET_EXEC always maps its own ELF header at + the fixed base (the first PT_LOAD covers file offset 0), so reading e_phoff + there is exact and needs no kernel auxv — simpler and more self-contained than + walking the auxv for AT_PHDR (which FreeBSD does supply, but relying on it + couples startup to the auxv layout for no benefit on a non-PIE image). } +procedure SetupTLS; var - P: PInt64; PhdrAddr, PhEnt, PhNum: Int64; - I, Tag, Val: Int64; + EhEPhOff: PInt64; + EhEPhEnt, EhEPhNum: ^Word; + I: Int64; Ph: PChar; TlsVaddr, TlsFileSz, TlsMemSz, TlsAlign: Int64; PType: ^Integer; @@ -133,20 +149,16 @@ var Src, Dst: PChar; TpSlot: PPointer; begin - PhdrAddr := 0; PhEnt := 56; PhNum := 0; - P := PInt64(Auxv); - while True do - begin - Tag := P^; - P := PInt64(Pointer(PChar(P) + 8)); - Val := P^; - P := PInt64(Pointer(PChar(P) + 8)); - if Tag = AT_NULL then Break; - if Tag = AT_PHDR then PhdrAddr := Val; - if Tag = AT_PHENT then PhEnt := Val; - if Tag = AT_PHNUM then PhNum := Val; - end; - if PhdrAddr = 0 then Exit; + { Read e_phoff / e_phentsize / e_phnum from our own ELF header at the load + base. e_phoff is a file offset; the first PT_LOAD maps file offset 0 to the + load base, so the phdr table is at base + e_phoff. } + EhEPhOff := PInt64(Pointer(ELF_LOAD_BASE + EH_PHOFF)); + EhEPhEnt := Pointer(ELF_LOAD_BASE + EH_PHENTSIZE); + EhEPhNum := Pointer(ELF_LOAD_BASE + EH_PHNUM); + PhdrAddr := ELF_LOAD_BASE + EhEPhOff^; + PhEnt := EhEPhEnt^; + PhNum := EhEPhNum^; + if (PhdrAddr = 0) or (PhEnt = 0) or (PhNum = 0) then Exit; { Find PT_TLS among the program headers. } TlsVaddr := 0; TlsFileSz := 0; TlsMemSz := 0; TlsAlign := 8; @@ -259,8 +271,7 @@ end; procedure _BlaiseStartC(SP: Pointer); var Argc: Int64; - Argv, Envp, Auxv: Pointer; - I: Int64; + Argv, Envp: Pointer; Ret: Integer; begin Argc := PInt64(SP)^; @@ -268,14 +279,12 @@ begin { envp = &argv[argc+1] } Envp := Pointer(PChar(Argv) + (Argc + 1) * 8); environ := Envp; - { auxv follows envp's NULL terminator. } - Auxv := Envp; - I := 0; - while PPointer(Pointer(PChar(Auxv) + I * 8))^ <> nil do - I := I + 1; - Auxv := Pointer(PChar(Auxv) + (I + 1) * 8); - SetupTLS(Auxv); + { TLS is set up by reading our own ELF program headers directly from this + binary's ELF header at the fixed non-PIE load base (see SetupTLS). A + non-PIE ET_EXEC always maps its own header there, so this is exact and + needs no kernel auxv. } + SetupTLS(); Ret := MainTrampoline(Integer(Argc), Argv); _exit(Ret); diff --git a/compiler/src/main/pascal/runtime.syscall.freebsd.pas b/compiler/src/main/pascal/runtime.syscall.freebsd.pas index f3f0f04..6d5b0d0 100644 --- a/compiler/src/main/pascal/runtime.syscall.freebsd.pas +++ b/compiler/src/main/pascal/runtime.syscall.freebsd.pas @@ -388,11 +388,25 @@ asm end; { mmap has 6 args; arg4 (Flags) arrives in %rcx (C ABI) but the kernel wants it - in %r10. Move it before the syscall. } + in %r10. Move it before the syscall. + + Flag translation: the shared allocator (runtime.mem) and other callers pass + the LINUX MAP_ANONYMOUS bit (0x20); FreeBSD's MAP_ANON is 0x1000. Without + MAP_ANON the kernel treats the mapping as file-backed on fd -1 and fails with + EBADF (then the caller derefs the -errno result and SIGSEGVs). So if the + Linux anon bit is set, clear it and set FreeBSD's — keeping runtime.mem + OS-agnostic (it uses one canonical, Linux-shaped flag set; the per-OS leaf + reconciles it). 0x1000 already set (e.g. from runtime.start.static.freebsd) + passes through untouched. } function mmap(Addr: Pointer; Length: Int64; Prot, Flags, Fd: Integer; Offset: Int64): Pointer; assembler; nostackframe; asm + testl $0x20, %ecx { Linux MAP_ANONYMOUS present? } + jz .Lnoanon_mmap + andl $0xffffffdf, %ecx { clear 0x20 } + orl $0x1000, %ecx { set FreeBSD MAP_ANON } +.Lnoanon_mmap: movq %rcx, %r10 movq $477, %rax { SYS_mmap (ino64) } syscall