Skip to content

Commit 0aa5c91

Browse files
fix: kext: sysfs_iokit: read kext list from gLoadedKextSummaries
The kext failed to start on macOS 26.5: the kernel rejected it with Failed to bind '__ZN6OSKext18copyLoadedKextInfoEP7OSArrayS1_' in 'com.beako.filesystems.sysfs' (at offset 0xc8 in __DATA_CONST, __auth_got) as could not find a kext which exports this symbol The release kernel pruned the kpi.libkern symbol set - nm against kernel.release.t8142 (25F84) shows neither OSKext::copyLoadedKextInfo() nor kmod_get_info() exported. Nothing in the loaded-kext query path survived. The module list is now read from gLoadedKextSummaries, the kernel's exported, write-protected loaded-kext summary table (the one the panic log and lldb consume). It is rebuilt on every kext load/unload under the summary lock; the build of the mirror structs mirrors OSKextLoadedKextSummaryHeader/Summary from OSKextLibPrivate.h. Read side is defensive: the table version and entry_size are validated before walking it, so a layout drift fails loudly instead of traversing garbage, and zero-address entries (empty slots) are skipped. The summary carries no retain count, so refcnt publishes as 0; OSKextVersion is decoded back to major.minor[.revision][d|a|b|f<n>] as the kernel's own version string would render it. The table is not reference-counted and can be freed when it grows, so a concurrent rebuild may race a read; as a best-effort snapshot taken off the VFS lock path this is acceptable, and a version==0 table mid-rebuild is detected and skipped.
1 parent ce3b5c2 commit 0aa5c91

1 file changed

Lines changed: 81 additions & 72 deletions

File tree

kext/sysfs_iokit.cpp

Lines changed: 81 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,45 @@
3737
#include <libkern/c++/OSCollectionIterator.h>
3838

