From: Shubham Antil <shubham@octane.security>
To: netdev@vger.kernel.org
Cc: oe-linux-nfc@lists.linux.dev, David Heidelberg <david@ixit.cz>,
"David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@google.com>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
Simon Horman <horms@kernel.org>, Johan Hovold <johan@kernel.org>,
linux-kernel@vger.kernel.org,
Giovanni Vignone <gio@octane.security>
Subject: [PATCH v4 2/2] nfc: nci: uart: fix use-after-free of write_work on ldisc teardown
Date: Tue, 6 Oct 2026 17:25:31 +0530 [thread overview]
Message-ID: <20261006115532.72100-3-shubham@octane.security> (raw)
In-Reply-To: <20261006115532.72100-1-shubham@octane.security>
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
next prev parent reply other threads:[~2026-10-06 11:55 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
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 [this message]
2026-10-09 5:55 ` [PATCH v4 2/2] nfc: nci: uart: fix use-after-free of write_work on ldisc teardown 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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261006115532.72100-3-shubham@octane.security \
--to=shubham@octane.security \
--cc=davem@davemloft.net \
--cc=david@ixit.cz \
--cc=edumazet@google.com \
--cc=gio@octane.security \
--cc=horms@kernel.org \
--cc=johan@kernel.org \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=oe-linux-nfc@lists.linux.dev \
--cc=pabeni@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox