Add an optional interrupt interface to the FreeBSD UIO module. A writable
file may reserve MSI-X vector zero, enable or disable its kernel handler,
and receive a cumulative 32-bit event count through read and EVFILT_READ.
Device-specific interrupt acknowledgment remains the PMD's responsibility.

One open file description owns the vector; other opens can still map
BARs. Serialize BAR allocation and IRQ lifecycle operations, preserve
pending events across failed copyout, and release the handler and vector
when the owner's last reference closes. Refuse detach while device files
remain open and publish initialized device state atomically.

Enable bus mastering on the first writable open and disable it on the
last writable close, including process exit. Preserve DMA across temporary
BAR-mapping opens, duplicated descriptors, and secondary processes.
Read-only opens neither enable DMA nor retain it after the last writer
exits. Disable bus mastering before publishing or detaching the device.

Use PCI interrupt setup and teardown to unmask and mask the MSI-X vector.
Support control events only; MSI, INTx, and per-queue Rx interrupts are
outside this interface.

Add a hardware lifecycle regression covering ioctl permissions, mapping
and descriptor lifetime, ownership transfer, fork, and SIGKILL. Document
that applications must retain a writable descriptor while using DMA.

Signed-off-by: Kevin Bowling <[email protected]>
---
 MAINTAINERS                           |   1 +
 app/test/test_nic_uio.py              | 140 ++++++++++
 doc/guides/freebsd_gsg/build_dpdk.rst |  25 ++
 kernel/freebsd/nic_uio/meson.build    |   2 +-
 kernel/freebsd/nic_uio/nic_uio.c      | 364 +++++++++++++++++++++++---
 kernel/freebsd/nic_uio/nic_uio.h      |  33 +++
 6 files changed, 531 insertions(+), 34 deletions(-)
 create mode 100644 app/test/test_nic_uio.py
 create mode 100644 kernel/freebsd/nic_uio/nic_uio.h

diff --git a/MAINTAINERS b/MAINTAINERS
index f78126b03d..435343e5d7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -393,6 +393,7 @@ F: kernel/freebsd/contigmem/
 FreeBSD UIO
 M: Bruce Richardson <[email protected]>
 F: kernel/freebsd/nic_uio/
+F: app/test/test_nic_uio.py
 
 Windows support
 M: Dmitry Kozlyuk <[email protected]>
diff --git a/app/test/test_nic_uio.py b/app/test/test_nic_uio.py
new file mode 100644
index 0000000000..721ca9c1e3
--- /dev/null
+++ b/app/test/test_nic_uio.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2026 Kevin Bowling
+
+"""Exercise an idle FreeBSD nic_uio device; requires root and MSI-X 
hardware."""
+
+import argparse
+import ctypes
+import errno
+import fcntl
+import mmap
+import os
+import signal
+import subprocess
+
+INIT, ENABLE, DISABLE = (0x20004E00 + i for i in (1, 2, 3))
+LIBC = ctypes.CDLL(None, use_errno=True)
+LIBC.mmap.argtypes = (
+    ctypes.c_void_p,
+    ctypes.c_size_t,
+    ctypes.c_int,
+    ctypes.c_int,
+    ctypes.c_int,
+    ctypes.c_int64,
+)
+LIBC.mmap.restype = ctypes.c_void_p
+LIBC.munmap.argtypes = (ctypes.c_void_p, ctypes.c_size_t)
+
+
+def expect_errno(fd, command, expected):
+    try:
+        fcntl.ioctl(fd, command)
+    except OSError as error:
+        assert error.errno == expected, error
+    else:
+        raise AssertionError(f"ioctl {command:#x} unexpectedly succeeded")
+
+
+def busmaster(pci, enabled):
+    command = int(subprocess.check_output(["pciconf", "-r", "-h", pci, "4"], 
text=True).strip(), 16)
+    assert command != 0xFFFF and bool(command & 4) == enabled, hex(command)
+
+
+def lifecycle(device, pci):
+    busmaster(pci, False)
+    readonly = os.open(device, os.O_RDONLY | os.O_NONBLOCK)
+    try:
+        busmaster(pci, False)
+        for command in (INIT, ENABLE, DISABLE):
+            expect_errno(readonly, command, errno.EBADF)
+        expect_errno(readonly, 0x20004EFF, errno.ENOTTY)
+
+        owner = os.open(device, os.O_RDWR | os.O_NONBLOCK)
+        try:
+            busmaster(pci, True)
+            fcntl.ioctl(owner, INIT)
+            fcntl.ioctl(owner, ENABLE)
+            mapping_fd = os.open(device, os.O_RDWR)
+            try:
+                # Python's mmap object retains a duplicate fd. Use the
+                # syscall directly to test the mapping's kernel lifetime.
+                mapping = LIBC.mmap(
+                    None,
+                    mmap.PAGESIZE,
+                    mmap.PROT_READ | mmap.PROT_WRITE,
+                    mmap.MAP_SHARED,
+                    mapping_fd,
+                    0,
+                )
+                assert mapping != ctypes.c_void_p(-1).value, ctypes.get_errno()
+            finally:
+                os.close(mapping_fd)
+            try:
+                busmaster(pci, True)
+                duplicate = os.dup(owner)
+                try:
+                    os.close(owner)
+                    owner = -1
+                    busmaster(pci, True)
+                    other = os.open(device, os.O_RDWR)
+                    try:
+                        expect_errno(other, INIT, errno.EBUSY)
+                        os.close(duplicate)
+                        duplicate = -1
+                        busmaster(pci, True)
+                        fcntl.ioctl(other, INIT)
+                        fcntl.ioctl(other, ENABLE)
+                    finally:
+                        os.close(other)
+                finally:
+                    if duplicate >= 0:
+                        os.close(duplicate)
+                # Neither an mmap nor a read-only descriptor retains DMA.
+                busmaster(pci, False)
+            finally:
+                assert LIBC.munmap(mapping, mmap.PAGESIZE) == 0
+        finally:
+            if owner >= 0:
+                os.close(owner)
+
+        owner = os.open(device, os.O_RDWR)
+        try:
+            busmaster(pci, True)
+            fcntl.ioctl(owner, INIT)
+            fcntl.ioctl(owner, ENABLE)
+            child = os.fork()
+            if child == 0:
+                while True:
+                    signal.pause()
+            try:
+                os.close(owner)
+                owner = -1
+                busmaster(pci, True)
+            finally:
+                os.kill(child, signal.SIGKILL)
+                _, status = os.waitpid(child, 0)
+                assert os.WIFSIGNALED(status)
+            busmaster(pci, False)
+        finally:
+            if owner >= 0:
+                os.close(owner)
+    finally:
+        os.close(readonly)
+    busmaster(pci, False)
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("device", help="idle nic_uio device node")
+    parser.add_argument("pci", help="matching pciconf selector, e.g. 
pci0:2:16:0")
+    args = parser.parse_args()
+    if os.geteuid() != 0:
+        parser.error("root is required")
+    for repetition in range(1, 4):
+        lifecycle(args.device, args.pci)
+        print(f"PASS lifecycle repetition {repetition}", flush=True)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/doc/guides/freebsd_gsg/build_dpdk.rst 
b/doc/guides/freebsd_gsg/build_dpdk.rst
index 9dafa64e84..48e499b6ca 100644
--- a/doc/guides/freebsd_gsg/build_dpdk.rst
+++ b/doc/guides/freebsd_gsg/build_dpdk.rst
@@ -158,6 +158,31 @@ To avoid this error, reduce the number of buffers or the 
buffer size.
 Loading the DPDK nic_uio Module
 -------------------------------
 
+The module supports one MSI-X vector per PCI function for control events,
+such as AdminQ messages and link changes. Interrupt resources are allocated
+only when an application requests them through the initialization ioctl.
+The application retains responsibility for acknowledging and rearming
+device interrupt causes.
+Per-queue Rx interrupts, MSI, and legacy INTx interrupts are not supported.
+
+The interrupt interface is optional. Applications can use the updated
+module without allocating interrupts.
+The file description that initializes interrupts owns the vector until its
+last reference is closed; other opens can still map device BARs. Detaching
+the device is refused while device files remain open.
+
+Interrupt-control ioctls require a writable descriptor. Bus mastering is
+enabled by the first writable open and disabled by the last writable close,
+including process exit. Temporary BAR-mapping opens do not interrupt DMA
+while another writable descriptor remains open. Applications must retain a
+writable descriptor while using DMA; a BAR mapping or read-only descriptor
+alone does not keep it enabled. This does not provide IOMMU isolation.
+
+The hardware lifecycle test requires an idle MSI-X device already bound to
+``nic_uio``. Run it as root with its device node and matching PCI selector::
+
+    python3 app/test/test_nic_uio.py /dev/uio@pci:2:16:0 pci0:2:16:0
+
 After loading the contigmem module, the ``nic_uio`` module must also be loaded 
into the
 running kernel prior to running any DPDK application, e.g. using::
 
diff --git a/kernel/freebsd/nic_uio/meson.build 
b/kernel/freebsd/nic_uio/meson.build
index 4bdaf96944..b4e73dccdf 100644
--- a/kernel/freebsd/nic_uio/meson.build
+++ b/kernel/freebsd/nic_uio/meson.build
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2017 Intel Corporation
 
-sources = files('nic_uio.c')
+sources = files('nic_uio.c', 'nic_uio.h')
diff --git a/kernel/freebsd/nic_uio/nic_uio.c b/kernel/freebsd/nic_uio/nic_uio.c
index 0043892870..34086d7f3d 100644
--- a/kernel/freebsd/nic_uio/nic_uio.c
+++ b/kernel/freebsd/nic_uio/nic_uio.c
@@ -12,7 +12,13 @@ __FBSDID("$FreeBSD$");
 #include <sys/rman.h>
 #include <sys/systm.h>
 #include <sys/lock.h>
+#include <sys/malloc.h>
+#include <sys/mutex.h>
 #include <sys/rwlock.h>
+#include <sys/sx.h>
+#include <sys/event.h>
+#include <sys/fcntl.h>
+#include <sys/uio.h>
 #include <sys/proc.h>
 
 #include <machine/bus.h>
@@ -24,6 +30,11 @@ __FBSDID("$FreeBSD$");
 #include <vm/vm_page.h>
 #include <vm/vm_pager.h>
 
+#include "nic_uio.h"
+
+static MALLOC_DEFINE(M_NIC_UIO, "nic_uio", "DPDK interrupt owner");
+
+struct nic_uio_file;
 
 #define MAX_BARS (PCIR_MAX_BAR_0 + 1)
 
@@ -38,11 +49,29 @@ struct nic_uio_softc {
        struct resource *bar_res[MAX_BARS];
        u_long           bar_start[MAX_BARS];
        u_long           bar_size[MAX_BARS];
+       struct sx        lock;
+       struct mtx       event_lock;
+       struct resource *irq;
+       void            *irq_cookie;
+       struct nic_uio_file *owner;
+       unsigned int     opens;
+       unsigned int     writers;
+       bool             detaching;
+};
+
+struct nic_uio_file {
+       struct nic_uio_softc *sc;
+       struct knlist    knlist;
+       uint32_t        events;
+       uint32_t        consumed;
+       bool            writable;
 };
 
 /* Function prototypes */
 static d_open_t         nic_uio_open;
-static d_close_t        nic_uio_close;
+static d_read_t         nic_uio_read;
+static d_ioctl_t        nic_uio_ioctl;
+static d_kqfilter_t     nic_uio_kqfilter;
 static d_mmap_t         nic_uio_mmap;
 static d_mmap_single_t  nic_uio_mmap_single;
 static int              nic_uio_probe(device_t dev);
@@ -55,7 +84,9 @@ static struct cdevsw uio_cdevsw = {
                .d_name        = "nic_uio",
                .d_version     = D_VERSION,
                .d_open        = nic_uio_open,
-               .d_close       = nic_uio_close,
+               .d_read        = nic_uio_read,
+               .d_ioctl       = nic_uio_ioctl,
+               .d_kqfilter    = nic_uio_kqfilter,
                .d_mmap        = nic_uio_mmap,
                .d_mmap_single = nic_uio_mmap_single,
 };
@@ -95,6 +126,24 @@ nic_uio_mmap(struct cdev *cdev, vm_ooffset_t offset, 
vm_paddr_t *paddr,
        return 0;
 }
 
+static int
+nic_uio_alloc_bar(struct nic_uio_softc *sc, unsigned int bar)
+{
+       int type;
+
+       sx_assert(&sc->lock, SA_XLOCKED);
+       if (bar >= MAX_BARS)
+               return EINVAL;
+       if (sc->bar_res[bar] != NULL)
+               return 0;
+       sc->bar_id[bar] = PCIR_BAR(bar);
+       type = PCI_BAR_IO(pci_read_config(sc->dev_t, sc->bar_id[bar], 4)) ?
+               SYS_RES_IOPORT : SYS_RES_MEMORY;
+       sc->bar_res[bar] = bus_alloc_resource_any(sc->dev_t, type,
+               &sc->bar_id[bar], RF_ACTIVE);
+       return sc->bar_res[bar] == NULL ? ENXIO : 0;
+}
+
 static int
 nic_uio_mmap_single(struct cdev *cdev, vm_ooffset_t *offset, vm_size_t size,
                struct vm_object **obj, int nprot)
@@ -106,22 +155,14 @@ nic_uio_mmap_single(struct cdev *cdev, vm_ooffset_t 
*offset, vm_size_t size,
         */
        unsigned bar = *offset/PAGE_SIZE;
        struct nic_uio_softc *sc = cdev->si_drv1;
+       int error;
 
-       if (bar >= MAX_BARS)
-               return EINVAL;
-
-       if (sc->bar_res[bar] == NULL) {
-               sc->bar_id[bar] = PCIR_BAR(bar);
-
-               if (PCI_BAR_IO(pci_read_config(sc->dev_t, sc->bar_id[bar], 4)))
-                       sc->bar_res[bar] = bus_alloc_resource_any(sc->dev_t, 
SYS_RES_IOPORT,
-                                       &sc->bar_id[bar], RF_ACTIVE);
-               else
-                       sc->bar_res[bar] = bus_alloc_resource_any(sc->dev_t, 
SYS_RES_MEMORY,
-                                       &sc->bar_id[bar], RF_ACTIVE);
+       sx_xlock(&sc->lock);
+       error = nic_uio_alloc_bar(sc, bar);
+       if (error != 0) {
+               sx_xunlock(&sc->lock);
+               return error;
        }
-       if (sc->bar_res[bar] == NULL)
-               return ENXIO;
 
        sc->bar_start[bar] = rman_get_start(sc->bar_res[bar]);
        sc->bar_size[bar] = rman_get_size(sc->bar_res[bar]);
@@ -132,20 +173,249 @@ nic_uio_mmap_single(struct cdev *cdev, vm_ooffset_t 
*offset, vm_size_t size,
        *offset = sc->bar_start[bar];
        *obj = vm_pager_allocate(OBJT_DEVICE, cdev, size, nprot, *offset,
                                curthread->td_ucred);
+       sx_xunlock(&sc->lock);
        return 0;
 }
 
+static void
+nic_uio_intr(void *arg)
+{
+       struct nic_uio_softc *sc = arg;
+       struct nic_uio_file *priv;
+
+       mtx_lock(&sc->event_lock);
+       priv = sc->owner;
+       priv->events++;
+       KNOTE_LOCKED(&priv->knlist, 0);
+       mtx_unlock(&sc->event_lock);
+}
 
-int
-nic_uio_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
+static void
+nic_uio_irq_disable(struct nic_uio_softc *sc)
+{
+       sx_assert(&sc->lock, SA_XLOCKED);
+       if (sc->irq_cookie != NULL) {
+               bus_teardown_intr(sc->dev_t, sc->irq, sc->irq_cookie);
+               sc->irq_cookie = NULL;
+       }
+}
+
+static int
+nic_uio_irq_init(struct nic_uio_file *priv)
 {
+       struct nic_uio_softc *sc = priv->sc;
+       int count = 1, rid = 1, error;
+
+       sx_assert(&sc->lock, SA_XLOCKED);
+       if (sc->owner != NULL)
+               return sc->owner == priv ? 0 : EBUSY;
+       if (pci_msix_count(sc->dev_t) == 0)
+               return EOPNOTSUPP;
+       error = nic_uio_alloc_bar(sc, 
PCI_RID2BAR(pci_msix_table_bar(sc->dev_t)));
+       if (error != 0)
+               return error;
+       error = nic_uio_alloc_bar(sc, PCI_RID2BAR(pci_msix_pba_bar(sc->dev_t)));
+       if (error != 0)
+               return error;
+       error = pci_alloc_msix(sc->dev_t, &count);
+       if (error != 0)
+               return error;
+       sc->irq = bus_alloc_resource_any(sc->dev_t, SYS_RES_IRQ, &rid, 
RF_ACTIVE);
+       if (sc->irq == NULL) {
+               pci_release_msi(sc->dev_t);
+               return ENXIO;
+       }
+       mtx_lock(&sc->event_lock);
+       sc->owner = priv;
+       mtx_unlock(&sc->event_lock);
        return 0;
 }
 
+static void
+nic_uio_disable_busmaster(struct nic_uio_softc *sc)
+{
+       int error;
+
+       sx_assert(&sc->lock, SA_XLOCKED);
+       error = pci_disable_busmaster(sc->dev_t);
+       if (error != 0)
+               device_printf(sc->dev_t, "Cannot disable bus mastering: %d\n", 
error);
+}
+
+static void
+nic_uio_file_destroy(void *arg)
+{
+       struct nic_uio_file *priv = arg;
+       struct nic_uio_softc *sc = priv->sc;
+
+       sx_xlock(&sc->lock);
+       KASSERT(sc->opens != 0, ("nic_uio open count underflow"));
+       KASSERT(!priv->writable || sc->writers != 0,
+               ("nic_uio writable open count underflow"));
+       if (priv->writable && --sc->writers == 0)
+               nic_uio_disable_busmaster(sc);
+       if (sc->owner == priv) {
+               nic_uio_irq_disable(sc);
+               bus_release_resource(sc->dev_t, SYS_RES_IRQ, 1, sc->irq);
+               pci_release_msi(sc->dev_t);
+               sc->irq = NULL;
+               mtx_lock(&sc->event_lock);
+               sc->owner = NULL;
+               mtx_unlock(&sc->event_lock);
+       }
+       knlist_clear(&priv->knlist, 0);
+       knlist_destroy(&priv->knlist);
+       free(priv, M_NIC_UIO);
+       sc->opens--;
+       sx_xunlock(&sc->lock);
+}
+
 int
-nic_uio_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
+nic_uio_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
 {
-       return 0;
+       struct nic_uio_softc *sc = dev->si_drv1;
+       struct nic_uio_file *priv;
+       int error;
+
+       priv = malloc(sizeof(*priv), M_NIC_UIO, M_WAITOK | M_ZERO);
+       priv->sc = sc;
+       priv->writable = (oflags & FWRITE) != 0;
+       knlist_init_mtx(&priv->knlist, &sc->event_lock);
+       sx_xlock(&sc->lock);
+       error = sc->detaching ? ENXIO : 0;
+       if (error == 0 && priv->writable && sc->writers == 0)
+               error = pci_enable_busmaster(sc->dev_t);
+       if (error == 0)
+               error = devfs_set_cdevpriv(priv, nic_uio_file_destroy);
+       if (error == 0) {
+               sc->opens++;
+               if (priv->writable)
+                       sc->writers++;
+       } else {
+               if (!sc->detaching && priv->writable && sc->writers == 0)
+                       nic_uio_disable_busmaster(sc);
+               knlist_destroy(&priv->knlist);
+               free(priv, M_NIC_UIO);
+       }
+       sx_xunlock(&sc->lock);
+       return error;
+}
+
+static int
+nic_uio_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
+               struct thread *td)
+{
+       struct nic_uio_file *priv;
+       struct nic_uio_softc *sc;
+       int error;
+
+       if (cmd != NIC_UIO_IRQ_INIT && cmd != NIC_UIO_IRQ_ENABLE &&
+                       cmd != NIC_UIO_IRQ_DISABLE)
+               return ENOTTY;
+       if ((fflag & FWRITE) == 0)
+               return EBADF;
+       error = devfs_get_cdevpriv((void **)&priv);
+       if (error != 0)
+               return error;
+       sc = priv->sc;
+       sx_xlock(&sc->lock);
+       if (cmd == NIC_UIO_IRQ_INIT) {
+               error = nic_uio_irq_init(priv);
+       } else if (sc->owner != priv) {
+               error = EPERM;
+       } else if (cmd == NIC_UIO_IRQ_DISABLE) {
+               nic_uio_irq_disable(sc);
+       } else if (sc->irq_cookie == NULL) {
+               error = bus_setup_intr(sc->dev_t, sc->irq,
+                               INTR_TYPE_NET | INTR_MPSAFE, NULL, nic_uio_intr,
+                               sc, &sc->irq_cookie);
+       }
+       sx_xunlock(&sc->lock);
+       return error;
+}
+
+static int
+nic_uio_read(struct cdev *dev, struct uio *uio, int ioflag)
+{
+       struct nic_uio_file *priv;
+       struct nic_uio_softc *sc;
+       uint32_t events;
+       int error;
+
+       if (uio->uio_resid < sizeof(events))
+               return EINVAL;
+       error = devfs_get_cdevpriv((void **)&priv);
+       if (error != 0)
+               return error;
+       sc = priv->sc;
+       /* Serialize readers while copying out without holding the event mutex. 
*/
+       sx_xlock(&sc->lock);
+       mtx_lock(&sc->event_lock);
+       events = priv->events;
+       if (sc->owner != priv)
+               error = EPERM;
+       else if (events == priv->consumed)
+               error = EAGAIN;
+       mtx_unlock(&sc->event_lock);
+       if (error == 0) {
+               error = uiomove(&events, sizeof(events), uio);
+               if (error == 0) {
+                       mtx_lock(&sc->event_lock);
+                       priv->consumed = events;
+                       mtx_unlock(&sc->event_lock);
+               }
+       }
+       sx_xunlock(&sc->lock);
+       return error;
+}
+
+static void
+nic_uio_filter_detach(struct knote *kn)
+{
+       struct nic_uio_file *priv = kn->kn_hook;
+
+       knlist_remove(&priv->knlist, kn, 0);
+}
+
+static int
+nic_uio_filter_read(struct knote *kn, long hint)
+{
+       struct nic_uio_file *priv = kn->kn_hook;
+
+       mtx_assert(&priv->sc->event_lock, MA_OWNED);
+       kn->kn_data = priv->events != priv->consumed ? sizeof(uint32_t) : 0;
+       return kn->kn_data != 0;
+}
+
+static const struct filterops nic_uio_read_filter = {
+       .f_isfd = 1,
+       .f_detach = nic_uio_filter_detach,
+       .f_event = nic_uio_filter_read,
+};
+
+static int
+nic_uio_kqfilter(struct cdev *dev, struct knote *kn)
+{
+       struct nic_uio_file *priv;
+       struct nic_uio_softc *sc;
+       int error;
+
+       if (kn->kn_filter != EVFILT_READ)
+               return EINVAL;
+       error = devfs_get_cdevpriv((void **)&priv);
+       if (error != 0)
+               return error;
+       sc = priv->sc;
+       mtx_lock(&sc->event_lock);
+       if (sc->owner != priv) {
+               error = ENODEV;
+       } else {
+               kn->kn_hook = priv;
+               kn->kn_fop = &nic_uio_read_filter;
+               knlist_add(&priv->knlist, kn, 1);
+       }
+       mtx_unlock(&sc->event_lock);
+       return error;
 }
 
 static int
@@ -213,33 +483,61 @@ nic_uio_probe (device_t dev)
 static int
 nic_uio_attach(device_t dev)
 {
-       int i;
+       struct make_dev_args args;
+       int i, error;
        struct nic_uio_softc *sc;
 
        sc = device_get_softc(dev);
        sc->dev_t = dev;
-       sc->my_cdev = make_dev(&uio_cdevsw, device_get_unit(dev),
-                       UID_ROOT, GID_WHEEL, 0600, "uio@pci:%u:%u:%u",
-                       pci_get_bus(dev), pci_get_slot(dev), 
pci_get_function(dev));
-       if (sc->my_cdev == NULL)
-               return ENXIO;
-       sc->my_cdev->si_drv1 = sc;
+       sx_init(&sc->lock, "nic_uio");
+       mtx_init(&sc->event_lock, "nic_uio events", NULL, MTX_DEF);
 
        for (i = 0; i < MAX_BARS; i++)
                sc->bar_res[i] = NULL;
-
-       pci_enable_busmaster(dev);
-
+       /* Leave DMA disabled until a writable device file is opened. */
+       error = pci_disable_busmaster(dev);
+       if (error != 0)
+               goto fail;
+       make_dev_args_init(&args);
+       args.mda_devsw = &uio_cdevsw;
+       args.mda_uid = UID_ROOT;
+       args.mda_gid = GID_WHEEL;
+       args.mda_mode = 0600;
+       args.mda_si_drv1 = sc;
+       args.mda_flags = MAKEDEV_WAITOK | MAKEDEV_CHECKNAME;
+       error = make_dev_s(&args, &sc->my_cdev, "uio@pci:%u:%u:%u",
+               pci_get_bus(dev), pci_get_slot(dev), pci_get_function(dev));
+       if (error != 0)
+               goto fail;
        return 0;
+
+fail:
+       mtx_destroy(&sc->event_lock);
+       sx_destroy(&sc->lock);
+       return error;
 }
 
 static int
 nic_uio_detach(device_t dev)
 {
-       int i;
+       int i, error;
        struct nic_uio_softc *sc;
        sc = device_get_softc(dev);
 
+       sx_xlock(&sc->lock);
+       if (sc->opens != 0) {
+               sx_xunlock(&sc->lock);
+               return EBUSY;
+       }
+       error = pci_disable_busmaster(dev);
+       if (error != 0) {
+               sx_xunlock(&sc->lock);
+               return error;
+       }
+       sc->detaching = true;
+       sx_xunlock(&sc->lock);
+       destroy_dev(sc->my_cdev);
+
        for (i = 0; i < MAX_BARS; i++)
                if (sc->bar_res[i] != NULL) {
 
@@ -251,8 +549,8 @@ nic_uio_detach(device_t dev)
                                                sc->bar_res[i]);
                }
 
-       if (sc->my_cdev != NULL)
-               destroy_dev(sc->my_cdev);
+       mtx_destroy(&sc->event_lock);
+       sx_destroy(&sc->lock);
        return 0;
 }
 
diff --git a/kernel/freebsd/nic_uio/nic_uio.h b/kernel/freebsd/nic_uio/nic_uio.h
new file mode 100644
index 0000000000..7061d4fccf
--- /dev/null
+++ b/kernel/freebsd/nic_uio/nic_uio.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Kevin Bowling
+ */
+
+#ifndef NIC_UIO_H
+#define NIC_UIO_H
+
+#include <sys/ioccom.h>
+
+/*
+ * Optional single-vector MSI-X interface. INIT reserves vector zero for this
+ * open file description; other opens may still mmap the BARs. Repeating INIT
+ * on the owner is harmless. Closing the owner releases the interrupt.
+ *
+ * ENABLE installs the handler and unmasks the vector; DISABLE removes the
+ * handler and masks the vector. Neither operation acknowledges device-specific
+ * interrupt causes: that remains the PMD's responsibility.
+ *
+ * EVFILT_READ reports unread interrupts. read() returns a native uint32_t
+ * cumulative event count, or EAGAIN when there is no unread event. Counts 
wrap.
+ * An older module rejects INIT with ENOTTY and continues to support BAR mmap.
+ * All interrupt-control ioctls require a writable descriptor.
+ *
+ * The first writable open enables bus mastering; the last writable close
+ * disables it, including process exit. dup() and fork() retain that open file
+ * description. A BAR mapping alone does not keep DMA enabled: applications
+ * must keep a writable descriptor open while using the device for DMA.
+ */
+#define NIC_UIO_IRQ_INIT               _IO('N', 1)
+#define NIC_UIO_IRQ_ENABLE     _IO('N', 2)
+#define NIC_UIO_IRQ_DISABLE    _IO('N', 3)
+
+#endif /* NIC_UIO_H */
-- 
2.56.0

Reply via email to