[PATCH 2/3] tests: Multi-Link element accessor boundary tests
Louis Kotze
loukot at gmail.com
Mon Aug 24 00:31:46 PDT 2026
Cover the Basic Multi-Link element accessors at and around the length
boundaries, including the case where the buffer ends exactly after the
Multi-Link Control field.
Each case is run against an exactly sized heap allocation so that a
one-octet over-read lands in a redzone instead of in adjacent heap data;
the boundary cases only mean anything when the test is built with
AddressSanitizer or run under valgrind.
Without the preceding fix this aborts with a heap-buffer-overflow in
get_basic_mle_link_id().
Signed-off-by: Louis Kotze <loukot at gmail.com>
---
tests/Makefile | 7 +-
tests/test-mle.c | 173 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 179 insertions(+), 1 deletion(-)
create mode 100644 tests/test-mle.c
diff --git a/tests/Makefile b/tests/Makefile
index ea2fa843f..63fc52161 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -3,7 +3,7 @@ ALL=test-base64 test-md4 test-milenage \
test-sha1 \
test-https test-https_server \
test-sha256 test-aes test-x509v3 test-list test-rc4 \
- test-bss
+ test-bss test-mle
include ../src/build.rules
@@ -142,6 +142,10 @@ CFLAGS_test-bss=$(WPA_CFLAGS)
OBJS_test-bss=$(WPA_OBJS)
include test.mk
+TEST=test-mle
+CFLAGS_test-mle=-DCONFIG_IEEE80211BE
+include test.mk
+
run-tests: $(ALL)
./test-aes
./test-list
@@ -151,6 +155,7 @@ run-tests: $(ALL)
./test-sha1
./test-sha256
./test-bss
+ ./test-mle
@echo
@echo All tests completed successfully.
diff --git a/tests/test-mle.c b/tests/test-mle.c
new file mode 100644
index 000000000..70f45c53e
--- /dev/null
+++ b/tests/test-mle.c
@@ -0,0 +1,173 @@
+/*
+ * Multi-Link element accessor boundary tests
+ * Copyright (c) 2026, Louis Kotze <loukot at gmail.com>
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ *
+ * These use exactly sized heap allocations on purpose: an off-by-one read is
+ * only visible to a memory checker when the redzone sits immediately after the
+ * last valid byte. Run under ASan or valgrind for the boundary cases to mean
+ * anything.
+ */
+
+#include "utils/includes.h"
+
+#include "utils/common.h"
+#include "common/ieee802_11_common.h"
+#include "common/ieee802_11_defs.h"
+#include "common/defs.h"
+
+static int failures;
+
+#define CHECK(cond) do { \
+ if (!(cond)) { \
+ wpa_printf(MSG_ERROR, \
+ "FAIL %s:%d: %s", \
+ __func__, __LINE__, #cond); \
+ failures++; \
+ } \
+ } while (0)
+
+/* Run fn against an exactly sized copy of buf so a one-past-the-end read lands
+ * in an ASan redzone rather than in adjacent heap data. */
+static void with_exact_alloc(const u8 *buf, size_t len,
+ void (*fn)(const u8 *, size_t))
+{
+ u8 *p = os_malloc(len ? len : 1);
+
+ if (!p)
+ return;
+ if (len)
+ os_memcpy(p, buf, len);
+ fn(p, len);
+ os_free(p);
+}
+
+
+static void probe_eml_capa(const u8 *p, size_t len)
+{
+ get_basic_mle_eml_capa(p, len);
+}
+
+
+static void probe_link_id(const u8 *p, size_t len)
+{
+ get_basic_mle_link_id(p, len);
+}
+
+
+static void probe_mld_addr(const u8 *p, size_t len)
+{
+ get_basic_mle_mld_addr(p, len);
+}
+
+
+/*
+ * Regression: the guard was "len < MULTI_LINK_CONTROL_LEN" (2) while the next
+ * read was buf[MULTI_LINK_CONTROL_LEN], so len == 2 read one byte past the end.
+ * Reachable from wpa_supplicant/scan.c when an AP beacons a Basic Multi-Link
+ * element whose element length is 3.
+ */
+static void test_truncated_common_info_len(void)
+{
+ /* The two inputs libFuzzer produced, plus every short length. */
+ const u8 crash1[] = { 0x00, 0xef };
+ const u8 crash2[] = { 0xc8, 0x0a };
+ u8 buf[8];
+ size_t len;
+
+ with_exact_alloc(crash1, sizeof(crash1), probe_link_id);
+ with_exact_alloc(crash2, sizeof(crash2), probe_eml_capa);
+ with_exact_alloc(crash1, sizeof(crash1), probe_eml_capa);
+ with_exact_alloc(crash2, sizeof(crash2), probe_link_id);
+
+ /* Every control value at exactly the boundary length. */
+ for (len = 0; len <= 4; len++) {
+ unsigned int c;
+
+ for (c = 0; c < 0x200; c++) {
+ buf[0] = c & 0xff;
+ buf[1] = (c >> 8) & 0xff;
+ buf[2] = 0xff;
+ buf[3] = 0xff;
+ with_exact_alloc(buf, len, probe_eml_capa);
+ with_exact_alloc(buf, len, probe_link_id);
+ with_exact_alloc(buf, len, probe_mld_addr);
+ }
+ }
+}
+
+
+/* A well-formed minimal Basic MLE must still parse, so the fix cannot simply
+ * reject everything. */
+static void test_valid_basic_mle(void)
+{
+ const u8 mld_addr[ETH_ALEN] = { 0x02, 0, 0, 0, 0, 0 };
+ u8 buf[32];
+ size_t pos = 0;
+ const u8 *addr;
+ int link_id;
+ u16 ctrl = MULTI_LINK_CONTROL_TYPE_BASIC |
+ BASIC_MULTI_LINK_CTRL_PRES_LINK_ID;
+
+ WPA_PUT_LE16(buf, ctrl);
+ pos = 2;
+ buf[pos++] = 1 + ETH_ALEN + 1; /* Common Info Length, incl. itself */
+ os_memcpy(&buf[pos], mld_addr, ETH_ALEN);
+ pos += ETH_ALEN;
+ buf[pos++] = 0x03; /* Link ID Info */
+
+ addr = get_basic_mle_mld_addr(buf, pos);
+ CHECK(addr != NULL);
+ if (addr)
+ CHECK(os_memcmp(addr, mld_addr, ETH_ALEN) == 0);
+
+ link_id = get_basic_mle_link_id(buf, pos);
+ CHECK(link_id == 3);
+
+ /* Truncating a valid element by one byte at a time must never read out
+ * of bounds and must never return a link ID it cannot have seen. */
+ while (pos > 0) {
+ pos--;
+ with_exact_alloc(buf, pos, probe_link_id);
+ with_exact_alloc(buf, pos, probe_eml_capa);
+ with_exact_alloc(buf, pos, probe_mld_addr);
+ }
+}
+
+
+/* A declared Common Info Length longer than the buffer must be rejected. */
+static void test_lying_common_info_len(void)
+{
+ u8 buf[16];
+ size_t len;
+
+ for (len = 3; len <= sizeof(buf); len++) {
+ WPA_PUT_LE16(buf, MULTI_LINK_CONTROL_TYPE_BASIC |
+ BASIC_MULTI_LINK_CTRL_PRES_LINK_ID |
+ BASIC_MULTI_LINK_CTRL_PRES_EML_CAPA);
+ buf[2] = 0xff; /* claims 255 bytes of Common Info */
+ os_memset(&buf[3], 0x41, len - 3);
+ with_exact_alloc(buf, len, probe_eml_capa);
+ with_exact_alloc(buf, len, probe_link_id);
+ }
+}
+
+
+int main(void)
+{
+ wpa_debug_level = MSG_INFO;
+
+ test_truncated_common_info_len();
+ test_valid_basic_mle();
+ test_lying_common_info_len();
+
+ if (failures) {
+ wpa_printf(MSG_ERROR, "%d test(s) FAILED", failures);
+ return 1;
+ }
+
+ wpa_printf(MSG_INFO, "test-mle: all tests passed");
+ return 0;
+}
--
2.55.0
More information about the Hostap
mailing list