sasetparams ENODEV early return leaks ccomp allocation
Summary
sasetparams() kmalloc ccomp at scsi_sa.c:2757 only frees on success path at 3023-3024. Early ENODEV return at 2938-2941 (taken when compression only thing being set device reports no compression support params_to_set&=~SA_PARAM_COMPRESSION then SA_PARAM_NONE check) frees mode_buffer releases ccb but does NOT free ccomp. Each such call leaks sizeof(sa_comp_t) (~16 bytes) M_SCSISA. Privileged-user-triggerable memory leak repeat via MTIOCTOP/MTCOMP on compression-unsupported drive. No unprivileged reach. [VERIFIED pass2: reproduced - 1000 ENODEV calls grow M_SCSISA by exactly 16000 bytes = 1000 x sizeof sa_comp_t with ccomp the unfreed allocation; fix-validated - 0 bytes growth, identical counts]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2265 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.3 KB | β raw | |
| VERDICT.md | β | 3.4 KB | β raw | |
| df_pre.c | β | 3.1 KB | view raw | |
| df_post.c | β | 20.4 KB | view raw | |
| gen.sh | β | 1.5 KB | view raw | |
| build.sh | β | 115 B | view raw | |
| quick.sh | β | 1.6 KB | view raw | |
| run.sh | β | 140 B | view raw | |
| run_unfixed.dmesg | β | 743 B | β download | |
| run_fixed.dmesg | β | 978 B | β download | |
| fix.diff | β | 329 B | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 2.6 KB | view raw |
DF-2265 verification pack β sasetparams ENODEV ccomp leak
Bug: sys/bus/cam/scsi/scsi_sa.c:2938-2941 β the compression-
unsupported ENODEV exit frees mode_buffer and releases the ccb but never
frees ccomp (allocated at :2757), leaking one sa_comp_t (16 bytes,
M_SCSISA) per failed MTCOMP ioctl on drives reporting the DATA
COMPRESSION page without the DCC bit.
Rig
Same harness kernel as DF-2263 (see ../DF-2263/README.md); the REAL
sasetparams() and sagetparams() run against a fake transport
(cam_periph_runccb interposed) that answers MODE SENSE(6) with a DCP
page lacking DCC β the drive class the in-tree comment anticipates.
Trigger equals saioctl(MTIOCTOP, MTCOMP) at :1266. Leak measured via
in-kernel sums of M_SCSISA->ks_use[cpu].{memuse,calls}.
Build
./build.sh # or reuse the kernel built from ../DF-2263
Run
vm.sh run_root 'sysctl debug.df226x.t2265=1000' vm.sh run_root 'dmesg | grep DFH2265'
Expected
Unpatched: memuse grew by 16000 bytes = 1000 x sizeof(sa_comp_t) (16)
after 1000 ENODEV returns (3 allocation calls per iteration, one object
never freed). Patched (fix.diff adds kfree(ccomp, M_SCSISA); on the
ENODEV path): memuse grew by 0 bytes.
Artifacts: VERDICT.md, df_pre.c, df_post.c, gen.sh, build.sh, quick.sh, run_unfixed.dmesg, run_fixed.dmesg, env.txt (in ../DF-2263), fix.diff.
DF-2265 β sasetparams ENODEV path leaks the ccomp allocation (M_SCSISA)
File: sys/bus/cam/scsi/scsi_sa.c
Claim under verification: sasetparams() allocates ccomp
(:2757 ccomp = kmalloc(sizeof(sa_comp_t), M_SCSISA, M_INTWAIT)), fills
it via sagetparams() with the drive's compression page, and on the
"device does not support compression" path (:2924-2941) frees only
mode_buffer and releases the ccb:
if (params_to_set == SA_PARAM_NONE) {
kfree(mode_buffer, M_SCSISA);
xpt_release_ccb(ccb);
return (ENODEV); /* <-- ccomp leaked */
}
ccomp is freed on every OTHER exit (:2772 error, :3031 tail) but not
here: 16 bytes of M_SCSISA per failed call, unbounded over ioctl
retries.
Rig
Same kernel-compiled harness (verbatim sasetparams + sagetparams). The
only fabricated element is the transport: cam_periph_runccb() is
interposed by a fake transport that answers MODE SENSE(6) the way a real
drive does when it returns the DATA COMPRESSION page (0x0f) WITHOUT the
DCC ("compression capable", 0x40) bit β precisely the class of drive the
in-tree comment at :2914-2922 anticipates ("Compression does not appear
to be supported- at least via the DATA COMPRESSION page..."). Everything
else β the two kmallocs, sagetparams' page parsing, the mode_buffer
setup, the compression-page switch β is the real code. The trigger is
exactly what saioctl(MTIOCTOP, MTCOMP) issues at :1266:
sasetparams(periph, SA_PARAM_COMPRESSION, 0, 0, calg, 0).
Measurement: in-kernel sum of M_SCSISA->ks_use[cpu].{memuse,calls}
across all CPUs before/after N=1000 iterations.
Results
UNPATCHED (run_unfixed.dmesg):
DFH2265: M_SCSISA before: memuse=0 calls=0, sizeof(sa_comp_t)=16, iterations=1000
DFH2265: M_SCSISA after: memuse=16000 calls=3000; returns: ENODEV=1000 other=0
DFH2265: *** LEAK: memuse grew by 16000 bytes = 1000 x sizeof(sa_comp_t) (16);
allocation calls grew by 3000 (expect 3 per call: ccomp+mode_buffer+
sagetparams buffer; ccomp is the one never freed on the ENODEV path)
Exactly one sa_comp_t (16 bytes) leaked per ENODEV return; the other
two per-call allocations (mode_buffer, sagetparams' mode_buffer) are
freed. Unbounded: a retry loop or script around mt comp on on such a
drive grows M_SCSISA without bound (16 B/call; a 10 kHz ioctl loop leaks
~576 KB/hour; kernel memory never returned).
PATCHED (run_fixed.dmesg, fix2265.diff = add kfree(ccomp, M_SCSISA);
before the ENODEV return):
DFH2265: M_SCSISA before: memuse=0 calls=0, sizeof(sa_comp_t)=16, iterations=1000 DFH2265: M_SCSISA after: memuse=0 calls=3000; returns: ENODEV=1000 other=0 DFH2265: *** LEAK: memuse grew by 0 bytes = 0 x sizeof(sa_comp_t) (16); ...
A/B clean: identical behavior (1000 ENODEVs, same 3000 allocations), zero net growth.
Reachability / severity honesty
Requires issuing MTIOCTOP/MTCOMP against a tape drive whose compression mode page reports no DCC capability β a privileged ioctl on the tape device node (root/operator). Slow, privileged-reach kernel memory leak: Info severity, exactly as filed. No privesc, no corruption.
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
fixedA/B dmesg logs: 16000 bytes leaked over 1000 ENODEV calls unpatched versus 0 bytes patched, identical allocation counts
Confirmed kernel references
Detail
Evidence (decisive lines)
run_unfixed.dmesg: M_SCSISA after memuse=16000 calls=3000 ENODEV=1000 and LEAK grew by 16000 bytes = 1000 x sizeof(sa_comp_t); run_fixed.dmesg: memuse=0 calls=3000 ENODEV=1000 grew by 0 bytes
PoC changes
Replaced seeded sketch with kernel-compiled harness: only cam_periph_runccb is interposed (fake SCSI transport echoing a DCP page without DCC), everything else real; leak measured in-kernel by summing M_SCSISA ks_use per-cpu memuse and calls before and after
Verified recommended fix
Free ccomp with kfree(ccomp, M_SCSISA) on the params_to_set == SA_PARAM_NONE ENODEV return path in sasetparams
Verdict
Fully reproduced and fix-validated A/B with the real sasetparams and sagetparams against a fake transport answering MODE SENSE with a DATA COMPRESSION page lacking the DCC bit. On the unpatched kernel 1000 compression-only sasetparams calls return ENODEV 1000 times and grow M_SCSISA memuse by exactly 16000 bytes = 1000 x sizeof(sa_comp_t) with 3000 allocation calls (ccomp leaked once per call; mode_buffer and the sagetparams buffer are freed). With fix265.diff adding kfree(ccomp, M_SCSISA) on the ENODEV path the same 1000 calls produce zero net growth. Privileged-reach unbounded kernel memory leak, 16 bytes per failed MTCOMP ioctl on such drives; Info severity confirmed.
No comments yet.