β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-1043

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 ufoma USB 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 β€” supportmode is CTLFLAG_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 runs usbconfig -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.

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

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1043 Β· 11 files
FileTypeDescriptionSize
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
VERDICT.md verdict full analysis: source-confirmed UAF, not triggerable on guest, finding's fix broken, corrected fix
↓ download 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:

  1. OID creation β€” ufoma_attach (sys/bus/u4b/serial/ufoma.c:453-463) creates three sysctl OIDs (supportmode, currentmode, openmode) via SYSCTL_ADD_PROC(sctx, …) where sctx = device_get_sysctl_ctx(dev) (the device's sysctl context). The handlers ufoma_sysctl_support (ufoma.c:1169) and ufoma_sysctl_open (ufoma.c:1208) dereference sc->sc_modetable at lines 1177, 1184, 1232 without holding sc->sc_lock.

  2. Free before OID removal β€” ufoma_detach (ufoma.c:475-491) calls kfree(sc->sc_modetable, M_USBDEV) at line 485 but never removes the three OIDs. The OIDs are torn down later by newbus in device_sysctl_fini β†’ sysctl_ctx_free (sys/kern/subr_bus.c:2151), which runs after DEVICE_DETACH returns (subr_bus.c:2139). So between kfree (line 485) and device_sysctl_fini (subr_bus.c:2151), the OIDs are still live and their handlers can fire on freed memory.

  3. Unprivileged read β€” supportmode and currentmode are CTLFLAG_RD, readable by any local user. openmode is CTLFLAG_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_attach never runs β†’ SYSCTL_ADD_PROC never executes β†’ no dev.ufoma.* OIDs exist.
  • sysctl dev.ufoma.0.supportmode β†’ "unknown oid" (ENOENT).
  • The PoC reader loop calls sysctlbyname in a tight loop β€” every call fails ENOENT, producing 0 output.
  • The ufoma.ko module can be kldloaded 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)

  1. Add struct sysctl_oid *sc_oid_support, *sc_oid_current, *sc_oid_open to struct ufoma_softc.
  2. Save the return values of SYSCTL_ADD_PROC in ufoma_attach.
  3. At the top of ufoma_detach, before ucom_detach and before kfree(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 β€” pairs sysctl_ctx_entry_del + sysctl_remove_oid (supersedes the finding's proposed fix which would introduce a sysctl_ctx_free UAF).
  • Original ufoma_uaf.c and run.sh unchanged in logic.

References

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 15:31:50 UTC 2026

Confirmed kernel references

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).