โฌข DragonFlyBSD Kernel Audit
DF-0856 / trigger.c
โ† back to finding โ†“ download raw
/*
 * DF-0856 โ€” dirfs_alloc_file openat error path leaks dirfs node + parent refcount
 *
 * Source-level analysis / PoC harness.
 *
 * dirfs is a vkernel64-only filesystem (sys/platform/vkernel64/conf/files
 * lists the three dirfs .c files as `optional dirfs`, and the default
 * sys/config/VKERNEL64 does NOT enable dirfs). dirfs is therefore not
 * compiled into the running guest kernel (X86_64_GENERIC) at all โ€”
 * `nm /boot/kernel/kernel.debug | grep -i dirfs` returns 0 hits โ€” and is
 * not even in the default VKERNEL64 build.
 *
 * The bug is a pure resource leak in an error path, fully visible in the
 * source at sys/vfs/dirfs/dirfs_subr.c:193-200. Reproduction for this
 * finding is therefore code-level (no escalation, no memory corruption โ€”
 * it is CWE-401 / Low).
 *
 * --- THE BUG (sys/vfs/dirfs/dirfs_subr.c, dirfs_alloc_file) -----------
 *
 *   182:  dnp = dirfs_node_alloc(mp);                  // kmalloc(sizeof(*dnp))
 *   ...
 *   186:  dirfs_node_setname(dnp, ncp->nc_name, ...);  // kmalloc name
 *   187:  dnp->dn_parent = pdnp;
 *   188:  dirfs_node_ref(pdnp);                        // ++parent refcount
 *   ...
 *   193:  if (openflags && vap != NULL) {
 *   194:      dnp->dn_fd = openat(pathnp->dn_fd, tmp,
 *   195:                          openflags, vap->va_mode);
 *   196:      if (dnp->dn_fd == -1) {
 *   197:          dirfs_dropfd(dmp, pathnp, pathfree); // frees path only
 *   198:          return errno;                        // LEAKS dnp + parent ref
 *   199:      }
 *   200:  }
 *
 * Compare the stat error path at lines 203-209 which does it correctly:
 *
 *   202:  error = dirfs_node_stat(pathnp->dn_fd, tmp, dnp);
 *   203:  if (error) {
 *   204:      error = errno;
 *   205:      if (vp)
 *   206:          dirfs_free_vp(dmp, dnp);
 *   207:      dirfs_node_free(dmp, dnp);                // <-- frees dnp AND
 *   208:      dirfs_dropfd(dmp, pathnp, pathfree);      //     drops parent ref
 *   209:      return error;
 *   210:  }
 *
 * dirfs_node_free() (line 106) drops the parent reference via
 * dirfs_node_drop(dmp, dnp->dn_parent) at lines 123-126, then kfrees
 * dn_name and the node itself. So the openat error path MUST call it too.
 *
 * --- IMPACT ----------------------------------------------------------
 *
 * Each failed openat (e.g. O_CREAT in a read-only or quota-exceeded
 * directory, or any openat that returns -1) leaks:
 *   - one struct dirfs_node (~sizeof dirfs_node, includes a lock and
 *     pointers, ~200+ bytes) and
 *   - one kmalloc'd dn_name (ncp->nc_nlen+1 bytes)
 *   - and permanently inflates the parent's refcount (dirfs_node_ref on
 *     line 188 is never balanced by dirfs_node_drop on the error path),
 *     which prevents the parent from ever being freed and can leave the
 *     mount un-unmountable.
 *
 * Sustained triggering yields kernel heap exhaustion (denial of service)
 * inside the vkernel. Because dirfs runs as a host userland process
 * (the vkernel), the leak is heap growth in the vkernel process, not in
 * the host kernel โ€” but it still denies service to the vkernel's
 * filesystem layer and can wedge unmount.
 *
 * --- NO ESCALATION ---------------------------------------------------
 *
 * This is a pure resource leak (CWE-401). No memory corruption, no
 * primitive for uid=0. Realistic impact: DoS of the vkernel via heap
 * exhaustion / unmount failure; no host-kernel impact because dirfs is
 * vkernel-only.
 */

int main(void) { return 0; }