feat(rtl): FreeBSD Tier-2 libc leaves + mremap/getrandom (Step 4b)

The Step-4b RTL leaves for a static FreeBSD build.  memcpy/memset/memcmp/
strlen are already target-invariant (runtime.cstub, pure Pascal — shared),
and the x86-64 jmp_buf is ABI-invariant (runtime.setjmp — shared), so this
step is small:

* runtime.syscall.freebsd gains getrandom (FreeBSD SYS 563, not Linux's
  318) and an mremap stub.  FreeBSD has no in-place-remap syscall, so
  mremap is a plain function returning MAP_FAILED (-1); runtime.mem's
  large-realloc path already treats an mremap failure as "grow the slow
  way" (alloc a fresh region, memcpy, free the old), so exporting a failing
  mremap gives the allocator its no-mremap fallback with zero allocator
  changes.

* runtime.libc2.freebsd — the FreeBSD sibling of runtime.libc2.linux: the
  atexit registry + `exit`, __cxa_atexit, abort, freestanding UTC
  civil-calendar gmtime_r/localtime_r/timegm, mkstemp, and system.  The
  calendar math, atexit registry, and struct tm layout are target-invariant
  (tm is identical on Linux/FreeBSD amd64), so the only differences from the
  Linux unit are the syscall leaf it builds on (runtime.syscall.freebsd) and
  the FreeBSD O_* flag values (O_CREAT=$0200, O_EXCL=$0800 — not Linux's
  $40/$80).

