Linux kernel crash report

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_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
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

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
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

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

  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:
  2425        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_addinclude/linux/refcount.h:-1 - __refcount_incinclude/linux/refcount.h:366 - refcount_incinclude/linux/refcount.h:383 - kref_getinclude/linux/kref.h:45 - kobject_getlib/kobject.c:643

5. kobject_get (lib/kobject.c:643)

lib/kobject.c:636 on elixir

   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

  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

   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

  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 identifiedget_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

  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 identifiedi2c_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 (“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.


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_adapterput_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


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 — *“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.


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