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

lptopen() sc_state check-then-set TOCTOU: double open double-allocates and the paired closes double-free the same M_LPT chunks (latent β€” currently blocked by the mplock)

Summary

The sc_state test-and-set has no lock; two concurrent opens both proceed, each allocating, the first pair leaked; both closes kfree the same (second) buffer pointers - heap double-free. Empirically blocked today because lpt's dev_ops carry no D_MPSAFE so every op runs under the kernel mplock (verified across 3 race configs, 0 hits). Becomes live the day the driver is converted to MPSAFE without per-unit serialization. Per-unit spinlock fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4536 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 2.5 KB ↓ raw
lpt_openrace.c β€” 3.3 KB view raw
build.sh β€” 97 B view raw
run.sh β€” 175 B view raw
run.log β€” 1.2 KB view raw
env.txt β€” 713 B view raw
fix.diff β€” 1.1 KB view raw
manifest.json β€” 800 B view raw
verdict.json β€” 3.4 KB view raw

DF-4536 β€” lptopen() sc_state check-then-set TOCTOU (latent: blocked by the mplock in the current configuration)

File: sys/dev/misc/lpt/lpt.c:458-462 (pass 2)

if (sc->sc_state) {
        return(EBUSY);
} else
        sc->sc_state |= LPTINIT;

No lock, no atomic: two concurrent lptopen() calls that both read sc_state == 0 both proceed β€” double kmalloc(BUFSIZE) + kmalloc(BUFSTATSIZE) (the first pair leaks), and the two later closes kfree() the same (second) buffer pair twice β†’ heap double-free in the private M_LPT zone.

Runtime status on this tree: BLOCKED (why the PoC cannot hit)

lpt's dev_ops head flags are 0 (lpt.c:191-198), so dev_needmplock() (sys/kern/kern_device.c:119-122) is true and dev_dopen() wraps d_open in get_mplock()/rel_mplock() (sys/kern/kern_device.c:142-156): all lpt cdev operations execute under Giant, fully serializing the check-then-set. (On top of that, same-vnode opens also pass through devfs's exclusive vnode section, sys/vfs/devfs/devfs_vnops.c devfs_spec_open.)

PoC lpt_openrace raced:

  • /dev/lpt0 vs /dev/lpt0 β€” 300 rounds (full open path, fds held 300 ms so sequential double-success is impossible): 0 hits;
  • /dev/lpt0 vs /dev/lpt0.ctl (different cdevs/vnodes, shared softc): 300 rounds, 0 hits;
  • /dev/lpt0.ctl vs /mnt/df2/lpt0.ctl (two devfs mounts β†’ two vnodes, no shared vnode lock): ~3,600 effective rounds, 0 hits β€” the mplock is the remaining serializer, and it is airtight for same-driver d_open entries.

(An early ".ctl vs .ctl same vnode" run printed many double-successes; those were sequential pairs β€” sibling's open after the holder's close β€” not races; the fd-hold fix in the PoC removes that false signal, after which all variants score 0.)

Why it is still filed (Info)

The driver-level TOCTOU is real source code; the serialization is incidental (a global side effect of not being D_MPSAFE). Any future conversion of lpt to D_MPSAFE (the direction DFly pushes all drivers) makes the window live with a double-free consequence. fix.diff adds the missing per-unit serialization now.

Reproduce (attempts)

cc -O -Wall -o lpt_openrace lpt_openrace.c
mount -t devfs devfs /mnt/df2
./lpt_openrace /dev/lpt0 /dev/lpt0 300 350          # full path
./lpt_openrace /dev/lpt0 /dev/lpt0.ctl 300 450      # mixed
./lpt_openrace /dev/lpt0.ctl /mnt/df2/lpt0.ctl 30000 3 25000

Expected on a hypothetical MPSAFE build: [+] HIT ... BOTH opens returned 0 followed (full path) by the double-free on the two closes and eventual heap corruption in the M_LPT zone. On this tree: zero hits everywhere (mplock).

VERDICT.md
↓ download raw

DF-4536 β€” VERDICT

Status: not_reproduced β€” the driver-level TOCTOU exists (sys/dev/misc/lpt/lpt.c:458-462, unsynchronized check-then-set), but on this tree it cannot manifest: every lpt cdev operation runs under the kernel mplock because lpt's dev_ops head flags are 0 (lpt.c:191-198) β†’ dev_needmplock() true (sys/kern/kern_device.c:119) β†’ dev_dopen() wraps d_open in Giant (sys/kern/kern_device.c:142). Two concurrent lptopen() entries are impossible as configured, so the double-kmalloc/double-kfree consequence is unreachable today. Filed at Info as a latent defect: converting lpt to D_MPSAFE without first adding per-unit serialization re-opens the window with a heap double-free payoff.

Attempts (guest, root, fd-hold detection so sequential pairs cannot false-positive)

race configuration rounds hits
/dev/lpt0 Γ— /dev/lpt0 (same vnode, full path) 300 0
/dev/lpt0 Γ— /dev/lpt0.ctl (different vnodes/cdevs) 300 0
/dev/lpt0.ctl Γ— /mnt/df2/lpt0.ctl (two devfs mounts) ~3,600 0

The last configuration removes both the shared vnode lock and any same-cdev devfs serialization; only the mplock remains β€” proving the mplock is the operative serializer.

Note on methodology: an early run without the fd-hold printed many "both opens returned 0" β€” these were sequential (sibling opened after the first closed, state already cleared), not races. The released PoC holds each fd 300 ms (configurable), making sequential double-success impossible; with that fix all configurations score 0.

Fix

fix.diff: per-unit spinlock sc_openlk in struct lpt_data; serialize the open state transition and the close-time buffer frees (+ NULL-ing). git-apply-clean against the tree. Runtime fix validation not applicable (nothing to observe on a non-manifesting bug).

Kernel references

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix validation not applicable: the defect does not manifest on the current (non-MPSAFE) configuration, so there is no baseline behavior to compare. The spinlock fix is defensive for a future D_MPSAFE conversion.

fix.diff (git apply --check clean against sys/)
↓ fix.diffper-fix-DF-4536

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (all three race configurations + the calibration false-positive analysis); VERDICT.md (the mplock argument with path:line); fix.diff (git-apply-clean spinlock serialization)

PoC changes

PoC written for this finding (no seed). Two hardening iterations during verification: (1) per-child paths so different cdevs/vnodes/devfs-mounts can be raced; (2) fd-hold before close so sequential open/close pairs cannot produce false positives - the initial .ctl-vs-.ctl 'hits' were shown to be sequential.

Verified recommended fix

Serialize the open state transition and close-time frees with a per-unit spinlock in struct lpt_data (fix.diff) - required before any D_MPSAFE conversion.

Verdict

The check-then-set of sc_state in lptopen() (lpt.c:458-462) is unsynchronized in the driver and, if ever executed concurrently, double-opens the unit: two kmalloc pairs (first leaks) and two closes that kfree the same second pair (heap double-free in the M_LPT zone). On the audited tree it cannot manifest: lpt's dev_ops flags are 0 (lpt.c:191-198), so dev_needmplock() is true and dev_dopen() executes d_open under the kernel mplock (sys/kern/kern_device.c:119,142) - concurrent lptopen() entries are impossible. Verified empirically: 0 double-opens in 300 same-vnode full-path rounds, 300 cross-cdev rounds, and ~3,600 cross-devfs-mount rounds (a configuration that removes every other serializer, isolating the mplock). An initial calibration run that printed many double-successes was proven to be sequential pairs (sibling opening after the holder's close) and the PoC was hardened with an fd-hold to eliminate the false signal. Filed at Info as a latent defect: it becomes a live double-free the day lpt is converted to D_MPSAFE without adding per-unit serialization (fix.diff adds a spinlock now).