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)
PoC verification
Evidence pack
findings/poc/DF-4536 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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/lpt0vs/dev/lpt0β 300 rounds (full open path, fds held 300 ms so sequential double-success is impossible): 0 hits;/dev/lpt0vs/dev/lpt0.ctl(different cdevs/vnodes, shared softc): 300 rounds, 0 hits;/dev/lpt0.ctlvs/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).
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
- sys/dev/misc/lpt/lpt.c:458-462 (unsynchronized check-then-set)
- sys/dev/misc/lpt/lpt.c:538-541 (second open's kmallocs overwrite sc_inbuf)
- sys/dev/misc/lpt/lpt.c:598-599 (both closes kfree the same pointer)
- sys/dev/misc/lpt/lpt.c:191-198 (dev_ops flags 0)
- sys/kern/kern_device.c:119-122 (dev_needmplock)
- sys/kern/kern_device.c:142-156 (dev_dopen mplock wrap)
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_testableFix 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/)
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).
No comments yet.