KextRW host-side integration study
Upstream: https://github.com/alfiecg24/KextRW (MIT, © 2025 Alfie CG), derived from
Siguza's IOKernelRW. Cloned and reviewed at
misc/ depth on 2026-10-26. This document is a feasibility study; nothing here is
implemented and no patch set changes are proposed.
What KextRW actually is
- Kext (
src/, arm64e, built withclang -mkernel -Wl,-kextfrom a plain Makefile): anIOServiceonIOResourcesplus a user client with 10 scalar selectors —kread/kwrite(thincopyout/copyin),physread/physwrite(IOMemoryDescriptorphysical mapping with alignment modes),getResetVector(vbar_el1),kvtophys(anat s1e1rsequence inroutines.S),phystokv(unimplemented),callKernelFunction(a 10-argument variadicarbitrary_callstub inroutines.S),kallocBuffer/kfreeBuffer(com.apple.kpi.unsupportedkalloc/kfree). - Client gate: the user client checks the client process entitlement
com.apple.security.alfie.kext-rwviacopyClientEntitlementininitWithTask. Nothing else authenticates the caller. - Userland library (
lib/libkextrw.c): thinIOConnectCallScalarMethodwrappers plusget_kernel_base()— walk down fromvbar_el1page-aligned until the Mach-O header is found, then compute the slide against a hardcodedSTATIC_KERNEL_BASE (0xFFFFFE0007004000). - Tested by upstream only on macOS 15.2 (
OSBundleLibraries24.2.0). Loading follows the IOKernelRW instructions: kext policy relaxed in Recovery (csrutil enable --without kextor SIP off) plus user approval.
Why the guest side is not an integration target
The CFW kernel patch set already ships a default-enabled primitive with the
same power: patch_kcall10 (JB-05, sysent[439] cave, target + 7 args →
x0), composed with the task-for-pid / port-to-map / TPRO / vm patches.
Kernel read/write composes from kcall (copyout/copyin/kalloc), so a
guest KextRW adds API shape, not capability. iOS also does not load kexts at
runtime; embedding one into the kernelcache would mean building kext-injection
machinery (kmod info, OSMetaClass registration, OSBundleLibraries) that the
patcher philosophy — modify existing code, locate dynamically, no new code
segments — exists to avoid. Rejected.
Host side: the actual gap
Current admission paths for vphone-vm's restricted entitlements
(com.apple.private.virtualization.*, com.apple.vm.networking,
com.apple.private.bmk.allow):
So a KextRW fork (call it Path C) targets exactly Path B's two pains: cdhash
churn and amfid-layout fragility. It does not reduce the Recovery burden —
approving a third-party kext on Apple Silicon is more interaction than
csrutil enable --without debug.
Path C sketch
- Fork hygiene (the fork is ours to carry):
- Rename bundle id / IOService / user client class (
com.Alfie.KextRW→ e.g.dev.vphone.kextrw). - Rename the client entitlement off
com.apple.security.*. The upstream key lives in Apple's entitlement namespace; if that prefix is treated as restricted, an ad hoc client cannot carry it without solving the very AMFI problem the kext is meant to replace (circularity). Adev.vphone.*key keepscopyClientEntitlementtrivially satisfiable. - Trim the selector table to what the admission flow needs (kcall alone suffices; kread/kwrite compose from it). Every removed selector removes resident-kernel attack surface.
- Replace
get_kernel_base's hardcodedSTATIC_KERNEL_BASEslide with a derivation from the on-disk kernelcache (the repo forbids hardcoded addresses in patch logic; the same discipline should apply to host tooling). Its unbounded downward scan also needs a lower bound.
- Rename bundle id / IOService / user client class (
- Kernel-side targets (each needs a written reveal procedure before any
implementation, mirroring the guest patch workflow):
- Target 1, narrow: flip
csr_active_configbits at runtime (CSR_ALLOW_TASK_FOR_PID, CSR_ALLOW_RESEARCH_GUESTS) so the twocsrutilRecovery commands collapse into the kext-approval trip. Must be validated per macOS release — some checks may read boot policy rather than the global. - Target 2, blunt: flip the AMFI "get out of my way" state at runtime — equivalent posture to Path A minus the nvram/reboot, still cdhash-free.
- Symbols come from the on-disk kernelcache (
kmutil inspectprints the path; release kernelcaches keep symbol tables). KextRW itself provides kernel base/slide so no file offsets are needed at runtime.
- Target 1, narrow: flip
- Distribution and UX:
- The kext cannot ship inside
VPhone.bundle(bundle-admission tier rules; it must live in/Library/Extensions). Build it likeVPhoneGuestComponents(Makefile, signed ad hoc, pinned by digest) or a small sibling project. - The Launchpad helper is already the only root surface and already runs
receipt-pinned tools; a new verb would verify the kext digest and run
kmutil install, then guide the user through the one-time approval. Approved kexts load at every boot, so admission applies automatically per boot — no per-build or per-boot user action. vphone-cli host preflightgrows a Path C probe: kext service present → self-test call → report.
- The kext cannot ship inside
Risks, honestly
- A resident arbitrary-kernel-RW kext is a kernel backdoor keyed only by an entitlement string. Trimming selectors and renaming the key is mitigation, not elimination. This is strictly worse standing risk than Path B's one amfid byte, and roughly Path A's risk class with SIP filesystem protection retained.
- Upstream is validated on macOS 15.2 only. macOS 26/27 hosts (including
ARM64E.X1machines) need load validation; kexts remain arm64e, but PAC diversity andOSBundleLibrariesversion bumps need checking per release. - Per-macOS-build symbol reveal becomes a standing maintenance item, the same class of work as guest patch retargeting.
- The runtime-writability/effect of the CSR config global is unverified; the narrow target may not survive future releases.
Recommendation
Do not integrate KextRW for the default flow. Path B's pains are better
addressed in place: cdhash churn is a signing/release-engineering problem, and
amfid-layout fragility is contained inside VPhoneEscalator (the M6 fix shows
the escalation path works). Keep Path C as an opt-in research-workstation
configuration for users who already run kernel extensions — validated first on
macOS 15 and 26 with both kernel targets' reveal procedures written here —
never as a requirement, and never wired into Launchpad's default preflight
gate.