* [PATCH v4 0/2] nfc: nci: uart: fix write_work teardown UAF (+ nfcmrvl drv_data)
@ 2026-10-06 11:55 Shubham Antil
2026-10-06 11:55 ` [PATCH v4 1/2] nfc: nfcmrvl: set drv_data before registering the nci device Shubham Antil
` (2 more replies)
0 siblings, 3 replies; 5+ messages in thread
From: Shubham Antil @ 2026-10-06 11:55 UTC (permalink / raw)
To: netdev
Cc: oe-linux-nfc, David Heidelberg, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Johan Hovold,
linux-kernel, Giovanni Vignone
This series fixes a use-after-free in the NFC NCI UART line-discipline
teardown and a NULL dereference in the Marvell NFC UART driver that the
same change surfaces.
nci_uart_tty_close() frees the tx skbs and purges the tx queue before it
cancels the write worker, while the NCI device is still registered. The
worker can therefore be re-queued -- by a tty hangup, or by the NCI core
still sending through the driver -- and run against freed memory. The
fix follows the Bluetooth hci_uart ldisc: gate the worker on a readiness
bit under a percpu_rwsem and drain it under the write lock on close.
Enabling the worker that way means it must be enabled before the device
is registered, since the Marvell driver may transmit (firmware download)
from within registration. That exposes a pre-existing NULL dereference:
nfcmrvl publishes nu->drv_data only after nfcmrvl_nci_register_dev()
returns, so a transmit during registration reaches
nfcmrvl_nci_uart_tx_start() with a NULL nu->drv_data.
1/2 nfcmrvl: publish nu->drv_data before nci_register_device(), so a
transmit during registration does not hit a NULL nu->drv_data.
2/2 nci: uart: the teardown use-after-free fix; NCI_UART_READY and the
module reference are taken before ops.open(), with shared error
labels and the tx_wakeup trylock comment aligned with hci_uart.
Both are runtime-tested together under KASAN: the unfixed tree crashes
within the first iterations (slab-use-after-free in nci_uart_write_work),
the series survives 56000+ register/hangup iterations cleanly.
Shubham Antil (2):
nfc: nfcmrvl: set drv_data before registering the nci device
nfc: nci: uart: fix use-after-free of write_work on ldisc teardown
drivers/nfc/nfcmrvl/main.c | 17 ++++++++
drivers/nfc/nfcmrvl/uart.c | 3 --
include/net/nfc/nci_core.h | 2 +
net/nfc/nci/uart.c | 80 +++++++++++++++++++++++++++++++-------
4 files changed, 85 insertions(+), 17 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v4 1/2] nfc: nfcmrvl: set drv_data before registering the nci device
2026-10-06 11:55 [PATCH v4 0/2] nfc: nci: uart: fix write_work teardown UAF (+ nfcmrvl drv_data) Shubham Antil
@ 2026-10-06 11:55 ` Shubham Antil
2026-10-06 11:55 ` [PATCH v4 2/2] nfc: nci: uart: fix use-after-free of write_work on ldisc teardown Shubham Antil
2026-10-06 11:59 ` [PATCH v4 0/2] nfc: nci: uart: fix write_work teardown UAF (+ nfcmrvl drv_data) netdev-bot+sinfo
2 siblings, 0 replies; 5+ messages in thread
From: Shubham Antil @ 2026-10-06 11:55 UTC (permalink / raw)
To: netdev
Cc: oe-linux-nfc, David Heidelberg, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Johan Hovold,
linux-kernel, Giovanni Vignone
nfcmrvl_nci_uart_open() stores the per-ldisc back-pointer nu->drv_data
only after nfcmrvl_nci_register_dev() has returned. That function
registers the nci device, and a transmit started during registration
reaches the UART write path
nci_uart_write_work() -> nu->ops.tx_start()
== nfcmrvl_nci_uart_tx_start()
which dereferences nu->drv_data:
struct nfcmrvl_private *priv = (struct nfcmrvl_private *)nu->drv_data;
if (priv->ndev->nfc_dev->fw_download_in_progress)
At that point nu->drv_data is still NULL:
BUG: KASAN: null-ptr-deref in nfcmrvl_nci_uart_tx_start+0x2c/0xc0
Workqueue: events nci_uart_write_work
Call Trace:
nfcmrvl_nci_uart_tx_start
nci_uart_write_work
worker_thread
Publish nu->drv_data (and nu->ndev) for the UART phy before
nci_register_device() is called, and clear them again if registration
fails, so that a transmit triggered during registration finds a valid
pointer. The now-redundant assignment in nfcmrvl_nci_uart_open() is
removed.
Fixes: e097dc624f78 ("NFC: nfcmrvl: add UART driver")
Assisted-by: LLM Claude
Signed-off-by: Shubham Antil <shubham@octane.security>
---
drivers/nfc/nfcmrvl/main.c | 17 +++++++++++++++++
drivers/nfc/nfcmrvl/uart.c | 3 ---
2 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/drivers/nfc/nfcmrvl/main.c b/drivers/nfc/nfcmrvl/main.c
index 6efa83219..8186e86d7 100644
--- a/drivers/nfc/nfcmrvl/main.c
+++ b/drivers/nfc/nfcmrvl/main.c
@@ -96,6 +96,7 @@ struct nfcmrvl_private *nfcmrvl_nci_register_dev(enum nfcmrvl_phy phy,
const struct nfcmrvl_platform_data *pdata)
{
struct nfcmrvl_private *priv;
+ struct nci_uart *nu = NULL;
int rc;
int headroom;
int tailroom;
@@ -110,6 +111,9 @@ struct nfcmrvl_private *nfcmrvl_nci_register_dev(enum nfcmrvl_phy phy,
priv->dev = dev;
priv->phy = phy;
+ if (phy == NFCMRVL_PHY_UART)
+ nu = drv_data;
+
memcpy(&priv->config, pdata, sizeof(*pdata));
if (!priv->config.reset_gpio) {
@@ -154,9 +158,22 @@ struct nfcmrvl_private *nfcmrvl_nci_register_dev(enum nfcmrvl_phy phy,
nci_set_drvdata(priv->ndev, priv);
+ /* For the UART phy the transmit path reaches the driver through
+ * nu->drv_data; publish it before nci_register_device() so a transmit
+ * triggered during registration does not dereference a NULL pointer.
+ */
+ if (nu) {
+ nu->drv_data = priv;
+ nu->ndev = priv->ndev;
+ }
+
rc = nci_register_device(priv->ndev);
if (rc) {
nfc_err(dev, "nci_register_device failed %d\n", rc);
+ if (nu) {
+ nu->drv_data = NULL;
+ nu->ndev = NULL;
+ }
goto error_fw_dnld_deinit;
}
diff --git a/drivers/nfc/nfcmrvl/uart.c b/drivers/nfc/nfcmrvl/uart.c
index 9aedd1687..421e6dc99 100644
--- a/drivers/nfc/nfcmrvl/uart.c
+++ b/drivers/nfc/nfcmrvl/uart.c
@@ -138,9 +138,6 @@ static int nfcmrvl_nci_uart_open(struct nci_uart *nu)
priv->support_fw_dnld = true;
- nu->drv_data = priv;
- nu->ndev = priv->ndev;
-
return 0;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v4 2/2] nfc: nci: uart: fix use-after-free of write_work on ldisc teardown
2026-10-06 11:55 [PATCH v4 0/2] nfc: nci: uart: fix write_work teardown UAF (+ nfcmrvl drv_data) Shubham Antil
2026-10-06 11:55 ` [PATCH v4 1/2] nfc: nfcmrvl: set drv_data before registering the nci device Shubham Antil
@ 2026-10-06 11:55 ` Shubham Antil
2026-10-09 5:55 ` netdev-bot+sashiko
2026-10-06 11:59 ` [PATCH v4 0/2] nfc: nci: uart: fix write_work teardown UAF (+ nfcmrvl drv_data) netdev-bot+sinfo
2 siblings, 1 reply; 5+ messages in thread
From: Shubham Antil @ 2026-10-06 11:55 UTC (permalink / raw)
To: netdev
Cc: oe-linux-nfc, David Heidelberg, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Johan Hovold,
linux-kernel, Giovanni Vignone
nci_uart_tty_close() frees nu->tx_skb and purges nu->tx_q before it
cancels nu->write_work, and the NCI device is still registered at that
point. Two paths can therefore (re)queue the write worker after the
skbs are freed and run it against freed memory:
- a tty hangup invokes the ldisc ->write_wakeup() (nci_uart_tty_wakeup
-> nci_uart_tx_wakeup -> schedule_work), and
- the NCI core keeps sending via the driver (nci_uart_send() ->
nci_uart_tx_wakeup -> schedule_work) until the device is unregistered
by nu->ops.close().
nci_uart_write_work() then dereferences the freed nu->tx_skb -- a
use-after-free. nu->ops.close() also frees driver state that the worker
dereferences via nu->ops.tx_start()/tx_done(), so the worker has to be
stopped before ops.close() runs.
Fix this the way the Bluetooth hci_uart ldisc does: gate
nci_uart_tx_wakeup() on a new NCI_UART_READY bit taken under a
per-connection rwsem. nci_uart_tty_close() clears NCI_UART_READY under
the write lock -- draining any in-flight nci_uart_tx_wakeup() -- so that
neither the tty nor the internal send path can requeue write_work once it
is cancelled; only then is the device closed and the skbs freed.
NCI_UART_READY is set, and the module reference taken, before
nu->ops.open() registers the device: the driver may transmit (e.g.
download firmware) from within registration and user space can use the
interface as soon as it is registered, so gating those transmits off
would drop or leak them. The open path uses shared error labels and, on
a failed open, drains the worker the same way the close path does.
Runtime-tested under KASAN with a line-discipline hangup reproducer: the
unfixed ldisc reports
BUG: KASAN: slab-use-after-free in nci_uart_write_work
within the first iterations, while the fixed ldisc runs the same race for
56000+ register/hangup cycles without any KASAN report.
Fixes: 9961127d4bce ("NFC: nci: add generic uart support")
Assisted-by: LLM Claude
Signed-off-by: Shubham Antil <shubham@octane.security>
---
include/net/nfc/nci_core.h | 2 +
net/nfc/nci/uart.c | 80 +++++++++++++++++++++++++++++++-------
2 files changed, 68 insertions(+), 14 deletions(-)
diff --git a/include/net/nfc/nci_core.h b/include/net/nfc/nci_core.h
index 664d5058e..c71648ab5 100644
--- a/include/net/nfc/nci_core.h
+++ b/include/net/nfc/nci_core.h
@@ -18,6 +18,7 @@
#define __NCI_CORE_H
#include <linux/interrupt.h>
+#include <linux/percpu-rwsem.h>
#include <linux/skbuff.h>
#include <linux/tty.h>
@@ -455,6 +456,7 @@ struct nci_uart {
struct work_struct write_work;
struct tty_struct *tty;
unsigned long tx_state;
+ struct percpu_rw_semaphore tx_lock;
struct sk_buff_head tx_q;
struct sk_buff *tx_skb;
struct sk_buff *rx_skb;
diff --git a/net/nfc/nci/uart.c b/net/nfc/nci/uart.c
index aa20e8603..a971fb3b1 100644
--- a/net/nfc/nci/uart.c
+++ b/net/nfc/nci/uart.c
@@ -33,6 +33,7 @@
/* TX states */
#define NCI_UART_SENDING 1
#define NCI_UART_TX_WAKEUP 2
+#define NCI_UART_READY 3
static struct nci_uart *nci_uart_drivers[NCI_UART_DRIVER_MAX];
@@ -58,13 +59,28 @@ static inline int nci_uart_queue_empty(struct nci_uart *nu)
static int nci_uart_tx_wakeup(struct nci_uart *nu)
{
+ /* This may be called in an IRQ context, so we can't sleep. Therefore
+ * we try to acquire the read lock only, and if that fails we assume
+ * the tty is being closed, because that is the only time the write
+ * lock is taken (nci_uart_tty_close()). If the write lock is ever
+ * taken elsewhere, this must be revisited.
+ */
+ if (!percpu_down_read_trylock(&nu->tx_lock))
+ return 0;
+
+ if (!test_bit(NCI_UART_READY, &nu->tx_state))
+ goto out;
+
if (test_and_set_bit(NCI_UART_SENDING, &nu->tx_state)) {
set_bit(NCI_UART_TX_WAKEUP, &nu->tx_state);
- return 0;
+ goto out;
}
schedule_work(&nu->write_work);
+out:
+ percpu_up_read(&nu->tx_lock);
+
return 0;
}
@@ -123,18 +139,46 @@ static int nci_uart_set_driver(struct tty_struct *tty, unsigned int driver)
INIT_WORK(&nu->write_work, nci_uart_write_work);
spin_lock_init(&nu->rx_lock);
- ret = nu->ops.open(nu);
- if (ret) {
- kfree(nu);
- return ret;
- } else if (!try_module_get(nu->owner)) {
- nu->ops.close(nu);
- kfree(nu);
- return -ENOENT;
+ ret = percpu_init_rwsem(&nu->tx_lock);
+ if (ret)
+ goto err_free;
+
+ /* Take the module reference and enable the write worker before the
+ * device is registered: ops.open() may already transmit (e.g. download
+ * firmware), and user space can use the interface as soon as it is
+ * registered.
+ */
+ if (!try_module_get(nu->owner)) {
+ ret = -ENOENT;
+ goto err_rwsem;
}
+
+ set_bit(NCI_UART_READY, &nu->tx_state);
+
+ ret = nu->ops.open(nu);
+ if (ret)
+ goto err_ready;
+
tty->disc_data = nu;
return 0;
+
+err_ready:
+ /* ops.open() may already have scheduled write_work; stop it before
+ * freeing, the same way nci_uart_tty_close() does.
+ */
+ percpu_down_write(&nu->tx_lock);
+ clear_bit(NCI_UART_READY, &nu->tx_state);
+ percpu_up_write(&nu->tx_lock);
+ cancel_work_sync(&nu->write_work);
+ kfree_skb(nu->tx_skb);
+ skb_queue_purge(&nu->tx_q);
+ module_put(nu->owner);
+err_rwsem:
+ percpu_free_rwsem(&nu->tx_lock);
+err_free:
+ kfree(nu);
+ return ret;
}
/* ------ LDISC part ------ */
@@ -180,16 +224,24 @@ static void nci_uart_tty_close(struct tty_struct *tty)
if (!nu)
return;
- kfree_skb(nu->tx_skb);
- kfree_skb(nu->rx_skb);
+ /* Drain in-flight tx_wakeups and block new ones, so write_work cannot
+ * be requeued once it is cancelled below.
+ */
+ percpu_down_write(&nu->tx_lock);
+ clear_bit(NCI_UART_READY, &nu->tx_state);
+ percpu_up_write(&nu->tx_lock);
- skb_queue_purge(&nu->tx_q);
+ cancel_work_sync(&nu->write_work);
nu->ops.close(nu);
nu->tty = NULL;
- module_put(nu->owner);
- cancel_work_sync(&nu->write_work);
+ kfree_skb(nu->tx_skb);
+ kfree_skb(nu->rx_skb);
+ skb_queue_purge(&nu->tx_q);
+
+ module_put(nu->owner);
+ percpu_free_rwsem(&nu->tx_lock);
kfree(nu);
}
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH v4 0/2] nfc: nci: uart: fix write_work teardown UAF (+ nfcmrvl drv_data)
2026-10-06 11:55 [PATCH v4 0/2] nfc: nci: uart: fix write_work teardown UAF (+ nfcmrvl drv_data) Shubham Antil
2026-10-06 11:55 ` [PATCH v4 1/2] nfc: nfcmrvl: set drv_data before registering the nci device Shubham Antil
2026-10-06 11:55 ` [PATCH v4 2/2] nfc: nci: uart: fix use-after-free of write_work on ldisc teardown Shubham Antil
@ 2026-10-06 11:59 ` netdev-bot+sinfo
2 siblings, 0 replies; 5+ messages in thread
From: netdev-bot+sinfo @ 2026-10-06 11:59 UTC (permalink / raw)
To: Shubham Antil
Cc: netdev, oe-linux-nfc, David Heidelberg, David S . Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Johan Hovold, linux-kernel, Giovanni Vignone
Hi!
This is an automated message. This series looks like a fix, but its
commit messages seem to be missing some information:
- How the issue was discovered, e.g. hit in production, hit during
development, syzbot report, manual code inspection, LLM or static
analysis tool scan.
Please do not repost the series just to address the above. Instead,
reply to this email with the missing information, so that reviewers
can take it into account. If the series needs another revision for
other reasons, please include the information in the commit messages
then.
The evaluation is done by an LLM so it may be wrong, if you think
that is the case please reply and explain.
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v4 2/2] nfc: nci: uart: fix use-after-free of write_work on ldisc teardown
2026-10-06 11:55 ` [PATCH v4 2/2] nfc: nci: uart: fix use-after-free of write_work on ldisc teardown Shubham Antil
@ 2026-10-09 5:55 ` netdev-bot+sashiko
0 siblings, 0 replies; 5+ messages in thread
From: netdev-bot+sashiko @ 2026-10-09 5:55 UTC (permalink / raw)
To: shubham
Cc: netdev, oe-linux-nfc, david, davem, edumazet, kuba, pabeni, horms,
johan, linux-kernel, gio
Thank you for your contribution! Sashiko AI review found 3 potential
issue(s) to consider.
Critical: 0 · High: 0 (1) · Medium: 1 (2) · Low: 2 (1)
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] Regression: CORE_RESET sent while the device is torn down
never reaches the NFCC.
- [Low] The commit message describes the tty-hangup race wrongly.
- [Low] The new comment in nci_uart_tx_wakeup() is wrong as soon as the
patch lands.
Pre-existing issues:
- [High] This problem wasn't introduced by this patch, but two
NCIUARTSETDRIVER ioctls can race on one tty (TOCTOU), orphaning an…
- [Medium] This problem wasn't introduced by this patch, but the TX state
machine can lose a wakeup.
- [Medium] This problem wasn't introduced by this patch, but the driver-
template registry has a lifetime race.
- [Low] This problem wasn't introduced by this patch, but tty->disc_data
is published only after nu->ops.open() has registered the NCI device…
--
nfc: nci: uart: fix use-after-free of write_work on ldisc teardown
This patch gates nci_uart_tx_wakeup() on a new NCI_UART_READY bit taken
under a percpu rwsem. It reorders nci_uart_tty_close() so write_work is
cancelled before ops.close() runs and before the tx skbs are freed. It
also takes the module reference and sets READY before ops.open() in
nci_uart_set_driver(), and adds shared error labels to that path.
> Two paths can therefore (re)queue the write worker after the
> skbs are freed and run it against freed memory:
>
> - a tty hangup invokes the ldisc ->write_wakeup() (nci_uart_tty_wakeup
> -> nci_uart_tx_wakeup -> schedule_work), and
[Severity: Low]
Is this description of the hangup path accurate?
In tty_ldisc_hangup(), ->write_wakeup() is called under a tty_ldisc_ref()
reference. That reference is dropped before tty_ldisc_lock() is taken and
before ->close() runs:
drivers/tty/tty_ldisc.c:tty_ldisc_hangup() {
ld = tty_ldisc_ref(tty);
...
ld->ops->write_wakeup(tty);
...
tty_ldisc_deref(ld);
...
tty_ldisc_lock(tty, MAX_SCHEDULE_TIMEOUT);
}
Other wakeups go through tty_wakeup()->tty_ldisc_ref(). That call fails
while the ldisc semaphore is write-held during close. So it looks like no
tty-originated wakeup can run inside nci_uart_tty_close().
The hangup use-after-free seems to have a different cause. Work queued
just before close is still pending or running when the old code frees
nu->tx_skb and calls nu->ops.close().
For that path, moving cancel_work_sync() ahead of the frees is what closes
the window. The NCI_UART_READY gate only matters for the internal
nci_uart_send() path.
The changelog later says:
> neither the tty nor the internal send path can requeue write_work once it
> is cancelled
Could the changelog describe which reordering closes which window?
> diff --git a/net/nfc/nci/uart.c b/net/nfc/nci/uart.c
> index aa20e8603f327..a971fb3b1bbb6 100644
> --- a/net/nfc/nci/uart.c
> +++ b/net/nfc/nci/uart.c
[ ... ]
> @@ -58,13 +59,28 @@ static inline int nci_uart_queue_empty(struct nci_uart *nu)
>
> static int nci_uart_tx_wakeup(struct nci_uart *nu)
> {
> + /* This may be called in an IRQ context, so we can't sleep. Therefore
> + * we try to acquire the read lock only, and if that fails we assume
> + * the tty is being closed, because that is the only time the write
> + * lock is taken (nci_uart_tty_close()). If the write lock is ever
> + * taken elsewhere, this must be revisited.
> + */
[Severity: Low]
This isn't a bug, but is this comment accurate? The same patch also takes
the write lock in the err_ready path of nci_uart_set_driver():
err_ready:
...
percpu_down_write(&nu->tx_lock);
clear_bit(NCI_UART_READY, &nu->tx_state);
percpu_up_write(&nu->tx_lock);
Dropping the wakeup there is harmless because that path is also teardown.
Still, the comment says nci_uart_tty_close() is the only place the write
lock is taken.
> + if (!percpu_down_read_trylock(&nu->tx_lock))
> + return 0;
> +
> + if (!test_bit(NCI_UART_READY, &nu->tx_state))
> + goto out;
> +
> if (test_and_set_bit(NCI_UART_SENDING, &nu->tx_state)) {
> set_bit(NCI_UART_TX_WAKEUP, &nu->tx_state);
> - return 0;
> + goto out;
> }
[Severity: Medium]
This is a pre-existing issue, but can a wakeup be lost here?
nci_uart_write_work() checks NCI_UART_TX_WAKEUP before it clears
NCI_UART_SENDING:
net/nfc/nci/uart.c:nci_uart_write_work() {
...
if (test_bit(NCI_UART_TX_WAKEUP, &nu->tx_state))
goto restart;
if (nu->ops.tx_done && nci_uart_queue_empty(nu))
nu->ops.tx_done(nu);
clear_bit(NCI_UART_SENDING, &nu->tx_state);
}
Consider this interleaving:
CPU1 (nci_uart_write_work)
dequeue loop ends with tx_q empty
test_bit(NCI_UART_TX_WAKEUP) -> false
CPU2 (nci_uart_send)
skb_queue_tail(&nu->tx_q, skb)
nci_uart_tx_wakeup()
test_and_set_bit(NCI_UART_SENDING) -> 1
set_bit(NCI_UART_TX_WAKEUP)
CPU1
tx_done skipped, queue not empty
clear_bit(NCI_UART_SENDING)
The new skb then stays on nu->tx_q with no work scheduled until an
unrelated wakeup arrives, so the pending NCI command can time out.
nci_uart_tty_wakeup() also clears TTY_DO_WRITE_WAKEUP. A tty wakeup that
lands while SENDING is set therefore doesn't reliably rescue the frame.
hci_uart_tx_wakeup() and hci_uart_write_work() avoid this race. They set
TX_WAKEUP before test_and_set_bit(SENDING), and clear SENDING before
rechecking TX_WAKEUP.
Since this function is being rewritten to follow hci_uart, could it use
that ordering too?
> @@ -123,18 +139,46 @@ static int nci_uart_set_driver(struct tty_struct *tty, unsigned int driver)
> INIT_WORK(&nu->write_work, nci_uart_write_work);
> spin_lock_init(&nu->rx_lock);
>
> - ret = nu->ops.open(nu);
> - if (ret) {
> - kfree(nu);
> - return ret;
> - } else if (!try_module_get(nu->owner)) {
> - nu->ops.close(nu);
> - kfree(nu);
> - return -ENOENT;
> + ret = percpu_init_rwsem(&nu->tx_lock);
> + if (ret)
> + goto err_free;
> +
> + /* Take the module reference and enable the write worker before the
> + * device is registered: ops.open() may already transmit (e.g. download
> + * firmware), and user space can use the interface as soon as it is
> + * registered.
> + */
> + if (!try_module_get(nu->owner)) {
> + ret = -ENOENT;
> + goto err_rwsem;
> }
[Severity: Medium]
This is a pre-existing issue, but is the module reference still taken too
late? Earlier in nci_uart_set_driver(), the template is read from the
registry with no lock and no module reference held:
if (!nci_uart_drivers[driver])
return -ENOENT;
...
memcpy(nu, nci_uart_drivers[driver], sizeof(struct nci_uart));
The template lives in the nfcmrvl_uart module image.
nci_uart_unregister() clears the slot without any synchronization:
nci_uart_drivers[nu->driver] = NULL;
Suppose an rmmod of nfcmrvl_uart completes between the slot check and
try_module_get(). Can the memcpy() then read freed module memory, or
try_module_get() run on a freed struct module?
Moving try_module_get() ahead of ops.open() narrows this, but the window
between lookup and reference remains. Triggering it needs CAP_SYS_MODULE
racing the ioctl.
> +
> + set_bit(NCI_UART_READY, &nu->tx_state);
> +
> + ret = nu->ops.open(nu);
> + if (ret)
> + goto err_ready;
> +
> tty->disc_data = nu;
[Severity: High]
This isn't a bug introduced by this patch, but can two concurrent
NCIUARTSETDRIVER ioctls on the same tty both get here?
tty_ioctl() takes only a shared ldisc reference before calling
ld->ops->ioctl. nci_uart_tty_ioctl() then checks tty->disc_data without
any serialization:
case NCIUARTSETDRIVER:
if (!nu)
return nci_uart_set_driver(tty, (unsigned int)arg);
else
return -EBUSY;
tty->disc_data is stored only after nu->ops.open() returns, so the window
covers all of device registration. Both callers can allocate their own nu,
set NCI_UART_READY and register a separate NFC device. The second store
then overwrites tty->disc_data.
nci_uart_tty_close() tears down only the instance in tty->disc_data. The
first nu is never unregistered or freed, its write_work is never
cancelled, and it keeps its module reference.
Setting N_NCI and issuing this ioctl need no capability, so an
unprivileged pty user can repeat the race.
Once the tty has been freed, bringing the orphaned NFC device up looks
like it would use the freed nu->tty:
NFC_CMD_DEV_UP
nci_open_device()
nci_uart_send()
nci_uart_tx_wakeup() <- NCI_UART_READY is still set
nci_uart_write_work()
set_bit(TTY_DO_WRITE_WAKEUP, &tty->flags);
tty->ops->write(tty, skb->data, skb->len);
That last step needs NFC_CMD_DEV_UP, which is GENL_ADMIN_PERM.
hci_ldisc guards HCIUARTSETPROTO with test_and_set_bit(HCI_UART_PROTO_SET,
...). Would a similar guard work here?
[Severity: Low]
This is a pre-existing issue, but tty->disc_data is published only after
nu->ops.open() has registered the NCI device. In that window,
nci_uart_tty_wakeup() and nci_uart_tty_receive() return early because
disc_data is still NULL:
struct nci_uart *nu = tty->disc_data;
if (!nu)
return;
A write_wakeup after a partial write, or response bytes arriving in this
window, are therefore dropped.
TX does not stall for good, because nci_uart_write_work() always clears
NCI_UART_SENDING at the end. The worst case seems to be one NCI command
timing out, for example a dev_up issued right after
nci_register_device(). This patch does not change that behaviour.
[ ... ]
> @@ -180,16 +224,24 @@ static void nci_uart_tty_close(struct tty_struct *tty)
> if (!nu)
> return;
>
> - kfree_skb(nu->tx_skb);
> - kfree_skb(nu->rx_skb);
> + /* Drain in-flight tx_wakeups and block new ones, so write_work cannot
> + * be requeued once it is cancelled below.
> + */
> + percpu_down_write(&nu->tx_lock);
> + clear_bit(NCI_UART_READY, &nu->tx_state);
> + percpu_up_write(&nu->tx_lock);
>
> - skb_queue_purge(&nu->tx_q);
> + cancel_work_sync(&nu->write_work);
>
> nu->ops.close(nu);
[Severity: Medium]
Does this prevent the CORE_RESET sent during unregistration from reaching
the controller?
For nfcmrvl_uart, ops.close() ends up in nci_close_device(), which sends
CORE_RESET when NCI_UP is set:
nfcmrvl_nci_uart_close()
nfcmrvl_nci_unregister_dev()
nci_unregister_device()
nci_close_device()
__nci_request(ndev, nci_reset_req, ...)
nfcmrvl_nci_send()
nfcmrvl_uart_nci_send()
nci_uart_send()
skb_queue_tail(&nu->tx_q, skb)
nci_uart_tx_wakeup()
NCI_UART_READY is already clear at this point. nci_uart_tx_wakeup() now
returns without scheduling write_work:
if (!test_bit(NCI_UART_READY, &nu->tx_state))
goto out;
The frame is never written to the tty. skb_queue_purge(&nu->tx_q) frees
it later.
Nothing else on this path quiesces the NFCC. nfcmrvl_uart_nci_close() is
a no-op, and nfcmrvl_nci_unregister_dev() neither calls
nfcmrvl_chip_halt() nor touches the reset GPIO.
Before this patch, write_work could still run during ops.close(). The
reset therefore reached the UART whenever the tty was still working at
ldisc close. Examples are a TIOCSETD switch, or a hangup where
tty_ldisc_hangup() runs before tty->ops->hangup().
If the ldisc is detached while the device is up and polling, can the NFCC
keep its RF and firmware state until the next power cycle, GPIO reset or
dev_up?
hci_ldisc makes the same choice by clearing HCI_UART_PROTO_READY before
hci_unregister_dev(). The commit message doesn't mention this behaviour
change for NCI UART, though.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261006115532.72100-1-shubham%40octane.security
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-09 5:55 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-06 11:55 [PATCH v4 0/2] nfc: nci: uart: fix write_work teardown UAF (+ nfcmrvl drv_data) Shubham Antil
2026-10-06 11:55 ` [PATCH v4 1/2] nfc: nfcmrvl: set drv_data before registering the nci device Shubham Antil
2026-10-06 11:55 ` [PATCH v4 2/2] nfc: nci: uart: fix use-after-free of write_work on ldisc teardown Shubham Antil
2026-10-09 5:55 ` netdev-bot+sashiko
2026-10-06 11:59 ` [PATCH v4 0/2] nfc: nci: uart: fix write_work teardown UAF (+ nfcmrvl drv_data) netdev-bot+sinfo
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox