Skip to content

Commit ce3b5c2

Browse files
feat: add /sys/module
One directory per loaded kernel extension, carrying the attribute files Linux exposes: refcnt, coresize, version and initstate. Structurally this is /sys/devices again: a single shared SFSmodule node backs every module directory, with the kext's OSBundleLoadTag standing in nodeid_regid - unique among loaded kexts and stable while one stays loaded, the role IORegistryEntryID plays for a device - and nodeid_objectid selecting either the directory itself or one attribute. The kext list is captured on the refresh thread with the registry snapshot, never in a vnop: OSKext::copyLoadedKextInfo() takes the kext lock and builds a dictionary describing every loaded kext, far too heavy and too lock-entangled to run while holding VFS locks. It is asked for only the four keys published rather than the full per-kext dictionary, to keep the work - and the time the kext lock is held - proportional to what is actually used. initstate always reads "live": a kext in the loaded-kext list has finished starting, and Linux's transient coming/going states have no observable equivalent.
1 parent add414f commit ce3b5c2

6 files changed

Lines changed: 604 additions & 9 deletions

File tree

include/fs/sysfs/sysfs.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@ typedef int (*sysfs_read_data_fn)(sfsnode_t *snp, uio_t uio, vfs_context_t ctx);
177177
*/
178178
#define SSN_FLAG_WHOLE_ONLY (1 << 1)
179179

180+
/*
181+
* The directory expands to the list of loaded kernel extensions (/sys/module).
182+
* Like SSN_FLAG_DYNAMIC this marks the directory itself; its single SFSmodule
183+
* child is the shared node every module is presented through, distinguished
184+
* per-vnode by the module's load tag.
185+
*/
186+
#define SSN_FLAG_MODULES (1 << 2)
187+
180188
/*
181189
* Special values for the nodeid_regid and nodeid_objectid fields.
182190
*/

include/fs/sysfs/sysfs_iokit.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,41 @@ int sysfs_iokit_class_child_named(uint32_t class_id, uint32_t match_flags,
125125
*/
126126
size_t sysfs_iokit_path(uint64_t regid, char *buf, size_t buflen);
127127

128+
/*
129+
* Loaded kernel extensions, for /sys/module.
130+
*
131+
* The kext list is captured with the registry snapshot on the refresh thread,
132+
* never in a vnop: OSKext::copyLoadedKextInfo() takes the kext lock and builds
133+
* a dictionary describing every loaded kext, far too heavy - and too
134+
* lock-entangled - to do while holding VFS locks. These accessors only read the
135+
* published snapshot.
136+
*
137+
* load_tag is the kext's OSBundleLoadTag: unique among loaded kexts and stable
138+
* while the kext stays loaded, so it keys a /sys/module node the way
139+
* IORegistryEntryID keys a /sys/devices node.
140+
*/
141+
struct sysfs_module_info {
142+
uint64_t load_tag;
143+
uint64_t load_size; /* OSBundleLoadSize, bytes */
144+
uint64_t refcnt; /* OSBundleRetainCount */
145+
char name[128]; /* CFBundleIdentifier */
146+
char version[32]; /* CFBundleVersion */
147+
};
148+
149+
/*
150+
* Fill *out with the index-th loaded kext; returns 1 while modules remain, 0
151+
* past the end. Enumeration order is whatever the kext list yields and is
152+
* stable for the life of one snapshot.
153+
*/
154+
int sysfs_iokit_module_at(unsigned int index, struct sysfs_module_info *out);
155+
156+
/* Resolve a module by bundle identifier. Returns 1 if found. */
157+
int sysfs_iokit_module_named(const char *name, size_t namelen,
158+
struct sysfs_module_info *out);
159+
160+
/* Resolve a module by its load tag (the per-node key). Returns 1 if found. */
161+
int sysfs_iokit_module_by_tag(uint64_t load_tag, struct sysfs_module_info *out);
162+
128163
#ifdef __cplusplus
129164
}
130165
#endif

kext/sysfs_iokit.cpp

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,30 @@
3030
#include <libkern/c++/OSIterator.h>
3131
#include <libkern/c++/OSBoolean.h>
3232
#include <libkern/c++/OSString.h>
33+
#include <libkern/c++/OSSymbol.h>
34+
#include <libkern/c++/OSNumber.h>
35+
#include <libkern/c++/OSArray.h>
36+
#include <libkern/c++/OSDictionary.h>
37+
#include <libkern/c++/OSCollectionIterator.h>
38+
39+
/*
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.
46+
*
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.
51+
*/
52+
class OSKext {
53+
public:
54+
static OSDictionary *copyLoadedKextInfo(OSArray *kextIdentifiers,
55+
OSArray *keys);
56+
};
3357
#include <libkern/libkern.h>
3458

