DF-3959 / trigger.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | /* 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); } |