โฌข DragonFlyBSD Kernel Audit
DF-3056 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-3056 โ€” dirfs_nrename updates the node's NAME but never its PARENT when
 * a file is renamed across directories:
 *
 *   sys/vfs/dirfs/dirfs_vnops.c:984-987
 *      if (error == 0) {
 *          vp = fncp->nc_vp;                     the moved file's vnode
 *          dnp = VP_TO_NODE(vp);
 *          dirfs_node_setname(dnp, tncp->nc_name, tncp->nc_nlen);   <-- name only
 *          (no dnp->dn_parent update for fdnp != tdnp)
 *
 * Every subsequent operation on that node builds its host path by walking
 * dn_parent (dirfs_findfd dirfs_subr.c:470-481 and
 * dirfs_node_absolute_path[_plus] dirfs_subr.c:412-425), i.e. through the OLD
 * directory.  fd-based I/O (dirfs_strategy pwrite/pread on dn_fd) keeps
 * hitting the ORIGINAL inode, while path-based operations
 *   - dirfs_getattr            (vnops.c:389-393, dirfs_node_stat)
 *   - setattr: chflags :477, chsize :495, chown :521, chmod :545,
 *              chtimes :563 (all via dirfs_node_absolute_path -> lchmod/
 *              lchown/lchflags/lutimes/truncate)
 *   - dirfs_nremove            (vnops.c:907-909, unlinkat via dirfs_findfd)
 * resolve the OLD path and therefore operate on WHATEVER FILE NOW OCCUPIES
 * THE OLD PATH.
 *
 * The generic VFS layer permission checks run against the *vnode's cached
 * attributes* (the file the user owns and is allowed to modify), and the host
 * syscalls execute with the vkernel process's uid โ€” so an unprivileged
 * vkernel user can rename their own file out of a shared directory and then
 * chmod/truncate/utimes/chflags the file another user creates at the old
 * path.  (Classic wrong-file / stale-handle confusion โ€” the DF-2979 lesson.)
 *
 * Additionally the passive-fd-list key (dnp->dn_parent, dnp->dn_name) used by
 * dirfs_nresolve (vnops.c:172-174) no longer matches the new location, so the
 * same host file can get a second dirfs node (duplicate vp/dnp for one
 * inode).
 *
 * This harness reproduces the DECISION LOGIC exactly: it maintains the dirfs
 * node graph (root->dirA->f and root->dirB), performs the host rename the way
 * dirfs_nrename does (rename(fpath, tpath) with absolute paths built by the
 * transcribed dirfs_node_absolute_path), applies the exact post-rename node
 * update (setname only), then transcribes dirfs_node_chmod (lchmod on the
 * path built from the node graph) and shows it hits the file at the OLD path.
 * All file operations are REAL syscalls on a real directory tree.
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 * Expect: VULN: chmod applies to <root>/dirA/f (the "victim" file that
 *         re-occupied the old path) while <root>/dirB/f (the actual target
 *         vnode) is untouched.  FIXED (parent updated): chmod applies to
 *         <root>/dirB/f.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.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)

static char dmp_path[MAXPATHLEN];	/* dm_path: host mount prefix */

/* dirfs_node_setname (dirfs_subr.c:57-67) */
static void
dirfs_node_setname(dirfs_node_t dnp, const char *name, int len)
{
	free(dnp->dn_name);
	dnp->dn_name = malloc(len + 1);
	memcpy(dnp->dn_name, name, len);
	dnp->dn_name[len] = 0;
	dnp->dn_namelen = len;
}

/*
 * verbatim transcription of dirfs_node_absolute_path_plus
 * (dirfs_subr.c:377-443) โ€” only the parts that matter for path building.
 */
static char *
absolute_path(dirfs_node_t cur, char *last, char **pathfreep)
{
	size_t len;
	dirfs_node_t dnp1;
	char *buf;
	int count;

	*pathfreep = NULL;
	if (cur == NULL)
		return NULL;
	buf = malloc(MAXPATHLEN + 1);

	count = 0;
	buf[MAXPATHLEN] = 0;
	if (last) {
		len = strlen(last);
		count += len;
		if (count <= MAXPATHLEN)
			memcpy(&buf[MAXPATHLEN - count], last, len);
		++count;
		if (count <= MAXPATHLEN)
			buf[MAXPATHLEN - count] = '/';
	}

	dnp1 = cur;
	while (dirfs_node_isroot(dnp1) == 0) {
		count += dnp1->dn_namelen;
		if (count <= MAXPATHLEN)
			memcpy(&buf[MAXPATHLEN - count], dnp1->dn_name,
			       dnp1->dn_namelen);
		++count;
		if (count <= MAXPATHLEN)
			buf[MAXPATHLEN - count] = '/';
		dnp1 = dnp1->dn_parent;
		if (dnp1 == NULL)
			break;
	}

	len = strlen(dmp_path);
	count += len;
	if (dnp1 && count <= MAXPATHLEN) {
		memcpy(&buf[MAXPATHLEN - count], dmp_path, len);
		*pathfreep = buf;
		return (&buf[MAXPATHLEN - count]);
	} else {
		free(buf);
		*pathfreep = NULL;
		return (NULL);
	}
}

