mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: 徐晗翔 <xuhx@hust.edu.cn>
To: cem@kernel.org
Cc: linux-xfs@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: xfs: use-after-free in stats_clear sysctl handler during module unload
Date: Fri, 13 Nov 2026 20:47:10 +0800 (GMT+08:00)	[thread overview]
Message-ID: <3434b976.263a1.1a1d4f294b5.Coremail.xuhx@hust.edu.cn> (raw)
In-Reply-To: <2026102727-data-sequel-ce41@gregkh>

[-- Attachment #1: Type: text/plain, Size: 8831 bytes --]

exit_xfs_fs() frees xfsstats.xs_stats before unregistering the fs/xfs
sysctl table.  A write to /proc/sys/fs/xfs/stats_clear that is already in
xfs_stats_clear_proc_handler(), or that enters between the free and the
unregister, calls xfs_stats_clearall() on the freed per-cpu pointer; that
function reads two counters from the area and then memsets the whole
per-cpu structure.  The result is a use-after-free read/write of a
per-cpu allocation, with the usual consequences when the area has been
reused: silent corruption of another subsystem's per-cpu data, random
crashes, or a KASAN report on a kernel whose per-cpu allocator is
instrumented.

The ordering bug is small but the synchronization argument is not
subtle.  unregister_sysctl_table() is the point that waits for an active
sysctl handler; it is called after free_percpu().  free_percpu() itself
does not wait for a handler and does not invalidate the existing sysctl
entry, so a writer can use the stale pointer before the wait starts.
The same free-then-unregister sequence exists on the init error path.
This is a teardown-order bug, not a missing lock in the handler.

The detailed description:

* Test environment / affected version

  Linux 7.3.0 snapshot (the tree used for this audit; Makefile reports
  VERSION=7 PATCHLEVEL=3 SUBLEVEL=0), x86_64, gcc 13.3.0, QEMU/KVM
  guest.  CONFIG_XFS_FS=m, CONFIG_PROC_FS=y, KASAN enabled.  The
  behavior is in the module load/unload path, so it is independent of
  the filesystem on disk; no XFS mount is required.

  The source lines below are from the 7.3.0 tree.  The order has been
  stable for a long time, but please re-check the exact line numbers on
  your tree before applying a patch.

* Mechanism

  The handler is registered for /proc/sys/fs/xfs/stats_clear at
  fs/xfs/xfs_sysctl.c:172-179 and executes:

      fs/xfs/xfs_sysctl.c:13-30
      xfs_stats_clear_proc_handler()
          ...
          ret = proc_dointvec_minmax(ctl, write, buffer, lenp, ppos);
          if (!ret && write && *valp) {
                  xfs_stats_clearall(xfsstats.xs_stats);
                  xfs_stats_clear = 0;
          }

  xfs_stats_clearall() walks every possible CPU, reads
  xs_inodes_active/xs_inodes_meta, and memsets the per-cpu structure:

      fs/xfs/xfs_stats.c:101-118
      for_each_possible_cpu(c) {
              preempt_disable();
              xs_inodes_active = per_cpu_ptr(stats, c)->s.xs_inodes_active;
              xs_inodes_meta = per_cpu_ptr(stats, c)->s.xs_inodes_meta;
              memset(per_cpu_ptr(stats, c), 0, sizeof(*stats));
              ...
              preempt_enable();
      }

  The unload path frees the pointer before the sysctl table is
  unregistered:

      fs/xfs/xfs_super.c:2748-2767
      exit_xfs_fs()
          ...
          xchk_global_stats_teardown();
          xfs_sysfs_del(&xfsstats.xs_kobj);
          free_percpu(xfsstats.xs_stats);        <-- pointer becomes stale
          kset_unregister(xfs_kset);
          debugfs_remove(xfs_debugfs);
          xfs_sysctl_unregister();               <-- wait happens too late

  unregister_sysctl_table() does wait for an active handler.  In
  fs/proc/proc_sysctl.c:300-307, start_unregistering() checks p->used;
  if a handler is active it arms a completion and waits.  The handler
  holds the sysctl header for the whole call
  (fs/proc/proc_sysctl.c:553-614), so the wait is effective once it is
  reached.  The bug is simply that free_percpu() is reached first.

  free_percpu() (mm/percpu.c:2240-2269) only returns the area to the
  per-cpu allocator.  It does not wait for sysctl users, does not
  synchronize_rcu(), and does not clear xfsstats.xs_stats.  After it
  returns, the area can be reused by the next alloc_percpu().

  An open fd on the sysctl file does not pin the module:
  proc_sys_open() (fs/proc/proc_sysctl.c:629-641) grabs the header only
  for the duration of open() and does not try_module_get().  So rmmod
  can proceed while a writer still has the file open.

  The init error path has the same order:

      fs/xfs/xfs_super.c:2727-2735
      out_remove_stats_kobj:
          xfs_sysfs_del(&xfsstats.xs_kobj);
      out_free_stats:
          free_percpu(xfsstats.xs_stats);
      out_kset_unregister:
          kset_unregister(xfs_kset);
      out_debugfs_unregister:
          debugfs_remove(xfs_debugfs);
          xfs_sysctl_unregister();

  If xfs_sysfs_init() fails, xs_stats is live and this is the same UAF.
  If alloc_percpu() failed earlier, xs_stats is NULL and
  xfs_stats_clearall(NULL) computes per_cpu_ptr(NULL, c) from the per-cpu
  base; the subsequent memset corrupts per-cpu data instead of doing a
  literal NULL dereference.  Both cases are fixed by unregistering the
  sysctl table before dropping the stats pointer.

  The race does not require the handler to start after free_percpu().
  A handler that entered xfs_stats_clearall() before the free and was
  preempted in the per-CPU loop will resume and write to the same area
  after free_percpu() returns.  unregister_sysctl_table() has not begun
  waiting at that point, so it cannot help.

* Trigger model

  - CONFIG_XFS_FS=m; XFS loaded as a module; CONFIG_PROC_FS=y.
  - No XFS filesystem is mounted (a mount holds a module reference and
    prevents rmmod).
  - A user able to write /proc/sys/fs/xfs/stats_clear (mode 0644,
    normally root) and to unload the module (root/CAP_SYS_MODULE).
  - SMP or preemptible kernel; a writer already inside the handler, or
    a writer entering between free_percpu() and
    xfs_sysctl_unregister(), races the unload.
  - Consequence: UAF read/write of the per-cpu area; if the area has
    been reused, corruption of another subsystem's per-cpu state,
    random crash, or memory-corruption symptoms.  This is a local
    privileged bug, not an unprivileged LPE.

* Reproducer 

  I used a KASAN-enabled 7.3.0 kernel with CONFIG_XFS_FS=m and a read-only kprobe detector
  (xfsrace.c).  The detector records the pointer passed to
  free_percpu() by the rmmod thread, confirms that
  xfs_sysctl_unregister() is entered after that free, and then compares
  the argument of every xfs_stats_clearall() call with the recorded
  pointer.  A match is reported as UAF-WINDOW.

  Controlled run: 16 writers loop on
  /proc/sys/fs/xfs/stats_clear, then rmmod xfs runs; the detector adds an
  800 ms stall at xfs_sysctl_unregister() to widen the window.  Result:

      XFSRACE: free_percpu(ffffffff9ff0f290) entry, comm=rmmod
      XFSRACE: xfs_sysctl_unregister() after free_percpu(ffffffff9ff0f290);
               stalling 800ms, comm=rmmod
      XFSRACE: UAF-WINDOW xfs_stats_clearall(ffffffff9ff0f290) after
               free_percpu, comm=init pid=124
      ...
      XFSRACE-INIT: rmmod status=0

  272 UAF-WINDOW lines, all with the same pointer ffffffff9ff0f290; the
  serial log is complete, the dmesg ring retained 132 of them.  Control
  runs: no writers -> 0; writers but no rmmod -> 0.

  KASAN did not produce a use-after-free report: free_percpu() uses the
  per-cpu allocator, and mm/percpu.c has no KASAN poison/unpoison hooks.
  The direct evidence is therefore the kprobe pointer comparison, not a
  KASAN stack.  I am stating this explicitly so the absence of a KASAN
  splat is not read as absence of the bug.

  The 800 ms stall is artificial.  It proves that the interleaving is
  reachable; it does not measure the natural hit rate.  The key lines
  and a stack excerpt are in the evidence/ directory of the report package;
  the full 512 KB serial log is available on request.

  Files attached:

      xfsrace.c             kprobe detector
      Makefile              detector build
      init-xfs              VM init: load xfs.ko, start writers, rmmod
      mkinitramfs-xfs.sh    build the test initramfs
      run-vm-xfs.sh         boot the XFS-enabled kernel under QEMU


* Suggested direction

  Move xfs_sysctl_unregister() before free_percpu(xfsstats.xs_stats) in
  exit_xfs_fs(), and apply the same ordering to the init error path.  The
  unregister then waits for active handlers, and the subsequent free
  cannot race with xfs_stats_clearall().

  As hardening, set xfsstats.xs_stats = NULL after free_percpu() and
  check it in xfs_stats_clear_proc_handler() (and in the sysfs
  stats_clear_store()); use READ_ONCE()/WRITE_ONCE() for the pointer if
  it is read outside the sysctl path.  The NULL check alone is not
  sufficient: a handler can still read the pointer while it is being
  freed, so the ordering fix is the root fix.

  I did not attach a patch because I do not know the introducing commit
  and because the fix should be reviewed together with the init error
  path.  I am happy to test a proposed patch on the same QEMU setup.


  Please keep me in Cc; I am not subscribed to the lists.

Regards,
Hanxiang Xu
xuhx@hust.edu.cn

[-- Attachment #2: poc.zip --]
[-- Type: application/x-zip-compressed, Size: 3752 bytes --]

  parent reply	other threads:[~2026-11-13 12:47 UTC|newest]

Thread overview: 49+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-27 12:35 usb: devio: usbdev_release() has no barrier against an in-flight async_completed() callback (use-after-free window on struct usb_dev_state)gregkh@linuxfoundation.org 徐晗翔
2026-10-27 14:12 ` Greg KH
2026-11-03 11:21   ` mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN blind spot: slub.o is uninstrumented) 徐晗翔
2026-11-04  9:41     ` Qing Wang
2026-11-04 10:07       ` Qing Wang
2026-11-03 11:30   ` futex/hrtimer: hrtimer_sleeper::task is read unsynchronized in 16 places on v7.3 -- the accessor series that fixes this is not in 7.3; please make sure it reaches the affected trees (backport nudge + stock-kernel KCSAN evidence) 徐晗翔
2026-11-03 11:48   ` cgroup: fork vs cgroup.procs write race in favordynmods mode -- v7.3 is affected; please make sure bf6fbf46b5ff reaches the stable trees (repro incl. uninstrumented natural hit) 徐晗翔
2026-11-03 14:51     ` Sasha Levin
2026-11-03 11:57   ` net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3 徐晗翔
2026-11-03 15:49     ` Eric Dumazet
2026-11-04 15:03     ` Sasha Levin
2026-11-03 12:07   ` wifi: wilc1000: SDIO/SPI remove destroys the srcu_struct before wiphy_unregister() -- late cfg80211 callbacks write through dangling srcu state (unfixed upstream; controlled + natural Oops) 徐晗翔
2026-11-03 12:10   ` gpio: gpiolib: final put of the gpio_device happens inside the SRCU read critical section on the setup-failure path -- deterministic use-after-free read at guard release (unfixed upstream) 徐晗翔
2026-11-05  8:52     ` Linus Walleij
2026-11-04 12:54   ` soc: qcom: pmic_glink_altmode: fix use-after-free on unbind (no cancel_work_sync for port work and enable_work) 徐晗翔
2026-11-04 13:04     ` Greg KH
2026-11-04 12:57   ` usb: typec: anx7411: unregister partner/altmodes before stopping the IRQ work -- use-after-free, double unregister, orphan partner on remove 徐晗翔
2026-11-04 13:28     ` Greg KH
2026-11-04 12:58   ` usb: typec: ucsi: deadlock on cros_ec unbind -- connector lock held across cancel_delayed_work_sync vs the mode-selection work's unbounded trylock polling 徐晗翔
2026-11-04 13:00   ` usb: typec: stusb160x/rt1719/hd3ss3220: IRQ thread vs remove -- double unregister / use-after-free on the partner and port 徐晗翔
2026-11-04 13:01   ` usb: typec: ucsi: displayport: data race on dp->header/ vdo_data/vdo_size -- work reads them unlocked while writers hold con->lock (VDM mismatch) 徐晗翔
2026-11-04 13:02   ` usb: typec: class: three lifetime/locking gaps around the mux/switch handles and mode_selection (drvdata UAF after provider unbind; no concurrent protocol for set/put; unlocked state_update vs delete) 徐晗翔
2026-11-10 12:56   ` driver core: uevent: netns teardown frees uevent_sock while a tagged uevent is still being broadcast (UAF) 徐晗翔
2026-11-10 13:00   ` 徐晗翔
2026-11-10 18:48     ` Greg KH
2026-11-10 13:05   ` ACPI: CPPC: per-CPU cpc_desc is freed on processor unbind without clearing the published pointer, causing UAF in cpufreq accessors 徐晗翔
2026-11-10 13:07   ` ACPI: CPPC: shared PCC subspace data can be freed while another CPU probes, reads, or tears it down 徐晗翔
2026-11-10 14:47     ` Christian Loehle
2026-11-10 13:11   ` ACPI: ACPICA: Reset can delete an event semaphore while another thread is blocked in Wait 徐晗翔
2026-11-10 13:14   ` power: supply: bq25890: pump-express work can call power_supply_changed after the psy is unregistered 徐晗翔
2026-11-10 13:17   ` power: supply: core: get/set property callbacks can run after unregister and read freed driver data 徐晗翔
2026-11-10 13:18   ` power: supply: core: system-battery enumeration can return an unregistered battery 徐晗翔
2026-11-10 13:21   ` power: supply: core: changed work can run after unregister and use freed LED triggers 徐晗翔
2026-11-10 13:23   ` power: supply: bq24190: IRQ handler can call power_supply_changed after the psy is unregistered 徐晗翔
2026-11-10 13:34   ` 徐晗翔
2026-11-11  7:17   ` genirq: NMI dispatch reads struct irqaction with no quiescence -- use-after-free across free_nmi()'s kfree (x86 verification on the arch-independent core path) 徐晗翔
2026-11-11  7:26   ` genirq: NMI API lacks the request_mutex / dev_id matching / idempotency semantics that free_irq() has -- double free_nmi releases chip PM and module references twice (verified) 徐晗翔
2026-11-11  7:30   ` kernfs: kernfs_drain()'s fast-path decision reads the active count without acquire ordering -- on weakly-ordered architectures the forced ->release()/unmap can be skipped 徐晗翔
2026-11-11 13:41     ` Greg KH
2026-11-13 11:58   ` ALSA: ctxfi: free atimer only after the timer IRQ is quiesced 徐晗翔
2026-11-13 12:03   ` ALSA: ctxfi: do not free a running system timer when timer_delete_sync_try() returns -1 (UAF) 徐晗翔
2026-11-13 12:04   ` ALSA: asihpi: free IRQ before snd_card_free()/HPI delete on HPI6205 removal (UAF) 徐晗翔
2026-11-13 12:06   ` ALSA: asihpi: quiesce PCM timer and IRQ before freeing dpcm on close (UAF) 徐晗翔
2026-11-13 12:15   ` ALSA: pcm: link-group walk in timer callback can use a closed substream's freed runtime (UAF) 徐晗翔
2026-11-13 12:28   ` ALSA: usb-audio: us144mkii: PCM close frees runtime while playback/feedback URB completion and capture work still use it (UAF) 徐晗翔
2026-11-13 12:29   ` ALSA: usb-audio: us144mkii: queued stop_pcm_work calls snd_pcm_stop() after PCM close and dereferences NULL runtime 徐晗翔
2026-11-13 12:33   ` sched/topology: old domain chains are queued for RCU freeing before the per-CPU pointers are updated, so a late reader can still dereference a freed sched_domain and sched_domain_shared (UAF) 徐晗翔
2026-11-13 12:47   ` 徐晗翔 [this message]
2026-11-13 12:48   ` xfs: data races on xfs_params and xfs_globals tunables 徐晗翔

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=3434b976.263a1.1a1d4f294b5.Coremail.xuhx@hust.edu.cn \
    --to=xuhx@hust.edu.cn \
    --cc=cem@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-xfs@vger.kernel.org \
    /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®