What is Forward Compatibility?
Imagine you manage a fleet of production servers running FreeBSD 12. Your team has just built a new deployment agent compiled against FreeBSD 15, and you want to run a validation pass on the existing fleet before scheduling the upgrade window — without touching the OS. Forward compatibility is the kernel feature that makes this work.
Backward compatibility = a newer system runs older programs. Common and expected.
Forward compatibility = an older system runs newer programs. Rare and surprisingly complex.
The Players
Before diving into the mechanics, meet the cast of characters involved in every execution attempt.
Key Terminology
- osreldate
- A 6-digit number encoding the FreeBSD version.
1500005= FreeBSD 15.0 patch 5. Format: XXYYZZ where XX=major, YY=minor, ZZ=patch. - sysent
- The system call entry table — an array where each slot contains a function pointer for handling that syscall number.
- sysvec
- The system vector — a structure that defines how to execute binaries of a particular type. Contains the sysent table, signal handling, memory layout, and more.
- ELF Brand
- A tag inside ELF binaries identifying which OS/ABI they were built for. FreeBSD uses this to decide how to run the program.
- trans_osrel
- A function that translates (validates) the osreldate from an ELF binary’s note section. Returns TRUE if the brand should handle this binary.
The Problem We’re Solving
When the FreeBSD kernel runs a program, it must answer two questions: which syscall table to use, and what version is this binary. Without fwdcompat, neither question has a good answer for future-version binaries.
Without Forward Compatibility
The kernel sees the binary’s osreldate of 1500005 (FreeBSD 15). Without fwdcompat, the default brand either claims the binary and fails on unknown syscalls, or the binary simply cannot run.
Two Challenges to Solve
The default FreeBSD ELF brand will claim all FreeBSD binaries, including ones from future versions it cannot properly run. We need to intercept this decision.
FreeBSD 15 adds new system calls (e.g. syscall #580). FreeBSD 12’s sysent only goes up to ~560. When the binary calls #580, the kernel has no entry for it.
If a FreeBSD 15 binary tries to call syscall #580 on an unmodified FreeBSD 12 kernel, what happens?
The Architecture
Forward compatibility works through two mechanisms acting in concert: ELF brand hijacking to intercept binary selection, and an extended syscall table to handle new syscalls. Neither works without the other.
How the Pieces Fit Together
Visual Architecture
🎯 ELF Brand Hijacking
- Patch default brand’s trans_osrel
- Add new brand for future binaries
- Route FreeBSD 13+ binaries to fwdcompat
📋 Extended Syscall Table
- sysent[0..559] = base kernel
- sysent[560..580] = new syscalls
- copy_file_range, shm_open2, close_range
⚙️ sysvec
- sv_size = 580+ (extended)
- sv_table = fwdcompat_sysent
- sv_name = "FreeBSD Fwd-compat"
The fwdcompat module doesn’t modify the base kernel at all. It adds a new ELF brand and patches one function pointer. When unloaded, everything reverts cleanly — the kernel is untouched.
ELF Brand Hijacking
The kernel already has a FreeBSD ELF brand that handles normal binaries. We can’t remove it — but we can modify its selection logic by swapping one function pointer. This is the elegance at the heart of fwdcompat.
How Brand Selection Works
Each ELF brand has a brandnote structure containing a trans_osrel function. The kernel calls this function to ask: "Do you want to handle this binary?"
/* The original FreeBSD brand */
static bool
freebsd_trans_osrel(const Elf_Note *note,
int32_t *osrelp) {
int32_t osrel = *(note_data);
*osrelp = osrel;
return (TRUE);
}
The original behavior:
1. Read the version number from the binary’s ELF note section.
2. Store it in osrelp for later use.
3. Always return TRUE — "yes, I’ll handle this binary."
⚠️ Problem: it accepts everything, even binaries it can’t properly run!
The Hijacking Trick
FwdCompat replaces that function pointer with a wrapper that rejects future-version binaries, leaving them for the fwdcompat brand to claim.
static bool
forward_compat_trans_osrel(
const Elf_Note *note, int32_t *osrelp) {
bool ret;
int32_t osrel;
ret = (*original_trans_osrel)(note, &osrel);
if (ret) {
if (P_OSREL_MAJOR(osrel) >
P_OSREL_MAJOR(__FreeBSD_version))
return (FALSE);
*osrelp = osrel;
}
return (ret);
}
The patched behavior:
1. Call the original function first (be polite — don’t break existing logic).
2. Check: is this binary’s major version greater than the kernel’s?
3. If yes → return FALSE (“not my problem, pass to the next brand”).
4. If no → proceed normally.
✅ Future binaries now fall through to the fwdcompat brand!
The P_OSREL_MAJOR Macro
/* Extract major version from osreldate */
#define P_OSREL_MAJOR(x) ((x) / 100000)
/* Examples: */
P_OSREL_MAJOR(1201524) == 12 // FreeBSD 12
P_OSREL_MAJOR(1500005) == 15 // FreeBSD 15
The osreldate format is like XXYYZZ — major, minor, patch packed into one number.
Dividing by 100,000 strips the minor and patch parts, leaving just the major version.
So we can compare just the major version numbers — 12 vs 15 — without caring about patch levels.
Installing and Removing the Patch
static void
patch_brandnote(Elf_Brandnote *brandnote) {
original_trans_osrel = brandnote->trans_osrel;
brandnote->trans_osrel =
forward_compat_trans_osrel;
}
static void
unpatch_brandnote(Elf_Brandnote *brandnote) {
brandnote->trans_osrel = original_trans_osrel;
}
On module load:
1. Save the original function pointer so we can restore it later.
2. Swap in our wrapper function.
On module unload:
1. Restore the original function pointer.
2. The kernel returns to its default behavior — cleanly, with no trace left.
Why does fwdcompat patch the existing FreeBSD brand instead of just adding its own brand?
The Extended Syscall Table
Once a future binary is running under fwdcompat’s brand, it will try to use system calls that don’t exist in the base kernel. Here’s how the extended syscall table provides them.
What’s in a System Call Table?
struct sysent {
int sy_narg; /* number of args */
sy_call_t *sy_call; /* function pointer */
au_event_t sy_auevent;/* audit event */
};
/* The table is just an array: */
struct sysent sysent[SYSCALL_COUNT];
/* syscall #3 → sysent[3].sy_call() */
A syscall table is like a phone directory for kernel functions.
sy_narg: how many arguments this syscall expects.
sy_call: the function pointer — “who picks up when you dial this number.”
When a binary calls syscall #3 (read), the kernel looks up sysent[3] and calls that function. That’s the whole dispatch mechanism.
The Extended Table
fwdcompat builds a complete table — all base syscalls plus the new ones from FreeBSD 13/14/15:
/* Base kernel: ~560 syscalls */
struct sysent sysent[560];
/* fwdcompat module: 580+ syscalls */
struct sysent fwdcompat_sysent[580] = {
/* 0–559: identical to base kernel */
{ AS(read_args), sys_read, ... }, /* #3 */
/* ... */
/* 560+: NEW from FreeBSD 13/14/15 */
{ AS(close_range_args),
sys_close_range, ... }, /* #575 */
{ AS(copy_file_range_args),
sys_copy_file_range, ... }, /* #580 */
};
The fwdcompat table is a superset of the base kernel’s table — it starts with all 560 base syscalls, then adds new ones.
Why include the base syscalls? Because the table is indexed by number — entry #3 must be read, entry #4 must be write. You can’t have gaps.
The new entries at 560+ are where the magic is: system calls that the base kernel doesn’t know about at all.
The System Vector (sysvec)
struct sysentvec
elf64_fwdcompat_freebsd_sysvec = {
.sv_size = SYS_FWDCOMPAT_MAXSYSCALL,
.sv_table = fwdcompat_sysent,
.sv_name = "FreeBSD Forward-compatible ELF64",
.sv_flags = SV_ABI_FREEBSD | SV_LP64,
.sv_sendsig = sendsig,
/* ... many more fields */
};
The sysvec is the kernel’s "operating manual" for running a binary.
sv_size: “my table has 580+ entries” — tells the kernel how far the table goes.
sv_table: “use this syscall table for lookups.”
sv_name: a human-readable name for debugging and kern.proc.pathname output.
Everything else reuses standard FreeBSD behaviors — signals, core dumps, etc.
The Lookup Chain
copy_file_range — Efficiently copy data between files in the kernel
close_range — Close a range of file descriptors in one call
shm_open2 — Enhanced shared memory with flags
__sysctlbyname — Sysctl lookup by name string
Why does fwdcompat_sysent include all syscalls (0–580), not just the new ones (560–580)?
The COMPAT_MODULE Magic
There’s a subtle problem: how can a module built against FreeBSD 12.4 headers also load cleanly on FreeBSD 12.1? The answer is a clever macro that does version arithmetic at compile time.
The Standard MODULE_DEPEND Problem
/* Normal module dependency */
MODULE_DEPEND(mymodule, kernel,
1201500, /* min: FreeBSD 12.1.500 */
1204000, /* pref: FreeBSD 12.4.000 */
1299000); /* max: FreeBSD 12.99.000 */
This declares: “I need kernel version 12.1.500 to 12.99.000.”
⚠️ Problem: if built on FreeBSD 12.4, the compiler inserts 1204000 as the minimum.
Now the module won’t load on 12.1, 12.2, or 12.3 — even though those kernels are perfectly capable of running it.
The COMPAT_MODULE Solution
#define COMPAT_MODULE(name, data, sub, order) \
MODULE_DEPEND(name, kernel, \
__FreeBSD_version - \
(__FreeBSD_version % 100000), \
__FreeBSD_version, \
MODULE_KERNEL_MAXVER); \
MODULE_METADATA(...); \
SYSINIT(name##module, sub, order, ...);
/* Usage: */
COMPAT_MODULE(fwdcompat64, fwdcompat_mod,
SI_SUB_FWDCOMPAT, SI_ORDER_MIDDLE);
The magic is this arithmetic: 1204000 - (1204000 % 100000)
1204000 % 100000 = 4000 → 1204000 - 4000 = 1200000
The % 100000 operation strips the minor+patch parts, rounding down to the major version base.
✅ A module built on 12.4 sets min=1200000 (FreeBSD 12.0), so it loads on any FreeBSD 12.x!
Module Initialization Order
fwdcompat must initialize at a very specific point in the boot sequence:
The ELF exec subsystem initializes. ELF brands now exist and can be patched.
fwdcompat loads immediately after. It patches the FreeBSD brand and registers its own brand.
/sbin/init launches. If init was built for FreeBSD 15, it’s now handled correctly.
If fwdcompat loaded after /sbin/init attempted to run, and init was a FreeBSD 15 binary, the kernel would reject it before the module had a chance to intercept. The one-step offset (SI_SUB_EXEC + 1) is the minimum safe window.
A fwdcompat module is built on FreeBSD 12.4.500 (osreldate = 1204500). What is the minimum kernel version it will load on?
Putting It All Together
Let’s trace the full execution sequence from kernel boot through a FreeBSD 15 binary running successfully on a FreeBSD 12 kernel.
Full Execution Sequence
Key Concepts Review
- ELF Brand Hijacking
- Patching the default FreeBSD brand’s
trans_osrelfunction to reject future binaries, allowing fwdcompat’s own brand to claim them instead. - Extended Syscall Table
- A complete
sysentarray containing all base kernel syscalls plus new ones from future FreeBSD versions — indexed directly by syscall number. - sysvec Configuration
- The master structure that links everything together: syscall table, signal handling, memory layout, and ABI name.
- COMPAT_MODULE
- A macro using modulo arithmetic to round the minimum version down to the major .0 release, so the module loads on any kernel in that major version family.
- SI_SUB_FWDCOMPAT
- Initialization order one step after SI_SUB_EXEC — ensures fwdcompat is installed before any user binary (including
/sbin/init) attempts to run.
You now understand how FreeBSD Forward Compatibility works at the kernel level. Two function pointer swaps and one extended array — that’s all it takes to let a FreeBSD 12 kernel run programs built for FreeBSD 15.