โฌข DragonFlyBSD Kernel Audit
DF-0744 / corrupt.c
โ† back to finding โ†“ download raw
/*
 * DF-0744 trigger #1 โ€” sticky-option corruption demonstrator.
 *
 * Bug: sys/netinet6/udp6_output.c:264
 *   On the failure path of ip6_setpktoptions() the per-call control
 *   handler `goto release`s without ever executing
 *   `in6p->in6p_outputopts = &opt;` (line 138).  At releaseopt the code
 *   unconditionally does
 *
 *       ip6_clearpktopts(in6p->in6p_outputopts, -1);   // line 264
 *       in6p->in6p_outputopts = stickyopt;             // line 265
 *
 *   but at that point in6p->in6p_outputopts is STILL the sticky options
 *   (not the local `opt`), so the user's persistent setsockopt() state is
 *   cleared instead of the per-call copy.
 *
 * Demonstrator:
 *   1. AF_INET6 SOCK_DGRAM socket.
 *   2. setsockopt(IPV6_PKTINFO) with a non-zero source address.  Sticky.
 *   3. getsockopt(IPV6_PKTINFO) -> the value we just set (baseline).
 *   4. sendmsg() with control = single cmsghdr whose cmsg_len == 0.
 *      ip6_setpktoptions() returns EINVAL at once, udp6_output hits the
 *      buggy releaseopt path and clears the STICKY options.
 *   5. getsockopt(IPV6_PKTINFO) -> the value is gone (zeroed).
 *
 * A correct kernel keeps the sticky value across the failed sendmsg.
 * A buggy kernel zeroes it.  We print both snapshots so the difference is
 * unambiguous.
 *
 * Unprivileged: only requires socket()+setsockopt()+sendmsg() on a
 * PF_INET6 SOCK_DGRAM socket.
 */
#include <sys/param.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <netinet/in.h>		/* pulls in netinet6/in6.h; provides
				 * IPV6_PKTINFO, struct in6_pktinfo */

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

/* ::ffff:0.0.0.42 โ€” recognisable sentinel that is NOT in6addr_any. */
static const unsigned char sentinel_addr[16] = {
	0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,42,
};

static void
dump_pktinfo(const char *label, const struct in6_pktinfo *pi)
{
	const unsigned char *a = pi->ipi6_addr.s6_addr;
	printf("  %-26s ifindex=%u addr="
	    "%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
	    "%02x%02x:%02x%02x:%02x%02x:%02x%02x\n",
	    label, pi->ipi6_ifindex,
	    a[0],a[1],a[2],a[3],a[4],a[5],a[6],a[7],
	    a[8],a[9],a[10],a[11],a[12],a[13],a[14],a[15]);
}

int
main(void)
{
	int s, rc;
	struct in6_pktinfo pi_set, pi_get;
	socklen_t optlen;
	struct sockaddr_in6 dst;

	/* --- 1. socket ------------------------------------------------ */
	s = socket(AF_INET6, SOCK_DGRAM, 0);
	if (s < 0)
		err(1, "socket(AF_INET6, SOCK_DGRAM)");

	/* --- 2. set sticky IPV6_PKTINFO ------------------------------ */
	memset(&pi_set, 0, sizeof(pi_set));
	memcpy(&pi_set.ipi6_addr, sentinel_addr, 16);
	pi_set.ipi6_ifindex = 0;	/* let kernel pick iface */
	rc = setsockopt(s, IPPROTO_IPV6, IPV6_PKTINFO, &pi_set, sizeof(pi_set));
	if (rc != 0)
		err(1, "setsockopt(IPV6_PKTINFO)");

	/* --- 3. read it back (baseline) ------------------------------ */
	optlen = sizeof(pi_get);
	memset(&pi_get, 0xee, sizeof(pi_get));
	rc = getsockopt(s, IPPROTO_IPV6, IPV6_PKTINFO, &pi_get, &optlen);
	if (rc != 0)
		err(1, "getsockopt(IPV6_PKTINFO) #1");
	printf("[baseline] sticky IPV6_PKTINFO after setsockopt:\n");
	dump_pktinfo("read-back", &pi_get);

	/* --- 4. sendmsg with one malformed cmsg (cmsg_len == 0) ------ */
	/*
	 * Control buffer = a single struct cmsghdr whose cmsg_len is 0.
	 * ip6_setpktoptions() returns EINVAL at once; udp6_output() then
	 * runs the buggy releaseopt path and clears the sticky options.
	 */
	{
		char cbuf[CMSG_SPACE(0)];	/* 16 bytes on amd64 */
		struct cmsghdr *cm;
		struct msghdr msg;
		struct iovec iov;
		unsigned char payload[1] = { '.' };

		memset(cbuf, 0, sizeof(cbuf));
		cm = (struct cmsghdr *)cbuf;
		cm->cmsg_level = IPPROTO_IPV6;
		cm->cmsg_type = IPV6_PKTINFO;
		cm->cmsg_len = 0;		/* <<< the trigger */

		memset(&dst, 0, sizeof(dst));
		dst.sin6_len = sizeof(dst);
		dst.sin6_family = AF_INET6;
		dst.sin6_addr = in6addr_loopback;	/* ::1 */
		dst.sin6_port = htons(9);		/* discard */

		memset(&msg, 0, sizeof(msg));
		iov.iov_base = payload;
		iov.iov_len = sizeof(payload);
		msg.msg_name = &dst;
		msg.msg_namelen = sizeof(dst);
		msg.msg_iov = &iov;
		msg.msg_iovlen = 1;
		msg.msg_control = cbuf;
		msg.msg_controllen = sizeof(cbuf);

		rc = sendmsg(s, &msg, 0);
		/* EINVAL is expected; we want the side-effect, not the send. */
		printf("[trigger]  sendmsg returned %d (errno=%d: %s)\n",
		    rc, rc < 0 ? errno : 0,
		    rc < 0 ? strerror(errno) : "ok");
	}

	/* --- 5. read sticky again โ€” buggy kernel: zeroed ------------- */
	optlen = sizeof(pi_get);
	memset(&pi_get, 0xee, sizeof(pi_get));
	rc = getsockopt(s, IPPROTO_IPV6, IPV6_PKTINFO, &pi_get, &optlen);
	if (rc != 0)
		err(1, "getsockopt(IPV6_PKTINFO) #2");
	printf("[after]    sticky IPV6_PKTINFO after buggy sendmsg:\n");
	dump_pktinfo("read-back", &pi_get);

	/*
	 * Verdict: if the address is still the sentinel -> safe kernel.
	 * If it is all-zero (in6addr_any) -> sticky state was corrupted.
	 */
	{
		const unsigned char zero[16] = {0};
		int is_zero = memcmp(&pi_get.ipi6_addr, zero, 16) == 0;
		int is_sentinel = memcmp(&pi_get.ipi6_addr, sentinel_addr, 16)
		    == 0;

		if (is_sentinel) {
			printf("\nVERDICT: SAFE   - sticky IPV6_PKTINFO "
			    "preserved.\n");
			return 0;
		}
		if (is_zero) {
			printf("\nVERDICT: BUG    - sticky IPV6_PKTINFO was "
			    "CLEARED (state corruption, DF-0744 reproduced).\n");
			return 2;
		}
		printf("\nVERDICT: DETERIORATED - sticky value unexpected "
		    "(partial corruption?).\n");
		return 3;
	}
}