[PATCH net v2 3/3] nfc: llcp: fix TLV parsing OOB and length underflow in nfc_llcp_recv_snl

Lekë Hapçiu
2026-04-09T19:01:01Z

From: Lekë Hapçiu <framemain@outlook.com>

nfc_llcp_recv_snl() contains four distinct vulnerabilities.

Issue 1 - missing minimum-length guard on skb:

nfc_llcp_dsap() and nfc_llcp_ssap() access pdu->data[0] and pdu->data[1]
unconditionally. The subsequent computation:

tlv_len = skb->len - LLCP_HEADER_SIZE; /* LLCP_HEADER_SIZE = 2 */

truncates to u16. If skb->len < 2, the unsigned subtraction wraps at
unsigned int width and the truncation to u16 yields up to 65534, causing
the while loop to iterate far beyond the skb data. No guard exists at
the dispatch path to prevent this.

Fix: add `if (skb->len < LLCP_HEADER_SIZE) return;` before any skb->data
access, matching the pattern already used in nfc_llcp_recv_agf().

Issue 2 - missing per-iteration TLV header guard:

The loop reads tlv[0] and tlv[1] with no prior check that two bytes
remain. When one byte remains, tlv[1] is one byte past the array end.

Fix: `if (tlv_len - offset < 2) break;`

Issue 3 - peer-controlled `length` field advances tlv past skb end:

`length` (tlv[1]) is advanced unconditionally into `offset` and `tlv`
without verifying that `length` bytes of TLV value exist. A malicious
peer sets `length` large enough that `offset` remains below `tlv_len` on
the next iteration while `tlv` points into adjacent kernel heap.

Fix: `if (tlv_len - offset - 2 < length) break;`

Issue 4 - per-type minimum-length hazards:

LLCP_TLV_SDREQ: `service_name_len = length - 1` is u8 arithmetic. When
length == 0 this wraps to 255, causing a 255-byte kernel memory scan via
strncmp. tlv[2] (tid) is also accessed unconditionally.
Fix: require length >= 1 before the tid/service_name access.

LLCP_TLV_SDRES: tlv[2] and tlv[3] are accessed without verifying
length >= 2. Unlike the GB/connection parsers, SDREQ/SDRES are not
processed via llcp_tlv8/16, so the llcp_tlv_length[] table provides no
protection here.
Fix: require length >= 2 before the tlv[2]/tlv[3] accesses.

In both cases a `break` from the inner switch falls through to the
unconditional `offset += length + 2; tlv += length + 2` at the loop
tail, correctly advancing past the malformed TLV. The outer two guards
break from the while loop entirely.

Reachability: SNL PDUs are processed during LLCP service discovery, before
any connection is established, from any NFC peer within ~4 cm with no
authentication or pairing.

Fixes: 19cfe5843e86 ("NFC: Initial SNL support")
Cc: stable@vger.kernel.org
Signed-off-by: Lekë Hapçiu <framemain@outlook.com>
---
net/nfc/llcp_core.c | 13 +++++++++++++
1 file changed, 13 insertions(+)

diff --git a/net/nfc/llcp_core.c b/net/nfc/llcp_core.c
index db5bc6a87..16acf7c2b 100644
--- a/net/nfc/llcp_core.c
+++ b/net/nfc/llcp_core.c
@@ -1284,6 +1284,11 @@ static void nfc_llcp_recv_snl(struct nfc_llcp_local *local,
size_t sdres_tlvs_len;
HLIST_HEAD(nl_sdres_list);

+ if (skb->len < LLCP_HEADER_SIZE) {
+ pr_err("Malformed SNL PDU\n");
+ return;
+ }
+
dsap = nfc_llcp_dsap(skb);
ssap = nfc_llcp_ssap(skb);

@@ -1300,11 +1305,17 @@ static void nfc_llcp_recv_snl(struct nfc_llcp_local *local,
sdres_tlvs_len = 0;

while (offset < tlv_len) {
+ if (tlv_len - offset < 2)
+ break;
type = tlv[0];
length = tlv[1];
+ if (tlv_len - offset - 2 < length)
+ break;

switch (type) {
case LLCP_TLV_SDREQ:
+ if (length < 1)
+ break;
tid = tlv[2];
service_name = (char *) &tlv[3];
service_name_len = length - 1;
@@ -1369,6 +1380,8 @@ static void nfc_llcp_recv_snl(struct nfc_llcp_local *local,
break;

case LLCP_TLV_SDRES:
+ if (length < 2)
+ break;
mutex_lock(&local->sdreq_lock);

pr_debug("LLCP_TLV_SDRES: searching tid %d\n", tlv[2]);
--
2.51.0