/* dirfs_node_chmod (dirfs_subr.c:711-725) โ€” lchmod on the built path */
static int
node_chmod(dirfs_node_t dnp, mode_t mode)
{
	char *tmp, *pathfree;
	int error = 0;

	tmp = absolute_path(dnp, NULL, &pathfree);
	if (lchmod(tmp, mode) < 0)			/* subr.c:719 */
		error = errno;
	free(pathfree);
	return error;
}

/* getattr flavour: stat through the built path (vnops.c:389-393) */
static int
node_stat_mode(dirfs_node_t dnp, mode_t *mode)
{
	char *tmp, *pathfree;
	struct stat st;
	int error = 0;

	tmp = absolute_path(dnp, NULL, &pathfree);
	if (lstat(tmp, &st) < 0)			/* dirfs_node_stat -> lstat */
		error = errno;
	else
		*mode = st.st_mode & 0777;
	free(pathfree);
	return error;
}

static void
mknode(dirfs_node_t n, const char *name, dirfs_node_t parent, int state, int fd)
{
	n->dn_state = state;
	n->dn_parent = parent;
	n->dn_fd = fd;
	n->dn_name = strdup(name);
	n->dn_namelen = strlen(name);
}

static void
write_file(const char *path, const char *content)
{
	FILE *f = fopen(path, "w");
	if (!f) { perror(path); exit(1); }
	fputs(content, f);
	fclose(f);
}

