DragonFlyBSD Kernel Audit
DF-3054 / harness.c
← back to finding ↓ download raw
/*
 * DF-3054 — dirfs_findfd() returns NULL when the accumulated relative path
 * exceeds MAXPATHLEN; every dirfs_vnops.c consumer dereferences the returned
 * pointer without a NULL check:
 *
 *   sys/vfs/dirfs/dirfs_vnops.c:389   pathnp = dirfs_findfd(dmp, dnp, &tmp, &pathfree);
 *   sys/vfs/dirfs/dirfs_vnops.c:391   KKASSERT(pathnp->dn_fd != DIRFS_NOFD);   <-- NULL deref
 *   sys/vfs/dirfs/dirfs_vnops.c:393   error = dirfs_node_stat(pathnp->dn_fd, tmp, dnp);
 *   sys/vfs/dirfs/dirfs_vnops.c:1326  pathnp = dirfs_findfd(dmp, dnp, &tmp, &pathfree);
 *   sys/vfs/dirfs/dirfs_vnops.c:1329  nlen = readlinkat(pathnp->dn_fd, ...);   <-- NULL deref
 *   sys/vfs/dirfs/dirfs_subr.c:194    dnp->dn_fd = openat(pathnp->dn_fd, ...)  <-- NULL deref
 *   sys/vfs/dirfs/dirfs_subr.c:202    error = dirfs_node_stat(pathnp->dn_fd, tmp, dnp);
 *          (reached from dirfs_nresolve :188, dirfs_ncreate :244,
 *           dirfs_nmkdir :1067, dirfs_nsymlink :1194)
 *
 * dirfs_findfd (sys/vfs/dirfs/dirfs_subr.c:450-497) only fails "cleanly"
 * (returns NULL, frees buf) when `count > MAXPATHLEN` at the final check
 * (subr.c:483).  The walk itself is guarded by `count <= MAXPATHLEN` on every
 * bcopy, so there is no overflow inside findfd — the crash is at the callers.
 *
 * Trigger model: root node always holds the mount's fd (opened at mount,
 * dirfs_vfsops.c:258-265).  Intermediate directory nodes only get an fd when
 * they are opened through dirfs (VOP_OPEN/getdents) or created via
 * dirfs_nmkdir's alloc_file(O_DIRECTORY).  Nodes created by dirfs_nresolve
 * NEVER have an fd (openflags==0 && vap==NULL skips openat).  The passive fd
 * cache (dirfs_fd_limit, default 100, dirfs_vfsops.c:77) evicts and closes
 * ancestor fds (dirfs_node_setpassive -> close, dirfs_subr.c:855-864; VINACTIVE
 * is set by vnode_terminate BEFORE VOP_INACTIVE, sys/kern/vfs_lock.c:505-508,
 * so the close condition is satisfied), and vnode recycling frees nodes
 * outright.  So a path whose component names sum to > 1024 bytes between the
 * node and the nearest fd-holding ancestor makes findfd return NULL and the
 * callers crash the (v)kernel.
 *
 * This harness transcribes the exact code (findfd loop verbatim; consumer
 * derefs verbatim) and proves the NULL dereference deterministically with
 * fork()+SIGSEGV detection.  It also proves the fixed variant (NULL check +
 * ENAMETOOLONG) does not crash.
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 * Expect: findfd(NULL) boundary check at 4x255 (count==1024, OK) and
 *         5x255 (count==1280 -> NULL); VULN getattr/readlink/alloc_file
 *         children die with SIGSEGV; FIXED children exit cleanly.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <assert.h>

#define MAXPATHLEN 1024
#define DIRFS_NOFD (-1)
#define DIRFS_ROOT 0x00000001

struct dirfs_node {
	int		dn_state;
	struct dirfs_node *dn_parent;
	int		dn_fd;
	char		*dn_name;
	int		dn_namelen;
};
typedef struct dirfs_node *dirfs_node_t;

#define dirfs_node_isroot(n)	((n)->dn_state & DIRFS_ROOT)

/* KKASSERT: active in INVARIANTS kernels; vkernel64 builds include it */
#define KKASSERT(exp)	assert(exp)

/*
 * verbatim transcription of dirfs_findfd (dirfs_subr.c:450-497)
 */
