OcAppleKernelLib: System KC loading and cross-KC dependency resolution#600
OcAppleKernelLib: System KC loading and cross-KC dependency resolution#600MattJackson wants to merge 6 commits into
Conversation
|
Thank you for your patch. It will take us some time to review, but could you first explain whether we absolutely need to copy the .kc file? The problem with this architecture is .kc desync, which happens:
I understand the problem with kext injection exists, but it feels like we ideally want a better approach than copying. |
|
Thanks for the early read. The copy-to-ESP design is driven by a constraint rather than a preference: the sealed APFS System volume is not readable from EFI. Only Apple's Your desync concerns are all real:
If you would prefer a design that avoids ESP staging entirely, the two directions I can see are: (a) an APFS-snapshot-aware reader on top of the existing Apple partition driver that can open the sealed System volume read-only at EFI time; (b) deferring cross-KC resolution until after |
|
Hmmm. Thank you for your analysis. I think option (a) can be considered a fallback if we can implement it cleanly, but I have doubts we can. It requires verifying image seal, and I am afraid EFI driver is not ready for this. I also think option (b) is not feasible as I believe EfiBoot does not load system kc at all. I may be wrong, but I have always thought it is part of XNU OSKext APIs, namely loadKCFileSet, which are not open-source. Are you sure EfiBoot does it? I honestly wonder whether it makes sense to try option (c) revive https://github.com/acidanthera/Lilu/pull/86/changes and work in mixed model: OC prepares, Lilu injects later stage kexts. |
|
Thanks so much for digging into this — the analysis on all three (a) — you're right that verifying the APFS image seal from an EFI (b) — you're correct, and I double-checked: (c) — thanks for pointing at Lilu/pull/86, that's a useful On this PR — I don't think it has to be all-or-nothing. Going back through the series with the System-KC-loading parts
So three concrete paths, in increasing ambition:
My instinct is B, but I'd genuinely rather defer to your read on Thanks again for the careful review. |
|
Right. Lilu solution is rather heavy, and we are not yet in the state we can pursue it. On the other side given macOS 26 is the last one supporting Intel, I believe there is room for compromise. I personally think we can definitely implement B + C. This will be helpful for other use cases of kernel injection. As for system KC loading — I am yet to decide whether we want it. Could you please tell me more about your use case? I wonder whether System KC loading in your scenario is practically the only choice? |
|
Thanks again for taking the time on this — really glad to hear B + C sound useful on their own merits. Happy to spin those off as a standalone PR on whatever cadence works for you; no rush on our end. On the use case for the System KC loading half: the work originated in a graphics context. We were injecting a couple of small kexts that hook into the IOKit graphics stack — concretely, their It's not the only conceivable choice — a Lilu-side late-stage injection would reach the same end state, and you raised that alternative earlier in the thread re: Lilu PR #86. For our particular needs the OpenCore-prelink approach was the cleanest fit, since we're not the right folks to drive the Lilu side and we wanted the resulting boot to look as close to a stock OpenCore boot as possible. That said: we're heads-down on a few other things right now and this isn't blocking us in any way. We mainly wanted to get the code into your tree as a contribution if it's useful to anyone else hitting the same prelink wall on graphics-stack kexts, and we're entirely happy to wait, iterate on whatever shape you'd prefer, or just close the System-KC half and go forward with B (or B + C) if you'd rather not carry the cross-KC machinery. Whichever way you'd like to take it is fine by us — please don't feel any pressure to merge on our account. Thanks again for the review. |
|
Per your endorsement of B + C: opened standalone PRs as #602 (B path: ~25 LOC, prelink dependency-scan + RIP-relative-overflow
The originally proposed missing-include item turned out to be a non-issue on master ( The arm64e stride bug from the original PR #600 commit B is fixed in the C-PR helpers: This PR's SystemKC machinery is deferred per your "yet to decide" comment — happy to leave it open for now and close it once #602 and #603 land, or to close it sooner if you'd prefer the cleaner queue. Equally happy to rebase or squash either side at your discretion. |
…nel driver This is the "C" path queued behind the "B" cleanup PR per the PR acidanthera#600 discussion. It introduces two static-lib helpers for walking LC_DYLD_CHAINED_FIXUPS chains in a kernel collection, with correct stride handling for both supported pointer formats, and exercises them from a TestProcessKernel subcommand so the helpers ship with an in-tree user. Helpers (KernelCollection.c, OcAppleKernelLib.h): * KcWalkChainedFixupsInSegment - given a containing buffer and a MACH_DYLD_CHAINED_STARTS_IN_SEGMENT, walk the chain on every populated page and invoke a caller-provided KC_CHAINED_FIXUP_VISIT callback per fixup slot. The visitor may rewrite the slot in place (translation) or just observe (counting/validation). * KcWalkChainedFixupsInImage - walks every segment in a MACH_DYLD_CHAINED_STARTS_IN_IMAGE by delegating to the per-segment helper. Returns the total slot count. Pointer-format support: X86_64_KERNEL_CACHE (11): stride 1 byte (Fixup->Next * 1) 64_KERNEL_CACHE (8): stride 4 bytes (Fixup->Next * 4) 64_OFFSET (6): stride 4 bytes (also accepted) ARM64E_KERNEL (7): stride 4 bytes (also accepted) Per Apple's mach-o/fixup-chains.h, Fixup->Next is a stride count, not a byte offset. The 64_KERNEL_CACHE variant is stride 4 (arm64e kernel caches), not 1; advancing by Fixup->Next bytes mis-walks every chain on those caches. The walker computes the correct stride per format. Other formats (ARM64E with auth, 32-bit variants, userland 64) and START_MULTI pages are not produced by current macOS kernel caches and return 0 / are skipped defensively rather than mis-walking. TestProcessKernel driver: * New --test-fixup-walk subcommand. Builds a synthetic single-page STARTS_IN_SEGMENT with a 4-link chain and walks it three times: - As X86_64_KERNEL_CACHE (Next=16, stride 1) - expect 4 visits. - As 64_KERNEL_CACHE (Next=4, stride 4) - expect 4 visits. - As ARM64E (unsupported) - expect 0 visits. Both stride layouts encode the same byte distance between slots, so the walker must produce identical visit counts and fixup-slot addresses for both. The test ASSERTs all three counts. Verified on x86_64+arm64e darwin: all three subtests pass. Tested: - TestKextInject and TestProcessKernel both build clean with WERROR=1 USE_SHARED_OBJS=1. - ./ProcessKernel --test-fixup-walk reports [OK] X86_64_KERNEL_CACHE walk visited 4 fixups (stride 1) [OK] 64_KERNEL_CACHE walk visited 4 fixups (stride 4) [OK] unsupported-format guard returned 0 Depends on the diagnostic + symbol-only fallback PR (B path). Signed-off-by: Matthew Jackson <matthew@pq.io>
…allback This is the "B" cleanup path from the discussion in #600. It groups two trivial DEBUG_INFO additions and one independent latent-bug fix into a single drop with no SystemKC machinery. * PrelinkedKext.c: in InternalInsertPrelinkedKextDependency (), log the dependency-scan failure path that currently returns a status code without naming the dependent or dependency. This turned up while reviewing prelink failures and is helpful in any kext-injection scenario, not just ours. * PrelinkedKext.c: in InternalCachedPrelinkedKext (), when running on a kernel collection (macOS 11+) and the requested bundle identifier is not in the Boot KC plist, materialise a minimal kernel-stub PRELINKED_KEXT aliasing PRELINKED_CONTEXT::InnerMachContext instead of returning NULL. Returning NULL silently breaks symbol-only dependents (weak references, build-time links): the caller skips the dependency and InternalSolveSymbol () later fails with "library kext ... not found". The stub carries no vtable source so it cannot satisfy subclassing - dependents that need real vtables still fail loudly during linking, exactly as before. Pre-Big Sur boots take the unchanged code path because IsKernelCollection is FALSE. * Link.c: in InternalCalculateDisplacementIntel64 (), downgrade the silent X86_64_RIP_RELATIVE_LIMIT no-op to a DEBUG_INFO trace that carries the kext identifier, source link PC, target VA and the computed difference. Currently the helper just returns FALSE; the caller propagates that to MAX_UINTN and the link fails without any log line identifying which kext or which target overflowed. The new log makes RIP-relative overflows debuggable from a single OCAK: trace without raising the message level. Tested: - TestKextInject and TestProcessKernel both build clean with WERROR=1 USE_SHARED_OBJS=1 against this tree (acidanthera/master, EDK2 stable202502, Xcode 16.4 / clang 17). Signed-off-by: Matthew Jackson <matthew@pq.io>
Macs on macOS 11 and newer split the kernel extension inventory into
two immutable kernel collections: the Boot KC that OpenCore injects
into, and the System KC that ships on the system volume. Kext libraries
that used to live in the prelinkedkernel - for example IOGraphicsFamily,
AppleGraphicsDeviceControl, AGPM, IOUSBFamily - have moved to the
System KC on modern releases, which means a Boot KC kext that declares
one of their classes in OSBundleLibraries cannot be linked at prelink
time today (the symbol table and vtables are not reachable).
This commit grows PRELINKED_CONTEXT with optional System KC state:
- SystemKC / SystemKCSize : raw buffer and length
- SystemKCMachContext : Mach-O context for the outer KC
- SystemKCInnerMachContext: context for the __TEXT_EXEC embedded
kernel (Boot KC-style) or an alias of
the outer (System KC-style)
- SystemKCValid : set once parsing and fixup completed
A new PrelinkedContextLoadSystemKC () function wires the buffer up,
probes for the __TEXT_EXEC inner kernel, connects the shared symtab
via MachoInitialiseSymtabsExternal (), and applies LC_DYLD_CHAINED_FIXUPS
on the outer collection so later consumers observe resolved virtual
addresses instead of encoded fixup slots. Only the x86_64 and arm64e
kernel-cache pointer formats are handled; other formats are skipped.
PrelinkedContextFree () now releases the System KC buffer (and clears
SystemKCValid) so callers do not have to special-case it.
No caller consumes the new state yet; wiring into kext dependency
resolution and into OcKernelProcessPrelinked () comes in follow-up
patches so each step is individually bisectable.
Signed-off-by: Matthew Jackson <matthew@pq.io>
With the previous patch a PRELINKED_CONTEXT can hold an optional System KC. This commit teaches the kext dependency resolver to look into that collection when a bundle identifier is not present in the Boot KC. InternalCachedPrelinkedKext () (the one call site for dependency lookup used by InternalInsertPrelinkedKextDependency ()) gains a new branch that runs only for kernel-collection boots: if the caller has loaded a System KC, walk its LC_FILESET_ENTRY load commands looking for the requested identifier. On a hit we materialise a PRELINKED_KEXT aliasing the System KC buffer - the Mach-O context is initialised with the fileset entry's FileOffset as HeaderOffset, exactly like Boot KC dependents - and wire the kext's symtab up to the System KC's inner context via MachoInitialiseSymtabsExternal (). Each fileset entry ships its own LC_DYLD_CHAINED_FIXUPS covering the kext's data segments; those are applied in place by a new static helper InternalApplyFilesetKextFixups () so later vtable patching sees resolved pointers instead of fixup slots. If the identifier is not in the System KC either, we fall back to the pre-existing kernel-stub path (CopyMem from InnerMachContext) which keeps compatibility for dependents that only look for weak test symbols and can live without a real vtable source. No change for non-KC boots: IsKernelCollection is FALSE and the new branch is skipped entirely. Signed-off-by: Matthew Jackson <matthew@pq.io>
System KC kexts carry symbol values that reference fileset virtual
addresses (typically in the 1 MB .. 512 MB range on x86_64, e.g.
0x149xxxxx). When the Boot KC linker resolves a symbol against a
System KC dependency it receives this raw value; feeding it into
InternalSolveSymbolValue () unchanged produces RIP-relative relocations
whose 32-bit displacements cannot reach the final kernel address
space, so InternalCalculateDisplacementIntel64 () trips the
X86_64_RIP_RELATIVE_LIMIT check and the link fails.
The adjustment is the same one that KcFixupValue () performs for vtable
entries:
address_in_kernel_space = raw + KERNEL_FIXUP_OFFSET + KERNEL_ADDRESS_BASE
On macOS 11+ all kernel collections share a single KASLR slide, so the
translation is a constant. Kernel-space values, zero, and the weak
test placeholder must not be translated, which is why the guard only
applies to values clearly in the fileset-VA band (1 MB .. 512 MB).
Also downgrade the RIP-relative displacement overflow log from an
unprinted no-op to a DEBUG_INFO trace with the target and difference,
which makes diagnosing related link failures practical without
introducing WARN-level noise for normal operation.
Signed-off-by: Matthew Jackson <matthew@pq.io>
Wire the new PrelinkedContextLoadSystemKC () API into the existing file-open hook. When OcKernelFileOpen () handles an apfs/kernel-cache request and a System KC has not yet been staged for this boot, attempt to read EFI/OC/SystemKernelExtensions.kc from the OpenCore ESP via mOcStorage. If present, remember the buffer and its size in three new file-scope statics; if absent, continue silently (Boot-KC-only injection is the long-standing default and must keep working). When OcKernelProcessPrelinked () subsequently creates a prelinked context, it now calls PrelinkedContextLoadSystemKC () to hand the staged buffer off. Ownership transfers to the context, which frees the buffer through PrelinkedContextFree (); the statics are cleared immediately so we neither double-free nor re-parse on the next pass. The System KC has to be staged on the ESP because APFS sealed-volume data (where macOS stores its own copy) is not readable from EFI. Administrators are expected to copy the file out of /System/Library/KernelCollections/SystemKernelExtensions.kc during OpenCore installation; doing so is version-locked to the installed macOS build. No behavioural change for boots where the file is absent: mSystemKCLoaded stays FALSE, the new branch in OcKernelProcessPrelinked () is skipped, and the existing Boot-KC-only injection path runs unchanged. Signed-off-by: Matthew Jackson <matthew@pq.io>
Signed-off-by: Matthew Jackson <matthew@pq.io>
Align multi-line parenthesized expression continuations per acidanthera's uncrustify.cfg (column-align to opening paren, not block-indent). No functional change. Signed-off-by: Matthew Jackson <matthew@pq.io>
657329a to
c36847e
Compare
What this PR does
Adds optional System Kernel Collection (System KC) loading to
OcAppleKernelLiband wires it throughOcMainLib, so OpenCore canresolve Boot-KC kext
OSBundleLibrariesdependencies against classesthat live in the System KC (for example
IOGraphicsFamily,IOUSBFamily,AGPM). When no System KC is staged on the OpenCoreESP, behaviour is identical to today's injection flow.
Why
Since macOS 11 the System KC hosts a large fraction of kext libraries
that used to be in the prelinkedkernel. OpenCore only injects into the
Boot KC, so any injected kext whose
OSBundleLibrariesnames aSystem-KC class fails at prelink with
library kext ... not found.Teaching OpenCore to optionally consult the System KC closes that gap
for any project whose kexts statically link against System-KC classes.
The feature is strictly opt-in: presence of
EFI/OC/SystemKernelExtensions.kcon the ESP activates it, and thefile is version-locked to the installed macOS build.
Test plan
Reviewer can reproduce with the following minimal flow. Pointers
indicate what output to expect in the OpenCore DEBUG log
(
DisplayLevel=0x80000042, i.e.DEBUG_INFO | DEBUG_WARN).Unit-level (no macOS required).
cd OpenCorePkg git am 0001-OcAppleKernelLib-Add-System-KC-context-loading.patch git am 0002-OcAppleKernelLib-Resolve-cross-KC-dependencies.patch git am 0003-OcAppleKernelLib-Translate-System-KC-symbols-at-link.patch git am 0004-OcMainLib-Stage-System-KC-for-prelinked-injection.patch git am 0005-Changelog-Note-System-KC-loading-support.patch make -C Utilities/TestProcessKernel Utilities/TestProcessKernel/ProcessKernel /path/to/prelinkedkernelExpect: exit 0,
out.binbyte-identical to a run without thepatches when no System KC is present. No new warnings.
Integration-level (requires a macOS 11+ guest or host).
Expect these log lines (order matters, format verbatim):
The
OC: Prelinked status - Successline that usually terminatesOcKernelFileOpen's cache work must remain. macOS boots normally.
Regression test (no System KC staged).
Remove
EFI/OC/SystemKernelExtensions.kc, rebuild image or simplydelete the file, reboot. Expect the DEBUG log to contain NONE of
the
OC: System KC ...orOCAK: System KC ...lines andmacOS to still boot.
Negative test (corrupt file).
Stage a file named
SystemKernelExtensions.kcthat is not aMach-O (
echo not-a-macho > SystemKernelExtensions.kc). Expect:No panic, no crash; Boot KC injection continues normally.
Review notes
A few specific spots that might benefit from review eyes:
PrelinkedContext.cInternalApplyKernelCollectionFixups ():only the x86_64 and arm64e kernel-cache pointer formats are
handled. Formats we have not observed in shipping kernel
collections are deliberately skipped with
continue- pleaseconfirm the list is complete enough for your intended targets.
PrelinkedKext.cInternalFindSystemKCDependency (): the fallbackat the end of
InternalCachedPrelinkedKext ()still materialises akernel-stub
PRELINKED_KEXTwhen a bundle identifier is inneither collection. This preserves today's behaviour for non-KC
dependencies (weak test symbols, etc.); open to changing that if
you'd prefer the fallback path to disappear when
IsKernelCollectionis true.Link.c: the translation inInternalSolveSymbolNonWeak ()usesthe same constant offsets as
KcFixupValue (). If a futuremacOS changes the shared-KC KASLR convention, both sites would need
updating in sync; consider whether they should share a helper.
OpenCoreKernel.creads the System KC throughmOcStorage, whichanchors to the OpenCore ESP root. If a reviewer prefers a config
knob (e.g.
Kernel/Scheme/SystemKernelCollectionPath) I'm happyto add one; the current hard-coded filename felt like the minimal
surface for a first cut.
Log snippet
With the patches applied and
EFI/OC/SystemKernelExtensions.kcstaged from macOS 15.7.5:Checklist
XCODE5 DEBUG(Xcode 16.2, EDK2stable202502).
Utilities/TestProcessKernelruns unchanged in the no-System-KCcase.
Apple-silicon hardware. (Pointer-format path compiles; no
Apple-silicon hardware on hand for a real boot test.)
Follow-up after #606
This PR contains its own inline chained-fixup walker code in
InternalApplyFilesetKextFixups(PrelinkedKext.c) andInternalKcTranslateSystemKCFixups(PrelinkedContext.c). Both walkerspredate the public
KcWalkChainedFixupsInImageAPI introduced by #603and refined by #606 to be alignment-safe.
After #606 merges, I will follow up with a refactor PR that:
KcWalkChainedFixupsInImageand a per-call visitor callback.CONST UINT8 *metadata,UINT8 *FixupLocvisitor parameter) so the same UB classes@vit9696 flagged on OcAppleKernelLib: chained-fixup pointer-format helpers + TestProcessKernel driver #603 (cast-and-deref of
MACH_DYLD_CHAINED_STARTS_IN_SEGMENT *on a potentially-unaligned base,
UINT64 *writes through unalignedfixup-slot pointers) are resolved here too.
Sequencing #606 first keeps that PR's review focused on the API +
walker alignment fix, and lets #600 land on its own merits without
blocking on the consumer-side refactor.