[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