pipe->open_count underflow on pipe_create partial failure leaks kernel KVA and pipe struct
| Field | Value |
|---|---|
| ID | DF-0031 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-401 Missing Release of Memory after Effective Lifetime |
| File | sys/kern/sys_pipe.c |
| Lines | 433-445, 286-290, 1272 |
| Area | kern |
| Confidence | likely |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
pipe_create sets *pipep = pipe before the pipespace() calls and only sets
open_count = 2 after both succeed. If the second pipespace() fails
(vm_map_find ENOMEM on kernel_map), pipe_create returns with
open_count still 0. kern_pipe's error path then calls pipeclose()
twice; each pipeclose does atomic_fetchadd_int(&open_count, -1) and frees
only when the old value is 1. With open_count = 0 the sequence underflows
0 โ 0xFFFFFFFF โ 0xFFFFFFFE, never == 1, so the pipe struct and
bufferA's KVA are leaked permanently and open_count is corrupted. This is a
self-amplifying KVA/struct leak (each open pipe holds kernel_map KVA, so
holding many pushes the system toward the failing precondition), reachable by
any unprivileged user โ a local availability amplifier.
Root cause
*pipep = pipe; /* :433 pipe handed out */
if ((error = pipespace(pipe, &pipe->bufferA, pipe_size)) != 0)
return (error); /* :434-435 */
if ((error = pipespace(pipe, &pipe->bufferB, pipe_size)) != 0)
return (error); /* :437-438 open_count still 0 */
...
pipe->open_count = 2; /* :445 reached only on full success */
kern_pipe (:286-290):
if (pipe_create(&pipe)) {
pipeclose(pipe, &pipe->bufferA, &pipe->bufferB); /* :287 */
pipeclose(pipe, &pipe->bufferB, &pipe->bufferA); /* :288 */
return (ENFILE);
}
pipeclose (:1272):
if (atomic_fetchadd_int(&pipe->open_count, -1) == 1) {
/* free/cache the pipe + buffers */
}
With open_count = 0 at the first pipeclose, fetchadd returns 0
(!= 1 โ no free), leaves open_count = 0xFFFFFFFF; the second pipeclose
returns 0xFFFFFFFF (!= 1), leaves 0xFFFFFFFE. Neither frees, so the pipe
struct + bufferA KVA leak, and open_count is left corrupted.
Threat model & preconditions
- Attacker position: any unprivileged local user.
- Privileges gained or impact: availability. Each failed
pipe_createleakssizeof(struct pipe)+PIPE_SIZEbytes ofkernel_mapKVA permanently and corruptsopen_count. Self-amplifying underkernel_mappressure (each held pipe consumes KVA, pushing toward the failing precondition). No confidentiality/integrity impact. - Required config or capabilities: none; default kernel.
- Reachability: hold many pipes to exhaust
kernel_map, thenpipe(2)repeatedly โ the secondpipespacefailure triggers the leak.
Proof of concept
PoC source: findings/poc/DF-0031/pipe_leak.c
Build & run (unprivileged, disposable VM)
cc -o pipe_leak findings/poc/DF-0031/pipe_leak.c ./pipe_leak
Expected output
Cumulative kernel_map free-space shrinkage and pipe-zone struct growth
(vmstat -z / vmstat -m) that never reclaims, worsening the OOM. No
standalone panic.
Impact
Low โ KVA/struct leak amplification, availability only. A maintainer- actionable error-path correctness fix.
Recommended fix
Set open_count = 2 before the pipespace() calls (so the pipeclose cleanup
on failure decrements cleanly to 0), or have pipe_create's failure path
free directly without relying on pipeclose's refcount gate:
--- a/sys/kern/sys_pipe.c
+++ b/sys/kern/sys_pipe.c
@@ -432,6 +432,7 @@
}
*pipep = pipe;
+ pipe->open_count = 2; /* set before pipespace() so error-path
+ pipeclose() cleanup decrements cleanly */
if ((error = pipespace(pipe, &pipe->bufferA, pipe_size)) != 0) {
return (error);
}
@@ -444,7 +445,6 @@
pipe->bufferB.atime = pipe->ctime;
pipe->bufferB.mtime = pipe->ctime;
- pipe->open_count = 2;
return (0);
(Verify the cached-pipe path at :421-424 resets open_count to 0 on
pop so the new open_count = 2 is correct there too.)
References
sys/kern/sys_pipe.c:433-445โopen_count = 2set after the failingpipespace.sys/kern/sys_pipe.c:286-290โkern_pipedouble-pipecloseerror cleanup.sys/kern/sys_pipe.c:1272โpipeclosefetchadd == 1free gate.- CWE-401 Missing Release of Memory after Effective Lifetime.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/sys_pipe.c. - pending Reported to DragonFlyBSD security contact.