[PATCH 3/3] tests: Add fuzzer for Multi-Link element parsing

Louis Kotze loukot at gmail.com
Mon Aug 24 00:31:47 PDT 2026


The existing fuzzers do not cover Multi-Link element parsing. Add one
that exercises ieee802_11_parse_elems(), the Basic Multi-Link element
accessors, per-STA profile parsing for every Link ID, and element
defragmentation.

Per-STA profiles are TLVs nested inside a TLV and are parsed from
unauthenticated frames, so they are worth reaching directly rather than
only through the higher level entry points.

The per-STA profile parsers are given a buffer that starts at the
Multi-Link Control field, which is the shape bss.c and events.c pass
them after ieee802_11_defrag(). Handing them a whole element instead
makes the control field read as a type the parsers ignore, so almost
nothing past the first check is reached.

Signed-off-by: Louis Kotze <loukot at gmail.com>
---
 tests/fuzzing/ml-elems/Makefile               |  24 ++++
 .../ml-elems/corpus/basic-eml-mldcapa.bin     | Bin 0 -> 17 bytes
 .../ml-elems/corpus/basic-fragmented.bin      | Bin 0 -> 267 bytes
 .../fuzzing/ml-elems/corpus/basic-minimal.bin | Bin 0 -> 12 bytes
 .../ml-elems/corpus/basic-per-sta-profile.bin | Bin 0 -> 23 bytes
 .../ml-elems/corpus/basic-two-profiles.bin    | Bin 0 -> 34 bytes
 .../ml-elems/corpus/real-3a23468610da.bin     | Bin 0 -> 18 bytes
 .../ml-elems/corpus/real-47a767cf0825.bin     | Bin 0 -> 7 bytes
 .../ml-elems/corpus/real-5b120aca880d.bin     | Bin 0 -> 140 bytes
 .../fuzzing/ml-elems/corpus/reconfig-type.bin | Bin 0 -> 12 bytes
 tests/fuzzing/ml-elems/ml-elems.c             | 117 ++++++++++++++++++
 11 files changed, 141 insertions(+)
 create mode 100644 tests/fuzzing/ml-elems/Makefile
 create mode 100644 tests/fuzzing/ml-elems/corpus/basic-eml-mldcapa.bin
 create mode 100644 tests/fuzzing/ml-elems/corpus/basic-fragmented.bin
 create mode 100644 tests/fuzzing/ml-elems/corpus/basic-minimal.bin
 create mode 100644 tests/fuzzing/ml-elems/corpus/basic-per-sta-profile.bin
 create mode 100644 tests/fuzzing/ml-elems/corpus/basic-two-profiles.bin
 create mode 100644 tests/fuzzing/ml-elems/corpus/real-3a23468610da.bin
 create mode 100644 tests/fuzzing/ml-elems/corpus/real-47a767cf0825.bin
 create mode 100644 tests/fuzzing/ml-elems/corpus/real-5b120aca880d.bin
 create mode 100644 tests/fuzzing/ml-elems/corpus/reconfig-type.bin
 create mode 100644 tests/fuzzing/ml-elems/ml-elems.c