static dirfs_node_t
dirfs_findfd(dirfs_node_t cur, char **pathto, char **pathfreep)
{
	dirfs_node_t dnp1;
	int count;
	char *buf;

	*pathfreep = NULL;
	*pathto = NULL;

	if (cur == NULL)
		return NULL;

	buf = malloc(MAXPATHLEN + 1);		/* kmalloc(MAXPATHLEN+1,...) */
	count = 0;

	dnp1 = cur;
	while (dnp1 == cur || dnp1->dn_fd == DIRFS_NOFD) {	/* :470 */
		count += dnp1->dn_namelen;			/* :471 */
		if (count <= MAXPATHLEN) {
			memcpy(&buf[MAXPATHLEN - count], dnp1->dn_name,
			       dnp1->dn_namelen);		/* :473 */
		}
		++count;					/* :476 */
		if (count <= MAXPATHLEN)
			buf[MAXPATHLEN - count] = '/';		/* :478 */
		dnp1 = dnp1->dn_parent;				/* :479 */
		KKASSERT(dnp1 != NULL);				/* :480 */
	}

	if (dnp1 && count <= MAXPATHLEN) {			/* :483 */
		*pathfreep = buf;
		*pathto = &buf[MAXPATHLEN - count + 1];
		return (dnp1);
	} else {
		free(buf);					/* kfree */
		*pathfreep = NULL;
		*pathto = NULL;
		return (NULL);					/* <-- NULL return */
	}
}

/*
 * Build a chain: root(fd) -> d1 -> d2 ... -> dN, intermediate nodes with
 * dn_fd == DIRFS_NOFD (as created by dirfs_nresolve; or after passive-fd
 * eviction / vnode recycling).
 */
static dirfs_node_t
build_chain(int depth, int namelen, int *out_count)
{
	char name[256];
	dirfs_node_t root, prev, n;
	int i, count = 0;

	memset(name, 'a', sizeof(name));
	root = calloc(1, sizeof(*root));
	root->dn_state = DIRFS_ROOT;
	root->dn_fd = 3;			/* mount root: fd opened at mount */
	root->dn_name = strdup("");
	root->dn_namelen = 0;

	prev = root;
	for (i = 0; i < depth; i++) {
		n = calloc(1, sizeof(*n));
		n->dn_fd = DIRFS_NOFD;		/* nresolve-created: no fd */
		n->dn_name = strndup(name, namelen);
		n->dn_namelen = namelen;
		n->dn_parent = prev;
		count += namelen + 1;
		prev = n;
	}
	*out_count = count;
	return prev;				/* deepest node */
}

/* ---- consumer transcriptions (dirfs_vnops.c) ---- */

/* dirfs_getattr :388-394 */
static void
vuln_getattr(dirfs_node_t dnp)
{
	dirfs_node_t pathnp;
	char *tmp, *pathfree;
	volatile int fd_sink;

	pathnp = dirfs_findfd(dnp, &tmp, &pathfree);	/* :389 */
	KKASSERT(pathnp->dn_fd != DIRFS_NOFD);		/* :391  NULL DEREF */
	fd_sink = pathnp->dn_fd;			/* :393  dirfs_node_stat(pathnp->dn_fd, ...) */
	printf("  getattr: not reached (fd=%d)\n", fd_sink);
}

/* dirfs_readlink :1326-1329 */
static void
vuln_readlink(dirfs_node_t dnp)
{
	dirfs_node_t pathnp;
	char *tmp, *pathfree;
	char buf[16];
	ssize_t nlen;

	pathnp = dirfs_findfd(dnp, &tmp, &pathfree);	/* :1326 */
	/* kmalloc(uio_resid) elided */
	nlen = readlinkat(pathnp->dn_fd, dnp->dn_name,	/* :1329  NULL DEREF */
			  buf, sizeof(buf));
	printf("  readlink: not reached (nlen=%zd)\n", nlen);
}

/* dirfs_alloc_file :191-202 via dirfs_nresolve :188 */
static void
vuln_alloc_file(dirfs_node_t dnp)
{
	dirfs_node_t pathnp;
	char *tmp, *pathfree;
	int fd;

	pathnp = dirfs_findfd(dnp, &tmp, &pathfree);	/* :191 */
	/* nresolve passes openflags==0 && vap==NULL -> openat skipped */
	fd = fstatat(pathnp->dn_fd, tmp, NULL, 0);	/* :202 stat(pathnp->dn_fd,...) NULL DEREF */
	printf("  alloc_file: not reached (fd=%d)\n", fd);
}

