โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0031

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

sys/kern/sys_pipe.c:

*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_create leaks sizeof(struct pipe) + PIPE_SIZE bytes of kernel_map KVA permanently and corrupts open_count. Self-amplifying under kernel_map pressure (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, then pipe(2) repeatedly โ€” the second pipespace failure 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.

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

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/sys_pipe.c.
  • pending Reported to DragonFlyBSD security contact.