Module 1

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.

🔁
Forward vs Backward Compatibility

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.
Module 2

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

15
Binary (FB 15)
12
Kernel (FB 12)
Failure
Click Next Step to trace what goes wrong

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

⚠️
Challenge 1: Brand Selection

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.

⚠️
Challenge 2: Missing Syscalls

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?

Module 3

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

🔧 FWDCOMPAT MODULE

🎯 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
↓ used by

⚙️ sysvec

  • sv_size = 580+ (extended)
  • sv_table = fwdcompat_sysent
  • sv_name = "FreeBSD Fwd-compat"
💡
Key Insight

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.

Module 4

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?"

CODE

/* 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);
}
        
PLAIN ENGLISH

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.

CODE

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);
}
        
PLAIN ENGLISH

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

CODE

/* 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
        
PLAIN ENGLISH

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

CODE

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;
}
        
PLAIN ENGLISH

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?

Module 5

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?

CODE

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() */
        
PLAIN ENGLISH

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:

CODE

/* 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 */
};
        
PLAIN ENGLISH

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)

CODE

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 */
};
        
PLAIN ENGLISH

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

BIN
Binary
VEC
sysvec
fwdcompat_sysent
Click Next Step to trace syscall dispatch
🆕
New Syscalls in FreeBSD 13–15

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)?

Module 6

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

CODE

/* 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 */
        
PLAIN ENGLISH

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

CODE

#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);
        
PLAIN ENGLISH

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:

1
SI_SUB_EXEC runs

The ELF exec subsystem initializes. ELF brands now exist and can be patched.

2
SI_SUB_FWDCOMPAT runs (= SI_SUB_EXEC + 1)

fwdcompat loads immediately after. It patches the FreeBSD brand and registers its own brand.

3
SI_SUB_RUN_INIT runs

/sbin/init launches. If init was built for FreeBSD 15, it’s now handled correctly.

⏱️
Why Timing Matters

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?

Module 7

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_osrel function to reject future binaries, allowing fwdcompat’s own brand to claim them instead.
Extended Syscall Table
A complete sysent array 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.
🎓
Congratulations!

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.