mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Robin Murphy <robin.murphy@arm.com>
To: 徐晗翔 <xuhx@hust.edu.cn>, joro@8bytes.org, will@kernel.org
Cc: iommu@lists.linux.dev, linux-kernel@vger.kernel.org
Subject: Re: iommu: iommu_device_unregister() traversal racing concurrent client removal -> group use-after-free / refcount underflow (unconditional iommu_group_put, unlocked TOCTOU reads)
Date: Fri, 23 Oct 2026 16:29:00 +0100	[thread overview]
Message-ID: <70ce461e-3c7e-40a5-9a23-082a0ad2f8f1@arm.com> (raw)
In-Reply-To: <4eed6353.14843.1a162dc1a8f.Coremail.xuhx@hust.edu.cn>

On 22/10/2026 10:05 am, 徐晗翔 wrote:
> When iommu_device_unregister()'s bus traversal releases a client
> device, nothing prevents iommu_release_device() from running on the
> same device a second time from the device_del() bus notifier -- and
> the release path's unlocked double-read of dev->iommu_group plus its
> unconditional iommu_group_put() (outside the "device actually found"
> branch) turn that double execution into a use-after-free of the group
> and its devices kobject, or a refcount underflow that frees the group
> early while other members still point at it.  Confirmed with KASAN on
> mainline 7.3: in a 3000-round race, 7 slab-use-after-free reports in
> kobject_put/refcount_warn_saturate with a closed three-stack witness
> (group allocated by the probe side, freed by the unregister traversal,
> touched again by the removal side), plus a NULL deref in
> __iommu_probe_device.
> 
> The two kernel paths involved have no lock in common: the traversal
> (remove_iommu_group(), drivers/iommu/iommu.c:273) and the bus notifier
> (device_del -> BUS_NOTIFY_REMOVED_DEVICE -> iommu_bus_notifier,
> iommu.c:1815) both funnel into iommu_release_device() (iommu.c:778),
> which takes neither iommu_probe_device_lock nor the group lock before
> its first read.  The kernel itself documents the hazard in the PCI
> reset helpers ("callers must ensure there will be no racy
> iommu_release_device() call, which otherwise would UAF the
> dev->iommu_group pointer", iommu.c:4029-4031) -- that contract is a
> caller-side convention with no mechanism behind it, and the traversal
> and the notifier are exactly the callers that were never made to
> ensure it.  Triggering needs a runtime-unregistrable IOMMU instance
> (virtio-iommu sysfs unbind, several platform IOMMUs) plus a concurrent
> client device removal (PCI remove/rescan, hotplug) -- both
> administrator actions, but each entirely legitimate on its own.

Drivers really shouldn't allow unbinding - if we can pull an IOMMU out 
from underneath client devices with active DMA (especially to translated 
addresses), a theoretical race against device hot-remove is hardly the 
biggest problem facing the system in that case...

Properly-implemented modular drivers can only be removed if and when 
they have no client devices - which in practice means they're usually 
non-removable - so one way or another this race would not exist.

That does still leave the case of unwinding from iommu_device_register() 
failure, but again that's already a sufficiently unexpected and serious 
condition that we can't necessarily expect the system to continue in a 
usable state at all, and if you want to start worrying about hot-remove 
during IOMMU registration that's probably a much bigger can of worms anyway.

Thanks,
Robin.

> 
> The detailed description:
> 
> * Test environment / affected version
> 
>    Linux version 7.3.0 (gcc 13.3.0, GNU ld 2.42) #1 SMP PREEMPT_DYNAMIC
>    -- vanilla mainline 7.3 release tarball (kernel.org, unmodified),
>    rebuilt with CONFIG_KASAN_GENERIC=y, CONFIG_IOMMU_SUPPORT=y,
>    CONFIG_IOMMU_API=y, QEMU/KVM x86_64 guest.  All functions cited
>    below are byte-identical in 7.1.5 and 7.3 (function-level diff);
>    line numbers are from 7.3.
> 
> * The two racing entries into the same release path
> 
>    A (instance unregister traversal):
>        iommu_device_unregister() (iommu.c:316)
>          -> bus_for_each_dev(..., remove_iommu_group)   /* :319 */
>          -> remove_iommu_group()                        /* :273 */
>               if (dev->iommu && dev->iommu->iommu_dev == data)   /* :275 */
>                       iommu_release_device(dev);
> 
>    B (client removal notification):
>        device_del() -> BUS_NOTIFY_REMOVED_DEVICE (fired without the
>        device_lock held) -> iommu_bus_notifier() (iommu.c:1815)
>          -> iommu_release_device(dev)
> 
>    Neither path takes iommu_probe_device_lock (which the probe side
>    uses to make dev->iommu stable, iommu.c:626/715); the group lock is
>    only acquired deep inside, after the unlocked reads.  The device
>    model does not serialize them: klist_del() only marks the node dead
>    and drops one reference, and the traversal's iterator pins the node
>    with kref_get -- so A's callback and B's notifier run concurrently
>    for the same device.
> 
> * What breaks inside iommu_release_device() (iommu.c:778-789)
> 
>        group = dev->iommu_group;             /* :781, unlocked read #1 */
>        if (group)
>                __iommu_group_remove_device(dev);
>        if (dev->iommu)
>                dev_iommu_free(dev);          /* :788, unlocked free when
>                                                  the group is gone */
> 
>    __iommu_group_remove_device() (iommu.c:751) then re-reads
>    dev->iommu_group (read #2 at :753) and locks it (:755) -- between
>    the two reads the other side can complete the whole removal inside
>    the group lock (iommu_deinit_device() clears dev->iommu_group at
>    :611 and frees the param at :613), so:
> 
>    - read #2 yields NULL -> mutex_lock(NULL) -> oops; or
>    - read #2 yields the same group, the find loop finds nothing (the
>      other side already list_del'ed it) and the code falls through to
>      the UNCONDITIONAL iommu_group_put(group) at :775 -- a put with no
>      matching get, because the only get paired with :775 is the group
>      membership reference (iommu_group_add_device(), iommu.c:1338),
>      which the winner already consumed.
> 
>    The group's reference sits on its devices kobject
>    (kobject_get/put(group->devices_kobj), iommu.c:1448/1462), so the
>    extra put either frees the group immediately (single-member,
>    no-external-ref topology -> every later access, including the
>    loser's own mutex_unlock and kobject_put, is a UAF) or drops the
>    kref early (refcount underflow -> premature kfree while other
>    members / the singleton pointer still reference it).
> 
>    Two more crossings of the same root, for completeness:
>    - remove_iommu_group()'s second dereference at :275
>      (dev->iommu->iommu_dev) races the param free (dev_iommu_free()
>      clears dev->iommu and kfrees the param), including the
>      probe-failure cleanup inside iommu_init_device() (iommu.c:544),
>      which holds the probe lock that the traversal does not take:
>      UAF read of the param.
>    - the "group == NULL but param != NULL" branch free at :788 races
>      the locked free at :613: double free of param/fwspec, or a
>      NULL+0x28 read in the loser's path.
> 
> * Evidence (7.3, KASAN)
> 
>    Reproducer attached (iommurace.c): registers a fake IOMMU instance
>    through the real exported core API and drives both entries with
>    real platform-bus devices -- kthread A loops
>    iommu_device_register/unregister, kthread B loops client
>    platform_device_add/del.  Usage:
> 
>      insmod iommurace.ko
>      echo setup > /sys/module/iommurace/parameters/action
>      echo "ab 3000" > /sys/module/iommurace/parameters/action
>      echo status > /sys/module/iommurace/parameters/action
> 
>    One 3000-round run (4 vCPU): 7 slab-use-after-free reports
>    (kobject_put x4, refcount_warn_saturate x3 -- the refcount
>    underflow family) plus one NULL deref at
>    __iommu_probe_device+0x2ad (address 0x28) as B's probe walked state
>    the concurrent unregister had already torn down; the module
>    reference count stays pinned at 1 afterwards (the poisoned state
>    skips the deinit that would module_put).  First report, closed
>    three-stack witness:
> 
>      access:  slab-use-after-free in kobject_put
>               by task iommurace_b (client removal side)
>      allocated: kobject_create_and_add <- iommu_group_alloc
>                 <- __iommu_probe_device <- iommu_probe_device
>                 <- iommu_bus_notifier        (removal side's earlier probe)
>      freed:     kfree <- kobject_put <- remove_iommu_group+0x67
>                 <- bus_for_each_dev
>                 <- iommu_device_unregister  (traversal side)
> 
>    This is exactly the "extra put frees the group / underflows the
>    kref" interleaving described above.  (Sequential baselines --
>    register/unregister loops and client add/del without the race --
>    produce zero reports during the test phases.  Note for running the
>    module: leave it loaded after the race actions; the poisoned state
>    intentionally pins its refcount, and the module's own rmmod path
>    has an unrelated software-node teardown ordering splat on 7.3.)
> 
> * Trigger model / affected configurations
> 
>    Any build where an IOMMU instance can be unregistered at runtime:
>    virtio-iommu (sysfs unbind), several platform IOMMU drivers (some
>    set suppress_bind_attrs; PCI hot-unplug paths remain), plus any
>    concurrent client removal the platform produces (PCI remove/rescan,
>    device_del on hotpluggable buses).  Both sides are administrator
>    actions; the consequence is kernel memory corruption (UAF
>    read/write, double free, premature group release leaving dangling
>    dev->iommu_group / singleton pointers for later users).
> 
> Thanks for taking a look.
> 
> Regards,
> Hanxiang Xu
> xuhx@hust.edu.cn
> 
> 


      reply	other threads:[~2026-10-23 15:29 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-22  9:05 徐晗翔
2026-10-23 15:29 ` Robin Murphy [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=70ce461e-3c7e-40a5-9a23-082a0ad2f8f1@arm.com \
    --to=robin.murphy@arm.com \
    --cc=iommu@lists.linux.dev \
    --cc=joro@8bytes.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=will@kernel.org \
    --cc=xuhx@hust.edu.cn \
    /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

all inboxes | Powered by JetHome®