[PATCH] wifi: mm81x: validate external host table TLV lengths

From: Heyang Tan

Date: Sun Nov 01 2026 - 09:11:28 EST


mm81x_fw_parse_ext_host_tbl() uses the length stored in the external host
table payload as the TLV walk boundary. The buffer is allocated using a
different length read from a device register, and the two values are not
cross-checked. A device can report a small register length so that the
driver allocates a small buffer, while the payload length field claims a
larger size and makes the walk read beyond the allocation.

Each branch also dereferences a complete fixed-size structure while only
the start of the TLV header has been bounds-checked. A truncated trailing
TLV can therefore make the capabilities, checksum, or YAPS parser read
past the allocated buffer.

Pass the allocation length out of mm81x_fw_read_ext_host_table() and clamp
the payload length to it. Require a complete TLV header before reading it
and verify that the full structure is present before dispatching it.

A KASAN reproducer mirroring the TLV walk reports a slab-out-of-bounds
with a small allocation and an oversized payload length. The same input
does not report after these checks are applied.

Signed-off-by: Heyang Tan <thy15333007817@xxxxxxx>
---
drivers/net/wireless/morsemicro/mm81x/fw.c | 84 +++++++++++++++++-----
1 file changed, 65 insertions(+), 19 deletions(-)

diff --git a/drivers/net/wireless/morsemicro/mm81x/fw.c b/drivers/net/wireless/morsemicro/mm81x/fw.c
index d6d2ad086..b5d6f87df 100644
--- a/drivers/net/wireless/morsemicro/mm81x/fw.c
+++ b/drivers/net/wireless/morsemicro/mm81x/fw.c
@@ -445,7 +445,8 @@ static int mm81x_fw_get_host_table_ptr(struct mm81x *mors)
}

static int mm81x_fw_read_ext_host_table(struct mm81x *mors,
- struct ext_host_tbl **ext_host_table)
+ struct ext_host_tbl **ext_host_table,
+ u32 *alloc_len)
{
int ret = 0;
u32 host_tbl_ptr = mors->host_table_ptr;
@@ -494,6 +495,7 @@ static int mm81x_fw_read_ext_host_table(struct mm81x *mors,

mm81x_release_bus(mors);
*ext_host_table = host_tbl;
+ *alloc_len = ext_host_tbl_len;
return ret;

exit:
@@ -548,47 +550,91 @@ int mm81x_fw_parse_ext_host_tbl(struct mm81x *mors)
int ret;
u8 *head;
u8 *end;
+ u32 alloc_len = 0;
+ u32 tbl_len;
+ size_t tlv_offset = offsetof(struct ext_host_tbl,
+ ext_host_table_data_tlvs);
struct ext_host_tbl *ext_host_table = NULL;

- ret = mm81x_fw_read_ext_host_table(mors, &ext_host_table);
+ ret = mm81x_fw_read_ext_host_table(mors, &ext_host_table, &alloc_len);
if (ret || !ext_host_table)
goto exit;

- /* Parse the TLVs */
+ if (alloc_len < tlv_offset) {
+ ret = -EINVAL;
+ goto exit_free;
+ }
+
head = ext_host_table->ext_host_table_data_tlvs;
- end = ((u8 *)ext_host_table) +
- le32_to_cpu(ext_host_table->ext_host_tbl_length);

- while (head < end) {
+ /*
+ * The length stored inside the table is device supplied and is not
+ * guaranteed to match the size allocated from the length register.
+ * Clamp it to the allocation so the walk stays inside the buffer.
+ */
+ tbl_len = le32_to_cpu(ext_host_table->ext_host_tbl_length);
+ if (tbl_len < tlv_offset) {
+ ret = -EINVAL;
+ goto exit_free;
+ }
+ if (tbl_len > alloc_len)
+ tbl_len = alloc_len;
+ end = ((u8 *)ext_host_table) + tbl_len;
+
+ while (end - head >= sizeof(struct ext_host_tbl_tlv_hdr)) {
struct ext_host_tbl_tlv_hdr *hdr =
(struct ext_host_tbl_tlv_hdr *)head;
+ size_t left = end - head;
+ u16 tlv_len = le16_to_cpu(hdr->length);

switch (le16_to_cpu(hdr->tag)) {
- case MM81X_FW_HOST_TABLE_TAG_S1G_CAPABILITIES:
- mm81x_fw_update_capabilities(
- mors, (struct ext_host_tbl_s1g_caps *)hdr);
+ case MM81X_FW_HOST_TABLE_TAG_S1G_CAPABILITIES: {
+ struct ext_host_tbl_s1g_caps *caps;
+
+ if (left < sizeof(*caps) || tlv_len < sizeof(*caps)) {
+ ret = -EINVAL;
+ goto exit_free;
+ }
+ caps = (struct ext_host_tbl_s1g_caps *)hdr;
+ mm81x_fw_update_capabilities(mors, caps);
break;
+ }
+ case MM81X_FW_HOST_TABLE_TAG_INSERT_SKB_CHECKSUM: {
+ struct ext_host_tbl_insert_skb_checksum *cks;

- case MM81X_FW_HOST_TABLE_TAG_INSERT_SKB_CHECKSUM:
- mm81x_fw_update_validate_skb_checksum(
- mors,
- (struct ext_host_tbl_insert_skb_checksum *)hdr);
+ if (left < sizeof(*cks) || tlv_len < sizeof(*cks)) {
+ ret = -EINVAL;
+ goto exit_free;
+ }
+ cks = (struct ext_host_tbl_insert_skb_checksum *)hdr;
+ mm81x_fw_update_validate_skb_checksum(mors, cks);
break;
+ }
+ case MM81X_FW_HOST_TABLE_TAG_YAPS_TABLE: {
+ struct ext_host_tbl_yaps_table *yt;

- case MM81X_FW_HOST_TABLE_TAG_YAPS_TABLE:
- mm81x_yaps_hw_read_table(
- mors, &((struct ext_host_tbl_yaps_table *)hdr)
- ->yaps_table);
+ if (left < sizeof(*yt) || tlv_len < sizeof(*yt)) {
+ ret = -EINVAL;
+ goto exit_free;
+ }
+ yt = (struct ext_host_tbl_yaps_table *)hdr;
+ mm81x_yaps_hw_read_table(mors, &yt->yaps_table);
break;
+ }
default:
break;
}

- head += le16_to_cpu(hdr->length);
- if (!hdr->length)
+ if (!tlv_len)
break;
+ if (tlv_len > left) {
+ ret = -EINVAL;
+ goto exit_free;
+ }
+ head += tlv_len;
}

+exit_free:
kfree(ext_host_table);
return ret;
exit: