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)
PoC verification
Evidence pack
findings/poc/DF-4896 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_testablenot_testable: no failure-injection facility on the guest to stage the leak
README.md proposed diff
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.
No comments yet.