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

complete_rqe() volume error path abandons the request: recover_io() re-strategizes the bio but nobody frees the old request β€” full request leak (rq + rqg chain + malloc'd buffers + locked bufs) per errored volume I/O, unbounded with VF_RETRYERRORS; VOL[].active never decremented on error

Summary

The daemon's recover_io re-strategizes the bio (a NEW request is built) but never calls freerq on the old one nor drops VOL[].active - with VF_RETRYERRORS every retry re-fails and re-queues: unbounded request+locked-buf leak at I/O speed. Inspection-certain (contrast the success path); not injectable on the guest's md backing (same gap as DF-4481). Own-the-handoff fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4896 Β· 3 files
FileTypeDescriptionSize
README.md β€” 3.5 KB ↓ raw
manifest.json β€” 745 B view raw
verdict.json β€” 2.8 KB view raw

DF-4896 β€” complete_rqe() volume error path abandons the request: recover_io() re-strategizes the bio but nobody frees the old request β€” full request leak (rq + rqg chain + malloc'd buffers + locked bufs) per errored volume I/O; VOL[].active accounting also leaked

Root cause (inspection-certain)

sys/dev/raid/vinum/vinuminterrupt.c:219-226:

if (rq->error) {            /* did we have an error? */
if (rq->isplex) {       /* plex operation,  (DF-4481) */
    ubio->bio_buf->b_flags |= B_ERROR;
    ubio->bio_buf->b_error = rq->error;
} else {            /* try to recover */
    di.rq = rq;
    queue_daemon_request(daemonrq_ioerror, di); /* let the daemon complete */
}
}

The daemon's only consumer of daemonrq_ioerror is recover_io() (sys/dev/raid/vinum/vinumdaemon.c:198-209):

void recover_io(struct request *rq) {
dev_dstrategy((cdev_t)rq->bio->bio_driver_info, rq->bio);
}

It re-strategizes the user bio; vinumstart() then builds a brand-new request. The OLD struct request β€” the whole rqg chain, every XFR_MALLOCED buffer (RAID-5 parity/group buffers, KBs each), and the XFR_BUFLOCKED buf locks taken in build_rq_buffer() β€” is never freed (freerq() is only called on the success path at vinuminterrupt.c:232 and in vinumstart's build-failure paths at vinumrequest.c:273/300).

Additionally VOL[rq->volplex.volno].active (incremented in vinumstart(), vinumrequest.c:231) is decremented only in the success branch (vinuminterrupt.c:229-230) β€” the error branch leaks the count too (currently a write-only counter; cosmetic but part of the same defect).

Amplification

If the failing subdisk has VF_RETRYERRORS set (the error path at :102/:113 then leaves the sd up), every daemon retry rebuilds, re-fails, re-queues: an unbounded retry loop leaking one complete request per iteration at I/O speed β†’ kernel memory exhaustion + vinum daemon livelock. Without the flag, the sd is crashed by the error path and the retry fails cleanly (EIO) β€” leak is then bounded by the error rate (persistent hardware faults still leak unboundedly over time).

Trigger / PoC

Requires a component bio error at completion time (failing device). PoC sketch (trigger.sh, untested): RAID-5 volume per DF-4895 setup; loop writes; make one drive's device fail; observe M_DEVBUF growth via vmstat -m and the missing freerq (locked bufs accumulate).

Guest verification not possible: this guest's vinum drives sit on md (malloc) devices that cannot be made to fail I/O β€” re-verified this run (2026-09-24): gpt destroy/gpt remove under open vinum I/O leave the stale slice cdev serving requests with no error; no mdconfig failure-injection facility exists on the image. Same blocker as DF-4481 (its verdict recorded the same). Classification: untested, certain by inspection.

Fix (proposed; not applied)

Own the handoff: free the old request when the retry is issued.

--- a/sys/dev/raid/vinum/vinumdaemon.c +++ b/sys/dev/raid/vinum/vinumdaemon.c @@ -205,7 +205,13 @@ recover_io(struct request rq) / * Reissue the command. / - dev_dstrategy((cdev_t)rq->bio->bio_driver_info, rq->bio); + { + struct bio bio = rq->bio; + cdev_t dev = (cdev_t)bio->bio_driver_info; + if (rq->isplex == 0) + VOL[rq->volplex.volno].active--; / error path never drops it / + freerq(rq); / the old request is abandoned / + dev_dstrategy(dev, bio); / reissue under a new request / + } }

(severity Low: requires root-triggerable I/O errors / failing hardware; impact is unbounded kernel-memory + locked-buf leak per error.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable: no failure-injection facility on the guest to stage the leak

README.md proposed diff
per-fix-DF-4896

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md (line-cited dataflow: vinuminterrupt.c:219-226 handoff, vinumdaemon.c:198-209 abandonment, success-path contrast); amplification via VF_RETRYERRORS loop

PoC changes

no seed; trigger sketch (trigger.sh waiting on a failure-injection facility) documented in README

Verified recommended fix

recover_io(): capture bio/dev, drop VOL[].active for volume requests, freerq(rq), then dev_dstrategy(dev, bio) (diff in README.md).

Verdict

Inspection-certain leak, not runnable on this guest: complete_rqe() hands the failed volume request to the daemon (vinuminterrupt.c:223-225) whose only consumer recover_io() (vinumdaemon.c:198-209) re-strategizes the bio via dev_dstrategy without ever calling freerq() on the old request β€” compare the success path (vinuminterrupt.c:231-232) and vinumstart's failure paths (vinumrequest.c:273/:300) which all freerq. Triggering requires a component bio error at completion; this guest's md-backed vinum drives cannot be made to fail I/O (re-verified 2026-09-24: gpt destroy/gpt remove under open vinum I/O leave the stale slice cdev serving requests with no error; no mdconfig fault-injection on the image) β€” same facility gap DF-4481 recorded. Proposed fix (freerq + vol->active-- in recover_io before re-strategy) in README.md; not buildable/validatable without error injection.