[PATCH] nfc: nci: Fix use-after-free on conn_info in nci_tx_work()

From: Sanghyun Park

Date: Fri May 29 2026 - 03:13:21 EST


nci_tx_work() calls nci_get_conn_info_by_conn_id() to look up a
conn_info and then dereferences it extensively (reading credits_cnt,
calling nci_send_frame, etc). The lookup and subsequent use are done
without any locking.

A concurrent nci_core_conn_close_rsp_packet(), processed on the
separate rx_wq workqueue, can call list_del() + devm_kfree() on the
same conn_info while nci_tx_work() is still using it, resulting in a
use-after-free.

Fix by flushing the tx workqueue before removing and freeing a
conn_info in nci_core_conn_close_rsp_packet(). This ensures any
in-progress nci_tx_work() has completed before the conn_info is freed.
The two workqueues (rx_wq, tx_wq) are independent, so this cannot
deadlock.

Race:

  CPU0 (nfc0_nci_tx_wq)              CPU1 (nfc0_nci_rx_wq)
  ============================       ==========================
  nci_tx_work():
    conn_info = nci_get_conn_info_by_conn_id()
    // no lock held, raw pointer
                                     nci_core_conn_close_rsp_packet():
                                       list_del(&conn_info->list)
                                       devm_kfree(conn_info)
    atomic_read(&conn_info->credits_cnt)
    // UAF: conn_info is freed

Reproduction:

  1. Build kernel >= 3.4 with CONFIG_KASAN=y, CONFIG_NFC=y,
     CONFIG_NFC_NCI=y, CONFIG_NFC_VIRTUAL_NCI=m
  2. Boot in a VM, load virtual_nci module
  3. Compile: gcc -O2 -o repro -static -pthread repro.c
  4. Run as root: ./repro
  5. Check dmesg for: BUG: KASAN: slab-use-after-free in nci_tx_work
     or: BUG: KASAN: invalid-free in nci_rsp_packet

  The reproducer opens /dev/virtual_nci, brings up an NFC device via
  generic netlink, activates an RF interface, then races raw NFC data
  sends against injected NCI CONN_CLOSE response packets. The tx_wq
  and rx_wq are separate singlethread workqueues, allowing the race.

KASAN report (reproduced on 6.12.91 via /dev/virtual_nci):

  BUG: KASAN: invalid-free in nci_rsp_packet+0x1424/0x21f0
  Free of addr ffff88810da31028 by task kworker/u8:0/12

  Workqueue: nfc0_nci_rx_wq nci_rx_work
  Call Trace:
   kfree+0x126/0x4d0
   nci_rsp_packet+0x1424/0x21f0
   nci_rx_work+0x2a1/0x440
   process_one_work+0x953/0x1820

  Allocated by task 37:
   devm_kmalloc+0xa8/0x230
   nci_rsp_packet+0x1a46/0x21f0
   nci_rx_work+0x2a1/0x440

Fixes: 6a2968aaf50c ("NFC: basic NCI protocol implementation")
Signed-off-by: Sanghyun Park <sanghyun.park.cnu@xxxxxxxxx>
---

Hi,

I'm Sanghyun Park, a security researcher. I found this while auditing
the NFC NCI core code. The bug has existed since NCI was introduced in
3.4 and affects all kernels since then (Ubuntu 14.04+, Fedora 17+,
Debian 8+, etc.) on systems with NFC hardware or the virtual_nci module.

The C reproducer is attached separately (repro.c).

 net/nfc/nci/rsp.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/net/nfc/nci/rsp.c b/net/nfc/nci/rsp.c
