โฌข DragonFlyBSD Kernel Audit
DF-3055 / harness.c
โ† back to finding โ†“ download raw
/*
 * 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;
}