DF-0751 / mpls_loop_harness.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 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | /* * DF-0751 โ mpls_input() infinite-loop harness. * * Faithful reproduction of the label-switch loop in * sys/netproto/mpls/mpls_input.c, lifted verbatim from the audit source. * The loop body is a byte-exact transcription of mpls_input() lines 88-171 * with TWO changes, both clearly marked: * * (A) The mbuf/mtod/m_adj/netisr_queue kernel primitives are replaced with * trivial userspace stand-ins that preserve the EXACT control flow: * struct mbuf -> a {char *data; size_t off; size_t len;} cursor * mtod(m,...) -> (m.data + m.off) * m_adj(m,n) -> m.off += n; m.len -= n; (THIS IS THE BUG: the * production code OMITS this call on the goto-again * paths, so the cursor never advances.) * netisr_queue -> no-op return (exits the loop) * m_freem -> no-op * * (B) A depth counter `iter` is added as an ESCAPE HATCH so the harness can * REPORT the infinite loop instead of actually hanging. The production * code at mpls_input.c:88-171 has NO such guard โ confirmed by reading * the entire function and its caller (mpls_input_handler, which only * does get_mplock / mpls_input / rel_mplock). * * The ONLY difference between the buggy and fixed behaviour is whether * m_adj(m, sizeof(struct mpls)) is called before `goto again`. We run BOTH * variants to demonstrate the contrast. * * Build: cc -O2 -o mpls_loop_harness mpls_loop_harness.c * Run: ./mpls_loop_harness */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> /* ---- mpls.h (verbatim from sys/netproto/mpls/mpls.h) ---- */ typedef uint32_t mpls_label_t; struct mpls { uint32_t mpls_shim; }; #define MPLS_LABEL_MASK 0xfffff000 #define MPLS_STACK_MASK 0x00000100 #define MPLS_LABEL(shim) (((shim) & MPLS_LABEL_MASK) >> 12) #define MPLS_STACK(shim) (((shim) & MPLS_STACK_MASK) >> 8) /* ntohl on a little-endian host = byte-swap */ static inline uint32_t ntohl(uint32_t v) { return ((v & 0xff) << 24) | ((v & 0xff00) << 8) | ((v & 0xff0000) >> 8) | ((v & 0xff000000) >> 24); } /* ---- userspace mbuf stand-in ---- */ struct mbuf { uint8_t *data; /* packet buffer */ size_t off; /* current read offset (what m_data points at) */ size_t len; /* remaining bytes from off to end */ size_t cap; /* total capacity */ }; /* match the kernel mtod: caller passes the type INCLUDING the '*', e.g. * mtod(m, struct mpls *) -> ((struct mpls *)((m)->data + (m)->off)) */ #define mtod(m, type) ((type)((m)->data + (m)->off)) static void m_adj(struct mbuf *m, int n) { m->off += n; m->len -= n; } static void m_freem(struct mbuf *m) { /* no-op for harness */ } /* ---- the loop, transcribed from mpls_input.c:88-171 ---- * The behaviour is selected by `do_fix`: 0 = unpatched (no m_adj before * goto again, matching the shipping code), 1 = patched (m_adj before each * goto again, matching the proposed fix). * * Returns the number of loop iterations performed. On the unpatched path * with a label=0/S=0 frame this grows without bound; the caller caps it. */ static unsigned long mpls_input_loop(struct mbuf *m, int do_fix, unsigned long cap, const char **exit_reason) { unsigned long iter = 0; struct mpls *mpls = NULL; mpls_label_t label; *exit_reason = "(still looping)"; again: if (++iter > cap) { *exit_reason = "DEPTH CAP HIT (would loop forever)"; return iter; } if (m->len < sizeof(struct mpls)) { *exit_reason = "m_pullup would fail (too small) -> drop"; return iter; } mpls = mtod(m, struct mpls*); label = MPLS_LABEL(ntohl(mpls->mpls_shim)); switch (label) { case 0: if (MPLS_STACK(ntohl(mpls->mpls_shim))) { m_adj(m, sizeof(struct mpls)); *exit_reason = "label 0 S=1: netisr_queue(NETISR_IP) -> return"; return iter; /* netisr_queue + return */ } if (do_fix) m_adj(m, sizeof(struct mpls)); /* <-- THE FIX */ goto again; case 1: break; case 2: if (MPLS_STACK(ntohl(mpls->mpls_shim))) { m_adj(m, sizeof(struct mpls)); *exit_reason = "label 2 S=1: netisr_queue(NETISR_IPV6) -> return"; return iter; } if (do_fix) m_adj(m, sizeof(struct mpls)); /* <-- THE FIX */ goto again; case 3: break; default: if (label <= 15) { *exit_reason = "reserved label (4-15) -> m_freem"; return iter; } *exit_reason = "label>15 -> mpls_forward"; return iter; } *exit_reason = "mplss_invalid++ -> m_freem"; return iter; } /* Build a wire-format MPLS label entry. * label, exp, S (bottom-of-stack), ttl -> 4 bytes in network byte order. * On the wire (big-endian): bytes are laid out MSB-first, so for * label=0/S=0/TTL=64 we emit 0x00 0x00 0x00 0x40. */ static void put_label(struct mbuf *m, mpls_label_t lab, int s, uint8_t ttl) { uint32_t shim = ((lab << 12) & MPLS_LABEL_MASK) | ((s << 8) & MPLS_STACK_MASK) | (ttl & 0xff); /* store big-endian (network byte order) into the buffer */ uint8_t *p = m->data + m->off + m->len; p[0] = (shim >> 24) & 0xff; p[1] = (shim >> 16) & 0xff; p[2] = (shim >> 8) & 0xff; p[3] = (shim ) & 0xff; m->len += 4; } int main(void) { uint8_t buf[256]; struct mbuf m; const char *reason; unsigned long cap = 1000000UL; /* 1e6 โ plenty to prove "forever" */ printf("=== DF-0751 mpls_input() infinite-loop harness ===\n"); printf("source: sys/netproto/mpls/mpls_input.c:88-171 (label-switch loop)\n"); printf("depth cap (escape hatch production code lacks): %lu iterations\n\n", cap); /* ---- Frame A: label=0, S=0 (the bug trigger) ---- * mpls_shim on wire = 00 00 00 40 (label 0, exp 0, S=0, ttl 64). * This is exactly the single-packet trigger described in the finding. */ memset(buf, 0, sizeof(buf)); m.data = buf; m.off = 0; m.len = 0; m.cap = sizeof(buf); put_label(&m, /*lab*/0, /*S*/0, /*ttl*/64); /* add some trailing payload so len comfortably exceeds sizeof(mpls) */ memset(buf + 4, 0, 32); m.len += 32; printf("Frame A: label=0 S=0 TTL=64 (%zu bytes total)\n", m.len); printf("\n--- UNPATCHED mpls_input (shipping code: goto again WITHOUT m_adj) ---\n"); { struct mbuf mu = m; /* copy, fresh cursor */ unsigned long it = mpls_input_loop(&mu, /*do_fix*/0, cap, &reason); printf("iterations: %lu\n", it); printf("exit : %s\n", reason); printf("cursor adv: off=%zu (started 0) => %s\n", mu.off, mu.off == 0 ? "M BUFFER NEVER ADVANCED (same label re-read every iter)" : "advanced"); if (it >= cap) printf("VERDICT : *** INFINITE LOOP CONFIRMED *** (hit %lu-iter cap; production kernel has NO cap => hard hang)\n", cap); } printf("\n--- PATCHED mpls_input (fix: m_adj(m, sizeof(struct mpls)) before goto again) ---\n"); { struct mbuf mp = m; unsigned long it = mpls_input_loop(&mp, /*do_fix*/1, cap, &reason); printf("iterations: %lu\n", it); printf("exit : %s\n", reason); printf("cursor adv: off=%zu (started 0)\n", mp.off); if (it < cap) printf("VERDICT : loop TERMINATES cleanly (no hang). Fix is effective.\n"); else printf("VERDICT : STILL LOOPS โ fix is insufficient!\n"); } /* ---- Frame B: label=2, S=0 (the IPv6 explicit-NULL twin of the bug) ---- */ printf("\n--- UNPATCHED, Frame B: label=2 S=0 (IPv6 explicit NULL, mpls_input.c:142) ---\n"); memset(buf, 0, sizeof(buf)); m.data = buf; m.off = 0; m.len = 0; m.cap = sizeof(buf); put_label(&m, /*lab*/2, /*S*/0, /*ttl*/64); memset(buf + 4, 0, 32); m.len += 32; { struct mbuf mb = m; unsigned long it = mpls_input_loop(&mb, /*do_fix*/0, cap, &reason); printf("iterations: %lu\n", it); printf("exit : %s\n", reason); printf("VERDICT : %s\n", it >= cap ? "*** INFINITE LOOP CONFIRMED (case 2 twin bug) ***" : "(terminated โ unexpected)"); } /* ---- Control: label=0, S=1 (bottom-of-stack โ must NOT loop even unpatched) ---- */ printf("\n--- Control: label=0 S=1 (bottom-of-stack; unpatched) ---\n"); memset(buf, 0, sizeof(buf)); m.data = buf; m.off = 0; m.len = 0; m.cap = sizeof(buf); put_label(&m, /*lab*/0, /*S*/1, /*ttl*/64); memset(buf + 4, 0, 32); m.len += 32; { struct mbuf mc = m; unsigned long it = mpls_input_loop(&mc, /*do_fix*/0, cap, &reason); printf("iterations: %lu\n", it); printf("exit : %s\n", reason); printf("VERDICT : %s\n", it < cap ? "terminates (S-bit path is correct)" : "LOOPS (unexpected!)"); } return 0; } |