1 // SPDX-License-Identifier: GPL-2.0-only 2 /* 3 * KUnit tests for negotiated TTLM (TID-To-Link Mapping) parsing 4 * 5 * Copyright (C) 2026 Michael Bommarito <michael.bommarito@gmail.com> 6 */ 7 #include <kunit/test.h> 8 #include <linux/ieee80211.h> 9 #include "../ieee80211_i.h" 10 11 MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING"); 12 13 /* 14 * Build a negotiated TTLM element in caller-supplied buffer. 15 * 16 * @buf: destination buffer (must be at least elem_size bytes) 17 * @elem_size: sizeof(ttlm_elem) + 1 (presence byte) + npresent * bm_size 18 * @presence: link_map_presence bitmask; each set bit => one map follows 19 * @bm_size: bytes per map (1 or 2); 2 => LINK_MAP_SIZE bit clear 20 * @maps: array of npresent u16 maps, one per set bit in presence 21 * 22 * Control field encodes direction=BOTH; no switch-time, no expected-dur, 23 * no DEF_LINK_MAP. LINK_MAP_SIZE bit is set iff bm_size==1. 24 * 25 * Returns pointer to the ieee80211_ttlm_elem at buf. 26 */ 27 static const struct ieee80211_ttlm_elem * 28 build_neg_ttlm_elem(u8 *buf, size_t elem_size, 29 u8 presence, u8 bm_size, const u16 *maps) 30 { 31 struct ieee80211_ttlm_elem *t = (void *)buf; 32 u8 control; 33 u8 *pos; 34 int i, tid; 35 36 memset(buf, 0, elem_size); 37 38 control = IEEE80211_TTLM_DIRECTION_BOTH; /* bits [1:0] = 2 */ 39 if (bm_size == 1) 40 control |= IEEE80211_TTLM_CONTROL_LINK_MAP_SIZE; 41 42 t->control = control; 43 44 pos = (u8 *)t->optional; 45 *pos++ = presence; 46 47 i = 0; 48 for (tid = 0; tid < IEEE80211_TTLM_NUM_TIDS; tid++) { 49 if (!(presence & BIT(tid))) 50 continue; 51 if (bm_size == 1) 52 *pos = (u8)maps[i]; 53 else 54 put_unaligned_le16(maps[i], pos); 55 pos += bm_size; 56 i++; 57 } 58 59 return t; 60 } 61 62 /* 63 * sparse_presence_no_oob_read - BIT(0)|BIT(7) presence, bm_size=2 64 * 65 * Only TID 0 and TID 7 have maps; TIDs 1-6 are absent. Element length 66 * is exactly 6 bytes (1 control + 1 presence + 2 * 2-byte maps). 67 * 68 * Pre-fix the parser advanced pos by bm_size AFTER the switch() block 69 * (i.e. unconditionally for every TID), so when processing TID 7 it 70 * had already advanced 6 * bm_size = 12 bytes past the presence byte 71 * for the absent TIDs before reading the TID-7 map - 14 bytes past the 72 * end of the 2-byte TID-7 map. Under KASAN that is a slab-out-of-bounds. 73 * 74 * After the fix pos is advanced only inside the presence-bit branch so 75 * the cursor lands exactly at end-of-element after processing TID 7. 76 */ 77 static void sparse_presence_no_oob_read(struct kunit *test) 78 { 79 /* 80 * presence = BIT(0)|BIT(7): 2 maps present. 81 * elem_size = sizeof(ttlm_elem) + 1 (presence) + 2*2 (maps) = 6. 82 */ 83 const u8 presence = BIT(0) | BIT(7); 84 const u8 bm_size = 2; 85 const int npresent = 2; 86 const size_t elem_size = sizeof(struct ieee80211_ttlm_elem) 87 + 1 + npresent * bm_size; 88 /* 89 * Allocate exact-size buffer so a pre-fix OOB read walks into the 90 * KASAN red zone immediately after the allocation. 91 */ 92 u8 *buf = kunit_kzalloc(test, elem_size, GFP_KERNEL); 93 const struct ieee80211_ttlm_elem *ttlm; 94 struct ieee80211_neg_ttlm neg_ttlm = {}; 95 /* Non-zero maps so the parser does not reject with -EINVAL. */ 96 const u16 maps[2] = { 0x0001, 0x0001 }; 97 u8 direction = 0; 98 int ret; 99 100 KUNIT_ASSERT_NOT_NULL(test, buf); 101 102 ttlm = build_neg_ttlm_elem(buf, elem_size, presence, bm_size, maps); 103 104 /* 105 * Pass NULL for sdata: the only sdata dereference in this code path 106 * is inside mlme_dbg() on error returns, which are guarded by 107 * MAC80211_MLME_DEBUG == 0 in non-debug builds and by the dead-code 108 * eliminator in KUnit builds. The success path does not touch sdata. 109 */ 110 ret = ieee80211_parse_neg_ttlm(NULL, ttlm, &neg_ttlm, &direction); 111 112 KUNIT_EXPECT_EQ(test, ret, 0); 113 KUNIT_EXPECT_EQ(test, (int)direction, IEEE80211_TTLM_DIRECTION_BOTH); 114 /* TID 0: map present */ 115 KUNIT_EXPECT_EQ(test, (int)neg_ttlm.downlink[0], 0x0001); 116 KUNIT_EXPECT_EQ(test, (int)neg_ttlm.uplink[0], 0x0001); 117 /* TID 3: absent => map should be 0 */ 118 KUNIT_EXPECT_EQ(test, (int)neg_ttlm.downlink[3], 0); 119 KUNIT_EXPECT_EQ(test, (int)neg_ttlm.uplink[3], 0); 120 /* TID 7: map present */ 121 KUNIT_EXPECT_EQ(test, (int)neg_ttlm.downlink[7], 0x0001); 122 KUNIT_EXPECT_EQ(test, (int)neg_ttlm.uplink[7], 0x0001); 123 } 124 125 /* 126 * dense_presence_baseline - presence=0xff (all 8 TIDs), bm_size=2 127 * 128 * Every TID has a map; this is the dense layout the parser handled 129 * correctly even before the fix. Confirms the cursor-advance fix 130 * does not regress the already-correct path. 131 */ 132 static void dense_presence_baseline(struct kunit *test) 133 { 134 const u8 presence = 0xff; 135 const u8 bm_size = 2; 136 const int npresent = 8; 137 const size_t elem_size = sizeof(struct ieee80211_ttlm_elem) 138 + 1 + npresent * bm_size; 139 u8 *buf = kunit_kzalloc(test, elem_size, GFP_KERNEL); 140 const struct ieee80211_ttlm_elem *ttlm; 141 struct ieee80211_neg_ttlm neg_ttlm = {}; 142 const u16 maps[8] = { 143 0x0003, 0x0003, 0x0003, 0x0003, 144 0x0003, 0x0003, 0x0003, 0x0003, 145 }; 146 u8 direction = 0; 147 int ret; 148 149 KUNIT_ASSERT_NOT_NULL(test, buf); 150 151 ttlm = build_neg_ttlm_elem(buf, elem_size, presence, bm_size, maps); 152 153 ret = ieee80211_parse_neg_ttlm(NULL, ttlm, &neg_ttlm, &direction); 154 155 KUNIT_EXPECT_EQ(test, ret, 0); 156 KUNIT_EXPECT_EQ(test, (int)direction, IEEE80211_TTLM_DIRECTION_BOTH); 157 /* All TIDs present: every downlink/uplink entry must be 0x0003. */ 158 for (int tid = 0; tid < IEEE80211_TTLM_NUM_TIDS; tid++) { 159 KUNIT_EXPECT_EQ(test, (int)neg_ttlm.downlink[tid], 0x0003); 160 KUNIT_EXPECT_EQ(test, (int)neg_ttlm.uplink[tid], 0x0003); 161 } 162 } 163 164 static struct kunit_case mac80211_ttlm_test_cases[] = { 165 KUNIT_CASE(sparse_presence_no_oob_read), 166 KUNIT_CASE(dense_presence_baseline), 167 {} 168 }; 169 170 static struct kunit_suite mac80211_ttlm = { 171 .name = "mac80211-ttlm", 172 .test_cases = mac80211_ttlm_test_cases, 173 }; 174 175 kunit_test_suite(mac80211_ttlm); 176