mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Hengbin Zhang <uqbarz@gmail.com>
To: netdev@vger.kernel.org, "David S. Miller" <davem@davemloft.net>,
	Eric Dumazet <edumazet@google.com>,
	Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>
Cc: Mina Almasry <almasrymina@google.com>,
	Taehee Yoo <ap420073@gmail.com>,
	Bobby Eshleman <bobbyeshleman@meta.com>,
	Stanislav Fomichev <sdf.kernel@gmail.com>,
	linux-kernel@vger.kernel.org
Subject: [BUG] net: devmem: TX dma-buf binding UAF on netdev-genl socket close
Date: Tue,  8 Sep 2026 04:31:15 +0000	[thread overview]
Message-ID: <20260908043115.216613-1-uqbarz@gmail.com> (raw)

Hi all,

Up front: I'm not a kernel developer, just someone who reads net code on
the side, so please bear with me if I get some terminology or conventions
wrong.  I found this issue while looking at the netmem/devmem code, and
with some help from AI tooling I managed to reproduce it in QEMU and
manually confirm the crash — I do believe it's a real bug.  Still, treat
the analysis below as my best guess rather than a certainty.  Thanks a lot
for taking the time to review this!

One-line summary
================

A TX (DMA_TO_DEVICE) dma-buf binding created with NETDEV_CMD_BIND_TX stores
binding->dev without taking a netdev reference and never registers itself
with any RX queue of the device.  When the bound device is unregistered and
freed while the owning netlink socket is still open, closing the socket
makes netdev_nl_sock_priv_destroy() call netdev_hold()/netdev_lock() on the
already freed net_device -> use-after-free.  Confirmed with KASAN and by a
real page fault / kernel panic on the same code path (log below).

Environment
===========

- Kernel: v7.3.0-rc1-00515-g9f0346dcbea3 (commit 9f0346dcbea3), x86_64,
  with KASAN enabled; relevant config knobs are in the reproducer README
  (link below).
- Test setup: QEMU (TCG) guest, initramfs only.  The kernel image has one
  local, test-only addition: a small stub PCI driver (source in the
  reproducer link below).  No core net code was modified.

Steps to reproduce
==================

I reproduced this in a QEMU guest.  QEMU cannot emulate any of the in-tree
NETMEM_TX_DMA devices (bnxt/mlx5/gve/fbnic are real NICs), so I wrote a
tiny test-only stub PCI driver (source in the reproducer link below) that
provides the same device-side properties: a net_device with
netmem_tx = NETMEM_TX_DMA and a DMA-capable parent.  The bug lives in core
net code, not in the driver, so this scenario should equally apply to real
hardware — e.g. binding TX on a real netmem-TX NIC, then removing/unbinding
that NIC while the netlink socket stays open.

1. Build the stub driver from the gist into the kernel (CONFIG_STUBNET=y)
   or as a module, boot the guest with "-device edu".
2. Boot with the static "init" program from the gist (repro.c, run as
   PID 1 in an initramfs).  It performs, in order:
   a. DMA_HEAP_IOCTL_ALLOC on /dev/dma_heap/system -> dmabuf fd;
   b. NETDEV_CMD_BIND_TX on the stub device (ifindex + dmabuf fd) and keeps
      the netlink socket open; BIND_TX succeeds and returns a dmabuf id;
   c. writes "1" to /sys/bus/pci/devices/<bdf>/remove, i.e. unregisters and
      FREES the stub net_device (refcount drops to 1 in netdev_run_todo);
   d. closes the netlink socket.
3. Expected: closing the socket cleanly tears the binding down.
   Actual: use-after-free, see log below.  On a kernel without KASAN the
   same path takes a page fault and panics (second log excerpt below), so
   this is not a KASAN-only artifact.

