← Back · de-risks .ko→.kext conversion (#179) & kextd (#177)

Prove kexts work — minimal kextload + converter + PR CI

Before committing to the full .ko.kext conversion, the kext_tools/OSKext port, or the mach-into-kernel work, prove the riskiest assumption end-to-end with the cheapest slice: convert one .ko to a .kext, load it with a ported kextload, and confirm the kernel loaded it — in PR CI. Walking skeleton, not the cathedral.

2026-06-03. Approach (a) chosen: minimal trio first to prove the path, with the larger faithful port tracked in tickets so it can’t get lost.

Why it’s cheap The expensive prerequisites already exist in nextbsd: libCoreFoundation ships full CFBundle (CFBundle_InfoPlist.c, CFBundle_Executable.c, CFBundle_Resources.c) — the exact machinery that opens a .kext, parses Info.plist, and locates Contents/MacOS/<binary> — and libIOKit is present. So the proof needs no OSKext: CFBundle + the kld syscalls are enough to load a kext.

Contents

  1. Goal & the bet we’re testing
  2. What already exists
  3. Phase 1 — minimal trio in nextbsd
  4. Phase 2 — converter + PR CI in nextbsd-kernel-modules
  5. The end-to-end proof
  6. Deferred (tracked in tickets)
  7. Tickets
  8. Sequencing
  9. Implementation-ready (deep scope)

1. Goal & the bet we’re testing

The whole Apple-shaped direction (#179 conversion, #177 kextd, #180 loader.conf) rests on one unproven assumption: a FreeBSD .ko wrapped as a .kext bundle can be loaded through an Apple-shaped tool and the kernel accepts it. Nobody has done this on a FreeBSD kernel. Rather than build the cathedral and discover a problem late, prove the load path now with the smallest real artifact, gated by CI so it stays proven.

Success = a PR pipeline that converts a real module, kextloads the bundle in a booted VM, and asserts the kernel shows it loaded (then kextunloads it).

2. What already exists

ComponentIn nextbsdRole in the proof
libCoreFoundation / CFBundleyes (full bundle + Info.plist)open Foo.kext, read CFBundleExecutable, find Contents/MacOS/Foo
libIOKityes (IOKitMatching.c, IOKitLib.c)personalities/matching — later, not needed for the load proof
kld syscallsbase FreeBSDkldload(2)/kldstat(2)/kldunload(2) — the actual load engine
Apple-suite build patternbuild.sh (configd, bootstrap_cmds, libxpc…)template for building/installing a new src/kext_tools
kext toolingnone yetwhat we add in Phase 1

3. Phase 1 — minimal trio in nextbsd

Three thin commands in src/kext_tools/, each a front-end over kld, parsing the bundle with CFBundle. No OSKext, no codesign, no personalities in this phase.

ToolDoes
kextload Foo.kextCFBundleCreate(Foo.kext) → CFBundleCopyExecutableURLkldload(2) that path. Print the assigned file id.
kextstatkldstat(2) / kldnext walk — list loaded files (Apple-shape columns).
kextunload Foo.kextresolve the bundle’s module name → kldunload(2).

Sourcing: vendor a pre-SIP kext_tools (APSL 2.0) and reduce kextload_main.c/kextstat_main.c/kextunload_main.c to the kld-backed path (the OSKext calls become kld calls). Build in the chroot against libCoreFoundation (already built earlier in build.sh); install to the image. Per the §9 of the conversion plan, the older tag avoids the SIP/collection machinery we don’t want anyway.

Scope discipline Phase 1 only proves load. It does not register IOKitPersonalities, resolve dependencies, or do matching — those ride the full port (deferred, ticketed). Wrapping a leaf module (no deps) keeps the proof honest.

4. Phase 2 — converter + PR CI in nextbsd-kernel-modules

  1. Minimal converter: pick a leaf .ko (no MODULE_DEPEND), wrap it as Foo.kext/Contents/{Info.plist,MacOS/Foo} with a generated Info.plist (CFBundleIdentifier, CFBundleExecutable; IOKitPersonalities from MODULE_PNP_INFO optional/decorative here).
  2. PR CI job (mirrors the nextbsd-kernel boot smoke test #6): boot the nextbsd continuous image (now shipping kextload) in a VM, mount in Foo.kext, run kextload /System/Library/Extensions/Foo.kext, assert kextstat/kldstat lists the module, then kextunload. Green = kexts work end-to-end.
  3. PR-only, no publish — consistent with the pipeline rule (PRs never publish; build-only/test-only).

5. The end-to-end proof

nextbsd-kernel-modules (PR)
  └─ build leaf .ko  →  converter wraps  →  Foo.kext
        └─ boot nextbsd `continuous` image (ships kextload)
              └─ kextload Foo.kext   →  kldload inner .ko
              └─ kextstat | grep Foo →  ASSERT loaded
              └─ kextunload Foo.kext →  kldunload
  GREEN ⇒ "a FreeBSD .ko, wrapped as .kext, loads via an Apple-shaped tool." ✔

6. Deferred (tracked in tickets, not lost)

7. Tickets

8. Sequencing

  1. Phase 1 — minimal trio in nextbsd; ships in continuous.
  2. Phase 2 — converter + PR CI in nextbsd-kernel-modules; first green = proof.
  3. Then — full kext_tools/OSKext port (tracked ticket) → bulk conversion (#179) → kextd (#177).
  4. Parallel/independent — mach-into-kernel (#181) & loader.conf (#180) whenever; not gated by the kext proof.

9. Implementation-ready (deep scope)

From a 2-agent code-grounded pass over nextbsd, nextbsd-kernel-modules, and a pre-SIP kext_tools.

9.1 Phase 1 — the OSKext→kld reduction

Base: kext_tools-65.76 (pre-SIP, APSL 2.0). Each tool keeps its CFBundle parsing and swaps the OSKext/Mach call for a kld syscall:

ToolApple callNextBSD
kextload_KXKextManagerLoadKextUsingOptions()CFBundleCreateCFBundleCopyExecutableURLCFURLGetFileSystemRepresentationkldload(path) (full paths bypass kern.module_path)
kextstatkmod_get_info() (Mach RPC)kldnext(0) loop + kldstat(id,&st) over struct kld_file_stat
kextunload_KXKextManagerUnloadKext()name-match loop (kldnext/kldstat) → kldunload(fileid)

Build: new src/kext_tools/; build.sh step after 3p (libCoreFoundation); link -lCoreFoundation -ldispatch -lBlocksRuntime -lsystem_kernel -lpthread (libs in /usr/lib/system); install to /usr/sbin/{kextload,kextstat,kextunload}. Drop codesign + personalities for this phase.

9.2 Phase 2 — converter + CI

Hard ordering Phase 2 CI boots the nextbsd continuous image and calls kextload — so Phase 1’s trio must already be shipping in that image before the Phase 2 job can pass. Phase 1 lands and republishes continuous first; then Phase 2.

Kext proof-of-concept plan, 2026-06-03. De-risks #179 / #177. Grounded in the nextbsd tree (libCoreFoundation/CFBundle, libIOKit, build.sh suite pattern) and Apple kext_tools (APSL 2.0).