fix(freebsd): translate MAP_ANONYMOUS in the mmap leaf; read phdrs from ELF header

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.
This commit is contained in:
Graeme Geldenhuys 2026-07-01 23:01:46 +01:00
parent 0ee32e51f5
commit db85aba08d
3 changed files with 69 additions and 42 deletions

View file

@ -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

View file

@ -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);

View file

@ -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