* [PATCH net v4 0/5] nfc: fix multiple OOB reads in NCI and LLCP parsing paths
@ 2026-04-24 18:01 Lekë Hapçiu
2026-04-24 18:13 ` [PATCH net v4 3/5] nfc: llcp: fix TLV parsing OOB in nfc_llcp_recv_snl Lekë Hapçiu
2026-04-28 12:55 ` [PATCH net v4 0/5] nfc: fix multiple OOB reads in NCI and LLCP parsing paths Simon Horman
0 siblings, 2 replies; 6+ messages in thread
From: Lekë Hapçiu @ 2026-04-24 18:01 UTC (permalink / raw)
To: netdev
Cc: davem, edumazet, kuba, pabeni, krzk, horms, linux-kernel, stable,
Lekë Hapçiu
This series fixes five out-of-bounds / underflow bugs in the kernel NFC
stack. All are reachable from a remote NFC peer that the local stack
has already associated with; in the LLCP cases the peer only needs to
send a malformed frame.
1/5 nci: u8 underflow in nci_store_general_bytes_nfc_dep() lets the
attacker-controlled atr_res_len skip the GT-offset subtraction
and cause an OOB read/write against general_bytes[].
2/5 llcp: parse_gb_tlv() / parse_connection_tlv() trust the TLV
length byte without checking remaining buffer, and the tlv16
accessors read past the end when length < 2.
3/5 llcp: nfc_llcp_recv_snl() has the same TLV-length trust bug, and
its SDRES handler uses an unbounded "%.16s" pr_debug() that
walks past service_name_len.
4/5 llcp: nfc_llcp_recv_dm() reads skb->data[3] without checking
skb->len, giving a 1-byte heap OOB read.
5/5 llcp: nfc_llcp_connect_sn() walks the TLV array with no length
validation; a crafted CONNECT frame drops it into OOB reads /
an unbounded service-name pointer.
The series applies on top of net/main.
Lekë Hapçiu (5):
nfc: nci: fix u8 underflow in nci_store_general_bytes_nfc_dep
nfc: llcp: fix TLV parsing in parse_gb_tlv and parse_connection_tlv
nfc: llcp: fix TLV parsing OOB in nfc_llcp_recv_snl
nfc: llcp: fix OOB read of DM reason byte in nfc_llcp_recv_dm
nfc: llcp: fix TLV parsing OOB in nfc_llcp_connect_sn
net/nfc/llcp_commands.c | 24 ++++++++++++++++++++++--
net/nfc/llcp_core.c | 35 ++++++++++++++++++++++++++++++++---
net/nfc/nci/ntf.c | 6 ++++++
3 files changed, 60 insertions(+), 5 deletions(-)
--
2.51.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH net v4 3/5] nfc: llcp: fix TLV parsing OOB in nfc_llcp_recv_snl
2026-04-24 18:01 [PATCH net v4 0/5] nfc: fix multiple OOB reads in NCI and LLCP parsing paths Lekë Hapçiu
@ 2026-04-24 18:13 ` Lekë Hapçiu
2026-04-24 18:13 ` [PATCH net v4 4/5] nfc: llcp: fix OOB read of DM reason byte in nfc_llcp_recv_dm Lekë Hapçiu
2026-04-24 18:13 ` [PATCH net v4 5/5] nfc: llcp: fix TLV parsing OOB in nfc_llcp_connect_sn Lekë Hapçiu
2026-04-28 12:55 ` [PATCH net v4 0/5] nfc: fix multiple OOB reads in NCI and LLCP parsing paths Simon Horman
1 sibling, 2 replies; 6+ messages in thread
From: Lekë Hapçiu @ 2026-04-24 18:13 UTC (permalink / raw)
To: netdev
Cc: davem, edumazet, kuba, pabeni, krzk, horms, linux-kernel, stable,
Lekë Hapçiu
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@vger.kernel.org
Signed-off-by: Lekë Hapçiu <snowwlake@icloud.com>
---
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
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH net v4 4/5] nfc: llcp: fix OOB read of DM reason byte in nfc_llcp_recv_dm
2026-04-24 18:13 ` [PATCH net v4 3/5] nfc: llcp: fix TLV parsing OOB in nfc_llcp_recv_snl Lekë Hapçiu
@ 2026-04-24 18:13 ` Lekë Hapçiu
2026-04-24 18:13 ` [PATCH net v4 5/5] nfc: llcp: fix TLV parsing OOB in nfc_llcp_connect_sn Lekë Hapçiu
1 sibling, 0 replies; 6+ messages in thread
From: Lekë Hapçiu @ 2026-04-24 18:13 UTC (permalink / raw)
To: netdev
Cc: davem, edumazet, kuba, pabeni, krzk, horms, linux-kernel, stable,
Lekë Hapçiu
nfc_llcp_recv_dm() reads skb->data[2] (the DM reason byte) without
first verifying that skb->len is at least LLCP_HEADER_SIZE + 1. A DM
PDU carrying only the 2-byte LLCP header from a rogue peer therefore
triggers a 1-byte OOB read.
Add the minimum-length guard at function entry, matching the pattern
used by nfc_llcp_recv_snl() and nfc_llcp_recv_agf().
Fixes: 5c0560b7a5c6 ("NFC: Handle LLCP Disconnected Mode frames")
Cc: stable@vger.kernel.org
Signed-off-by: Lekë Hapçiu <snowwlake@icloud.com>
---
net/nfc/llcp_core.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/net/nfc/llcp_core.c b/net/nfc/llcp_core.c
index 3284be517204..ca0abfd329e5 100644
--- a/net/nfc/llcp_core.c
+++ b/net/nfc/llcp_core.c
@@ -1239,6 +1239,11 @@ static void nfc_llcp_recv_dm(struct nfc_llcp_local *local,
struct sock *sk;
u8 dsap, ssap, reason;
+ if (skb->len < LLCP_HEADER_SIZE + 1) {
+ pr_err("Malformed DM PDU\n");
+ return;
+ }
+
dsap = nfc_llcp_dsap(skb);
ssap = nfc_llcp_ssap(skb);
reason = skb->data[2];
--
2.51.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH net v4 5/5] nfc: llcp: fix TLV parsing OOB in nfc_llcp_connect_sn
2026-04-24 18:13 ` [PATCH net v4 3/5] nfc: llcp: fix TLV parsing OOB in nfc_llcp_recv_snl Lekë Hapçiu
2026-04-24 18:13 ` [PATCH net v4 4/5] nfc: llcp: fix OOB read of DM reason byte in nfc_llcp_recv_dm Lekë Hapçiu
@ 2026-04-24 18:13 ` Lekë Hapçiu
1 sibling, 0 replies; 6+ messages in thread
From: Lekë Hapçiu @ 2026-04-24 18:13 UTC (permalink / raw)
To: netdev
Cc: davem, edumazet, kuba, pabeni, krzk, horms, linux-kernel, stable,
Lekë Hapçiu
nfc_llcp_connect_sn() walks the TLV array of an LLCP CONNECT PDU
looking for the Service Name TLV, but shares the same class of bugs
as nfc_llcp_recv_snl() / nfc_llcp_parse_gb_tlv():
1. tlv_array_len = skb->len - LLCP_HEADER_SIZE wraps when skb->len
is 0 or 1. The subsequent loop then runs far past the buffer.
2. The per-iteration guard `offset < tlv_array_len` only proves one
byte is available, but the body reads both tlv[0] (type) and
tlv[1] (length).
3. The peer-supplied `length` field is used to advance `tlv` without
being checked against the remaining array space, so a crafted
length walks `tlv` past the buffer. On the following iteration
tlv[0]/tlv[1] are read from adjacent memory.
4. When an LLCP_TLV_SN is found, the function returns &tlv[2] with
*sn_len = length but without verifying that `length` bytes at
tlv[2..] are still inside the TLV array. The caller in
nfc_llcp_recv_connect() then uses this (pointer, length) pair as
a service name, so it may read past the PDU.
Fix: reject frames smaller than LLCP_HEADER_SIZE up front; add TLV
header and TLV value guards at the top of each iteration. The value
guard also ensures that the (&tlv[2], length) pair returned on
LLCP_TLV_SN lies fully inside the TLV array.
Also use LLCP_HEADER_SIZE instead of the magic literal `2` to match
the style of neighbouring LLCP receive paths.
Reported-by: Simon Horman <horms@kernel.org>
Closes: https://lore.kernel.org/netdev/20260417160438.GH31784@horms.kernel.org/
Fixes: d646960f7986 ("NFC: Initial LLCP support")
Cc: stable@vger.kernel.org
Signed-off-by: Lekë Hapçiu <snowwlake@icloud.com>
---
net/nfc/llcp_core.c | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/net/nfc/llcp_core.c b/net/nfc/llcp_core.c
index ca0abfd329e5..df5567ca7fa8 100644
--- a/net/nfc/llcp_core.c
+++ b/net/nfc/llcp_core.c
@@ -849,12 +849,22 @@ static struct nfc_llcp_sock *nfc_llcp_sock_get_sn(struct nfc_llcp_local *local,
static const u8 *nfc_llcp_connect_sn(const struct sk_buff *skb, size_t *sn_len)
{
u8 type, length;
- const u8 *tlv = &skb->data[2];
- size_t tlv_array_len = skb->len - LLCP_HEADER_SIZE, offset = 0;
+ const u8 *tlv;
+ size_t tlv_array_len, offset = 0;
+
+ if (skb->len < LLCP_HEADER_SIZE)
+ return NULL;
+
+ tlv = &skb->data[LLCP_HEADER_SIZE];
+ tlv_array_len = skb->len - LLCP_HEADER_SIZE;
while (offset < tlv_array_len) {
+ if (tlv_array_len - offset < 2)
+ break;
type = tlv[0];
length = tlv[1];
+ if (tlv_array_len - offset - 2 < length)
+ break;
pr_debug("type 0x%x length %d\n", type, length);
--
2.51.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH net v4 0/5] nfc: fix multiple OOB reads in NCI and LLCP parsing paths
2026-04-24 18:01 [PATCH net v4 0/5] nfc: fix multiple OOB reads in NCI and LLCP parsing paths Lekë Hapçiu
2026-04-24 18:13 ` [PATCH net v4 3/5] nfc: llcp: fix TLV parsing OOB in nfc_llcp_recv_snl Lekë Hapçiu
@ 2026-04-28 12:55 ` Simon Horman
2026-04-28 13:46 ` Paolo Abeni
1 sibling, 1 reply; 6+ messages in thread
From: Simon Horman @ 2026-04-28 12:55 UTC (permalink / raw)
To: Lekë Hapçiu
Cc: netdev, davem, edumazet, kuba, pabeni, krzk, linux-kernel, stable
On Fri, Apr 24, 2026 at 08:01:46PM +0200, Lekë Hapçiu wrote:
> This series fixes five out-of-bounds / underflow bugs in the kernel NFC
> stack. All are reachable from a remote NFC peer that the local stack
> has already associated with; in the LLCP cases the peer only needs to
> send a malformed frame.
>
> 1/5 nci: u8 underflow in nci_store_general_bytes_nfc_dep() lets the
> attacker-controlled atr_res_len skip the GT-offset subtraction
> and cause an OOB read/write against general_bytes[].
> 2/5 llcp: parse_gb_tlv() / parse_connection_tlv() trust the TLV
> length byte without checking remaining buffer, and the tlv16
> accessors read past the end when length < 2.
> 3/5 llcp: nfc_llcp_recv_snl() has the same TLV-length trust bug, and
> its SDRES handler uses an unbounded "%.16s" pr_debug() that
> walks past service_name_len.
> 4/5 llcp: nfc_llcp_recv_dm() reads skb->data[3] without checking
> skb->len, giving a 1-byte heap OOB read.
> 5/5 llcp: nfc_llcp_connect_sn() walks the TLV array with no length
> validation; a crafted CONNECT frame drops it into OOB reads /
> an unbounded service-name pointer.
>
> The series applies on top of net/main.
>
> Lekë Hapçiu (5):
> nfc: nci: fix u8 underflow in nci_store_general_bytes_nfc_dep
> nfc: llcp: fix TLV parsing in parse_gb_tlv and parse_connection_tlv
> nfc: llcp: fix TLV parsing OOB in nfc_llcp_recv_snl
> nfc: llcp: fix OOB read of DM reason byte in nfc_llcp_recv_dm
> nfc: llcp: fix TLV parsing OOB in nfc_llcp_connect_sn
Hi,
My only feedback on v4 of this patchset is that somehow the
threading is broken: each of patch 1/5 - 5/5 should be a reply
to the cover letter - 0/5 - but that does not seem to be the case.
And some tooling, notably Sashiko, seems to rely on the
entire patchset being contained in a single email thread.
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH net v4 0/5] nfc: fix multiple OOB reads in NCI and LLCP parsing paths
2026-04-28 12:55 ` [PATCH net v4 0/5] nfc: fix multiple OOB reads in NCI and LLCP parsing paths Simon Horman
@ 2026-04-28 13:46 ` Paolo Abeni
0 siblings, 0 replies; 6+ messages in thread
From: Paolo Abeni @ 2026-04-28 13:46 UTC (permalink / raw)
To: Simon Horman, Lekë Hapçiu
Cc: netdev, davem, edumazet, kuba, krzk, linux-kernel, stable,
David Heidelberg
On 4/28/26 2:55 PM, Simon Horman wrote:
> On Fri, Apr 24, 2026 at 08:01:46PM +0200, Lekë Hapçiu wrote:
>> This series fixes five out-of-bounds / underflow bugs in the kernel NFC
>> stack. All are reachable from a remote NFC peer that the local stack
>> has already associated with; in the LLCP cases the peer only needs to
>> send a malformed frame.
>>
>> 1/5 nci: u8 underflow in nci_store_general_bytes_nfc_dep() lets the
>> attacker-controlled atr_res_len skip the GT-offset subtraction
>> and cause an OOB read/write against general_bytes[].
>> 2/5 llcp: parse_gb_tlv() / parse_connection_tlv() trust the TLV
>> length byte without checking remaining buffer, and the tlv16
>> accessors read past the end when length < 2.
>> 3/5 llcp: nfc_llcp_recv_snl() has the same TLV-length trust bug, and
>> its SDRES handler uses an unbounded "%.16s" pr_debug() that
>> walks past service_name_len.
>> 4/5 llcp: nfc_llcp_recv_dm() reads skb->data[3] without checking
>> skb->len, giving a 1-byte heap OOB read.
>> 5/5 llcp: nfc_llcp_connect_sn() walks the TLV array with no length
>> validation; a crafted CONNECT frame drops it into OOB reads /
>> an unbounded service-name pointer.
>>
>> The series applies on top of net/main.
>>
>> Lekë Hapçiu (5):
>> nfc: nci: fix u8 underflow in nci_store_general_bytes_nfc_dep
>> nfc: llcp: fix TLV parsing in parse_gb_tlv and parse_connection_tlv
>> nfc: llcp: fix TLV parsing OOB in nfc_llcp_recv_snl
>> nfc: llcp: fix OOB read of DM reason byte in nfc_llcp_recv_dm
>> nfc: llcp: fix TLV parsing OOB in nfc_llcp_connect_sn
>
> Hi,
>
> My only feedback on v4 of this patchset is that somehow the
> threading is broken: each of patch 1/5 - 5/5 should be a reply
> to the cover letter - 0/5 - but that does not seem to be the case.
> And some tooling, notably Sashiko, seems to rely on the
> entire patchset being contained in a single email thread.
Given the above, I suggest re-posting.
Also note that we are moving NFC to a specific subtree, see:
https://lore.kernel.org/netdev/938496c6-84c1-4d53-bb56-73bbd7b2bdd7@ixit.cz/
please wait a bit for resubmission, possibly David will be already ready
to catch them.
Thanks,
Paolo
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-04-28 13:46 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-24 18:01 [PATCH net v4 0/5] nfc: fix multiple OOB reads in NCI and LLCP parsing paths Lekë Hapçiu
2026-04-24 18:13 ` [PATCH net v4 3/5] nfc: llcp: fix TLV parsing OOB in nfc_llcp_recv_snl Lekë Hapçiu
2026-04-24 18:13 ` [PATCH net v4 4/5] nfc: llcp: fix OOB read of DM reason byte in nfc_llcp_recv_dm Lekë Hapçiu
2026-04-24 18:13 ` [PATCH net v4 5/5] nfc: llcp: fix TLV parsing OOB in nfc_llcp_connect_sn Lekë Hapçiu
2026-04-28 12:55 ` [PATCH net v4 0/5] nfc: fix multiple OOB reads in NCI and LLCP parsing paths Simon Horman
2026-04-28 13:46 ` Paolo Abeni
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox