โฌข DragonFlyBSD Kernel Audit
DF-3959 / trigger.c
โ† back to finding โ†“ download raw
/* DF-3959 โ€” vtnet_exec_ctrl_cmd() polls the control virtqueue with an
 * UNBOUNDED spin loop.
 *
 *   sys/dev/virtual/virtio/net/if_vtnet.c:2372-2409
 *       virtqueue_notify(vq, NULL);
 *       c = virtqueue_poll(vq, NULL);          <- if_vtnet.c:2407
 *
 *   sys/dev/virtual/virtio/virtio/virtqueue.c:572-582
 *       while ((cookie = virtqueue_dequeue(vq, len)) == NULL)
 *               ;                               <- spins forever, no yield,
 *                                                 no timeout, serializer held
 *
 * virtqueue_dequeue() only returns non-NULL when the device advances
 * used->idx (virtqueue.c:550-554).  A wedged, malicious, or compromised
 * virtio device that acknowledges VIRTIO_NET_F_CTRL_VQ but never completes
 * a control command hangs the calling kernel thread permanently.
 *
 * Reachable with NO privileges at attach:
 *   vtnet_attach -> vtnet_set_promisc (if_vtnet.c:361) -> vtnet_ctrl_rx_cmd
 *   -> vtnet_exec_ctrl_cmd -> virtqueue_poll          [boot-time wedge]
 * and via root ioctls at runtime:
 *   SIOCSIFFLAGS/SIOCADDMULTI -> vtnet_rx_filter[_mac] -> ctrl cmd
 *
 * This harness replicates the exact loop condition against a model
 * used-ring whose idx never advances, bounds the replica (so the harness
 * itself terminates) and reports what the kernel loop does.
 */
#include <stdio.h>
#include <stdint.h>

/* model of struct virtqueue fields used by virtqueue_dequeue (virtqueue.c:544) */
struct vq_model {
	volatile uint16_t used_idx;	/* device-owned, MMIO-backed in kernel */
	uint16_t	 used_cons_idx;
	int		 spins;
};

static void *
vq_dequeue_model(struct vq_model *vq)
{
	/* virtqueue.c:550-554 */
	if (vq->used_cons_idx == vq->used_idx)
		return (NULL);		/* device has not completed anything */
	vq->used_cons_idx++;
	return ((void *)&vq->spins);	/* some cookie */
}

/* replica of virtqueue_poll's loop (virtqueue.c:578-579), bounded only so
 * the harness can print and exit โ€” the KERNEL copy has no bound. */
#define	BENCH_BOUND	200000000	/* would spin forever without this */

static void *
vq_poll_model(struct vq_model *vq, uint64_t bound)
{
	void *cookie;

	while ((cookie = vq_dequeue_model(vq)) == NULL) {
		vq->spins++;
		if ((uint64_t)vq->spins >= bound)
			return (NULL);	/* harness-only cutoff */
	}
	return (cookie);
}

int
main(void)
{
	struct vq_model vq;
	void *c;

	printf("== DF-3959 vtnet_exec_ctrl_cmd unbounded ctrl-vq poll ==\n");

	/* Case 1: wedged device โ€” used->idx never advances */
	vq.used_idx = 0;
	vq.used_cons_idx = 0;
	vq.spins = 0;
	c = vq_poll_model(&vq, BENCH_BOUND);
	printf("wedged device (used->idx never advances):\n");
	printf("  kernel loop spun %d times and %s โ€” in the kernel this "
	    "loop has NO cutoff: if_vtnet.c:2407 -> virtqueue_poll -> "
	    "virtqueue.c:578 spins forever\n", vq.spins,
	    c == NULL ? "never completed (harness cutoff hit)"
		      : "completed");
	printf("  caller holds the ifnet serializers "
	    "(ASSERT_IFNET_SERIALIZED_ALL, if_vtnet.c:2380) => unkillable "
	    "kernel thread wedge / CPU burn\n");
	printf("  no timeout, no tsleep, no kthread_exit, no SIGKILL "
	    "escapes a kernel spin loop\n");

	/* Case 2: FIXED path โ€” bounded poll with DELAY (fix.diff) */
	vq.used_idx = 0;
	vq.used_cons_idx = 0;
	vq.spins = 0;
	{
		int i, ok = 0;
		/* fix.diff: for (i = 0; i < 10000; i++) { dequeue; DELAY(100); } */
		for (i = 0; i < 10000; i++) {
			if (vq_dequeue_model(&vq) != NULL) { ok = 1; break; }
			/* DELAY(100) elided in the model */
		}
		printf("wedged device, FIXED driver: poll gives up after %d "
		    "iterations (~1s), ack stays VIRTIO_NET_ERR -> caller "
		    "gets EIO, thread survives: %s\n",
		    ok ? i : 10000, ok ? "completed" : "timed out cleanly");
	}

	return (0);
}