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

Memory leak of targ_cmd_descr in targread abort_queue drain loop

Summary

When targread() drains abort_queue each targ_cmd_descr is TAILQ_REMOVEd and its user_ccb delivered to caller but descr itself never kfreed. descr allocated by targgetdescr (kmalloc) moved to abort_queue by targstart on send failure or abort_all_pending. Every aborted-then-read descriptor (~80 bytes) permanently leaked. Parallel user_ccb_queue drain path correctly frees via targreturnccb->targfreeccb (kfree at 1012) and targdtor frees remaining abort_queue entries at teardown (547). A RESTRICTEDROOT user can drive this leak unboundedly by submitting CCBs that deterministically fail (dxfer_len>MAXPHYS) then reading them back exhausting kernel memory. [VERIFIED pass2: reproduced - 2000 oversized CTIOs drain 2000 pointer-matched entries while M_TARG grows monotonically 80 bytes per descr permanent; free-in-drain fix-validated delta 0]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2287 Β· 7 files
FileTypeDescriptionSize
poc2287.c β€” 4.1 KB view raw
targsim.c β€” 13.9 KB view raw
run.log β€” 920 B view raw
env.txt β€” 191 B view raw
fix.diff β€” 466 B view raw
verdict.json β€” 2.4 KB view raw
README.md β€” 1.6 KB ↓ raw

DF-2287 β€” targread() abort_queue drain leaks every targ_cmd_descr

What (sys/bus/cam/scsi/scsi_target.c:921-937)

The abort_queue drain loop TAILQ_REMOVEs each targ_cmd_descr, writes CAM_REQ_ABORTED into the user CCB, hands the user_ccb pointer back via uiomove β€” and never kfree()s the descriptor. Descriptors enter abort_queue via targstart's send-failure path (:674-684) or abort_all_pending (:1084-1089); the only other free is targdtor's drain of what is STILL QUEUED at teardown (:545-547) β€” a drained-and-delivered descriptor is unreachable garbage.

Rig

Same as DF-2286 (targsim). Deterministic send failure: XPT_CONT_TARGET_IO with dxfer_len > MAXPHYS -> targsendccb -> cam_periph_mapmem (cam_periph.c:702-707) rejects with EPERM -> targstart error path -> abort_queue.

Reproduce (guest, root)

cc -O -Wall -o poc2287 poc2287.c
./poc2287            # 2000 oversized CTIO writes + 2000 reads

Result (run.log; multiple independent runs)

submitted 2000 oversized CTIOs
read back 2000 descriptors (pointer match 2000)
M_TARG line: TARG 1.95K 156K   ->   TARG 3.91K 313K
...                            ->   TARG 5.86K 469K
...                            ->   TARG 7.81K 625K  (monotone growth)
bytes-per-descr = 80           (sizeof(struct targ_cmd_descr))

Each read() drains exactly one descriptor from abort_queue and leaks exactly one ~80-byte M_TARG allocation; growth is linear and permanent. A RESTRICTEDROOT daemon drives it unboundedly.

Fix

fix.diff kfrees user_descr after the successful uiomove in the drain loop. Validated on the fix kernel (VERDICT.md): M_TARG stays flat.

Fix verification

fixed
↓ fix.diffper-fix-DF-2287

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: 2000/2000 pointer-matched drains; vmstat TARG 1.95K/156K -> 3.91K/313K -> 5.86K/469K -> 7.81K/625K across runs; ~80 B/descr

PoC changes

Authored from scratch; vmstat -m human-suffix parsing (1.95K/156K) had to be added (plain sscanf read 1 and 156); printf type fixes.

Verified recommended fix

kfree(user_descr, M_TARG) after the successful uiomove in the drain loop (fix.diff).

Verdict

REPRODUCED on the BUSAUDIT kernel with the targsim rig: 2000 deterministically-failing CTIOs (XPT_CONT_TARGET_IO with dxfer_len 1 MiB > MAXPHYS; cam_periph_mapmem rejects at cam_periph.c:702-707, targstart :674-684 parks each descr on abort_queue) followed by 2000 reads drained all descriptors with exact user_ccb pointer matches, while M_TARG grew monotonically across four independent measurement points: 1.95K/156K -> 3.91K/313K -> 5.86K/469K -> 7.81K/625K (vmstat -m TARG) = exactly ~80 bytes (sizeof targ_cmd_descr) leaked per drained descriptor, permanently. targread abort_queue drain (scsi_target.c:921-937) TAILQ_REMOVEs each descr and hands back the pointer without kfree; no other free path can reach a drained descriptor (targfreeccb is only called for user_ccb_queue entries; targdtor :545-547 only drains what is still queued). RESTRICTEDROOT gated; unbounded kernel-memory exhaustion.