KASAN log
=========

  stub0 ifindex = 2
  dmabuf fd = 4
  netdev family id=20 version=1
  nl got: type=20 ... genl cmd=15 plen=8      <- BIND_TX ok (dmabuf id)
  writing 1 to /sys/bus/pci/devices/0000:00:04.0/remove
  stub0 gone (unregistered); net_device should now be freed
  === CLOSING NETLINK SOCKET (expect UAF in netdev_hold) ===
  [   43.895794] BUG: KASAN: slab-use-after-free in
             netdev_nl_sock_priv_destroy+0x196/0x1c0
  [   43.896434] Read of size 8 at addr ffff888002f06580 by task init/1
  Call Trace:
   netdev_nl_sock_priv_destroy+0x196/0x1c0
   genl_release+0xee/0x190
   netlink_release+0x715/0x13a0
   __sock_release+0xa1/0x260
   sock_close+0x10/0x20
   __x64_sys_close+0x78/0xd0
  Allocated by task 1:
   __kvmalloc_node_noprof+0x202/0x5b0
   alloc_netdev_mqs+0x7e/0x1270
   stubnet_probe+0x120/0x3c0
  Freed by task 1:
   kfree+0x127/0x3b0
   device_release+0xc8/0x240
   kobject_put+0x101/0x1e0
   netdev_run_todo+0x5a5/0xd70
   unregister_netdev+0x104/0x180
   stubnet_remove+0x3f/0x60
   pci_device_remove+0xa6/0x180
   remove_store+0xcc/0xe0
  The buggy address belongs to the object ... which belongs to the cache
  kmalloc-4k of size 4096; freed 4096-byte region [ffff888002f06000,
  ffff888002f07000).

The kernel then continued in the same function and faulted for real:

  [   43.905270] BUG: unable to handle page fault for address: ffff8880b0ef9000
  [   43.927189] RIP: 0010:netdev_nl_sock_priv_destroy+0xad/0x1c0
  ...
  [   43.938987] Kernel panic - not syncing: Fatal exception

Analysis
========

The flaw is a four-step chain: BIND_TX stores a raw net_device pointer
without taking a reference; TX bindings never populate bound_rxqs; the
unregister cleanup that clears binding->dev only matches RX queues; so when
the socket is closed after the device was unregistered and freed,
netdev_nl_sock_priv_destroy() runs netdev_hold()/netdev_lock() on the freed
net_device.

1) Binding creation - raw pointer, no reference (net/core/devmem.c,
   net_devmem_bind_dmabuf(), DMA_TO_DEVICE = TX path):

    binding->dev = dev;              // raw store, NO netdev_hold()
    xa_init_flags(&binding->bound_rxqs, XA_FLAGS_ALLOC);
    // TX path never calls net_devmem_bind_dmabuf_to_queue()
    // -> bound_rxqs stays EMPTY; the unregister cleanup (which matches
    //    queues) cannot see this binding

2) Device unregister - the only binding->dev clearing point is RX-only:

    // net/core/dev.c:12395 dev_memory_provider_uninstall()
    for (i = 0; i < dev->real_num_rx_queues; i++)
            __netif_mp_uninstall_rxq(&dev->_rx[i], &dev->_rx[i].mp_params);
            // scans the device's OWN RX queues only -> TX binding invisible

    // net/core/devmem.c:537 mp_dmabuf_devmem_uninstall() - the ONLY place
    // that clears binding->dev:
    WRITE_ONCE(binding->dev, NULL);  // reached only when a bound queue is
                                     // uninstalled (RX-only); never fires
                                     // for TX bindings

3) Free - the binding contributes zero references (net/core/dev.c):

    // netdev_wait_allrefs_any()/netdev_run_todo()
    if (netdev_refcnt_read(dev) == 1)    // no holder left; the binding holds
                                         // no reference either
            free_netdev(dev);            // net_device freed while the socket
                                         // and its binding are still alive

4) Socket close - teardown on freed memory (net/core/netdev-genl.c:1445,
   netdev_nl_sock_priv_destroy()):

    dev = binding->dev;              // RX: NULL (cleared in step 2);
                                     // TX: dangling
    if (!dev) { unbind; continue; }  // safe branch - never taken for TX
    netdev_hold(dev, ...);           // UAF #1: refcount/tracker increment on
                                     // the freed net_device
    netdev_lock(dev);                // UAF #2: mutex on freed memory

Full reproducer materials (stub driver source, trigger program, build/run
README and the complete serial log):
https://gist.github.com/hharryz/119f067937448ac6e30eb31e7f08dc5a

I'd be happy to keep testing, digging deeper into the analysis, and trying
to put together a possible fix if that helps.  Thanks a lot for your time!

Hengbin Zhang

             reply	other threads:[~2026-09-08  4:32 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-08  4:31 Hengbin Zhang [this message]
2026-09-08 17:34 ` Stanislav Fomichev
2026-09-08 18:25   ` Mina Almasry
2026-09-09  1:39     ` Stanislav Fomichev
2026-09-09  9:16     ` Dragos Tatulea

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=20260908043115.216613-1-uqbarz@gmail.com \
    --to=uqbarz@gmail.com \
    --cc=almasrymina@google.com \
    --cc=ap420073@gmail.com \
    --cc=bobbyeshleman@meta.com \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=sdf.kernel@gmail.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

all inboxes | Powered by JetHome®