[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