index 839a5c80de..c4d8e9f1a2 100644
--- a/net/nfc/nci/rsp.c
+++ b/net/nfc/nci/rsp.c
@@ -333,6 +333,14 @@ static void nci_core_conn_close_rsp_packet(struct nci_dev *ndev,
  conn_info = nci_get_conn_info_by_conn_id(ndev,
  ndev->cur_conn_id);
  if (conn_info) {
+ /*
+ * Flush any pending nci_tx_work before removing the
+ * conn_info.  nci_tx_work looks up conn_info without
+ * locking, so it must not be running while we free
+ * the entry.  The two workqueues (rx_wq, tx_wq) are
+ * independent, so this cannot deadlock.
+ */
+ flush_workqueue(ndev->tx_wq);
  list_del(&conn_info->list);
  if (conn_info == ndev->rf_conn_info)
  ndev->rf_conn_info = NULL;

[ 43.417619] BUG: KASAN: invalid-free in nci_rsp_packet+0x1424/0x21f0
[ 43.418353] Free of addr ffff88810da31028 by task kworker/u8:0/12

[ 43.419245] CPU: 1 UID: 0 PID: 12 Comm: kworker/u8:0 Not tainted 6.12.91-dirty #24
[ 43.419260] Hardware name: QEMU Ubuntu 25.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 43.419264] Workqueue: nfc0_nci_rx_wq nci_rx_work
[ 43.419274] Call Trace:
[ 43.419276] <TASK>
[ 43.419279] dump_stack_lvl+0xba/0x110
[ 43.419287] print_report+0x174/0x4f6
[ 43.419328] ? __virt_addr_valid+0x86/0x670
[ 43.419355] ? nci_rsp_packet+0x1424/0x21f0
[ 43.419361] ? nci_rsp_packet+0x1424/0x21f0
[ 43.419366] kasan_report_invalid_free+0xaa/0xd0
[ 43.419384] ? nci_rsp_packet+0x1424/0x21f0
[ 43.419390] ? nci_rsp_packet+0x1424/0x21f0
[ 43.419395] check_slab_allocation+0x116/0x120
[ 43.419400] kfree+0x126/0x4d0
[ 43.419407] ? nfc_send_to_raw_sock+0x3a/0x230
[ 43.419412] ? nci_rsp_packet+0x1424/0x21f0
[ 43.419418] nci_rsp_packet+0x1424/0x21f0
[ 43.419430] ? nfc_send_to_raw_sock+0x101/0x230
[ 43.419439] nci_rx_work+0x2a1/0x440
[ 43.419447] process_one_work+0x953/0x1820
[ 43.419454] ? __pfx_process_one_work+0x10/0x10
[ 43.419462] ? __pfx_nci_rx_work+0x10/0x10
[ 43.419469] worker_thread+0x5cd/0xe20
[ 43.419476] ? __pfx_worker_thread+0x10/0x10
[ 43.419481] kthread+0x2b2/0x360
[ 43.419485] ? __pfx_kthread+0x10/0x10
[ 43.419490] ret_from_fork+0x4d/0x80
[ 43.419496] ? __pfx_kthread+0x10/0x10
[ 43.419500] ret_from_fork_asm+0x1a/0x30
[ 43.419507] </TASK>

[ 43.435161] Allocated by task 37:
[ 43.435566] kasan_save_stack+0x30/0x50
[ 43.436047] kasan_save_track+0x14/0x30
[ 43.436498] __kasan_kmalloc+0xaa/0xb0
[ 43.436960] __kmalloc_node_track_caller_noprof+0x216/0x490
[ 43.437594] devm_kmalloc+0xa8/0x230
[ 43.438034] nci_rsp_packet+0x1a46/0x21f0
[ 43.438508] nci_rx_work+0x2a1/0x440
[ 43.438968] process_one_work+0x953/0x1820
[ 43.439446] worker_thread+0x5cd/0xe20
[ 43.439892] kthread+0x2b2/0x360
[ 43.440278] ret_from_fork+0x4d/0x80
[ 43.440701] ret_from_fork_asm+0x1a/0x30

[ 43.441368] The buggy address belongs to the object at ffff88810da31000
which belongs to the cache kmalloc-128 of size 128
[ 43.442751] The buggy address is located 40 bytes inside of
128-byte region [ffff88810da31000, ffff88810da31080)

[ 43.444271] The buggy address belongs to the physical page:
[ 43.444920] page: refcount:1 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x10da31
[ 43.445826] flags: 0x200000000000000(node=0|zone=2)
[ 43.446407] page_type: f5(slab)
[ 43.446785] raw: 0200000000000000 ffff888100041a00 ffffea000440d180 dead000000000004
[ 43.447668] raw: 0000000000000000 0000000000100010 00000001f5000000 0000000000000000
[ 43.448544] page dumped because: kasan: bad access detected

Attachment: repro.c
Description: Binary data