# Linux kernel crash report Report from syzbot email: [69e93024.a00a0220.17a17.0031.GAE@google.com](https://lore.kernel.org/r/69e93024.a00a0220.17a17.0031.GAE@google.com) Subject: [syzbot] [i2c?] WARNING: refcount bug in i2c_get_adapter (2) > **Root cause:** `i2c_get_adapter` races with `i2c_del_adapter` — the adapter's device refcount reaches zero inside `device_unregister`, but the adapter remains visible in the IDR until after `wait_for_completion`, allowing a concurrent `get_device` call on a dead object. ## Key elements | Field | Value | Implication | | ----- | ----- | ----------- | | MSGID | `<69e93024.a00a0220.17a17.0031.GAE@google.com>` | | | MSGID_URL | [69e93024.a00a0220.17a17.0031.GAE@google.com](https://lore.kernel.org/r/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`](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=611e12ea0f121a31d9e9c4ce2a18a77abc2f28d6) — "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` | ## Kernel modules | Module | Flags | Backtrace | Location | Flag Implication | | ------ | ----- | --------- | -------- | ---------------- | | *(no modules linked in — all built-in)* | | | | | ## Backtrace | # | Address | Function | Offset | Size | Context | Module | Source location | |---|---------|----------|--------|------|---------|--------|-----------------| | 0 | `0xffffffff849eaa6f (0xffffffff849ea9d0 + 0x9f)` | `refcount_warn_saturate` | `0x9f` | `0x110` | Task | | [lib/refcount.c:25](#0-refcount_warn_saturate--crash-site-librefcountc25) | | 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](#5-kobject_get-libkobjectc643) | | 6 | `0xffffffff8742a42d (0xffffffff8742a3c0 + 0x6d)` | `i2c_get_adapter` | `0x6d` | `0xa0` | Task | | [drivers/i2c/i2c-core-base.c:2612](#6-i2c_get_adapter-driversi2ci2c-core-basec2612) | | 7 | `0xffffffff8743ec48 (0xffffffff8743ec00 + 0x48)` | `i2cdev_open` | `0x48` | `0x190` | Task | | [drivers/i2c/i2c-dev.c:603](#7-i2cdev_open-driversi2ci2c-devc603) | | 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` | ## CPU Registers | 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 | ## Backtrace source code ### 0. `refcount_warn_saturate` — crash site (`lib/refcount.c:25`) [`lib/refcount.c` on elixir](https://elixir.bootlin.com/linux/v7.0-rc7/source/lib/refcount.c#L25) ```c 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` ### 5. `kobject_get` (`lib/kobject.c:643`) [`lib/kobject.c:636` on elixir](https://elixir.bootlin.com/linux/v7.0-rc7/source/lib/kobject.c#L636) ```c 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`. ### 6. `i2c_get_adapter` (`drivers/i2c/i2c-core-base.c:2612`) [`drivers/i2c/i2c-core-base.c:2602` on elixir](https://elixir.bootlin.com/linux/v7.0-rc7/source/drivers/i2c/i2c-core-base.c#L2602) ```c 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`. ### 7. `i2cdev_open` (`drivers/i2c/i2c-dev.c:603`) [`drivers/i2c/i2c-dev.c:597` on elixir](https://elixir.bootlin.com/linux/v7.0-rc7/source/drivers/i2c/i2c-dev.c#L597) ```c 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. --- ## Analysis — What / How / Where ### What 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](https://elixir.bootlin.com/linux/v7.0-rc7/source/drivers/i2c/i2c-core-base.c#L2602) ```c 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. --- ### How #### Q1: How did the device refcount reach 0 before `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](https://elixir.bootlin.com/linux/v7.0-rc7/source/drivers/i2c/i2c-core-base.c#L1755) ```c 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. #### Q2: Why does `i2c_del_adapter` remove from IDR so late? A2: Commit [`611e12ea0f121`](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=611e12ea0f121a31d9e9c4ce2a18a77abc2f28d6) ("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`](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=611e12ea0f121a31d9e9c4ce2a18a77abc2f28d6) in 2015. --- ### Where 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. ```diff --- 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` --- ## Patch **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` --- ## Bug Introduction Introduced by commit [`611e12ea0f121`](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=611e12ea0f121a31d9e9c4ce2a18a77abc2f28d6) — *"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`](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=611e12ea0f121a31d9e9c4ce2a18a77abc2f28d6), `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. --- ## Patch Review **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. ## Fact Check - ⚠️ **`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.