new file mode 100644
@@ -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
new file mode 100644
GIT binary patch
literal 17
Tcmey*pFM$*hlv3M7(oO8Ak6|A
literal 0
HcmV?d00001
new file mode 100644
GIT binary patch
literal 267
YcmezWKbwJporwVi97iJf#Nh-50P=W4vH$=8
literal 0
HcmV?d00001
new file mode 100644
GIT binary patch
literal 12
Qcmey*mCeAw&cpx$01(*$egFUf
literal 0
HcmV?d00001
new file mode 100644
GIT binary patch
literal 23
Ycmew_n$5t#&cpx)oQw=CASNRN03yNxng9R*
literal 0
HcmV?d00001
new file mode 100644
GIT binary patch
literal 34
bcmeybkj=or&cpx)oQw=CASNRN1A++vHdq0+
literal 0
HcmV?d00001
new file mode 100644
GIT binary patch
literal 18
Zcmew_kiCJCmx+Oaft`twp^<@60RSga0_Ok#
literal 0
HcmV?d00001
new file mode 100644
GIT binary patch
literal 7
Ocmey*nk~q{!~g&ZeF1|2
literal 0
HcmV?d00001
new file mode 100644
GIT binary patch
literal 140
zcmez0mA!$Hmx+Oafs28Gv5|pEfx-GCqc9T#!+qz%6b3k8Vq{@vWU>_lse}u{8UNW;
iKG-uX{qvu}`V&~Q95)NPW@cM4X1E4q21N6sKmP$W3KJy&
literal 0
HcmV?d00001
new file mode 100644
GIT binary patch
literal 12
Qcmey*mCeGy&cpx$01*8Ff&c&j
literal 0
HcmV?d00001
new file mode 100644
@@ -0,0 +1,117 @@
+/*
+ * Multi-Link element parsing - fuzzer
+ * Copyright (c) 2026, Louis Kotze <loukot@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;
+}
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@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