/* ---- FIXED variants: NULL check before use ---- */
static void
fixed_getattr(dirfs_node_t dnp)
{
	dirfs_node_t pathnp;
	char *tmp, *pathfree;
	int error = 0;

	pathnp = dirfs_findfd(dnp, &tmp, &pathfree);
	if (pathnp == NULL) {
		error = ENAMETOOLONG;
		goto out;
	}
	error = fstatat(pathnp->dn_fd, tmp, NULL, 0);
out:
	if (pathfree)
		free(pathfree);
	printf("  FIXED getattr: handled, error=%d (ENAMETOOLONG=%d)\n",
	       error, ENAMETOOLONG);
}

static void
fixed_readlink(dirfs_node_t dnp)
{
	dirfs_node_t pathnp;
	char *tmp, *pathfree;
	int error = 0;

	pathnp = dirfs_findfd(dnp, &tmp, &pathfree);
	if (pathnp == NULL) {
		error = ENAMETOOLONG;
		goto out;
	}
	error = readlinkat(pathnp->dn_fd, dnp->dn_name, NULL, 0) == -1;
out:
	if (pathfree)
		free(pathfree);
	printf("  FIXED readlink: handled, error=%d\n", error ? EIO : 0);
}

static void
fixed_alloc_file(dirfs_node_t dnp)
{
	dirfs_node_t pathnp;
	char *tmp, *pathfree;

	pathnp = dirfs_findfd(dnp, &tmp, &pathfree);
	if (pathnp == NULL) {
		printf("  FIXED alloc_file: handled, return ENAMETOOLONG\n");
		return;
	}
	(void)fstatat(pathnp->dn_fd, tmp, NULL, 0);
}

static int
run_child(void (*fn)(dirfs_node_t), dirfs_node_t dnp, const char *what)
{
	pid_t pid;
	int status;

	fflush(NULL);
	pid = fork();
	if (pid == 0) {
		fn(dnp);
		_exit(0);
	}
	waitpid(pid, &status, 0);
	if (WIFSIGNALED(status)) {
		printf("  [%s] child killed by SIGSEGV -- NULL DEREF "
		       "CONFIRMED (fault at deref of findfd() result)\n", what);
		return 1;
	}
	if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
		return 0;
	printf("  [%s] child exited abnormally status=%d\n", what, status);
	return -1;
}

int
main(void)
{
	int count4, count5, crashes = 0;
	dirfs_node_t deep4, deep5;
	char *tmp, *pathfree;

	setvbuf(stdout, NULL, _IONBF, 0);

	deep4 = build_chain(4, 255, &count4);
	deep5 = build_chain(5, 255, &count5);

	printf("== dirfs_findfd boundary transcription (dirfs_subr.c:450-497)\n");
	printf("chain 4x255: accumulated count=%d (MAXPATHLEN=%d)\n",
	       count4, MAXPATHLEN);
	if (dirfs_findfd(deep4, &tmp, &pathfree) != NULL) {
		printf("  -> returns node (count <= MAXPATHLEN): path OK: "
		       "\"%.32s...\"\n", tmp);
		free(pathfree);
	} else {
		printf("  -> NULL (unexpected at boundary)\n");
		return 1;
	}
	printf("chain 5x255: accumulated count=%d (MAXPATHLEN=%d)\n",
	       count5, MAXPATHLEN);
	if (dirfs_findfd(deep5, &tmp, &pathfree) == NULL) {
		printf("  -> returns NULL cleanly (count > MAXPATHLEN): "
		       "findfd itself does NOT crash -- crash is at callers\n");
	} else {
		printf("  -> unexpected non-NULL\n");
		return 1;
	}

	printf("\n== VULNERABLE consumer transcriptions (dirfs_vnops.c)\n");
	printf("dirfs_getattr :391/:393\n");
	if (run_child(vuln_getattr, deep5, "getattr") == 1) crashes++;
	printf("dirfs_readlink :1329\n");
	if (run_child(vuln_readlink, deep5, "readlink") == 1) crashes++;
	printf("dirfs_alloc_file :202 (via nresolve/ncreate/nmkdir/nsymlink)\n");
	if (run_child(vuln_alloc_file, deep5, "alloc_file") == 1) crashes++;

	printf("\n== FIXED consumer transcriptions (NULL check + error)\n");
	fixed_getattr(deep5);
	fixed_readlink(deep5);
	fixed_alloc_file(deep5);

	printf("\nRESULT: %d/3 vulnerable consumers crashed with SIGSEGV; "
	       "fixed variants survived => BUG %s, FIX %s\n",
	       crashes,
	       crashes == 3 ? "CONFIRMED" : "PARTIAL",
	       crashes == 3 ? "VALIDATED" : "CHECK");
	return crashes == 3 ? 2 : 0;
}