* [PATCH v2] nfc: nci: add data_len bound checks to activation parameter extractors
@ 2026-06-10 6:41 Bryam Vargas
2026-06-12 13:18 ` Simon Horman
0 siblings, 1 reply; 2+ messages in thread
From: Bryam Vargas @ 2026-06-10 6:41 UTC (permalink / raw)
To: David Heidelberg; +Cc: oe-linux-nfc, netdev, linux-kernel
nci_extract_activation_params_iso_dep() and
nci_extract_activation_params_nfc_dep() read an inner length byte from
the NCI RF_INTF_ACTIVATED_NTF payload and use it to memcpy() into fixed
kernel buffers, but neither function receives the caller-validated
activation_params_len. A crafted NCI notification with
activation_params_len=1 and an inner length byte of up to 20 (NFC-A) or
50 (NFC-B) causes memcpy() to read that many bytes past the one valid
byte in the activation params region -- a slab out-of-bounds read of
kernel memory adjacent to the NCI skb.
The sibling nci_extract_rf_params_*() family was given equivalent
protection by commit 571dcbeb8e63 ("net: nfc: nci: Fix parameter
validation for packet data"), but the two activation parameter
extractors were not updated at that time.
Add a data_len parameter to both functions, guard against an empty
region before consuming the inner length byte, decrement the remaining
count after consuming it, and clamp the copy length to what is actually
available. Update both call sites to pass ntf.activation_params_len,
which is already validated against the skb at ntf.c:801.
Fixes: e8c0dacd9836 ("NFC: Update names and structs to NCI spec 1.0 d18")
Cc: stable@vger.kernel.org
Signed-off-by: Bryam Vargas <hexlabsecurity@proton.me>
---
v2: no functional change. Resend of
https://lore.kernel.org/all/20260607094822.322125-1-hexlabsecurity@proton.me/
(2026-06-07), which did not reach the NFC maintainer: the MAINTAINERS
address for NFC, david+nfc@ixit.cz, rejected delivery ("550 User
doesn't exist" -- the +nfc subaddress expanded to
#/Kernel/Linux/NFC@ixit.cz), so this is resent to david@ixit.cz. Also
added the Cc: stable tag for the backport and dropped stable@vger from
the direct Cc (it should follow the mainline merge, not precede it).
Verification (NFC-A ISO-DEP, NFC_ATS_MAXSIZE = 20):
data_len inner_len without patch with patch
-------- --------- ------------------------- --------------------
1 0 rats_res_len=0, clean same
1 1 memcpy +1B OOB clamped to 0, clean
1 20 memcpy +20B OOB <-- PoC clamped to 0, clean
2 2 memcpy +1B OOB clamped to 1, clean
21 20 memcpy 20B clean same
NFC-B (attrib_res, max 50) and NFC-DEP poll/listen (atr_res/atr_req,
max 62) have the same shape and receive the same fix.
net/nfc/nci/ntf.c | 26 ++++++++++++++++++++++----
1 file changed, 22 insertions(+), 4 deletions(-)
diff --git a/net/nfc/nci/ntf.c b/net/nfc/nci/ntf.c
index c96512bb8653..753f4cf08748 100644
--- a/net/nfc/nci/ntf.c
+++ b/net/nfc/nci/ntf.c
@@ -525,15 +525,19 @@ static int nci_rf_discover_ntf_packet(struct nci_dev *ndev,
static int nci_extract_activation_params_iso_dep(struct nci_dev *ndev,
struct nci_rf_intf_activated_ntf *ntf,
- const __u8 *data)
+ const __u8 *data, __u8 data_len)
{
struct activation_params_nfca_poll_iso_dep *nfca_poll;
struct activation_params_nfcb_poll_iso_dep *nfcb_poll;
switch (ntf->activation_rf_tech_and_mode) {
case NCI_NFC_A_PASSIVE_POLL_MODE:
+ if (data_len < 1)
+ return -EINVAL;
nfca_poll = &ntf->activation_params.nfca_poll_iso_dep;
nfca_poll->rats_res_len = min_t(__u8, *data++, NFC_ATS_MAXSIZE);
+ data_len--;
+ nfca_poll->rats_res_len = min_t(__u8, nfca_poll->rats_res_len, data_len);
pr_debug("rats_res_len %d\n", nfca_poll->rats_res_len);
if (nfca_poll->rats_res_len > 0) {
memcpy(nfca_poll->rats_res,
@@ -542,8 +546,12 @@ static int nci_extract_activation_params_iso_dep(struct nci_dev *ndev,
break;
case NCI_NFC_B_PASSIVE_POLL_MODE:
+ if (data_len < 1)
+ return -EINVAL;
nfcb_poll = &ntf->activation_params.nfcb_poll_iso_dep;
nfcb_poll->attrib_res_len = min_t(__u8, *data++, 50);
+ data_len--;
+ nfcb_poll->attrib_res_len = min_t(__u8, nfcb_poll->attrib_res_len, data_len);
pr_debug("attrib_res_len %d\n", nfcb_poll->attrib_res_len);
if (nfcb_poll->attrib_res_len > 0) {
memcpy(nfcb_poll->attrib_res,
@@ -562,7 +570,7 @@ static int nci_extract_activation_params_iso_dep(struct nci_dev *ndev,
static int nci_extract_activation_params_nfc_dep(struct nci_dev *ndev,
struct nci_rf_intf_activated_ntf *ntf,
- const __u8 *data)
+ const __u8 *data, __u8 data_len)
{
struct activation_params_poll_nfc_dep *poll;
struct activation_params_listen_nfc_dep *listen;
@@ -570,9 +578,13 @@ static int nci_extract_activation_params_nfc_dep(struct nci_dev *ndev,
switch (ntf->activation_rf_tech_and_mode) {
case NCI_NFC_A_PASSIVE_POLL_MODE:
case NCI_NFC_F_PASSIVE_POLL_MODE:
+ if (data_len < 1)
+ return -EINVAL;
poll = &ntf->activation_params.poll_nfc_dep;
poll->atr_res_len = min_t(__u8, *data++,
NFC_ATR_RES_MAXSIZE - 2);
+ data_len--;
+ poll->atr_res_len = min_t(__u8, poll->atr_res_len, data_len);
pr_debug("atr_res_len %d\n", poll->atr_res_len);
if (poll->atr_res_len > 0)
memcpy(poll->atr_res, data, poll->atr_res_len);
@@ -580,9 +592,13 @@ static int nci_extract_activation_params_nfc_dep(struct nci_dev *ndev,
case NCI_NFC_A_PASSIVE_LISTEN_MODE:
case NCI_NFC_F_PASSIVE_LISTEN_MODE:
+ if (data_len < 1)
+ return -EINVAL;
listen = &ntf->activation_params.listen_nfc_dep;
listen->atr_req_len = min_t(__u8, *data++,
NFC_ATR_REQ_MAXSIZE - 2);
+ data_len--;
+ listen->atr_req_len = min_t(__u8, listen->atr_req_len, data_len);
pr_debug("atr_req_len %d\n", listen->atr_req_len);
if (listen->atr_req_len > 0)
memcpy(listen->atr_req, data, listen->atr_req_len);
@@ -806,12 +822,14 @@ static int nci_rf_intf_activated_ntf_packet(struct nci_dev *ndev,
switch (ntf.rf_interface) {
case NCI_RF_INTERFACE_ISO_DEP:
err = nci_extract_activation_params_iso_dep(ndev,
- &ntf, data);
+ &ntf, data,
+ ntf.activation_params_len);
break;
case NCI_RF_INTERFACE_NFC_DEP:
err = nci_extract_activation_params_nfc_dep(ndev,
- &ntf, data);
+ &ntf, data,
+ ntf.activation_params_len);
break;
case NCI_RF_INTERFACE_FRAME:
--
2.43.0
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH v2] nfc: nci: add data_len bound checks to activation parameter extractors
2026-06-10 6:41 [PATCH v2] nfc: nci: add data_len bound checks to activation parameter extractors Bryam Vargas
@ 2026-06-12 13:18 ` Simon Horman
0 siblings, 0 replies; 2+ messages in thread
From: Simon Horman @ 2026-06-12 13:18 UTC (permalink / raw)
To: hexlabsecurity; +Cc: Simon Horman, david, oe-linux-nfc, netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
Full review at: https://sashiko.dev
Simon says:
The error type issue below - the first issue - does seem of
concern in the context of this patch.
While I think the other issues, which are flagged as pre-existing,
can be considered in the context of possible follow-up.
---
nfc: nci: add data_len bound checks to activation parameter extractors
This patch adds data_len bounds checking to the NCI activation parameter
extractors to prevent slab out-of-bounds reads. It introduces a data_len
parameter to guard against empty regions before consuming inner length
bytes.
> diff --git a/net/nfc/nci/ntf.c b/net/nfc/nci/ntf.c
> index c96512bb86531..753f4cf087489 100644
> --- a/net/nfc/nci/ntf.c
> +++ b/net/nfc/nci/ntf.c
> @@ -525,15 +525,19 @@ static int nci_rf_discover_ntf_packet(struct nci_dev *ndev,
>
> static int nci_extract_activation_params_iso_dep(struct nci_dev *ndev,
> struct nci_rf_intf_activated_ntf *ntf,
> - const __u8 *data)
> + const __u8 *data, __u8 data_len)
> {
> struct activation_params_nfca_poll_iso_dep *nfca_poll;
> struct activation_params_nfcb_poll_iso_dep *nfcb_poll;
>
> switch (ntf->activation_rf_tech_and_mode) {
> case NCI_NFC_A_PASSIVE_POLL_MODE:
> + if (data_len < 1)
> + return -EINVAL;
[Severity: Medium]
Is the correct error type being returned here?
nci_extract_activation_params_iso_dep() is called by
nci_rf_intf_activated_ntf_packet() and its return value is assigned to err,
which is later passed to nci_req_complete(ndev, err).
nci_req_complete() expects an NCI_STATUS_* positive integer constant.
If the POSIX error code -EINVAL (-22) is returned, nci_to_errno() will
truncate it to 234. It will then be incorrectly translated to -ENOSYS
through the default case of its switch statement.
Should a valid NCI status code like NCI_STATUS_INVALID_PARAM or
NCI_STATUS_RF_PROTOCOL_ERROR be used instead? The same behavior appears
in the updated nci_extract_activation_params_nfc_dep() below.
> nfca_poll = &ntf->activation_params.nfca_poll_iso_dep;
> nfca_poll->rats_res_len = min_t(__u8, *data++, NFC_ATS_MAXSIZE);
[ ... ]
> @@ -806,12 +822,14 @@ static int nci_rf_intf_activated_ntf_packet(struct nci_dev *ndev,
> switch (ntf.rf_interface) {
> case NCI_RF_INTERFACE_ISO_DEP:
> err = nci_extract_activation_params_iso_dep(ndev,
> - &ntf, data);
> + &ntf, data,
> + ntf.activation_params_len);
[Severity: Critical]
This isn't a bug introduced by this patch, but could the parsing of
atr_res_len allow for a stack memory disclosure?
In nci_rf_intf_activated_ntf_packet(), the ntf structure is allocated on
the stack without zero-initialization.
If a malformed packet specifies an activation_params_len >= 1 but the inner
atr_res_len is clamped to less than 15 (NFC_ATR_RES_GT_OFFSET), would this
cause an integer underflow?
net/nfc/nci/ntf.c:nci_store_general_bytes_nfc_dep() {
...
ndev->remote_gb_len = min_t(__u8,
(ntf->activation_params.poll_nfc_dep.atr_res_len
- NFC_ATR_RES_GT_OFFSET),
NFC_ATR_RES_GB_MAXSIZE);
...
memcpy(ndev->remote_gb,
(ntf->activation_params.poll_nfc_dep.atr_res
+ NFC_ATR_RES_GT_OFFSET),
ndev->remote_gb_len);
}
Because __u8 is unsigned, a negative result from the subtraction underflows
to a large positive value, which min_t() then clamps to
NFC_ATR_RES_GB_MAXSIZE (47).
Could this result in copying up to 47 uninitialized stack bytes into
ndev->remote_gb, which might later be broadcast to userspace?
[Severity: High]
This is a pre-existing issue, but is there also a 1-byte stack disclosure
in the NFC-A passive poll parameter parsing?
In nci_extract_rf_params_nfca_passive_poll(), sel_res is only initialized
if sel_res_len != 0:
net/nfc/nci/ntf.c:nci_extract_rf_params_nfca_passive_poll() {
if (nfca_poll->sel_res_len != 0) {
if (data_len < 1)
return ERR_PTR(-EINVAL);
nfca_poll->sel_res = *data++;
}
}
If sel_res_len is 0, nfca_poll->sel_res remains uninitialized. Because
nfca_poll points into the local uninitialized ntf stack variable in
nci_rf_intf_activated_ntf_packet(), it retains whatever garbage was on
the stack.
Later, nci_add_new_protocol() unconditionally copies this uninitialized byte:
net/nfc/nci/ntf.c:nci_add_new_protocol() {
...
target->sel_res = nfca_poll->sel_res;
...
}
When userspace queries the targets, this byte is sent out unconditionally:
net/nfc/netlink.c:nfc_genl_send_target() {
...
if (nla_put_u8(msg, NFC_ATTR_TARGET_SEL_RES, target->sel_res))
...
}
Would it be safer to zero-initialize the ntf structure at the start of
nci_rf_intf_activated_ntf_packet(), or set sel_res to 0 when sel_res_len
is 0?
> break;
>
> case NCI_RF_INTERFACE_NFC_DEP:
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-06-12 13:19 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-10 6:41 [PATCH v2] nfc: nci: add data_len bound checks to activation parameter extractors Bryam Vargas
2026-06-12 13:18 ` Simon Horman
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox