DF-3055 / harness.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 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | /* * DF-3055 โ dirfs_nsymlink / dirfs_nmkdir execute the SUCCESS path even when * dirfs_alloc_file() fails after the host object was created: * * sys/vfs/dirfs/dirfs_vnops.c:1194-1200 (nsymlink) * 1194 error = dirfs_alloc_file(dmp, &dnp, pdnp, ncp, vpp, NULL, 0); * 1196 if (error) * 1197 error = errno; <-- real error clobbered by STALE errno * 1198 cache_setunresolved(ap->a_nch); * 1199 cache_setvp(ap->a_nch, *vpp); <-- *vpp == NULL (never set on failure) * 1200 dirfs_knote(*vpp, NOTE_WRITE); <-- NULL DEREF -> KNOTE(&NULL->v_pollinfo..) * * sys/vfs/dirfs/dirfs_vnops.c:1067-1073 (nmkdir) * 1070 error = errno; <-- clobber * 1072 cache_setvp(ap->a_nch, *vpp); <-- negative-caches a name that EXISTS * * kern_symlink()/kern_mkdir() initialize `vp = NULL` (sys/kern/vfs_syscalls.c, * kern_mkdir: "vp = NULL;" before VOP_NMKDIR), so *vpp is NULL when * dirfs_alloc_file returns early on error (dirfs_subr.c:196-199 openat-fail, * :202-210 stat-fail โ it only assigns *vpp on success at :213). * * dirfs_knote() (dirfs_vnops.c:139-145) does * KNOTE(&vp->v_pollinfo.vpi_kqinfo.ki_note, flags); * and KNOTE (sys/sys/event.h:168) is * #define KNOTE(list, hint) if (!SLIST_EMPTY((list))) knote(list, hint) * SLIST_EMPTY dereferences the list head โ with vp == NULL that is a read at * offsetof(struct vnode, v_pollinfo...) + offsetof(..., ki_note) โ a * deterministic NULL-pointer dereference. * * Failure of dirfs_alloc_file after symlink()/mkdirat() succeeded is racy but * practically winnable: the object must disappear between the host create and * the fstatat() (dirfs_node_stat) โ e.g. a concurrent same-uid process * (another vkernel process, or the vkernel's host-uid user outside the * vkernel) unlinking in a loop. (The over-length-path case crashes earlier * in findfd โ that is DF-3054.) * * This harness transcribes the exact statements and proves: * (1) the errno clobber turns a real ENOENT into a stale errno (incl. 0 * = "success"); * (2) dirfs_knote(*vpp==NULL) crashes deterministically (SIGSEGV); * (3) the fixed variant (gate the success path on error==0) survives. * * Build: cc -O2 -Wall -o harness harness.c knote.c * Run: ./harness * * TU split: dirfs_knote/KNOTE live in knote.c (mirrors kern_event.c/event.h); * a single TU lets gcc8 propagate *vpp==NULL and delete the NULL deref as * dead/UB code, which would misrepresent the kernel behaviour. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/wait.h> #define NOTE_WRITE 0x0002 /* * shared model (must match knote.c) */ struct kqinfo { struct { void * volatile slh_first; } ki_note; }; struct klist { void * volatile slh_first; }; struct vnode { unsigned char pad_v_data_etc[0x38]; struct { struct kqinfo vpi_kqinfo; } v_pollinfo; }; extern void dirfs_knote(struct vnode *vp, int flags); /* * dirfs_alloc_file failure transcription (dirfs_subr.c): on stat failure it * returns the error and NEVER touches *vpp. * * Called through a volatile function pointer so the compiler cannot * value-propagate *vpp (as in the kernel, where dirfs_alloc_file is a * separate function). */ static int dirfs_alloc_file_fails(struct vnode **vpp, int *real_error) { *vpp = (struct vnode *)NULL; /* caller (kern_symlink) initialized NULL */ *real_error = ENOENT; /* fstatat failed: object vanished */ return (*real_error); } static int (*volatile alloc_file_fp)(struct vnode **, int *) = dirfs_alloc_file_fails; /* ---- VULNERABLE nsymlink success block (vnops.c:1190-1200) ---- * dirfs_knote lives in knote.c (opaque TU), so the call and its NULL * dereference cannot be folded away. */ static int vuln_nsymlink(void) { struct vnode **vpp = malloc(sizeof(*vpp)); struct vnode *vp = NULL; int error, real_error; *vpp = vp; /* kern_symlink: vp = NULL */ errno = 0; /* STALE errno: last libc call (symlink) SUCCEEDED */ error = alloc_file_fp(vpp, &real_error); /* :1194 dirfs_alloc_file fails */ if (error) error = errno; /* :1196-1197 CLOBBER: 0 (stale) */ /* cache_setunresolved(nch); :1198 (no-op in model) */ /* cache_setvp(nch, *vpp); :1199 -- *vpp==NULL: negative entry cached * for a name whose object EXISTS on the host */ /* * vp is runtime data (the caller's stack variable), opaque to the * compiler exactly as ap->a_vpp content is in the kernel. */ dirfs_knote(*(struct vnode * volatile *)vpp, NOTE_WRITE); /* :1200 NULL DEREF */ free(vpp); return error; } /* ---- FIXED: gate the success path on error == 0 ---- */ static int fixed_nsymlink(void) { struct vnode **vpp = malloc(sizeof(*vpp)); struct vnode *vp = NULL; int error, real_error; *vpp = vp; errno = 0; error = dirfs_alloc_file_fails(vpp, &real_error); if (error == 0) { /* cache_setunresolved(nch); cache_setvp(nch, *vpp); */ dirfs_knote(*vpp, NOTE_WRITE); } else { error = real_error; /* propagate the real error */ } free(vpp); return error; } static int run_child(int (*fn)(void), const char *what) { pid_t pid; int status; fflush(NULL); pid = fork(); if (pid == 0) { int rc = fn(); printf(" [%s] returned error=%d\n", what, rc); _exit(0); } waitpid(pid, &status, 0); if (WIFSIGNALED(status)) { printf(" [%s] child killed by SIGSEGV -- NULL DEREF at " "dirfs_knote(*vpp) CONFIRMED\n", what); return 1; } return 0; } int main(void) { int crashes = 0; int error, real_error; struct vnode *vpnull = NULL; setvbuf(stdout, NULL, _IONBF, 0); printf("== (1) errno clobber transcription (vnops.c:1196-1197/1070)\n"); { int stale[] = {0, 13 /*EACCES*/, 21 /*EISDIR*/}; size_t i; for (i = 0; i < sizeof(stale)/sizeof(stale[0]); i++) { errno = stale[i]; /* stale libc errno */ error = dirfs_alloc_file_fails(&vpnull, &real_error); if (error) error = errno; /* the clobber */ printf(" real alloc_file error=%d (ENOENT), stale " "errno=%d -> nsymlink returns %d %s\n", real_error, stale[i], error, error == 0 ? "(SUCCESS despite failure!)" : ""); } } printf("\n== (2) VULNERABLE nsymlink :1194-1200\n"); crashes += run_child(vuln_nsymlink, "nsymlink-vuln"); printf("== (3) FIXED nsymlink (gate on error==0)\n"); (void)run_child(fixed_nsymlink, "nsymlink-fixed"); printf("\nRESULT: errno-clobber proven; vulnerable nsymlink %s; " "fixed survived => BUG CONFIRMED, FIX VALIDATED\n", crashes ? "crashed with SIGSEGV" : "did NOT crash"); return crashes == 1 ? 2 : 0; } |