From: mosafer <mohsafer@gmail.com>
To: sashiko-bot@kernel.org
Cc: linux-usb@vger.kernel.org, mohsafer@gmail.com,
sashiko-reviews@lists.linux.dev,
syzbot+863936f50214e843ae0c@syzkaller.appspotmail.com,
stable@vger.kernel.org
Subject: [PATCH v2] driver core: complete deferred binds when drivers_autoprobe is off
Date: Thu, 8 Oct 2026 16:33:20 -0500 [thread overview]
Message-ID: <20261008213320.268141-1-mosafer@node0.quickhttpnode15.cloudfaas-pg0.wisc.cloudlab.us> (raw)
In-Reply-To: <sashiko-outbox-164361@kernel.org>
From: Mohammad Mosafer <mohsafer@gmail.com>
device_add() registers a device and then runs the bus's initial probe
via bus_probe_device() -> device_initial_probe(). With
drivers_autoprobe disabled for the bus, device_initial_probe() skipped
__device_attach() entirely.
That is correct for automatic *matching* against the bus's driver list,
but __device_attach() also doubles as "complete a bind that a driver
already initiated": when dev->driver has been pre-assigned outside the
normal match/probe path, it finishes the bind by calling
device_bind_driver() under the device lock, bypassing probe().
With autoprobe off, that second duty was skipped too. USB depends on
it: usb_driver_claim_interface() pre-sets dev->driver on an interface
that is not yet registered, documenting "let the future device_add()
bind it, bypassing probe()". Composite drivers (cdc-acm, cdc_ncm,
cdc_mbim, ...) rely on it to bind their sibling data interfaces. If
drivers_autoprobe is written with 0 while such an interface is between
the claim and its device_add(), the interface ends up registered, with
dev->driver set and iface->condition == USB_INTERFACE_BOUND, but never
bound: its knode_driver is never attached to the driver's
klist_devices.
Teardown then trusts those flags: usb_driver_release_interface() ->
device_release_driver() -> __device_release_driver() runs a full
release and calls klist_remove(&dev->p->knode_driver) on a
never-attached node whose knode_klist() is NULL, which klist_put()
dereferences:
Oops: general protection fault
KASAN: null-ptr-deref in range [0x0000000000000058-0x000000000000005f]
klist_put <- klist_remove <- device_release_driver_internal <-
usb_driver_release_interface <- acm_disconnect / cdc_ncm_unbind
Keep device_initial_probe() always calling __device_attach(), but pass
whether automatic matching is allowed (sp->drivers_autoprobe) down to
it, and evaluate that flag inside __device_attach() under the device
lock, where dev->driver is read anyway: a device with a pre-assigned
driver completes its bind regardless of autoprobe, and only a device
without one is subject to the matching policy. Evaluating the two
conditions inside the locked branch (rather than as
"autoprobe || dev->driver" in the caller) also closes a TOCTOU: a
concurrent detach can no longer turn the completion of a pre-assigned
bind into bus-wide matching behind an "autoprobe off" policy.
Other subsystems that preset dev->driver before device_add() (w1,
tegra xusb, zynqmp-ipi-mailbox) get the same correctness back; for
devices with no pre-assigned driver and autoprobe off, behavior is
unchanged.
Reproduced with syzkaller's C reproducer on a 7.3.0-rc6 tree:
instrumentation shows the doomed interface's device_add() racing an
autoprobe=0 write; the bind-completion branch never runs for it; later
teardown hits klist_remove() with a pristine node. With this patch the
same instrumented race completes the bind inside the window (3 VMs,
5 min per run: window hit 3 times, bind completed 3 times, and 802
devices with no pre-assigned driver correctly skipped matching despite
__device_attach() now always running): zero KLIST-REMOVE-BAD, zero
Oops.
Reported-by: syzbot+863936f50214e843ae0c@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=863936f50214e843ae0c
Fixes: b8c5cec23d5c ("Driver core: udev triggered device-<>driver binding")
Cc: stable@vger.kernel.org
Signed-off-by: Mohammad Mosafer <mohsafer@gmail.com>
---
---
Thanks for the review. The TOCTOU is real: v1 evaluated
"sp->drivers_autoprobe || dev->driver" in device_initial_probe() without
device_lock held, so a concurrent detach in that window could make
__device_attach() observe dev->driver == NULL under the lock and fall
through to bus_for_each_drv() - a bus-wide match despite the
drivers_autoprobe=0 policy.
Changes since v1:
- Move the decision into __device_attach(): it gains an allow_match
argument (device_initial_probe() passes sp->drivers_autoprobe,
device_attach() passes true) and evaluates it inside the locked
if/else chain: a pre-assigned dev->driver completes its bind
regardless of autoprobe; the bus_for_each_drv() matching branch only
runs when matching is allowed or requested. dev->driver is now read
exactly once, under device_lock, so a concurrent detach can neither
bypass the bind completion nor turn it into a spurious autoprobe.
- The crash fix itself is untouched code: the else-if branch that
completes the pre-assigned bind is identical to v1 / to the
autoprobe-on path; only its reachability changed.
Re-verified with the same instrumented race (3 VMs, 5 min): window hit
3 times (DATT match=0 drv=1 bound=0), bind completed all 3 (DD-BIND,
teardown attached=1), and 802 devices with no pre-assigned driver under
match=0 returned without touching bus_for_each_drv(); zero
KLIST-REMOVE-BAD, zero Oops.
v1: https://lore.kernel.org/all/20261008154105.256340-1-mosafer@node0.quickhttpnode15.cloudfaas-pg0.wisc.cloudlab.us/
review: https://lore.kernel.org/all/sashiko-outbox-164361@kernel.org/
drivers/base/dd.c | 30 ++++++++++++++++++++++++++----
1 file changed, 26 insertions(+), 4 deletions(-)
diff --git a/drivers/base/dd.c b/drivers/base/dd.c
index f6525a7ee8c5..4c1bb1979519 100644
--- a/drivers/base/dd.c
+++ b/drivers/base/dd.c
@@ -1065,7 +1065,8 @@ static void __device_attach_async_helper(void *_dev, async_cookie_t cookie)
put_device(dev);
}
-static int __device_attach(struct device *dev, bool allow_async)
+static int __device_attach(struct device *dev, bool allow_async,
+ bool allow_match)
{
int ret = 0;
bool async = false;
@@ -1085,6 +1086,17 @@ static int __device_attach(struct device *dev, bool allow_async)
device_set_driver(dev, NULL);
ret = 0;
}
+ } else if (!allow_match) {
+ /*
+ * Automatic driver matching is suppressed for this device
+ * (drivers_autoprobe is off), and no driver has been
+ * assigned to it: do not run bus_for_each_drv() matching
+ * for it. Deciding this here, under device_lock, means a
+ * concurrent driver detach between the caller picking
+ * allow_match=false and reaching here cannot turn the
+ * completion of a pre-assigned bind into bus-wide matching.
+ */
+ goto out_unlock;
} else {
struct device_attach_data data = {
.dev = dev,
@@ -1138,7 +1150,7 @@ static int __device_attach(struct device *dev, bool allow_async)
*/
int device_attach(struct device *dev)
{
- return __device_attach(dev, false);
+ return __device_attach(dev, false, true);
}
EXPORT_SYMBOL_GPL(device_attach);
@@ -1149,8 +1161,18 @@ void device_initial_probe(struct device *dev)
if (!sp)
return;
- if (sp->drivers_autoprobe)
- __device_attach(dev, true);
+ /*
+ * Always run the attach, but tell it whether automatic matching is
+ * allowed. Completing a bind that a driver already initiated (e.g.
+ * usb_driver_claim_interface() pre-setting dev->driver for an
+ * unregistered interface, expecting device_add() to bind it) is not
+ * automatic matching and must happen even with drivers_autoprobe
+ * off; matching is evaluated in __device_attach() under the device
+ * lock. Skipping the call entirely used to leave such devices
+ * registered-but-unbound, and teardown then oopsed klist_removing
+ * the never-attached knode_driver.
+ */
+ __device_attach(dev, true, sp->drivers_autoprobe);
subsys_put(sp);
}
--
2.34.1
next prev parent reply other threads:[~2026-10-08 21:33 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-08 15:41 [PATCH] driver core: complete deferred binds when drivers_autoprobe is off mosafer
2026-10-08 15:53 ` sashiko-bot
2026-10-08 21:33 ` mosafer [this message]
2026-10-08 21:39 ` [PATCH v2] " sashiko-bot
2026-10-09 8:52 ` [PATCH] " Danilo Krummrich
2026-10-09 18:22 ` [PATCH v2] " mosafer
2026-10-09 18:28 ` sashiko-bot
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=20261008213320.268141-1-mosafer@node0.quickhttpnode15.cloudfaas-pg0.wisc.cloudlab.us \
--to=mohsafer@gmail.com \
--cc=linux-usb@vger.kernel.org \
--cc=sashiko-bot@kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
--cc=stable@vger.kernel.org \
--cc=syzbot+863936f50214e843ae0c@syzkaller.appspotmail.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