3559
#include <string.h>
@@ -47,6 +71,12 @@ extern "C" {
4771
#define SYSFS_IOKIT_BSDNAMEMAX 32
4872
/* Sanity cap on service-plane entries in one snapshot. */
4973
#define SYSFS_IOKIT_MAXNODES 16384u
74+
75+
/*
76+
* Upper bound on loaded kexts recorded for /sys/module. A running system has a
77+
* few hundred; the cap only stops a pathological case from unbounded growth.
78+
*/
79+
#define SYSFS_IOKIT_MAXMODULES 1024u
5080
/* How long a snapshot is reused before a rebuild (ns). */
5181
#define SYSFS_SNAP_TTL_NS 2000000000ULL /* 2 seconds */
5282

@@ -105,7 +135,30 @@ struct sysfs_hash_slot {
105135
};
106136

107137
/* Container holding a full self-contained IOKit registry snapshot */
138+
/*
139+
* One loaded kernel extension, as presented under /sys/module/<name>.
140+
*
141+
* Captured on the refresh thread with the rest of the snapshot, never in a
142+
* vnop: OSKext::copyLoadedKextInfo() takes the kext lock and builds a
143+
* dictionary describing every loaded kext, which is far too heavy - and too
144+
* lock-entangled - to do while holding VFS locks.
145+
*
146+
* load_tag is the kext's OSBundleLoadTag: unique among loaded kexts and stable
147+
* for as long as the kext stays loaded, which makes it the natural per-node key
148+
* (the same role IORegistryEntryID plays for /sys/devices).
149+
*/
150+
struct sysfs_snap_module {
151+
uint64_t load_tag;
152+
uint64_t load_size; /* OSBundleLoadSize, bytes */
153+
uint64_t refcnt; /* OSBundleRetainCount */
154+
char name[128]; /* CFBundleIdentifier */
155+
char version[32]; /* CFBundleVersion */
156+
};
157+
108158
struct sysfs_snapshot {
159+
struct sysfs_snap_module *modules;
160+
uint32_t module_count;
161+
uint32_t module_cap;
109162
struct sysfs_snap_node *nodes;
110163
uint32_t *child_idx;
111164
struct sysfs_hash_slot *hash;
@@ -217,6 +270,10 @@ sysfs_snap_free(struct sysfs_snapshot *snap)
217270
if (snap == nullptr) {
218271
return;
219272
}
273+
if (snap->modules != nullptr) {
274+
sysfs_free(snap->modules,
275+
(size_t)snap->module_cap * sizeof(*snap->modules));
276+
}
220277
if (snap->nodes != nullptr) {
221278
sysfs_free(snap->nodes, (size_t)snap->node_cap * sizeof(*snap->nodes));
222279
}
@@ -300,6 +357,109 @@ sysfs_count_entries(void)
300357
}
301358

302359

360+
361+
/*
362+
* Capture the loaded kext list into the snapshot, for /sys/module.
363+
*
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.
369+
*
370+
* Failure here is never fatal: the snapshot is published regardless and
371+
* /sys/module reads as empty, exactly as it does before the first refresh.
372+
*/
373+
static void
374+
sysfs_snap_build_modules(struct sysfs_snapshot *snap)
375+
{
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+
}
394+
395+
OSDictionary *info = OSKext::copyLoadedKextInfo(nullptr, keys);
396+
keys->release();
397+
if (info == nullptr) {
398+
return;
399+
}
400+
401+
uint32_t cap = info->getCount();
402+
if (cap == 0) {
403+
info->release();
404+
return;
405+
}
406+
if (cap > SYSFS_IOKIT_MAXMODULES) {
407+
cap = SYSFS_IOKIT_MAXMODULES;
408+
}
409+
410+
size_t bytes = (size_t)cap * sizeof(struct sysfs_snap_module);
411+
struct sysfs_snap_module *arr =
412+
(struct sysfs_snap_module *)sysfs_alloc(bytes);
413+
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();
422+
return;
423+
}
424+
425+
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) {
434+
continue;
435+
}
436+
437+
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));
450+
}
451+
n++;
452+
}
453+
it->release();
454+
info->release();
455+
456+
snap->modules = arr;
457+
snap->module_cap = cap;
458+
snap->module_count = n;
459+
snap->bytes += bytes;
460+
OSAddAtomic64((int64_t)bytes, &sysfs_stat_snap_bytes);
461+
}
462+
303463
/* Builds a fresh snapshot completely in isolation without holding global locks. */
304464
static struct sysfs_snapshot *
305465
sysfs_snap_build(void)
@@ -534,6 +694,8 @@ sysfs_snap_build(void)
534694
}
535695
}
536696

