Report from syzbot email: 69e93024.a00a0220.17a17.0031.GAE@google.com
Subject: [syzbot] [i2c?] WARNING: refcount bug in i2c_get_adapter
(2)
Root cause:
i2c_get_adapterraces withi2c_del_adapter— the adapter’s device refcount reaches zero insidedevice_unregister, but the adapter remains visible in the IDR until afterwait_for_completion, allowing a concurrentget_devicecall on a dead object.
| Field | Value | Implication |
|---|---|---|
| MSGID | <69e93024.a00a0220.17a17.0031.GAE@google.com> |
|
| MSGID_URL | 69e93024.a00a0220.17a17.0031.GAE@google.com | |
| CRASH_TYPE | WARNING | |
| WARNING_TEXT | refcount_t: addition on 0; use-after-free. |
Runtime refcount consistency check: refcount_inc was
called on an object whose refcount was already 0 |
| WARNING_SOURCE | lib/refcount.c:25 |
WARN_ONCE inside refcount_warn_saturate()
case REFCOUNT_ADD_UAF |
| CONFIG_REQUIRED | (unconditional — fires in all builds) | WARN_ONCE in lib/refcount.c has no
CONFIG_ guard |
| UNAME | syzkaller #0 PREEMPT_{RT,(full)} |
syzbot kernel build |
| DISTRO | (syzbot — not a distribution kernel) | |
| HEAD_COMMIT | e753c16cb3dd (Merge tag ‘spi-fix-v7.0-rc7’) |
|
| INTRODUCED-BY | 611e12ea0f121
— “i2c: core: manage i2c bus device refcount in i2c_[get |
put]_adapter” |
| PATCH_BASE | e753c16cb3dd |
|
| PROCESS | syz.1.506 (PID 7352, CPU 0, UID 0) |
|
| HARDWARE | Google Compute Engine | syzbot VM |
| BIOS | Google 03/18/2026 | |
| TAINT | L — softlockup |
Indicates a system stall; may be coincidental to the refcount bug |
| VMLINUX | oops-workdir/syzbot/vmlinux-e753c16c |
|
| SOURCEDIR | oops-workdir/linux |
Checked out to HEAD commit e753c16cb3dd |
| Module | Flags | Backtrace | Location | Flag Implication |
|---|---|---|---|---|
| (no modules linked in — all built-in) |
| # | Address | Function | Offset | Size | Context | Module | Source location |
|---|---|---|---|---|---|---|---|
| 0 | 0xffffffff849eaa6f (0xffffffff849ea9d0 + 0x9f) |
refcount_warn_saturate |
0x9f |
0x110 |
Task | lib/refcount.c:25 | |
| 1 | __refcount_add (inlined) |
Task | include/linux/refcount.h:-1 |
||||
| 2 | __refcount_inc (inlined) |
Task | include/linux/refcount.h:366 |
||||
| 3 | refcount_inc (inlined) |
Task | include/linux/refcount.h:383 |
||||
| 4 | kref_get (inlined) |
Task | include/linux/kref.h:45 |
||||
| 5 | 0xffffffff8b1ab49a (0xffffffff8b1ab3a0 + 0xfa) |
kobject_get |
0xfa |
0x120 |
Task | lib/kobject.c:643 | |
| 6 | 0xffffffff8742a42d (0xffffffff8742a3c0 + 0x6d) |
i2c_get_adapter |
0x6d |
0xa0 |
Task | drivers/i2c/i2c-core-base.c:2612 | |
| 7 | 0xffffffff8743ec48 (0xffffffff8743ec00 + 0x48) |
i2cdev_open |
0x48 |
0x190 |
Task | drivers/i2c/i2c-dev.c:603 | |
| 8 | 0xffffffff82392340 (0xffffffff82391e70 + 0x4d0) |
chrdev_open |
0x4d0 |
0x5f0 |
Task | fs/char_dev.c:411 |
|
| 9 | 0xffffffff8236e11d (0xffffffff8236d8e0 + 0x83d) |
do_dentry_open |
0x83d |
0x13e0 |
Task | fs/open.c:949 |
|
| 10 | 0xffffffff8236edbb (0xffffffff8236ed80 + 0x3b) |
vfs_open |
0x3b |
0x350 |
Task | fs/open.c:1081 |
|
| 11 | do_open (inlined) |
Task | fs/namei.c:4677 |
||||
| 12 | 0xffffffff823bc223 (0xffffffff823b93e0 + 0x2e43) |
path_openat |
0x2e43 |
0x38a0 |
Task | fs/namei.c:4836 |
|
| 13 | 0xffffffff823b916e (0xffffffff823b8f30 + 0x23e) |
do_file_open |
0x23e |
0x4a0 |
Task | fs/namei.c:4865 |
|
| 14 | 0xffffffff823701a3 (0xffffffff82370090 + 0x113) |
do_sys_openat2 |
0x113 |
0x200 |
Task | fs/open.c:1366 |
|
| 15 | do_sys_open (inlined) |
Task | fs/open.c:1372 |
||||
| 16 | __do_sys_openat (inlined) |
Task | fs/open.c:1388 |
||||
| 17 | __se_sys_openat (inlined) |
Task | fs/open.c:1383 |
||||
| 18 | 0xffffffff82370698 (0xffffffff82370560 + 0x138) |
__x64_sys_openat |
0x138 |
0x170 |
Task | fs/open.c:1383 |
|
| 19 | do_syscall_x64 (inlined) |
Task | arch/x86/entry/syscall_64.c:63 |
||||
| 20 | 0xffffffff8b23c1cd (0xffffffff8b23c080 + 0x14d) |
do_syscall_64 |
0x14d |
0xf80 |
Task | arch/x86/entry/syscall_64.c:94 |
|
| 21 | 0xffffffff81000130 (0xffffffff810000b9 + 0x77) |
entry_SYSCALL_64_after_hwframe |
0x77 |
0x7f |
Task | arch/x86/entry/entry_64.S:121 |
| Register | Value | Notes |
|---|---|---|
| RIP | 0xffffffff849eaa6f |
refcount_warn_saturate+0x9f — crash site |
| RSP | 0xffffc9001ca9f6d8 |
kernel stack |
| EFLAGS | 0x00010283 |
|
| RAX | 0xffffffff849eaa68 |
address 7 bytes before RIP; RIP−7 = 0xffffffff849eaa68
(the lea loading the bug-table entry) |
| RBX | 0x0000000000000002 |
REFCOUNT_ADD_UAF = 2 — the saturation type enum
value |
| RCX | 0x0000000000080000 |
|
| RDX | 0xffffc90006421000 |
|
| RSI | 0x00000000000006c7 |
|
| RDI | 0xffffffff8f74abf0 |
pointer to bug-table entry string
("refcount_t: addition on 0; use-after-free") |
| RBP | 0x0000000000000000 |
|
| R08 | 0xffff888020331e80 |
|
| R09 | 0x0000000000000005 |
|
| R10 | 0x0000000000000100 |
|
| R11 | 0x0000000000000004 |
|
| R12 | 0xffffffff8c04a688 |
|
| R13 | 0xdffffc0000000000 |
KASAN shadow offset pattern |
| R14 | 0xffff88803b531188 |
kernel heap address — likely struct kobject * or
related |
| R15 | 0xdffffc0000000000 |
KASAN shadow offset pattern |
refcount_warn_saturate — crash site
(lib/refcount.c:25) 11 #define REFCOUNT_WARN(str) WARN_ONCE(1, "refcount_t: " str ".\n")
12
13 void refcount_warn_saturate(refcount_t *r, enum refcount_saturation_type t)
14 {
15 refcount_set(r, REFCOUNT_SATURATED);
16
17 switch (t) {
18 case REFCOUNT_ADD_NOT_ZERO_OVF:
19 case REFCOUNT_ADD_OVF:
20 REFCOUNT_WARN("saturated; leaking memory");
21 break;
22 case REFCOUNT_ADD_UAF:
23 case REFCOUNT_SUB_UAF:
24 → 25 REFCOUNT_WARN("addition on 0; use-after-free");
25 break;
26 case REFCOUNT_DEC_LEAK:
27 REFCOUNT_WARN("decrement hit 0; leaking memory");
28 break;
29 default:
30 REFCOUNT_WARN("unknown saturation event!?");
31 }
32 }Crash instruction (Code: bytes, <67> marks
the fault):
eb 66 85 db 74 3e 83 fb 01 75 4c e8 bb d6 25 fd
48 8d 3d 84 01 d6 0a 67 48 0f b9 3a eb 4a e8 a8
d6 25 fd 48 8d 3d 81 01 d6 0a <67> 48 0f b9 3a
eb 37 e8 95 d6 25 fd 48 8d 3d 7e 01 d6 0a 67 48 0f
Crash instruction: 67 48 0f b9 3a = UD1
with REX.W + address-size override — Linux’s WARN mechanism.
Disasm window around crash:
ffffffff849eaa63: call __sanitizer_cov_trace_pc
ffffffff849eaa68: lea 0xad60181(%rip),%rdi # bug-table entry for REFCOUNT_ADD_UAF
ffffffff849eaa6f: call __SCT__WARN_trap <<< crash (UD1 static-call trampoline)
ffffffff849eaa74: jmp refcount_warn_saturate+0xdd
Inline chain leading to crash (kobject_get
entry, resolved by addr2line -i): - __refcount_add
→ include/linux/refcount.h:-1 - __refcount_inc
→ include/linux/refcount.h:366 - refcount_inc
→ include/linux/refcount.h:383 - kref_get →
include/linux/kref.h:45 - kobject_get →
lib/kobject.c:643
kobject_get
(lib/kobject.c:643) 636 struct kobject *kobject_get(struct kobject *kobj)
637 {
638 if (kobj) {
639 if (!kobj->state_initialized)
640 WARN(1, KERN_WARNING
641 "kobject: '%s' (%p): is not initialized, yet kobject_get() is being called.\n",
642 kobject_name(kobj), kobj);
→ 643 kref_get(&kobj->kref);
644 }
645 return kobj;
646 }kref_get unconditionally calls refcount_inc
without checking whether the refcount is already zero. Factual
observation: kobject_get_unless_zero (line 649) exists and
does check first via kref_get_unless_zero.
i2c_get_adapter
(drivers/i2c/i2c-core-base.c:2612)drivers/i2c/i2c-core-base.c:2602
on elixir
2602 struct i2c_adapter *i2c_get_adapter(int nr)
2603 {
2604 struct i2c_adapter *adapter;
2605
2606 mutex_lock(&core_lock);
2607 adapter = idr_find(&i2c_adapter_idr, nr);
2608 if (!adapter)
2609 goto exit;
2610
2611 if (try_module_get(adapter->owner))
→ 2612 get_device(&adapter->dev);
2613 else
2614 adapter = NULL;
2615
2616 exit:
2617 mutex_unlock(&core_lock);
2618 return adapter;
2619 }get_device at line 2612 calls
kobject_get(&dev->kobj) which calls
kref_get unconditionally. Factual observation:
idr_find returned a non-NULL adapter;
try_module_get succeeded; but the device’s refcount was
already 0 when get_device was called. Factual observation:
the function holds core_lock (mutex) when calling
get_device.
i2cdev_open
(drivers/i2c/i2c-dev.c:603)drivers/i2c/i2c-dev.c:597
on elixir
597 static int i2cdev_open(struct inode *inode, struct file *file)
598 {
599 unsigned int minor = iminor(inode);
600 struct i2c_client *client;
601 struct i2c_adapter *adap;
602
→ 603 adap = i2c_get_adapter(minor);
604 if (!adap)
605 return -ENODEV;
606
614 client = kzalloc_obj(*client);
615 if (!client) {
616 i2c_put_adapter(adap);
617 return -ENOMEM;
618 }
619 snprintf(client->name, I2C_NAME_SIZE, "i2c-dev %d", adap->nr);
620
621 client->adapter = adap;
622 file->private_data = client;
623
624 return 0;
625 }i2cdev_open is triggered by opening
/dev/i2c-N. It calls i2c_get_adapter(minor)
where minor is the device file’s minor number.
A refcount_t: addition on 0; use-after-free WARNING
fires in lib/refcount.c:25 inside
refcount_warn_saturate(), triggered by the
REFCOUNT_ADD_UAF case (RBX = 0x2 confirms this enum value).
The warning means refcount_inc was called on an object
whose refcount was already 0 — the object had been
fully released before the increment.
The call chain that triggers the crash:
i2cdev_open (drivers/i2c/i2c-dev.c:603)
└─ i2c_get_adapter (drivers/i2c/i2c-core-base.c:2612)
└─ get_device → kobject_get → kref_get → refcount_inc → WARN
drivers/i2c/i2c-core-base.c:2602
on elixir
2602 struct i2c_adapter *i2c_get_adapter(int nr)
2603 {
2604 struct i2c_adapter *adapter;
2605
2606 mutex_lock(&core_lock);
2607 adapter = idr_find(&i2c_adapter_idr, nr); // non-NULL: adapter still in IDR
2608 if (!adapter)
2609 goto exit;
2610
2611 if (try_module_get(adapter->owner)) // succeeds
→ 2612 get_device(&adapter->dev); // ← CRASH: dev.kobj.kref.refcount == 0
2613 else
2614 adapter = NULL;
2615
2616 exit:
2617 mutex_unlock(&core_lock);
2618 return adapter;
2619 }R14 = 0xffff88803b531188 — a kernel heap address that is
almost certainly &adapter->dev.kobj (or
&adapter->dev) of the dying adapter.
get_device calls kobject_get which
unconditionally calls kref_get without checking whether the
refcount is already zero. kobject_get_unless_zero exists
and does check first, but is not used here.
⛔ What identified —
get_device(&adapter->dev) on an adapter whose device
refcount is already 0.
get_device was
called?A1: i2c_del_adapter drops the device refcount to zero
and then removes the adapter from the IDR — leaving a window
where idr_find can still return the adapter even though its
device refcount is already 0.
drivers/i2c/i2c-core-base.c:1755
on elixir
1755 void i2c_del_adapter(struct i2c_adapter *adap)
1756 {
// ... client cleanup, driver unbinding ...
1796 /* device name is gone after device_unregister */
1797 dev_dbg(&adap->dev, "adapter [%s] unregistered\n", adap->name);
1798
1799 pm_runtime_disable(&adap->dev);
1800
1801 i2c_host_notify_irq_teardown(adap);
1802
1803 debugfs_remove_recursive(adap->debugfs);
1804
1805 /* wait until all references to the device are gone
...
1811 * FIXME: This is old code ... */
1812 init_completion(&adap->dev_released);
1813 device_unregister(&adap->dev); // ← (1) drops refcount; if only 1 ref,
1814 wait_for_completion(&adap->dev_released); // refcount reaches 0 here
1815
1816 /* free bus id */
1817 mutex_lock(&core_lock);
1818 idr_remove(&i2c_adapter_idr, adap->nr); // ← (2) only NOW removed from IDR
1819 mutex_unlock(&core_lock);
1820
1823 memset(&adap->dev, 0, sizeof(adap->dev));
1824 }The race window is between step (1) and step (2):
Thread A: i2c_del_adapter
device_unregister(&adap->dev) ← refcount 1 → 0
wait_for_completion(...) ← returns (refcount was 1)
--- WINDOW: adapter in IDR but refcount == 0 ---
mutex_lock(&core_lock)
idr_remove(...) ← too late
Thread B: i2c_get_adapter
mutex_lock(&core_lock)
adapter = idr_find(...) ← non-NULL! still in IDR
try_module_get(...) ← succeeds
get_device(&adapter->dev) ← CRASH: refcount == 0
i2c_get_adapter holds core_lock throughout
its body. i2c_del_adapter only takes core_lock
for the idr_remove step — it releases
core_lock before calling device_unregister. So
the two functions can execute concurrently: one inside
wait_for_completion, the other inside
get_device.
⛔ How identified — i2c_del_adapter
removes the adapter from the IDR after the device
refcount has already reached zero. This exposes a window where a
concurrent i2c_get_adapter can find the dying adapter in
the IDR and call get_device on it.
i2c_del_adapter remove from IDR so late?A2: Commit 611e12ea0f121
(“i2c: core: manage i2c bus device refcount in i2c_[get|put]_adapter”,
2015) added get_device/put_device calls to
i2c_get_adapter/i2c_put_adapter to pin the
device structure while an adapter is in use. However, it did
not reorder the idr_remove call in
i2c_del_adapter to happen before
device_unregister. The result is that the IDR can now
expose an adapter whose device has already been unregistered and whose
refcount has already reached zero.
Before that commit, i2c_get_adapter only called
try_module_get (no get_device), so the order
of idr_remove vs device_unregister was
inconsequential.
⛔ How Q2 identified — Bug introduced by 611e12ea0f121
in 2015.
The fix belongs in i2c_del_adapter in
drivers/i2c/i2c-core-base.c.
Fix: move idr_remove to before
device_unregister.
Once the adapter is removed from the IDR under
core_lock, any subsequent call to
i2c_get_adapter will receive NULL from
idr_find and return -ENODEV. Any call that had
already obtained a reference (by completing get_device
before the idr_remove) holds the reference legitimately;
wait_for_completion will still wait for those callers to
finish (i.e. call i2c_put_adapter →
put_device). The device_unregister +
wait_for_completion machinery is not affected.
Diff review (pass): - Resource leaks: none — all
existing put paths unchanged. - Lock imbalance: none — we keep the same
mutex_lock/mutex_unlock pairing. - NULL
dereference introduced: none — idr_remove does not
dereference adap. - Error path coverage: n/a —
i2c_del_adapter returns void. - Side effects on callers:
i2c_get_adapter returns NULL (ENODEV) sooner during
deletion. This is the desired behaviour and is the same as what callers
receive once deletion completes.
--- a/drivers/i2c/i2c-core-base.c
+++ b/drivers/i2c/i2c-core-base.c
@@ -1797,6 +1797,12 @@ void i2c_del_adapter(struct i2c_adapter *adap)
/* device name is gone after device_unregister */
dev_dbg(&adap->dev, "adapter [%s] unregistered\n", adap->name);
+/*
+ * Remove from IDR before device_unregister() so that a concurrent
+ * i2c_get_adapter() cannot find this adapter (via idr_find) and call
+ * get_device() on a kobject whose refcount has already reached zero.
+ */
+mutex_lock(&core_lock);
+idr_remove(&i2c_adapter_idr, adap->nr);
+mutex_unlock(&core_lock);
+
pm_runtime_disable(&adap->dev);
i2c_host_notify_irq_teardown(adap);
@@ -1812,11 +1818,6 @@ void i2c_del_adapter(struct i2c_adapter *adap)
device_unregister(&adap->dev);
wait_for_completion(&adap->dev_released);
-/* free bus id */
-mutex_lock(&core_lock);
-idr_remove(&i2c_adapter_idr, adap->nr);
-mutex_unlock(&core_lock);
-
/* Clear the device structure in case this adapter is ever going to be
added again */
memset(&adap->dev, 0, sizeof(adap->dev));PATCH_BASE: e753c16cb3dd
Status: Success
Base commit used for validation:
e753c16cb3dd (exact — matches PATCH_BASE)
Validation: git apply --check passed
against e753c16cb3dd (exact match, no fuzz required)
Note: The rough report.patch from the
analysis agent contained an invalid index line
(XXXXXXX..YYYYYYY). The fix was applied directly to the
source tree at PATCH_BASE, a clean diff generated via
git diff, and the source reverted. The resulting patch was
verified with git apply --check.
Output files produced: -
patch-email.txt — LKML-ready patch email -
report.patch — corrected unified diff (replaced invalid
rough patch) - git-send-email.sh — send script targeting
linux-i2c@vger.kernel.org
Introduced by commit 611e12ea0f121
— *“i2c: core: manage i2c bus device refcount in i2c_[get|put]_adapter”*
(Vladimir Zapolskiy, 2015-07-27).
That commit added get_device(&adapter->dev)
inside i2c_get_adapter (under core_lock) but
did not reorder the idr_remove call in
i2c_del_adapter to happen before
device_unregister. This created the race where the IDR can
expose an adapter whose device refcount has already reached zero.
Before 611e12ea0f121,
i2c_get_adapter only called try_module_get —
no device refcount operation was involved, so the ordering of
idr_remove vs device_unregister was
benign.
Verdict: PASS
All six checklist items clear — no serious issues found. The patch
correctly moves idr_remove() before
device_unregister() to close the race window. Resource
leaks, lock imbalance, NULL dereferences, error handling, caller
contracts, and patch logic all verified as correct. Patch applies
cleanly against base commit.
lib/refcount.c code quote mismatch
(backtrace section §0, lines 18–23): The report shows
REFCOUNT_ADD_NOT_ZERO_OVF and REFCOUNT_ADD_OVF
as a fallthrough pair (lines 18–19), and REFCOUNT_ADD_UAF
and REFCOUNT_SUB_UAF as a fallthrough pair (lines 22–23).
The actual tree (lib/refcount.c at
e753c16cb3dd) has four independent cases,
each with its own REFCOUNT_WARN call and break
statement — there are no fallthroughs. The crash line (line 25:
REFCOUNT_WARN("addition on 0; use-after-free")) and its
associated case (REFCOUNT_ADD_UAF at line 24) are correct.
Only the surrounding context (lines 18–23) is wrong.