3939
/*
40-
* OSKext::copyLoadedKextInfo() is declared here rather than by including
41-
* <libkern/c++/OSKext.h>, which is unusable from a kext build: it pulls in
42-
* <libkern/OSKextLibPrivate.h>, a private header the SDK does not ship. A
43-
* one-method declaration is enough - a static member call mangles from the
44-
* class name and parameter types alone, so this emits exactly the same symbol
45-
* reference the real header would, without vendoring private headers.
40+
* The release kernel no longer exports OSKext::copyLoadedKextInfo() - the
41+
* kpi.libkern symbol set was pruned (verified against kernel.release.t8142 for
42+
* macOS 26.5.2, build 25F84). The loaded-kext snapshot is instead read from
43+
* the kernel's own loaded-kext summary table, published as the exported global
44+
* gLoadedKextSummaries: a write-protected header followed by one entry per
45+
* loaded kext, rebuilt on every kext load/unload
46+
* (libkern/c++/OSKext.cpp: OSKext::updateLoadedKextSummaries()). It is the same
47+
* table the panic log and lldb consume.
4648
*
47-
* The return type is spelled OSDictionary* because OSPtr<T> is a plain T* in
48-
* kext builds, and the return type takes no part in C++ mangling regardless.
49-
*
50-
* Caller owns a reference to the returned dictionary.
49+
* The structures mirror OSKextLoadedKextSummaryHeader/OSKextLoadedKextSummary
50+
* from <libkern/OSKextLibPrivate.h>. entry_size is validated at read time so a
51+
* layout drift fails loudly instead of walking garbage. name[] is KMOD_MAX_NAME
52+
* (64) bytes as fixed in <mach/kmod.h>.
5153
*/
52-
class OSKext {
53-
public:
54-
static OSDictionary *copyLoadedKextInfo(OSArray *kextIdentifiers,
55-
OSArray *keys);
54+
#define SYSFS_KEKXT_SUMMARY_VERSION 2
55+
56+
struct sysfs_oskext_summary {
57+
char name[64];
58+
uint8_t uuid[16];
59+
uint64_t address;
60+
uint64_t size;
61+
uint64_t version; /* packed OSKextVersion, decoded below */
62+
uint32_t loadTag;
63+
uint32_t flags;
64+
uint64_t reference_list;
65+
uint64_t text_exec_address;
66+
size_t text_exec_size;
67+
};
68+
69+
struct sysfs_oskext_summary_header {
70+
uint32_t version;
71+
uint32_t entry_size;
72+
uint32_t numSummaries;
73+
uint32_t reserved;
74+
struct sysfs_oskext_summary summaries[0];
5675
};
76+
77+
extern struct sysfs_oskext_summary_header *gLoadedKextSummaries;
78+
5779
#include <libkern/libkern.h>
5880

5981
#include <string.h>
@@ -361,46 +383,34 @@ sysfs_count_entries(void)
361383
/*
362384
* Capture the loaded kext list into the snapshot, for /sys/module.
363385
*
364-
* copyLoadedKextInfo() is asked for only the four keys we publish rather than
365-
* the full per-kext dictionary, which keeps the work (and the time the kext
366-
* lock is held) proportional to what we actually use. A kext missing any of
367-
* them is still listed - the value simply reads as zero or empty - because a
368-
* present-but-incomplete module is more useful than a hidden one.
386+
* The list is read from gLoadedKextSummaries, the kernel's exported loaded-kext
387+
* summary table (mirror structs above). It is write-protected and rebuilt on
388+
* every kext load/unload, and it is built for exactly this kind of external
389+
* read (panic log, lldb). Entries with a zero address are empty slots and are
390+
* skipped. The table is not reference-counted, so refcnt is published as 0;
391+
* name, load_tag, load_size and version are populated from the summary.
392+
*
393+
* version is a packed OSKextVersion: major*1e8 + minor*1e6 + revision*1e4 +
394+
* stage*1e3 + stage_level, with stages dev=1, alpha=3, beta=5, candidate=7,
395+
* release=9 (libkern/OSKextVersion.c). It is rendered the way the kernel's own
396+
* OSKextVersionGetString() would: "major.minor[.revision][a|b|f|d<n>]".
369397
*
370398
* Failure here is never fatal: the snapshot is published regardless and
371399
* /sys/module reads as empty, exactly as it does before the first refresh.
372400
*/
373401
static void
374402
sysfs_snap_build_modules(struct sysfs_snapshot *snap)
375403
{
376-
static const char * const wanted[] = {
377-
"OSBundleLoadTag",
378-
"OSBundleLoadSize",
379-
"OSBundleRetainCount",
380-
"CFBundleVersion",
381-
};
382-
383-
OSArray *keys = OSArray::withCapacity(4);
384-
if (keys == nullptr) {
385-
return;
386-
}
387-
for (unsigned i = 0; i < 4; i++) {
388-
const OSSymbol *sym = OSSymbol::withCString(wanted[i]);
389-
if (sym != nullptr) {
390-
keys->setObject(const_cast<OSSymbol *>(sym));
391-
sym->release();
392-
}
393-
}
404+
struct sysfs_oskext_summary_header *hdr = gLoadedKextSummaries;
394405

395-
OSDictionary *info = OSKext::copyLoadedKextInfo(nullptr, keys);
396-
keys->release();
397-
if (info == nullptr) {
406+
if (hdr == nullptr ||
407+
hdr->version != SYSFS_KEKXT_SUMMARY_VERSION ||
408+
hdr->entry_size != sizeof(struct sysfs_oskext_summary)) {
398409
return;
399410
}
400411

401-
uint32_t cap = info->getCount();
412+
uint32_t cap = hdr->numSummaries;
402413
if (cap == 0) {
403-
info->release();
404414
return;
405415
}
406416
if (cap > SYSFS_IOKIT_MAXMODULES) {
@@ -411,47 +421,46 @@ sysfs_snap_build_modules(struct sysfs_snapshot *snap)
411421
struct sysfs_snap_module *arr =
412422
(struct sysfs_snap_module *)sysfs_alloc(bytes);
413423
if (arr == nullptr) {
414-
info->release();
415-
return;
416-
}
417-
418-
OSCollectionIterator *it = OSCollectionIterator::withCollection(info);
419-
if (it == nullptr) {
420-
sysfs_free(arr, bytes);
421-
info->release();
422424
return;
423425
}
424426

425427
uint32_t n = 0;
426-
OSObject *k;
427-
while ((k = it->getNextObject()) != nullptr && n < cap) {
428-
OSString *ident = OSDynamicCast(OSString, k);
429-
if (ident == nullptr) {
430-
continue;
431-
}
432-
OSDictionary *kd = OSDynamicCast(OSDictionary, info->getObject(ident));
433-
if (kd == nullptr) {
428+
for (uint32_t i = 0; i < cap; i++) {
429+
const struct sysfs_oskext_summary *s = &hdr->summaries[i];
430+
431+
if (s->address == 0 && s->text_exec_address == 0) {
434432
continue;
435433
}
436434

437435
struct sysfs_snap_module *m = &arr[n];
438-
strlcpy(m->name, ident->getCStringNoCopy(), sizeof(m->name));
439-
440-
OSNumber *num = OSDynamicCast(OSNumber, kd->getObject("OSBundleLoadTag"));
441-
m->load_tag = (num != nullptr) ? num->unsigned64BitValue() : 0;
442-
num = OSDynamicCast(OSNumber, kd->getObject("OSBundleLoadSize"));
443-
m->load_size = (num != nullptr) ? num->unsigned64BitValue() : 0;
444-
num = OSDynamicCast(OSNumber, kd->getObject("OSBundleRetainCount"));
445-
m->refcnt = (num != nullptr) ? num->unsigned64BitValue() : 0;
446-
447-
OSString *ver = OSDynamicCast(OSString, kd->getObject("CFBundleVersion"));
448-
if (ver != nullptr) {
449-
strlcpy(m->version, ver->getCStringNoCopy(), sizeof(m->version));
436+
strlcpy(m->name, s->name, sizeof(m->name));
437+
m->load_tag = s->loadTag;
438+
m->load_size = s->size;
439+
m->refcnt = 0; /* not present in the summary */
440+
441+
uint64_t v = s->version;
442+
if (v != 0) {
443+
unsigned major = (unsigned)(v / 100000000u);
444+
unsigned minor = (unsigned)((v / 1000000u) % 100u);
445+
unsigned rev = (unsigned)((v / 10000u) % 100u);
446+
unsigned stage = (unsigned)((v / 1000u) % 10u);
447+
if (rev != 0) {
448+
snprintf(m->version, sizeof(m->version),
449+
"%u.%u.%u", major, minor, rev);
450+
} else {
451+
snprintf(m->version, sizeof(m->version),
452+
"%u.%u", major, minor);
453+
}
454+
if (stage != 9) { /* kOSKextVersionStageRelease */
455+
size_t o = strlen(m->version);
456+
snprintf(m->version + o, sizeof(m->version) - o, "%c%u",
457+
stage == 1 ? 'd' : stage == 3 ? 'a' :
458+
stage == 5 ? 'b' : stage == 7 ? 'f' : '?',
459+
(unsigned)(v % 1000u));
460+
}
450461
}
451462
n++;
452463
}
453-
it->release();
454-
info->release();
455464

456465
snap->modules = arr;
457466
snap->module_cap = cap;

0 commit comments

Comments
 (0)