697+
sysfs_snap_build_modules(snap);
698+
537699
clock_get_uptime(&snap->uptime);
538700
return snap;
539701
}
@@ -984,3 +1146,83 @@ sysfs_iokit_name(uint64_t regid, char *buf, size_t buflen)
9841146
IOLockUnlock(g_lock);
9851147
return n;
9861148
}
1149+
1150+
#pragma mark - loaded kernel extensions (/sys/module)
1151+
1152+
/*
1153+
* Copy one snapshot module record out to the caller. Must hold g_lock.
1154+
*/
1155+
static void
1156+
sysfs_module_copy_out(const struct sysfs_snap_module *m,
1157+
struct sysfs_module_info *out)
1158+
{
1159+
out->load_tag = m->load_tag;
1160+
out->load_size = m->load_size;
1161+
out->refcnt = m->refcnt;
1162+
strlcpy(out->name, m->name, sizeof(out->name));
1163+
strlcpy(out->version, m->version, sizeof(out->version));
1164+
}
1165+
1166+
extern "C" int
1167+
sysfs_iokit_module_at(unsigned int index, struct sysfs_module_info *out)
1168+
{
1169+
if (out == nullptr || g_lock == nullptr) {
1170+
return 0;
1171+
}
1172+
sysfs_snap_ensure();
1173+
IOLockLock(g_lock);
1174+
int ok = 0;
1175+
if (g_snapshot != nullptr && g_snapshot->modules != nullptr &&
1176+
index < g_snapshot->module_count) {
1177+
sysfs_module_copy_out(&g_snapshot->modules[index], out);
1178+
ok = 1;
1179+
}
1180+
IOLockUnlock(g_lock);
1181+
return ok;
1182+
}
1183+
1184+
extern "C" int
1185+
sysfs_iokit_module_named(const char *name, size_t namelen,
1186+
struct sysfs_module_info *out)
1187+
{
1188+
if (name == nullptr || out == nullptr || g_lock == nullptr) {
1189+
return 0;
1190+
}
1191+
sysfs_snap_ensure();
1192+
IOLockLock(g_lock);
1193+
int ok = 0;
1194+
if (g_snapshot != nullptr && g_snapshot->modules != nullptr) {
1195+
for (uint32_t i = 0; i < g_snapshot->module_count; i++) {
1196+
const char *nm = g_snapshot->modules[i].name;
1197+
if (strlen(nm) == namelen && strncmp(nm, name, namelen) == 0) {
1198+
sysfs_module_copy_out(&g_snapshot->modules[i], out);
1199+
ok = 1;
1200+
break;
1201+
}
1202+
}
1203+
}
1204+
IOLockUnlock(g_lock);
1205+
return ok;
1206+
}
1207+
1208+
extern "C" int
1209+
sysfs_iokit_module_by_tag(uint64_t load_tag, struct sysfs_module_info *out)
1210+
{
1211+
if (out == nullptr || g_lock == nullptr) {
1212+
return 0;
1213+
}
1214+
sysfs_snap_ensure();
1215+
IOLockLock(g_lock);
1216+
int ok = 0;
1217+
if (g_snapshot != nullptr && g_snapshot->modules != nullptr) {
1218+
for (uint32_t i = 0; i < g_snapshot->module_count; i++) {
1219+
if (g_snapshot->modules[i].load_tag == load_tag) {
1220+
sysfs_module_copy_out(&g_snapshot->modules[i], out);
1221+
ok = 1;
1222+
break;
1223+
}
1224+
}
1225+
}
1226+
IOLockUnlock(g_lock);
1227+
return ok;
1228+
}

kext/sysfs_structure.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,18 @@ sysfs_structure_init(void)
305305
(void)add_directory(root_node, "hypervisor", SFSdir, next_node_id++, 0, 0, NULL, NULL);
306306
sfssnode_t *kernel_dir =
307307
add_directory(root_node, "kernel", SFSdir, next_node_id++, 0, 0, NULL, NULL);
308-
(void)add_directory(root_node, "module", SFSdir, next_node_id++, 0, 0, NULL, NULL);
308+
/*
309+
* /sys/module/<name>/ - one directory per loaded kernel extension, with
310+
* the attribute files Linux exposes (refcnt, coresize, initstate,
311+
* version). The directory is dynamic and its single SFSmodule child is
312+
* the shared marker every module is presented through; which module a
313+
* node is stands in its load tag, exactly as a registry id identifies a
314+
* /sys/devices node.
315+
*/
316+
sfssnode_t *module_dir = add_directory(root_node, "module",
317+
SFSdir, next_node_id++, SSN_FLAG_MODULES, 0, NULL, NULL);
318+
(void)add_node(module_dir, "__member__", SFSmodule, next_node_id++,
319+
SSN_FLAG_MODULES, 0, NULL, NULL);
309320
(void)add_directory(root_node, "power", SFSdir, next_node_id++, 0, 0, NULL, NULL);
310321

311322
/*

kext/sysfs_subr.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,16 @@ sysfs_allocvp(sfstype sfs_type)
3131
case SFSsubsystem:
3232
return VDIR;
3333

34-
case SFSfile: /* FALLTHROUGH */
35-
case SFSattr: /* FALLTHROUGH */
3634
case SFSmodule:
35+
/*
36+
* /sys/module/<name> is a directory, as on Linux. Its attribute files
37+
* share this node type and are distinguished by nodeid_objectid, which
38+
* sysfs_create_vnode() resolves to VREG.
39+
*/
40+
return VDIR;
41+
42+
case SFSfile: /* FALLTHROUGH */
43+
case SFSattr:
3744
return VREG;
3845

3946
case SFSlink:

0 commit comments

Comments
 (0)