diff --git a/tests/fuzzing/ml-elems/Makefile b/tests/fuzzing/ml-elems/Makefile
new file mode 100644
index 000000000..ec8ddd3ec
--- /dev/null
+++ b/tests/fuzzing/ml-elems/Makefile
@@ -0,0 +1,24 @@
+ALL=ml-elems
+include ../rules.include
+
+CFLAGS += -DCONFIG_IEEE80211BE
+
+OBJS += $(SRC)/common/ieee802_11_common.o
+OBJS += $(SRC)/utils/common.o
+OBJS += $(SRC)/utils/os_unix.o
+OBJS += $(SRC)/utils/wpa_debug.o
+OBJS += $(SRC)/utils/wpabuf.o
+
+OBJS += ml-elems.o
+
+_OBJS_VAR := OBJS
+include ../../../src/objs.mk
+
+_OBJS_VAR := LIBS
+include ../../../src/objs.mk
+
+ml-elems: $(OBJS) $(LIBS)
+	$(LDO) $(LDFLAGS) -o $@ $^ $(LIBS) $(ELIBS)
+
+clean: common-clean
+	rm -f ml-elems *~ *.o *.d ../*~ ../*.o ../*.d
diff --git a/tests/fuzzing/ml-elems/corpus/basic-eml-mldcapa.bin b/tests/fuzzing/ml-elems/corpus/basic-eml-mldcapa.bin
new file mode 100644
index 0000000000000000000000000000000000000000..99de9b445ca2a92441e79c0231de7df05a1e9219
GIT binary patch
literal 17
Tcmey*pFM$*hlv3M7(oO8Ak6|A

literal 0
HcmV?d00001

diff --git a/tests/fuzzing/ml-elems/corpus/basic-fragmented.bin b/tests/fuzzing/ml-elems/corpus/basic-fragmented.bin
new file mode 100644
index 0000000000000000000000000000000000000000..c9bfc16233d081c2aec369b7707ab663d48fc15a
GIT binary patch
literal 267
YcmezWKbwJporwVi97iJf#Nh-50P=W4vH$=8

literal 0
HcmV?d00001

diff --git a/tests/fuzzing/ml-elems/corpus/basic-minimal.bin b/tests/fuzzing/ml-elems/corpus/basic-minimal.bin
new file mode 100644
index 0000000000000000000000000000000000000000..86e619e6a1664be67ac9ca2af48f2ed7613ce28d
GIT binary patch
literal 12
Qcmey*mCeAw&cpx$01(*$egFUf

literal 0
HcmV?d00001

diff --git a/tests/fuzzing/ml-elems/corpus/basic-per-sta-profile.bin b/tests/fuzzing/ml-elems/corpus/basic-per-sta-profile.bin
new file mode 100644
index 0000000000000000000000000000000000000000..a00137e03dbe536b07feb5959bda35cda2538351
GIT binary patch
literal 23
Ycmew_n$5t#&cpx)oQw=CASNRN03yNxng9R*

literal 0
HcmV?d00001

diff --git a/tests/fuzzing/ml-elems/corpus/basic-two-profiles.bin b/tests/fuzzing/ml-elems/corpus/basic-two-profiles.bin
new file mode 100644
index 0000000000000000000000000000000000000000..986f8c02514ba7b8cdd9edf210c67613c25ff542
GIT binary patch
literal 34
bcmeybkj=or&cpx)oQw=CASNRN1A++vHdq0+

literal 0
HcmV?d00001

diff --git a/tests/fuzzing/ml-elems/corpus/real-3a23468610da.bin b/tests/fuzzing/ml-elems/corpus/real-3a23468610da.bin
new file mode 100644
index 0000000000000000000000000000000000000000..ab3f8ba355aa1d3fdc9ad95fd15c109272935277
GIT binary patch
literal 18
Zcmew_kiCJCmx+Oaft`twp^<@60RSga0_Ok#

literal 0
HcmV?d00001

diff --git a/tests/fuzzing/ml-elems/corpus/real-47a767cf0825.bin b/tests/fuzzing/ml-elems/corpus/real-47a767cf0825.bin
new file mode 100644
index 0000000000000000000000000000000000000000..a5160bd05945e2aff3e4879c9317b44a661b76c8
GIT binary patch
literal 7
Ocmey*nk~q{!~g&ZeF1|2

literal 0
HcmV?d00001

diff --git a/tests/fuzzing/ml-elems/corpus/real-5b120aca880d.bin b/tests/fuzzing/ml-elems/corpus/real-5b120aca880d.bin
new file mode 100644
index 0000000000000000000000000000000000000000..8a201bf41d0e2d8c0da12ab6d74b25d28d46439a
GIT binary patch
literal 140
zcmez0mA!$Hmx+Oafs28Gv5|pEfx-GCqc9T#!+qz%6b3k8Vq{@vWU>_lse}u{8UNW;
iKG-uX{qvu}`V&~Q95)NPW at cM4X1E4q21N6sKmP$W3KJy&

literal 0
HcmV?d00001

diff --git a/tests/fuzzing/ml-elems/corpus/reconfig-type.bin b/tests/fuzzing/ml-elems/corpus/reconfig-type.bin
new file mode 100644
index 0000000000000000000000000000000000000000..889250eda762865cefcf36fe98940e07cbe23570
GIT binary patch
literal 12
Qcmey*mCeGy&cpx$01*8Ff&c&j

literal 0
HcmV?d00001

diff --git a/tests/fuzzing/ml-elems/ml-elems.c b/tests/fuzzing/ml-elems/ml-elems.c
new file mode 100644
index 000000000..2b9014091
--- /dev/null
+++ b/tests/fuzzing/ml-elems/ml-elems.c
@@ -0,0 +1,117 @@
+/*
+ * Multi-Link element parsing - fuzzer
+ * 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.
+ */
+
+#include "utils/includes.h"
+#include "utils/common.h"
+#include "common/ieee802_11_common.h"
+#include "common/ieee802_11_defs.h"
+#include "common/defs.h"
+#include "../fuzzer-common.h"
+
+
+/*
+ * Drive the per-STA profile parsers the way the callers in bss.c and events.c
+ * do: the buffer starts at the Multi-Link Control field, not at the element
+ * header. Both parsers may defragment subelements in place, so each call gets
+ * its own copy.
+ */
+static void fuzz_link_profile(const u8 *mle, size_t mle_len)
+{
+	struct ieee802_11_elems elems;
+	struct wpabuf *mlbuf;
+	u8 link_id;
+
+	if (!mle || !mle_len)
+		return;
+
+	for (link_id = 0; link_id < MAX_NUM_MLD_LINKS; link_id++) {
+		mlbuf = ieee802_11_defrag(mle, mle_len, true);
+		if (!mlbuf)
+			return;
+		os_memset(&elems, 0, sizeof(elems));
+		ieee802_11_parse_link_assoc_req(&elems, mlbuf, link_id, 1);
+		wpabuf_free(mlbuf);
+
+		mlbuf = ieee802_11_defrag(mle, mle_len, true);
+		if (!mlbuf)
+			return;
+		os_memset(&elems, 0, sizeof(elems));
+		ieee802_11_parse_link_assoc_resp(&elems, mlbuf, link_id, 1);
+		wpabuf_free(mlbuf);
+	}
+
+	/* parent_subelem must point inside mlbuf and, as every in-tree caller
+	 * guarantees by looping on "len > 2", must have a readable two octet
+	 * subelement header. Passing anything shorter breaks the callee's
+	 * contract and would only report a defect in this harness. */
+	if (mle_len >= 2) {
+		mlbuf = ieee802_11_defrag(mle, mle_len, true);
+		if (mlbuf) {
+			size_t defrag_len = 0;
+
+			ieee802_11_defrag_mle_subelem(mlbuf, wpabuf_head(mlbuf),
+						      &defrag_len);
+			wpabuf_free(mlbuf);
+		}
+	}
+}
+
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+	struct ieee802_11_elems elems;
+	struct wpabuf *buf;
+	u8 type;
+
+	wpa_fuzzer_set_debug_level();
+
+	/* Full element parsing; dispatches to the Multi-Link element handling
+	 * and to element fragment reassembly. */
+	ieee802_11_parse_elems(data, size, &elems, 1);
+
+	/* Each Multi-Link element variant the parser recognised, handed on in
+	 * the same shape the real callers use. */
+	fuzz_link_profile(elems.basic_mle, elems.basic_mle_len);
+	fuzz_link_profile(elems.probe_req_mle, elems.probe_req_mle_len);
+	fuzz_link_profile(elems.reconf_mle, elems.reconf_mle_len);
+	fuzz_link_profile(elems.tdls_mle, elems.tdls_mle_len);
+	fuzz_link_profile(elems.prior_access_mle, elems.prior_access_mle_len);
+
+	/* Basic Multi-Link element accessors, both on the element payload the
+	 * parser found and on the raw input, so the short-buffer rejection
+	 * paths are covered too. */
+	if (elems.basic_mle) {
+		get_basic_mle_mld_addr(elems.basic_mle, elems.basic_mle_len);
+		get_basic_mle_eml_capa(elems.basic_mle, elems.basic_mle_len);
+		get_basic_mle_link_id(elems.basic_mle, elems.basic_mle_len);
+	}
+	get_basic_mle_mld_addr(data, size);
+	get_basic_mle_eml_capa(data, size);
+	get_basic_mle_link_id(data, size);
+
+	for (type = 0; type < 8; type++)
+		get_ml_ie(data, size, type);
+
+	/* Element defragmentation, both element and extended-element forms. */
+	buf = ieee802_11_defrag(data, size, true);
+	if (buf) {
+		/* Also drive subelement defragmentation straight off the input,
+		 * not only via a Multi-Link element the parser recognised. */
+		if (wpabuf_len(buf) >= 2) {
+			size_t defrag_len = 0;
+
+			ieee802_11_defrag_mle_subelem(buf, wpabuf_head(buf),
+						      &defrag_len);
+		}
+		wpabuf_free(buf);
+	}
+	buf = ieee802_11_defrag(data, size, false);
+	wpabuf_free(buf);
+
+	return 0;
+}
-- 
2.55.0




More information about the Hostap mailing list