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:
parent
0ee32e51f5
commit
db85aba08d
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue