From: Pavitra Jha <jhapavitra98@gmail.com>
To: Dave Penkler <dpenkler@gmail.com>
Cc: gregkh@linuxfoundation.org, linux-kernel@vger.kernel.org,
stable@vger.kernel.org, Pavitra Jha <jhapavitra98@gmail.com>
Subject: [PATCH v2] gpib: fix use-after-free between iboffline() detach and in-flight I/O
Date: Wed, 8 Jul 2026 03:36:18 -0400 [thread overview]
Message-ID: <20260708073618.147714-1-jhapavitra98@gmail.com> (raw)
In-Reply-To: <akKZBpW135YW2Def@egonzo>
iboffline() calls board->interface->detach(), which frees
board->private_data, with no guarantee that a read/write/command
ioctl already in progress on that board has finished touching it.
v1 of this fix took board->big_gpib_mutex around the detach() call.
That was wrong on two counts, both pointed out by Dave Penkler:
1. iboffline() is reached from ibioctl() via the IBONL ioctl, which
is dispatched while big_gpib_mutex is already held. Taking it
again inside iboffline() self-deadlocks.
2. IBRD/IBWRT/IBCMD explicitly drop big_gpib_mutex before calling
into board->interface, because those calls can block for the
duration of board->usec_timeout. So even without the deadlock,
the mutex was never actually held during the in-flight callback
it was meant to exclude.
Fix this by tracking in-flight callbacks directly instead of
overloading big_gpib_mutex. Add board->io_active, an atomic counter
incremented around the body of read_ioctl(), write_ioctl(), and
command_ioctl() (which is where board->interface is dereferenced,
via ibrd()/ibwrt()/ibcmd()), and board->io_drain_wait, a waitqueue
woken when the counter reaches zero.
iboffline() waits uninterruptibly on io_drain_wait before calling
detach(). big_gpib_mutex is already held at that point (via IBONL),
which blocks any *new* I/O ioctl from starting, so the wait only
drains whatever was already in flight; it does not need a timeout,
and adding one would just let detach() free private_data out from
under a callback that is still running.
gpib_unregister_driver() also calls iboffline(), but unlike the
IBONL path it holds no lock at all, so a fresh ioctl could still
race the detach()/board->interface=NULL sequence there. Take
big_gpib_mutex around that call site to close the same window.
The KASAN reproducer from v1 (kprobe on a driver read callback,
concurrent kfree from a detach kthread) demonstrates the class of
bug this closes: a blocking read callback dereferencing freed
private_data. It is reproduced against board->io_active rather than
the mutex in this version; behavior against the harness is
unchanged, since the harness only exercises the missing exclusion
between detach() and an in-flight read, not the specific mechanism
used to provide it.
Fixes: e6ab504633e4 ("staging: gpib: Destage gpib")
Cc: stable@vger.kernel.org
Signed-off-by: Pavitra Jha <jhapavitra98@gmail.com>
---
drivers/gpib/common/gpib_os.c | 21 +++++++++++++++++++++
drivers/gpib/common/iblib.c | 27 ++++++++++++++++-----------
drivers/gpib/include/gpib_types.h | 9 +++++++++
3 files changed, 46 insertions(+), 11 deletions(-)
diff --git a/drivers/gpib/common/gpib_os.c b/drivers/gpib/common/gpib_os.c
index 5909274dd..86921856f 100644
--- a/drivers/gpib/common/gpib_os.c
+++ b/drivers/gpib/common/gpib_os.c
@@ -912,6 +912,7 @@ static int read_ioctl(struct gpib_file_private *file_priv, struct gpib_board *bo
mutex_unlock(&file_priv->descriptors_mutex);
atomic_set(&desc->io_in_progress, 1);
+ atomic_inc(&board->io_active);
/* Read buffer loads till we fill the user supplied buffer */
while (remain > 0 && end_flag == 0) {
@@ -944,6 +945,8 @@ static int read_ioctl(struct gpib_file_private *file_priv, struct gpib_board *bo
retval = copy_to_user((void __user *)arg, &read_cmd, sizeof(read_cmd));
atomic_set(&desc->io_in_progress, 0);
+ atomic_dec(&board->io_active);
+ wake_up(&board->io_drain_wait);
atomic_dec(&desc->descriptor_busy);
wake_up_interruptible(&board->wait);
@@ -1003,6 +1006,7 @@ static int command_ioctl(struct gpib_file_private *file_priv,
*/
atomic_set(&desc->io_in_progress, 1);
+ atomic_inc(&board->io_active);
do {
fault = copy_from_user(board->buffer, userbuf, (board->buffer_length < remain) ?
@@ -1038,6 +1042,8 @@ static int command_ioctl(struct gpib_file_private *file_priv,
*/
if (!no_clear_io_in_prog || fault)
atomic_set(&desc->io_in_progress, 0);
+ atomic_dec(&board->io_active);
+ wake_up(&board->io_drain_wait);
atomic_dec(&desc->descriptor_busy);
wake_up_interruptible(&board->wait);
@@ -1085,6 +1091,7 @@ static int write_ioctl(struct gpib_file_private *file_priv, struct gpib_board *b
mutex_unlock(&file_priv->descriptors_mutex);
atomic_set(&desc->io_in_progress, 1);
+ atomic_inc(&board->io_active);
/* Write buffer loads till we empty the user supplied buffer */
while (remain > 0) {
@@ -1118,6 +1125,8 @@ static int write_ioctl(struct gpib_file_private *file_priv, struct gpib_board *b
fault = copy_to_user((void __user *)arg, &write_cmd, sizeof(write_cmd));
atomic_set(&desc->io_in_progress, 0);
+ atomic_dec(&board->io_active);
+ wake_up(&board->io_drain_wait);
atomic_dec(&desc->descriptor_busy);
wake_up_interruptible(&board->wait);
@@ -2115,8 +2124,18 @@ void gpib_unregister_driver(struct gpib_interface *interface)
if (board->use_count > 0)
pr_warn("gpib: Warning: deregistered interface %s in use\n",
interface->name);
+ /*
+ * Unlike the IBONL ioctl path, nothing else holds
+ * big_gpib_mutex here, so a fresh ioctl could race
+ * this teardown and dispatch into board->interface
+ * right as it becomes NULL below. Hold the mutex
+ * across iboffline() and clearing board->interface
+ * so ibioctl() cannot enter until this is done.
+ */
+ mutex_lock(&board->big_gpib_mutex);
iboffline(board);
board->interface = NULL;
+ mutex_unlock(&board->big_gpib_mutex);
}
}
for (list_ptr = registered_drivers.next; list_ptr != ®istered_drivers;) {
@@ -2149,6 +2168,8 @@ void init_gpib_board(struct gpib_board *board)
init_waitqueue_head(&board->wait);
mutex_init(&board->user_mutex);
mutex_init(&board->big_gpib_mutex);
+ atomic_set(&board->io_active, 0);
+ init_waitqueue_head(&board->io_drain_wait);
board->locking_pid = 0;
spin_lock_init(&board->locking_pid_spinlock);
spin_lock_init(&board->spinlock);
diff --git a/drivers/gpib/common/iblib.c b/drivers/gpib/common/iblib.c
index 07a30d520..0de6ca50c 100644
--- a/drivers/gpib/common/iblib.c
+++ b/drivers/gpib/common/iblib.c
@@ -257,22 +257,27 @@ int iboffline(struct gpib_board *board)
}
/*
- * Acquire big_gpib_mutex before calling detach() to prevent a
- * use-after-free race. I/O callbacks (read/write/command) hold
- * big_gpib_mutex while caching board->private_data on their stack.
- * Without this lock, iboffline() can kfree(board->private_data)
- * inside detach() while an I/O callback is still running and holds
- * a stale pointer to the freed memory.
+ * iboffline() is always called with board->big_gpib_mutex already
+ * held (via the IBONL ioctl), so it cannot take that mutex itself.
+ * That mutex is also dropped by IBRD/IBWRT/IBCMD before they call
+ * into board->interface, since those calls can block for a long
+ * time, so it never actually excludes an in-flight read/write/
+ * command callback in the first place.
*
- * Affected board drivers: cb7210, ines_gpib, tnt4882 (all delegate
- * to nec7210_read/pio_read which blocks in wait_event_interruptible
- * for up to board->usec_timeout microseconds while holding priv).
+ * board->io_active counts callbacks currently executing inside
+ * board->interface. Wait here, uninterruptibly and without a
+ * timeout, for it to reach zero before calling detach(). Since
+ * big_gpib_mutex is held, no *new* I/O ioctl can start while we
+ * wait, so this only drains whatever was already in flight.
+ * Returning early would let detach() free board->private_data
+ * while that in-flight callback is still dereferencing it, which
+ * is the use-after-free this is closing.
*/
- mutex_lock(&board->big_gpib_mutex);
+ wait_event(board->io_drain_wait, atomic_read(&board->io_active) == 0);
+
board->interface->detach(board);
gpib_deallocate_board(board);
board->online = 0;
- mutex_unlock(&board->big_gpib_mutex);
dev_dbg(board->gpib_dev, "board offline\n");
return 0;
diff --git a/drivers/gpib/include/gpib_types.h b/drivers/gpib/include/gpib_types.h
index 28b73157f..1c1f29c12 100644
--- a/drivers/gpib/include/gpib_types.h
+++ b/drivers/gpib/include/gpib_types.h
@@ -288,6 +288,15 @@ struct gpib_board {
* store additional variables for this board
*/
void *private_data;
+ /*
+ * Counts read/write/command callbacks currently executing in
+ * board->interface. iboffline() waits for this to reach zero
+ * before calling detach(), so its teardown of private_data
+ * cannot race an in-flight callback still dereferencing it.
+ */
+ atomic_t io_active;
+ /* Woken when io_active reaches zero, so iboffline() can wait for drain. */
+ wait_queue_head_t io_drain_wait;
/* Number of open file descriptors using this board */
unsigned int use_count;
/* list of open devices connected to this board */
--
2.53.0
prev parent reply other threads:[~2026-07-08 7:36 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-22 11:14 [PATCH] gpib: fix use-after-free in iboffline() detach path Pavitra Jha
2026-06-29 16:10 ` Dave Penkler
2026-07-08 7:36 ` Pavitra Jha [this message]
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=20260708073618.147714-1-jhapavitra98@gmail.com \
--to=jhapavitra98@gmail.com \
--cc=dpenkler@gmail.com \
--cc=gregkh@linuxfoundation.org \
--cc=linux-kernel@vger.kernel.org \
--cc=stable@vger.kernel.org \
/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