ufoma sysctl handlers access freed sc_modetable after detach (UAF)
| Field | Value |
|---|---|
| ID | DF-1043 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:L |
| CWE | CWE-416 Use After Free |
| File | sys/bus/u4b/serial/ufoma.c |
| Lines | 453-463 (OID create), 484-486 (kfree), 1177, 1184, 1232 (handler read) |
| Area | bus/u4b/serial (USB CDC-ACM mobile serial driver) |
| Confidence | likely |
| Discovered | 2026-07-14 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
ufoma_attach creates three sysctl OIDs (supportmode, currentmode, openmode) whose
handlers dereference sc->sc_modetable without holding sc->sc_lock. ufoma_detach frees
sc->sc_modetable (ufoma.c:485) but never removes those OIDs β they are torn down later
by newbus's device_sysctl_fini, which runs after device_detach returns. In the
window between kfree and OID removal, any local user reading dev.ufoma.N.supportmode or
.openmode triggers a use-after-free on the freed heap buffer β a kernel heap OOB read
(info leak) or panic.
Root cause
Three SYSCTL_ADD_PROC calls at sys/bus/u4b/serial/ufoma.c:453-463 create sysctl OIDs
whose handlers (ufoma_sysctl_support at ufoma.c:1169, ufoma_sysctl_open at
ufoma.c:1208) dereference sc->sc_modetable without holding sc->sc_lock:
/* ufoma.c:1177 */
for (i = 1; i < sc->sc_modetable[0]; i++) {
mode = ufoma_mode_to_str(sc->sc_modetable[i]);
...
}
ufoma_detach (ufoma.c:475-491) calls kfree(sc->sc_modetable, M_USBDEV) at line 485 but
never removes those OIDs β it relies on newbus to call device_sysctl_fini β
sysctl_ctx_free (subr_bus.c:2151β238), which happens only after device_detach returns
(subr_bus.c:2139 calls DEVICE_DETACH, then line 2151 calls device_sysctl_fini).
sysctl_remove_oid with del=1 does drain in-flight handlers (kern_sysctl.c:391-398,
while oid_running>0 tsleep), but that drain only happens during the later newbus cleanup β
not before the kfree. So a handler that begins executing after kfree but before
device_sysctl_fini reads sc->sc_modetable[0] from freed memory; if the freed slab was
reused and the new occupant wrote a large value to that byte, the loop at
ufoma.c:1177/:1184/:1232 reads sc->sc_modetable[1..N] past the original 2-252-byte
allocation β a heap OOB read.
ufoma_sysctl_support is CTLFLAG_RD so it is readable by any unprivileged local user.
Threat model & preconditions
- Attacker position: Local unprivileged user on a system with a
ufomaUSB device attached (or a malicious USB device that can trigger its own port reset/re-enumeration). - Privileges gained or impact: Kernel heap OOB read leaking bytes from adjacent or reused slab objects (info leak), or kernel panic if the freed page is in an invalid state.
- Required config or capabilities: A
ufoma-attached USB CDC-ACM mobile-broadband device. The detach trigger requires physical unplug, root (usbconfig detach), or a USB error severe enough to trigger a port reset. The sysctl read side needs no privilege βsupportmodeisCTLFLAG_RD. - Reachability: Two cooperating processes:
1. Reader loop β unprivileged process calling
sysctlbyname("dev.ufoma.0.supportmode", buf, &len, NULL, 0)in a tight loop. 2. Detach trigger β root user runsusbconfig -d X.Y detach, or a physical unplug, or a USB error forces a port reset.
The race window is between kfree in ufoma_detach and device_sysctl_fini in
newbus. The window is short but reliably winnable with a tight reader loop.
Proof of concept
PoC source: findings/poc/DF-1043/ufoma_uaf.c and findings/poc/DF-1043/run.sh
Two-process race:
/* Reader: unprivileged user, tight loop. */
for (;;) {
char buf[256];
size_t len = sizeof(buf);
sysctlbyname("dev.ufoma.0.supportmode", buf, &len, NULL, 0);
/* look for slab-poison bytes (0xde on DEBUG builds) or unexpected
strings that indicate the slab was reused */
}
# Trigger: run as root while the reader loop is hot.
usbconfig -d 0.1 detach
Build & run
cc -o ufoma_uaf findings/poc/DF-1043/ufoma_uaf.c ./ufoma_uaf & # unprivileged user, tight sysctl reader loop sudo usbconfig -d 0.1 detach # root triggers the UAF window
Expected output
On a DEBUG/INVARIANTS kernel with slab poisoning (0xDE), the reader output contains
0xDE bytes instead of valid mode strings β proving it read freed memory. For a crash
variant, groom the heap so the freed 2-252-byte M_USBDEV slab is reused by an object
whose first byte is 0xFF, causing ufoma_sysctl_support to loop i=1..254 and read 254
bytes from a β€252-byte allocation, hitting an unallocated/poisoned page and panicking.
Impact
Local info leak / DoS via a race during USB device detach. The high attack complexity
(short window) and the requirement for a ufoma device + detach trigger keep this at Low
severity, but the UAF is real and the fix is straightforward.
Recommended fix
Save the struct sysctl_oid * returned by each SYSCTL_ADD_PROC in the softc, and call
sysctl_remove_oid(oid, 1, 0) for each at the top of ufoma_detach β before ucom_detach
and before kfree(sc->sc_modetable). sysctl_remove_oid with del=1 blocks (tsleep on
oid_running) until all in-flight handlers drain, so after it returns no handler can touch
sc->sc_modetable, making the subsequent kfree safe. The later device_sysctl_fini call
by newbus will find the OIDs already gone and skip them.
--- a/sys/bus/u4b/serial/ufoma.c
+++ b/sys/bus/u4b/serial/ufoma.c
@@ -185,6 +185,11 @@ struct ufoma_softc {
uint8_t sc_modetoactivate;
uint8_t sc_currentmode;
uint8_t sc_name[16];
+
+ /* sysctl OIDs β removed in detach before sc_modetable is freed */
+ struct sysctl_oid *sc_oid_support;
+ struct sysctl_oid *sc_oid_current;
+ struct sysctl_oid *sc_oid_open;
};
/* prototypes */
@@ -450,17 +455,20 @@ ufoma_attach(device_t dev)
sctx = device_get_sysctl_ctx(dev);
soid = device_get_sysctl_tree(dev);
- SYSCTL_ADD_PROC(sctx, SYSCTL_CHILDREN(soid), OID_AUTO, "supportmode",
+ sc->sc_oid_support = SYSCTL_ADD_PROC(sctx, SYSCTL_CHILDREN(soid),
+ OID_AUTO, "supportmode",
CTLFLAG_RD|CTLTYPE_STRING, sc, 0, ufoma_sysctl_support,
"A", "Supporting port role");
- SYSCTL_ADD_PROC(sctx, SYSCTL_CHILDREN(soid), OID_AUTO, "currentmode",
+ sc->sc_oid_current = SYSCTL_ADD_PROC(sctx, SYSCTL_CHILDREN(soid),
+ OID_AUTO, "currentmode",
CTLFLAG_RD|CTLTYPE_STRING, sc, 0, ufoma_sysctl_current,
"A", "Current port role");
- SYSCTL_ADD_PROC(sctx, SYSCTL_CHILDREN(soid), OID_AUTO, "openmode",
+ sc->sc_oid_open = SYSCTL_ADD_PROC(sctx, SYSCTL_CHILDREN(soid),
+ OID_AUTO, "openmode",
CTLFLAG_RW|CTLTYPE_STRING, sc, 0, ufoma_sysctl_open,
"A", "Mode to transit when port is opened");
SYSCTL_ADD_UINT(sctx, SYSCTL_CHILDREN(soid), OID_AUTO, "comunit",
@@ -478,6 +486,15 @@ static int
ufoma_detach(device_t dev)
{
struct ufoma_softc *sc = device_get_softc(dev);
+
+ /* Remove sysctl OIDs before freeing sc_modetable. The handlers
+ * (ufoma_sysctl_support, ufoma_sysctl_open) dereference sc_modetable
+ * without holding sc_lock; sysctl_remove_oid(β¦,1,0) drains all
+ * in-flight handlers before returning. */
+ if (sc->sc_oid_support) sysctl_remove_oid(sc->sc_oid_support, 1, 0);
+ if (sc->sc_oid_current) sysctl_remove_oid(sc->sc_oid_current, 1, 0);
+ if (sc->sc_oid_open) sysctl_remove_oid(sc->sc_oid_open, 1, 0);
ucom_detach(&sc->sc_super_ucom, &sc->sc_ucom);
usbd_transfer_unsetup(sc->sc_ctrl_xfer, UFOMA_CTRL_ENDPT_MAX);
References
sys/bus/u4b/serial/ufoma.c:453-463βSYSCTL_ADD_PROCfor the three OIDssys/bus/u4b/serial/ufoma.c:484-486βkfree(sc->sc_modetable)with no prior OID removalsys/bus/u4b/serial/ufoma.c:1169-1193βufoma_sysctl_supportderefssc_modetableunlockedsys/bus/u4b/serial/ufoma.c:1208-1242βufoma_sysctl_openderefssc_modetableunlockedsys/kern/subr_bus.c:2139-2151β newbus order:DEVICE_DETACHthendevice_sysctl_fini(the gap that allows the race)sys/kern/kern_sysctl.c:391-398βsysctl_remove_oid(del=1)drains handlers; only invoked fromdevice_sysctl_fini, not fromufoma_detach
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1043 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ufoma_uaf.c | trigger-source | sysctl reader loop (original, unchanged) | 2.2 KB | view raw |
| build.sh | build-script | exact cc command for the reader | 275 B | view raw |
| run.sh | run-script | exact run command with USB HW precondition note | 920 B | view raw |
| fix.diff | suggested-fix | corrected fix: sysctl_ctx_entry_del + sysctl_remove_oid before kfree | 2.7 KB | view raw |
| VERDICT.md | verdict | full analysis: source-confirmed UAF, not triggerable on guest, finding's fix broken, corrected fix | 8.0 KB | β raw |
| build.log | build-log | PoC reader build output | 270 B | view raw |
| run.log | run-log | PoC reader run on unpatched baseline (ENOENT β no USB HW) | 792 B | view raw |
| fix_build.log | build-log | full nativekernel build with fix.diff applied (rc=0, 35757 lines) | 5.6 MB | β download |
| env.txt | environment | guest uname, cc version, ufoma.ko loaded, USB HW count=0 | 275 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1043 β ufoma sysctl handlers access freed sc_modetable after detach (UAF)
Verdict
NOT REPRODUCED (latent β source-confirmed real bug, not triggerable on this guest due to no USB hardware). Fix VALIDATED as compile+boot+source-trace correct; dynamic before/after not testable (no USB HW on either kernel).
Mechanism (source-level confirmation β the bug IS real)
The UAF race window described in the finding is genuine and confirmed by source:
-
OID creation β
ufoma_attach(sys/bus/u4b/serial/ufoma.c:453-463) creates three sysctl OIDs (supportmode,currentmode,openmode) viaSYSCTL_ADD_PROC(sctx, β¦)wheresctx = device_get_sysctl_ctx(dev)(the device's sysctl context). The handlersufoma_sysctl_support(ufoma.c:1169) andufoma_sysctl_open(ufoma.c:1208) dereferencesc->sc_modetableat lines 1177, 1184, 1232 without holdingsc->sc_lock. -
Free before OID removal β
ufoma_detach(ufoma.c:475-491) callskfree(sc->sc_modetable, M_USBDEV)at line 485 but never removes the three OIDs. The OIDs are torn down later by newbus indevice_sysctl_finiβsysctl_ctx_free(sys/kern/subr_bus.c:2151), which runs afterDEVICE_DETACHreturns (subr_bus.c:2139). So betweenkfree(line 485) anddevice_sysctl_fini(subr_bus.c:2151), the OIDs are still live and their handlers can fire on freed memory. -
Unprivileged read β
supportmodeandcurrentmodeareCTLFLAG_RD, readable by any local user.openmodeisCTLFLAG_RW.
Why the bug is real (rwlock analysis)
The finding cites oid_running drain (kern_sysctl.c:391-398) as the handler-drain mechanism. In fact, grep -rn oid_running sys/ shows oid_running is never incremented anywhere in the kernel β that drain loop is dead code. However, the real protection is the SYSCTL_SLOCK() held across the entire handler invocation in userland_sysctl (kern_sysctl.c:1572-1574):
SYSCTL_SLOCK();
error = sysctl_root(0, name, namelen, &req); /* handler runs here */
SYSCTL_SUNLOCK();
sysctl_remove_oid / sysctl_unregister_oid need SYSCTL_XLOCK() (exclusive), so they block until all shared-lock holders (in-flight handlers) release. This means sysctl_remove_oid does drain β via the rwlock, not oid_running. But since ufoma_detach never calls sysctl_remove_oid before kfree, the drain never happens in the vulnerable window. A handler that wins the rwlock shared-lock before kfree can read sc->sc_modetable[0] after the buffer is freed.
Why NOT reproducible on this guest
The audit QEMU guest has no USB host controller at all (pciconf -l shows 0 USB devices β only virtio-net, virtio-blk, IDE, ISA bridge, ACPI, VGA). The ufoma driver's ufoma_attach is called only when ufoma_probe matches a connected USB CDC-ACM mobile device. With no USB hardware:
ufoma_attachnever runs βSYSCTL_ADD_PROCnever executes β nodev.ufoma.*OIDs exist.sysctl dev.ufoma.0.supportmodeβ "unknown oid" (ENOENT).- The PoC reader loop calls
sysctlbynamein a tight loop β every call fails ENOENT, producing 0 output. - The
ufoma.komodule can bekldloaded as root, but it only registers the probe/attach methods β no device nodes or sysctl OIDs are created without a matching USB device.
This is a valid hard blocker (Phase 6): the vulnerable code path is unreachable at runtime on this guest, and no harness can exercise the actual bug path from userspace without either adding USB hardware to the VM (outside scope) or building a kernel module that bypasses ufoma_probe (which violates the bright-line rule β it would not be driving the real bug path).
The finding's threat model is honest about this: it requires "a ufoma-attached USB CDC-ACM mobile-broadband device" plus a detach trigger.
Finding's proposed fix is BROKEN β corrected fix authored
The finding's recommended fix saves the OID pointers and calls sysctl_remove_oid(oid, 1, 0) in ufoma_detach before kfree. This fix is itself broken: sysctl_remove_oid(del=1) frees the OID struct but does NOT remove the corresponding entry from the device's sysctl context list. The later device_sysctl_fini β sysctl_ctx_free (kern_sysctl.c:226) iterates the context list and calls sysctl_remove_oid_locked(e->entry, β¦) on each entry β e->entry would be a pointer to already-freed memory β a new UAF in sysctl_ctx_free.
The correct pattern (used in sys/kern/kern_sensors.c:444-445) is to call sysctl_ctx_entry_del(sctx, oid) BEFORE sysctl_remove_oid(oid, 1, 0). The fix.diff in this evidence pack uses this corrected pattern.
Corrected fix (fix.diff)
- Add
struct sysctl_oid *sc_oid_support, *sc_oid_current, *sc_oid_opentostruct ufoma_softc. - Save the return values of
SYSCTL_ADD_PROCinufoma_attach. - At the top of
ufoma_detach, beforeucom_detachand beforekfree(sc->sc_modetable):c sctx = device_get_sysctl_ctx(dev); if (sc->sc_oid_support != NULL) { sysctl_ctx_entry_del(sctx, sc->sc_oid_support); sysctl_remove_oid(sc->sc_oid_support, 1, 0); sc->sc_oid_support = NULL; } /* β¦ same for current and open β¦ */
sysctl_remove_oid(oid, 1, 0) blocks on the sysctl rwlock until all in-flight handlers drain, so after it returns no handler can touch sc_modetable. sysctl_ctx_entry_del removes the stale context entry so device_sysctl_fini's later sysctl_ctx_free won't dereference a freed OID.
Fix validation (Phase 8)
| Step | Result |
|---|---|
patch -p1 --dry-run |
All 3 hunks apply cleanly |
make -j6 nativekernel |
rc=0 (0 errors, 35757 lines of log) |
Kernel install (kernel.stripped β /boot/kernel/kernel) |
sha256 a61e073f⦠|
| Boot | #1 build, today's timestamp β boots and is stable |
kldload ufoma |
rc=0 β module loads cleanly |
| PoC reader on patched kernel | Same ENOENT (no USB HW) β no regression |
fix_status = not_testable: The PoC cannot trigger the UAF on this guest (no USB hardware on either the unpatched or patched kernel), so a dynamic before/after comparison is impossible. The fix is validated as: (a) applies cleanly, (b) compiles with -Werror (rc=0), (c) boots and is stable, (d) source trace confirms it closes the code path (sysctl_remove_oid drains via rwlock; sysctl_ctx_entry_del prevents sysctl_ctx_free UAF).
Exploit chain
Not applicable. This is a read-freed/UAF class bug that is (a) not dynamically triggerable on this guest (no USB HW), and (b) classified Low severity by the finding. No escalation chain developed β the valid hard blocker (code path unreachable at runtime on this guest, no harness can exercise the actual bug path without USB hardware) applies.
PoC changes
build.sh(new): exact build command for the reader.run.sh(new): exact run command with documentation of the USB HW precondition.fix.diff(new): corrected fix β pairssysctl_ctx_entry_del+sysctl_remove_oid(supersedes the finding's proposed fix which would introduce asysctl_ctx_freeUAF).- Original
ufoma_uaf.candrun.shunchanged in logic.
References
sys/bus/u4b/serial/ufoma.c:453-463βSYSCTL_ADD_PROCfor the three OIDssys/bus/u4b/serial/ufoma.c:484-486βkfree(sc->sc_modetable)with no prior OID removalsys/bus/u4b/serial/ufoma.c:1169-1193βufoma_sysctl_supportderefssc_modetableunlockedsys/bus/u4b/serial/ufoma.c:1208-1240βufoma_sysctl_openderefssc_modetableunlockedsys/kern/subr_bus.c:2129-2153β newbus order:DEVICE_DETACH(2139) thendevice_sysctl_fini(2151) β the gapsys/kern/kern_sysctl.c:1519-1578βuserland_sysctl:SYSCTL_SLOCK()held acrosssysctl_root(the real drain mechanism)sys/kern/kern_sysctl.c:213-261βsysctl_ctx_free: iterates context entries (would UAF on stale entries)sys/kern/kern_sysctl.c:300-317βsysctl_ctx_entry_del: the missing call in the finding's proposed fixsys/kern/kern_sensors.c:444-445β correct pattern:sysctl_ctx_entry_del+sysctl_remove_oid
Fix verification
not_testablenot_testable (no USB HW). Compile+boot+kldload validated. Corrected fix closes code path by source trace.
Build rc=0. kldload ufoma OK. Source trace: sysctl_ctx_entry_del + sysctl_remove_oid drains handlers before kfree.
Confirmed kernel references
- sys/bus/u4b/serial/ufoma.c:453
- sys/bus/u4b/serial/ufoma.c:461
- sys/bus/u4b/serial/ufoma.c:485
- sys/bus/u4b/serial/ufoma.c:1177
- sys/bus/u4b/serial/ufoma.c:1232
- sys/kern/subr_bus.c:2139
- sys/kern/subr_bus.c:2151
- sys/kern/kern_sysctl.c:1572
- sys/kern/kern_sysctl.c:213
- sys/kern/kern_sysctl.c:300
- sys/kern/kern_sensors.c:444
Detail
Exploit chain
none -- code path unreachable at runtime (no USB HW). Read-freed info-leak/panic only. No userspace harness possible.
Evidence (decisive lines)
sysctl dev.ufoma.0.supportmode -> unknown oid (ENOENT). pciconf: 0 USB controllers. Build #1 rc=0, kldload ufoma OK.
PoC changes
Authored: fix.diff (corrected: sysctl_ctx_entry_del + sysctl_remove_oid pair, supersedes finding's broken proposal), VERDICT.md, manifest.json.
Verified recommended fix
At top of ufoma_detach: for each of 3 OIDs call sysctl_ctx_entry_del(sctx,oid) + sysctl_remove_oid(oid,1,0) BEFORE kfree(sc_modetable). Matches kern_sensors.c:444-445. Finding's original proposal (sysctl_remove_oid only) is BROKEN: leaves stale ctx entries -> new UAF in sysctl_ctx_free. Full diff in findings/poc/DF-1043/fix.diff.
Verdict
NOT REPRODUCED -- latent UAF (source-confirmed) but no USB HW on guest. ufoma_detach frees sc_modetable while 3 SYSCTL_ADD_PROC OIDs still live (removed later by device_sysctl_fini). Handlers deref sc_modetable without sc_lock. sysctl dev.ufoma.* = ENOENT (no attach).
No comments yet.