int
main(void)
{
	char pathbuf[MAXPATHLEN];
	struct dirfs_node root, dirA, dirB, fnode;
	char *fpath, *fpathfree, *tpath, *tpathfree;
	struct stat st;
	mode_t mode = 0;
	int error;
	char rootdir[] = "/tmp/df3056XXXXXX";

	if (mkdtemp(rootdir) == NULL) { perror("mkdtemp"); return 1; }
	snprintf(dmp_path, sizeof(dmp_path), "%s", rootdir);

	/* host tree: <root>/dirA/f (owned content), <root>/dirB */
	snprintf(pathbuf, sizeof(pathbuf), "%s/dirA", rootdir);
	if (mkdir(pathbuf, 0755) < 0) { perror("mkdir dirA"); return 1; }
	snprintf(pathbuf, sizeof(pathbuf), "%s/dirB", rootdir);
	if (mkdir(pathbuf, 0755) < 0) { perror("mkdir dirB"); return 1; }
	snprintf(pathbuf, sizeof(pathbuf), "%s/dirA/f", rootdir);
	write_file(pathbuf, "ORIGINAL-MOVED-FILE-CONTENT\n");
	if (chmod(pathbuf, 0644) < 0) { perror("chmod"); return 1; }

	/* dirfs node graph exactly as dirfs would build it */
	mknode(&root, "", NULL, DIRFS_ROOT, 3);
	mknode(&dirA, "dirA", &root, 0, DIRFS_NOFD);
	mknode(&dirB, "dirB", &root, 0, DIRFS_NOFD);
	mknode(&fnode, "f", &dirA, 0, DIRFS_NOFD);	/* f lives in dirA */

	printf("mount root (dm_path) = %s\n", rootdir);

	/* --- dirfs_nrename transcription (vnops.c:977-987) --- */
	tpath = absolute_path(&dirB, "f", &tpathfree);	/* :977 absolute_path_plus(tdnp,"f") */
	fpath = absolute_path(&dirA, "f", &fpathfree);	/* :979 absolute_path_plus(fdnp,"f") */
	printf("nrename: rename(\"%s\", \"%s\")\n", fpath, tpath);
	if (rename(fpath, tpath) < 0) { perror("rename"); return 1; } /* :981 */
	/* post-rename node update โ€” THE BUG (vnops.c:987): name only, no parent */
	dirfs_node_setname(&fnode, "f", 1);
	printf("post-rename node: name=\"%s\" parent=\"%s\" (parent NOT updated "
	       "-- dirfs_vnops.c:987 has no dn_parent update)\n",
	       fnode.dn_name, fnode.dn_parent->dn_name);
	free(fpathfree); free(tpathfree);

	/* --- victim re-occupies the OLD path (another user's file) --- */
	snprintf(pathbuf, sizeof(pathbuf), "%s/dirA/f", rootdir);
	write_file(pathbuf, "VICTIM-SECRET-CONTENT\n");
	if (chmod(pathbuf, 0600) < 0) { perror("chmod victim"); return 1; }
	printf("\nvictim file created at OLD path %s/dirA/f (mode 0600)\n",
	       rootdir);

	/* --- user chmods THEIR OWN (moved) file via the stale node --- */
	printf("\nVULN: user calls chmod 0777 on their moved file "
	       "(vnode == dirB/f)\n");
	printf("      generic layer checks pass (user owns the vnode); "
	       "dirfs_node_chmod builds:\n");
	{
		char *tmp, *pf;
		tmp = absolute_path(&fnode, NULL, &pf);
		printf("      lchmod(\"%s\", 0777)   <-- STALE PATH\n", tmp);
		free(pf);
	}
	error = node_chmod(&fnode, 0777);
	printf("      lchmod returned error=%d\n", error);

	snprintf(pathbuf, sizeof(pathbuf), "%s/dirA/f", rootdir);
	lstat(pathbuf, &st);
	printf("      stat(%s/dirA/f) [the VICTIM]:  mode=%04o  %s\n",
	       rootdir, st.st_mode & 0777,
	       (st.st_mode & 0777) == 0777 ?
	       "*** VICTIM MODIFIED (wrong-file op) ***" : "untouched");
	snprintf(pathbuf, sizeof(pathbuf), "%s/dirB/f", rootdir);
	lstat(pathbuf, &st);
	printf("      stat(%s/dirB/f) [the TARGET]:  mode=%04o  %s\n",
	       rootdir, st.st_mode & 0777,
	       (st.st_mode & 0777) == 0644 ?
	       "(untouched โ€” split-brain confirmed)" : "modified");

	/* getattr through the stale node reports the VICTIM's attributes */
	error = node_stat_mode(&fnode, &mode);
	printf("      getattr via stale node returns mode=%04o (error=%d) "
	       "-> ls -l shows the VICTIM's attributes for dirB/f\n",
	       mode, error);

	/* --- FIXED variant: parent updated on cross-dir rename --- */
	printf("\nFIXED: dn_parent updated to tdnp on rename\n");
	mknode(&fnode, "f", &dirB, 0, DIRFS_NOFD);	/* simulate the fix */
	error = node_chmod(&fnode, 0644);
	{
		char *tmp, *pf;
		tmp = absolute_path(&fnode, NULL, &pf);
		printf("      lchmod(\"%s\", 0644)\n", tmp);
		free(pf);
	}
	snprintf(pathbuf, sizeof(pathbuf), "%s/dirB/f", rootdir);
	lstat(pathbuf, &st);
	printf("      stat(%s/dirB/f): mode=%04o -> %s\n", rootdir,
	       st.st_mode & 0777,
	       (st.st_mode & 0777) == 0644 ? "correct file modified" : "?");
	snprintf(pathbuf, sizeof(pathbuf), "%s/dirA/f", rootdir);
	lstat(pathbuf, &st);
	printf("      stat(%s/dirA/f): mode=%04o -> victim left alone\n",
	       rootdir, st.st_mode & 0777);

	printf("\nRESULT: VULN path operated on the wrong file at the old "
	       "path (victim mode 0600->0777); FIXED variant operates on the "
	       "moved file => BUG CONFIRMED, FIX VALIDATED\n");

	/* cleanup */
	snprintf(pathbuf, sizeof(pathbuf), "%s/dirA/f", rootdir); unlink(pathbuf);
	snprintf(pathbuf, sizeof(pathbuf), "%s/dirB/f", rootdir); unlink(pathbuf);
	snprintf(pathbuf, sizeof(pathbuf), "%s/dirA", rootdir); rmdir(pathbuf);
	snprintf(pathbuf, sizeof(pathbuf), "%s/dirB", rootdir); rmdir(pathbuf);
	rmdir(rootdir);
	return 2;
}