[PATCH net v4 3/5] nfc: llcp: fix TLV parsing OOB in nfc_llcp_recv_snl

From: Lekë Hapçiu

Date: Fri Apr 24 2026 - 14:14:19 EST


nfc_llcp_recv_snl() has five problems when handling a hostile peer:

1. nfc_llcp_dsap()/nfc_llcp_ssap() dereference skb->data[0..1] without
verifying skb->len; a 0- or 1-byte frame leads to an OOB read.
Additionally tlv_len = skb->len - LLCP_HEADER_SIZE wraps when
skb->len < 2, causing the following loop to run far past the
buffer.

2. The per-iteration loop guard `offset < tlv_len` only proves one
byte is available, but the body reads tlv[0] and tlv[1].

3. The peer-supplied `length` field is used to advance `tlv` without
being checked against the remaining array space.

4. The SDREQ handler reads tid (tlv[2]) and the first byte of
service_name (tlv[3]) whenever length >= 1, so length == 1 reads
one byte past the TLV. A length of 2 is required to cover both
accesses and to yield a non-empty service_name.

The pr_debug("Looking for %.16s\n", service_name) additionally
treats the buffer as a NUL-terminated string and can read up to
15 bytes past service_name_len. Use a length-bounded format
("%.*s") so the print cannot escape the validated TLV region.

5. The SDRES handler reads tlv[2] (tid) and tlv[3] (sap) without
checking length. A length of 0 or 1 therefore triggers the same
1- or 2-byte OOB read as SDREQ; require length >= 2 here as well.

Fix: reject frames smaller than LLCP_HEADER_SIZE up front; add TLV
header and TLV value guards at the top of each iteration; require
length >= 2 for SDREQ and SDRES; bound the pr_debug of service_name
with %.*s.

Fixes: 19cfe5843e86 ("NFC: Initial SNL support")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Lekë Hapçiu <snowwlake@xxxxxxxxxx>
---
net/nfc/llcp_core.c | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/net/nfc/llcp_core.c b/net/nfc/llcp_core.c
index db5bc6a878dd..3284be517204 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,16 +1305,23 @@ 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 < 2)
+ break;
tid = tlv[2];
service_name = (char *) &tlv[3];
service_name_len = length - 1;

- pr_debug("Looking for %.16s\n", service_name);
+ pr_debug("Looking for %.*s\n",
+ (int)service_name_len, service_name);

if (service_name_len == strlen("urn:nfc:sn:sdp") &&
!strncmp(service_name, "urn:nfc:sn:sdp",
@@ -1369,6 +1381,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