Both units are standalone (nothing in the Linux build graph uses them), so
the Linux self-host is untouched — confirmed by the clean fixpoints/suite.
Test TestLink_FreeBSDSyscallLeaf_GetrandomAndMremapStub asserts getrandom
uses 563 (and NOT Linux's 318) and the mremap stub returns -1.  Threads
(thr_new/_umtx_op) are Step 4c.
This commit is contained in:
Graeme Geldenhuys 2026-07-01 02:27:11 +01:00
parent 2662535579
commit bded5294d5
3 changed files with 391 additions and 0 deletions

View file

@ -0,0 +1,296 @@
{
Blaise - An Object Pascal Compiler
Copyright (c) 2026 Graeme Geldenhuys
SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
Licensed under the Apache License v2.0 with Runtime Library Exception.
See LICENSE file in the project root for full license terms.
}
unit runtime.libc2.freebsd;
// FreeBSD sibling of runtime.libc2.linux — the Tier 2 libc leaves that need real
// logic, not a raw syscall:
// * the atexit registry + `exit` (runs handlers then _exit)
// * __cxa_atexit (register a handler)
// * abort (raise SIGABRT, then _exit 134)
// * gmtime_r / localtime_r / timegm (freestanding civil-calendar math, UTC)
// * mkstemp (random-named O_CREAT|O_EXCL file)
// * system (fork + execve /bin/sh -c + wait4)
//
// The civil-calendar math, the atexit registry, and the tm layout are all
// target-invariant (struct tm is identical on Linux/FreeBSD amd64), so the only
// differences from the Linux unit are the syscall leaf it builds on
// (runtime.syscall.freebsd) and the FreeBSD O_* flag values. Linked only in a
// --static (libc-free) FreeBSD build (docs/freebsd-x86_64-backend-design.adoc,
// Step 4b).
interface
uses
runtime.syscall.freebsd;
{ __cxa_atexit(func, arg, dso_handle): register func(arg) to run at exit, LIFO.
Returns 0 on success. dso_handle is ignored (single program, no DSO unload). }
function __cxa_atexit(Func, Arg, DsoHandle: Pointer): Integer;
{ exit(code): run the registered handlers (LIFO), then _exit(code). This is the
bare `exit` main's epilogue calls; it is a reserved word in Pascal, so the
Pascal proc is _libc2_exit and the asm body publishes `exit`. }
procedure _RunAtExitAndExit(Code: Integer);
{ abort(): raise SIGABRT to self; if the signal is caught/ignored, force-exit
with status 134 (128 + SIGABRT). }
procedure abort;
{ struct tm split/join (UTC). T points at a time_t (Int64 epoch seconds). }
function gmtime_r(T: Pointer; Tm: Pointer): Pointer;
function localtime_r(T: Pointer; Tm: Pointer): Pointer;
function timegm(Tm: Pointer): Int64;
{ mkstemp(template): template ends with "XXXXXX"; replace with random chars and
open O_CREAT|O_EXCL|O_RDWR mode 0600. Returns the fd or -1. }
function mkstemp(Template: PChar): Integer;
{ system(cmd): fork, child execve "/bin/sh -c <cmd>", parent wait4; returns the
child's raw wait status (the RTL extracts the exit code). }
function system(Cmd: PChar): Integer;
implementation
type
PInt64 = ^Int64;
PInteger = ^Integer;
const
SIGABRT = 6;
O_RDWR = 2;
{ FreeBSD open(2) flag bits — differ from Linux ($40 / $80). See
rtl.platform.layout.freebsd (O_CREAT=$0200, O_TRUNC=$0400, O_APPEND=$0008);
O_EXCL is $0800 (sys/fcntl.h). }
O_CREAT = $0200;
O_EXCL = $0800;
MAX_ATEXIT = 64;
{ struct tm field byte offsets (SysV layout — identical on Linux and FreeBSD
amd64; the RTL's TTm mirrors it):
Sec@0 Min@4 Hour@8 MDay@12 Mon@16 Year@20 WDay@24 YDay@28 IsDST@32 (all int). }
const
TM_SEC=0; TM_MIN=4; TM_HOUR=8; TM_MDAY=12; TM_MON=16; TM_YEAR=20;
TM_WDAY=24; TM_YDAY=28; TM_ISDST=32;
{ --- atexit registry --- }
var
GAtExitFns: array[0..MAX_ATEXIT - 1] of Pointer;
GAtExitArgs: array[0..MAX_ATEXIT - 1] of Pointer;
GAtExitCount: Integer;
function __cxa_atexit(Func, Arg, DsoHandle: Pointer): Integer;
begin
if GAtExitCount >= MAX_ATEXIT then
begin
Result := -1;
Exit;
end;
GAtExitFns[GAtExitCount] := Func;
GAtExitArgs[GAtExitCount] := Arg;
GAtExitCount := GAtExitCount + 1;
Result := 0;
end;
{ Call a registered handler: void (*func)(void* arg). The handler is invoked
with its arg in %rdi (a 0-arg Pascal handler simply ignores it). }
procedure CallHandler(Func, Arg: Pointer); assembler; nostackframe;
asm
movq %rsi, %rax { Arg -> scratch; Func is in %rdi }
movq %rdi, %r11 { Func }
movq %rax, %rdi { Arg -> first C arg }
jmp *%r11
end;
procedure _RunAtExitAndExit(Code: Integer);
var
I: Integer;
begin
{ LIFO. }
I := GAtExitCount - 1;
while I >= 0 do
begin
if GAtExitFns[I] <> nil then
CallHandler(GAtExitFns[I], GAtExitArgs[I]);
I := I - 1;
end;
_exit(Code);
end;
{ Publish the bare `exit` symbol as a tail-call into _RunAtExitAndExit (Code is
already in %edi). }
procedure _libc2_exit(Code: Integer); assembler; nostackframe;
asm
.globl exit
exit:
jmp _RunAtExitAndExit
end;
procedure abort;
begin
kill(getpid(), SIGABRT);
_exit(134);
end;
{ --- civil calendar (UTC), based on Howard Hinnant's days_from_civil --- }
{ Days since 1970-01-01 for a y/m/d (proleptic Gregorian). }
function DaysFromCivil(Y, M, D: Int64): Int64;
var
Era, Yoe, Doy, Doe: Int64;
YY: Int64;
begin
if M <= 2 then YY := Y - 1 else YY := Y;
if YY >= 0 then Era := YY div 400 else Era := (YY - 399) div 400;
Yoe := YY - Era * 400;
if M > 2 then Doy := (153 * (M - 3) + 2) div 5 + D - 1
else Doy := (153 * (M + 9) + 2) div 5 + D - 1;
Doe := Yoe * 365 + Yoe div 4 - Yoe div 100 + Doy;
Result := Era * 146097 + Doe - 719468;
end;
{ Inverse: y/m/d from days-since-epoch. }
procedure CivilFromDays(Z: Int64; var Y, M, D: Int64);
var
Era, Doe, Yoe, Doy, Mp: Int64;
begin
Z := Z + 719468;
if Z >= 0 then Era := Z div 146097 else Era := (Z - 146096) div 146097;
Doe := Z - Era * 146097;
Yoe := (Doe - Doe div 1460 + Doe div 36524 - Doe div 146096) div 365;
Y := Yoe + Era * 400;
Doy := Doe - (365 * Yoe + Yoe div 4 - Yoe div 100);
Mp := (5 * Doy + 2) div 153;
D := Doy - (153 * Mp + 2) div 5 + 1;
if Mp < 10 then M := Mp + 3 else M := Mp - 9;
if M <= 2 then Y := Y + 1;
end;
procedure FillTm(T: Int64; Tm: Pointer);
var
Days, Secs, Y, M, D: Int64;
P: PInteger;
begin
Days := T div 86400;
Secs := T - Days * 86400;
if Secs < 0 then
begin
Secs := Secs + 86400;
Days := Days - 1;
end;
CivilFromDays(Days, Y, M, D);
P := PInteger(Pointer(PChar(Tm) + TM_SEC)); P^ := Integer(Secs mod 60);
P := PInteger(Pointer(PChar(Tm) + TM_MIN)); P^ := Integer((Secs div 60) mod 60);
P := PInteger(Pointer(PChar(Tm) + TM_HOUR)); P^ := Integer(Secs div 3600);
P := PInteger(Pointer(PChar(Tm) + TM_MDAY)); P^ := Integer(D);
P := PInteger(Pointer(PChar(Tm) + TM_MON)); P^ := Integer(M - 1); { 0-based }
P := PInteger(Pointer(PChar(Tm) + TM_YEAR)); P^ := Integer(Y - 1900); { years since 1900 }
{ WDay: 1970-01-01 was a Thursday (4). }
P := PInteger(Pointer(PChar(Tm) + TM_WDAY));
P^ := Integer(((Days mod 7) + 4 + 7) mod 7);
P := PInteger(Pointer(PChar(Tm) + TM_YDAY));
P^ := Integer(Days - DaysFromCivil(Y, 1, 1));
P := PInteger(Pointer(PChar(Tm) + TM_ISDST)); P^ := 0;
end;
function gmtime_r(T: Pointer; Tm: Pointer): Pointer;
begin
FillTm(PInt64(T)^, Tm);
Result := Tm;
end;
function localtime_r(T: Pointer; Tm: Pointer): Pointer;
begin
{ No timezone database in the freestanding RTL - treat local as UTC. }
Result := gmtime_r(T, Tm);
end;
function timegm(Tm: Pointer): Int64;
var
Y, M, D, Hh, Mm, Ss: Int64;
P: PInteger;
begin
P := PInteger(Pointer(PChar(Tm) + TM_SEC)); Ss := P^;
P := PInteger(Pointer(PChar(Tm) + TM_MIN)); Mm := P^;
P := PInteger(Pointer(PChar(Tm) + TM_HOUR)); Hh := P^;
P := PInteger(Pointer(PChar(Tm) + TM_MDAY)); D := P^;
P := PInteger(Pointer(PChar(Tm) + TM_MON)); M := P^ + 1;
P := PInteger(Pointer(PChar(Tm) + TM_YEAR)); Y := P^ + 1900;
Result := DaysFromCivil(Y, M, D) * 86400 + Hh * 3600 + Mm * 60 + Ss;
end;
{ --- mkstemp --- }
function mkstemp(Template: PChar): Integer;
const
ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var
Len, I, Tries, Got: Integer;
N: Int64;
RandBuf: array[0..5] of Byte;
Fd: Integer;
begin
Len := 0;
while (Template[Len] and $FF) <> 0 do Len := Len + 1;
if Len < 6 then Exit(-1);
Tries := 0;
while Tries < 256 do
begin
{ Fill all 6 bytes; getrandom may short-read, so loop until satisfied rather
than leave stale/zero bytes that would weaken the random suffix. }
Got := 0;
while Got < 6 do
begin
N := getrandom(@RandBuf[Got], 6 - Got, 0);
if N <= 0 then Continue; { EINTR / transient: retry }
Got := Got + Integer(N);
end;
for I := 0 to 5 do
Template[Len - 6 + I] := Ord(ALPHABET[(RandBuf[I] mod 62)]);
Fd := open(Template, O_RDWR or O_CREAT or O_EXCL, 384); { 0600 }
if Fd >= 0 then Exit(Fd);
Tries := Tries + 1;
end;
Result := -1;
end;
{ --- system --- }
function system(Cmd: PChar): Integer;
var
Pid, Status: Integer;
Argv: array[0..3] of Pointer;
ShPath, DashC: array[0..15] of Byte;
begin
{ Build argv = /bin/sh, -c, cmd, NULL. The /bin/sh path and -c flag are short
literals copied into local byte buffers so they have stable addresses. }
ShPath[0]:=Ord('/'); ShPath[1]:=Ord('b'); ShPath[2]:=Ord('i'); ShPath[3]:=Ord('n');
ShPath[4]:=Ord('/'); ShPath[5]:=Ord('s'); ShPath[6]:=Ord('h'); ShPath[7]:=0;
DashC[0]:=Ord('-'); DashC[1]:=Ord('c'); DashC[2]:=0;
Argv[0] := @ShPath[0];
Argv[1] := @DashC[0];
Argv[2] := Pointer(Cmd);
Argv[3] := nil;
Pid := fork();
if Pid = 0 then
begin
{ Child: replace image; on failure exit 127 like the shell. Forward the
process environment so the spawned shell sees PATH/HOME/etc. }
execve(PChar(@ShPath[0]), @Argv[0], environ);
_exit(127);
end;
if Pid < 0 then Exit(-1);
Status := 0;
wait4(Pid, @Status, 0, nil);
Result := Status;
end;
end.

View file

@ -83,6 +83,17 @@ function mmap(Addr: Pointer; Length: Int64; Prot, Flags, Fd: Integer;
Offset: Int64): Pointer; Offset: Int64): Pointer;
function munmap(Addr: Pointer; Length: Int64): Integer; function munmap(Addr: Pointer; Length: Int64): Integer;
{ mremap(old, oldsz, newsz, flags): FreeBSD has NO in-place remap syscall, so
this is a stub that always reports failure (MAP_FAILED = -1). runtime.mem's
large-realloc path already treats an mremap failure as "grow the slow way":
it allocates a fresh region, memcpy's the old contents across, and frees the
old one (see the MapFailed fall-through in _BlaiseReallocMem). Exporting the
`mremap` symbol as a failing stub therefore gives the allocator its no-mremap
grow fallback with no allocator changes. Signature matches the Linux leaf so
runtime.mem's `external name 'mremap'` binds on both targets. }
function mremap(OldAddr: Pointer; OldSize, NewSize: Int64; Flags: Integer;
NewAddr: Pointer): Pointer;
{ Time. } { Time. }
function nanosleep(Req, Rem: Pointer): Integer; function nanosleep(Req, Rem: Pointer): Integer;
function clock_gettime(ClockId: Integer; Ts: Pointer): Integer; function clock_gettime(ClockId: Integer; Ts: Pointer): Integer;
@ -93,6 +104,9 @@ function execve(Path: PChar; Argv, Envp: Pointer): Integer;
function wait4(Pid: Integer; Status: Pointer; Options: Integer; function wait4(Pid: Integer; Status: Pointer; Options: Integer;
Rusage: Pointer): Integer; Rusage: Pointer): Integer;
function kill(Pid, Sig: Integer): Integer; function kill(Pid, Sig: Integer): Integer;
{ getrandom(buf, count, flags): fill buf with count random bytes; returns the
count written or -errno. Used by mkstemp's random suffix. }
function getrandom(Buf: Pointer; Count: Int64; Flags: Integer): Int64;
{ Raw __getcwd(2): fills Buf (up to Size); returns 0 on success or -errno. The { Raw __getcwd(2): fills Buf (up to Size); returns 0 on success or -errno. The
wrapper in the posix layer adapts that to libc's buffer-pointer contract. } wrapper in the posix layer adapts that to libc's buffer-pointer contract. }
function sys_getcwd(Buf: PChar; Size: Int64): Int64; function sys_getcwd(Buf: PChar; Size: Int64): Int64;
@ -134,6 +148,7 @@ const
SYS_mkdir = 136; SYS_mkdir = 136;
SYS_rmdir = 137; SYS_rmdir = 137;
SYS_getcwd = 326; { __getcwd } SYS_getcwd = 326; { __getcwd }
SYS_getrandom = 563;
SYS_clock_gettime = 232; SYS_clock_gettime = 232;
SYS_nanosleep = 240; SYS_nanosleep = 240;
SYS_mmap = 477; { ino64 (legacy freebsd6 mmap = 197) } SYS_mmap = 477; { ino64 (legacy freebsd6 mmap = 197) }
@ -360,6 +375,14 @@ asm
ret ret
end; end;
{ mremap: no FreeBSD in-place-remap syscall. Always return MAP_FAILED (-1) so
the allocator takes its alloc-new-copy-free fallback (see the interface note). }
function mremap(OldAddr: Pointer; OldSize, NewSize: Int64; Flags: Integer;
NewAddr: Pointer): Pointer;
begin
Result := Pointer(-1);
end;
function nanosleep(Req, Rem: Pointer): Integer; function nanosleep(Req, Rem: Pointer): Integer;
assembler; nostackframe; assembler; nostackframe;
asm asm
@ -429,6 +452,17 @@ asm
ret ret
end; end;
function getrandom(Buf: Pointer; Count: Int64; Flags: Integer): Int64;
assembler; nostackframe;
asm
movq $563, %rax { SYS_getrandom }
syscall
jae .Lok_getrandom
negq %rax
.Lok_getrandom:
ret
end;
function sys_getcwd(Buf: PChar; Size: Int64): Int64; function sys_getcwd(Buf: PChar; Size: Int64): Int64;
assembler; nostackframe; assembler; nostackframe;
asm asm

View file

@ -92,6 +92,11 @@ type
CF-error-translation idiom (jae + negq). Standalone — no FreeBSD CF-error-translation idiom (jae + negq). Standalone — no FreeBSD
emulation, header/byte shape only. } emulation, header/byte shape only. }
procedure TestLink_FreeBSDSyscallLeaf_SymbolsAndNumbers; procedure TestLink_FreeBSDSyscallLeaf_SymbolsAndNumbers;
{ Step 4b: the FreeBSD leaf additions — getrandom uses the FreeBSD number
(563, not Linux's 318) and the mremap stub is a plain function that returns
MAP_FAILED (-1) so runtime.mem takes its alloc-copy-free grow fallback (no
in-place remap syscall on FreeBSD). Standalone byte/symbol shape. }
procedure TestLink_FreeBSDSyscallLeaf_GetrandomAndMremapStub;
end; end;
TDynLinkerTests = class(TTestCase) TDynLinkerTests = class(TTestCase)
@ -1209,6 +1214,62 @@ begin
Pos(Chr($49) + Chr($89) + Chr($CA), Bytes) >= 0); Pos(Chr($49) + Chr($89) + Chr($CA), Bytes) >= 0);
end; end;
procedure TLinkerE2ETests.TestLink_FreeBSDSyscallLeaf_GetrandomAndMremapStub;
const
{ getrandom is a raw syscall with the FreeBSD number 563 (Linux's is 318).
mremap has no FreeBSD syscall, so the leaf's stub just returns MAP_FAILED
(-1) — a mov of -1 into %rax then ret; runtime.mem's realloc path treats
that as "grow the slow way". A leading _start keeps the entry resolvable. }
FixtureAsm =
'.text' + LineEnding +
'.globl _start' + LineEnding +
'_start:' + LineEnding +
' callq getrandom' + LineEnding +
' callq mremap' + LineEnding +
' xorl %edi, %edi' + LineEnding +
' movq $1, %rax' + LineEnding + { SYS_exit }
' syscall' + LineEnding +
' hlt' + LineEnding +
'.globl getrandom' + LineEnding +
'getrandom:' + LineEnding +
' movq $563, %rax' + LineEnding + { SYS_getrandom (FreeBSD) }
' syscall' + LineEnding +
' jae .Lok_gr' + LineEnding +
' negq %rax' + LineEnding +
'.Lok_gr:' + LineEnding +
' ret' + LineEnding +
'.globl mremap' + LineEnding +
'mremap:' + LineEnding +
' movq $-1, %rax' + LineEnding + { MAP_FAILED — no FreeBSD mremap }
' ret' + LineEnding;
function MovRaxImm4b(AImm: Integer): string;
begin
Result := Chr($48) + Chr($C7) + Chr($C0) +
Chr(AImm and $FF) + Chr((AImm shr 8) and $FF) +
Chr((AImm shr 16) and $FF) + Chr((AImm shr 24) and $FF);
end;
var
Lk: TLinker;
Obj: TElfObjectFile;
Bytes: string;
begin
Lk := TLinker.Create(FreeBSDX86_64Target());
try
Obj := ParseElfObject(AssembleToBytes(FixtureAsm), 'fbsys2.o');
Lk.AddOwnedObject(Obj);
Bytes := Lk.LinkToBytes('_start');
AssertTrue('getrandom defined', Lk.AddrOfSymbol('getrandom') > 0);
AssertTrue('mremap defined', Lk.AddrOfSymbol('mremap') > 0);
finally
Lk.Free();
end;
{ getrandom uses the FreeBSD number 563 — NOT Linux's 318. }
AssertTrue('SYS_getrandom=563 movq imm present', Pos(MovRaxImm4b(563), Bytes) >= 0);
AssertTrue('Linux SYS_getrandom=318 NOT present', Pos(MovRaxImm4b(318), Bytes) < 0);
{ mremap stub loads -1 (movq $-1,%rax = 48 C7 C0 FF FF FF FF). }
AssertTrue('mremap stub returns -1 (MAP_FAILED)', Pos(MovRaxImm4b(-1), Bytes) >= 0);
end;
{ ---- TDynLinkerTests ---- } { ---- TDynLinkerTests ---- }
function TDynLinkerTests.ProjectRoot: string; function TDynLinkerTests.ProjectRoot: string;