mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: 徐晗翔 <xuhx@hust.edu.cn>
To: akpm@linux-foundation.org
Cc: linux-kernel@vger.kernel.org, linux-usb@vger.kernel.org
Subject: lib/genalloc: gen_pool_destroy() frees chunks with no RCU  grace period while every library reader is an RCU reader
Date: Fri, 30 Oct 2026 19:47:46 +0800 (GMT+08:00)	[thread overview]
Message-ID: <4aae234b.1ae67.1a18ca32d72.Coremail.xuhx@hust.edu.cn> (raw)

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

The gen_pool publishing protocol is asymmetric: chunks are published
with list_add_rcu() and every reader in the library traverses under
rcu_read_lock() -- but gen_pool_destroy() walks the chunk list with
list_for_each_safe, list_del's each chunk and vfrees it immediately,
with no synchronize_rcu() anywhere; its only documented precondition
is "no outstanding allocations".  Any caller that invokes
gen_pool_destroy() while a reader is inside its RCU critical section
turns that reader's subsequent chunk->start_addr/end_addr/phys_addr
accesses into use-after-free of vfree'd memory -- and the RCU usage on
the reader side actively creates the illusion that concurrent reads
are safe, so callers have no signal that they must quiesce.  I
confirmed the mechanism on a pristine Linux 7.3 KASAN kernel both
deterministically (a reader parked inside gen_pool_virt_to_phys's RCU
critical section while destroy runs: slab-use-after-free in
gen_pool_virt_to_phys+0x21, plus a second-order wild-memory-access
when the freed memory got reused) and naturally, with no
instrumentation at all (four reader kthreads against a
destroy/rebuild builder loop: 17 reports of the same shape in 20
seconds).  I also audited the in-tree USB host callers of the
devm_gen_pool_create path (ohci-sm501, ohci-sa1111 via
usb_hcd_setup_local_mem) and they are safe today because the devres
release happens after usb_remove_hcd() has fully quiesced the HCD --
so this is an API contract gap waiting for a future caller, not a
live in-tree bug; still, the fix is one call and makes the contract
match what the library's own reader side advertises.

The detailed description:

* The asymmetry (Linux 7.3, lib/genalloc.c)
  - Publish: gen_pool_add_owner() -> list_add_rcu(&chunk->next_chunk,
    &pool->chunks) (:202-204).
  - Read: every library entry point holds rcu_read_lock() while
    walking the chunk list (:222-223, :296-297, :502-503, :537-538,
    :560-561, :585-586, :604-605, ...).
  - Destroy: gen_pool_destroy() (:242-262): list_for_each_safe,
    list_del(&chunk->next_chunk), vfree(chunk) -- no grace period;
    the comment requires only "no outstanding allocations"
    (:239-240).
  An RCU-publish + RCU-read API whose destructor skips the RCU
  waiting side is half a protocol: the burden is silently shifted to
  callers who were given every indication (by the reader side) that
  concurrent access is handled.  Contrast dma_pool_destroy(), which
  at least documents its quiescence assumption.

* Evidence (pristine 7.3 KASAN; instrument attached)
  genrace.c has two modes:
  - Controlled: a kthread calls gen_pool_virt_to_phys() and is
    parked (kprobe) INSIDE the RCU critical region, then another
    thread destroys and rebuilds the pool:
      BUG: KASAN: slab-use-after-free in gen_pool_virt_to_phys+0x21
      Read of size 8 ... by task genrd
      Allocated by: gen_pool_create
      Freed by: kfree <- (destroy path)
    followed by a wild-memory-access (reused-garbage dereference) in
    the same reader -- showing what a production splat would look
    like after slab reuse.
  - Natural (no kprobe): four reader kthreads loop on
    gen_pool_virt_to_phys()/gen_pool_alloc() while a builder thread
    destroys and recreates pools -- 17 identical
    slab-use-after-free reports within 20 s.
  The pool/chunk objects are the library's own; nothing about the
  test fabricates kernel state.

* In-tree exposure today (audited, for honesty)
  The devm_gen_pool_create() users in USB host land
  (usb_hcd_setup_local_mem(), called only by ohci-sm501 and
  ohci-sa1111) release the pool from devres after the driver's
  .remove returned, by which time usb_remove_hcd() has disconnected
  the root hub, freed the IRQ and run .stop -- no reader can be
  inside the library.  I did not audit the remaining genalloc users
  outside USB; the gap is reported at the API level.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn



[-- Attachment #2: genrace.c --]
[-- Type: text/plain, Size: 6462 bytes --]

// SPDX-License-Identifier: GPL-2.0
/*
 * genrace.c - API-1 (gen_pool_destroy() lacks an RCU grace period) driver.
 *
 * Audit case: drivers/usb/host manual review 2026-10-29, API-1.
 *
 *   gen_pool_add_owner():  list_add_rcu(&chunk->next_chunk, &pool->chunks)
 *   readers:               rcu_read_lock(); list_for_each_entry_rcu(...)
 *   gen_pool_destroy():    list_del(&chunk->next_chunk); vfree(chunk)
 *                          [NO synchronize_rcu() anywhere]
 *
 * A destroy that overlaps any in-flight RCU reader is a use-after-free on
 * the vfree()d chunk. The USB host callers (ohci-sm501/sa1111 via devm) are
 * serialized by teardown order, so this verifies the API-level mechanism
 * itself with a minimal in-tree-API-only reproducer:
 *
 *   ctl mode: a kprobe stalls one reader inside gen_pool_virt_to_phys()
 *             (i.e. inside its RCU critical section); main thread destroys
 *             the pool; the stall expires and the traversal continues on
 *             the freed chunk -> deterministic KASAN vmalloc-UAF.
 *   nat mode: reader threads loop rcu_read_lock+lookup while a builder
 *             thread destroys/rebuilds the pool, no instrumentation.
 */
#define pr_fmt(fmt) "GENRACE: " fmt

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/kprobes.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/genalloc.h>
#include <linux/vmalloc.h>
#include <linux/mm.h>
#include <linux/atomic.h>
#include <linux/completion.h>

static char *mode = "ctl";
module_param(mode, charp, 0600);
MODULE_PARM_DESC(mode, "ctl=controlled kprobe stall, nat=natural hammer");

static int stall_us = 300000;
module_param(stall_us, int, 0600);

static int nat_secs = 20;
module_param(nat_secs, int, 0600);

static int nat_readers = 4;
module_param(nat_readers, int, 0600);

#define POOL_ORDER 4
#define CHUNK_PAGES 4
static struct gen_pool *g_pool;
static void *g_va;
static phys_addr_t g_pa;
static size_t g_size = CHUNK_PAGES * PAGE_SIZE;

static atomic_t stall_hits = ATOMIC_INIT(0);
static atomic_t stall_seen = ATOMIC_INIT(0);
static atomic_t uaf_reads = ATOMIC_INIT(0);

/* ---- controlled-mode kprobe: stall a reader mid-RCU-critical-section ---- */
static int vtp_pre(struct kprobe *p, struct pt_regs *regs)
{
	int h;

	h = atomic_inc_return(&stall_hits);
	if (h != 1)
		return 0;
	atomic_set(&stall_seen, 1);
	pr_warn("stalling reader inside gen_pool_virt_to_phys (RCU section) for %d us\n",
		stall_us);
	{
		u64 target = ktime_get_mono_fast_ns() + (u64)stall_us * 1000;

		while (ktime_get_mono_fast_ns() < target)
			cpu_relax();
	}
	atomic_set(&stall_seen, 2);
	return 0;
}

static struct kprobe kp_vtp = {
	.symbol_name = "gen_pool_virt_to_phys",
	.pre_handler = vtp_pre,
};

static struct completion reader_done;
static struct task_struct *reader_th;

static int reader_once(void *unused)
{
	unsigned long a = (unsigned long)g_va + 64;
	int i;

	rcu_read_lock();
	for (i = 0; i < 64 && !kthread_should_stop(); i++) {
		phys_addr_t pa = gen_pool_virt_to_phys(g_pool, a + i * 16);

		(void)pa;
		if (i == 8)
			pr_info("reader: mid-loop (this lookup may be the stalled one)\n");
	}
	atomic_inc(&uaf_reads);	/* survived; reads may have been on freed mem */
	rcu_read_unlock();
	complete(&reader_done);
	return 0;
}

static int pool_build(void)
{
	g_pool = gen_pool_create(POOL_ORDER, -1);
	if (!g_pool)
		return -ENOMEM;
	g_va = vmalloc(g_size);
	if (!g_va) {
		gen_pool_destroy(g_pool);
		return -ENOMEM;
	}
	/* phys address is bookkeeping only for this test; any value works */
	g_pa = 0x200000000ULL;
	return gen_pool_add_virt(g_pool, (unsigned long)g_va, g_pa, g_size, -1);
}

static void pool_teardown(void)
{
	gen_pool_destroy(g_pool);
	if (g_va)
		vfree(g_va);
	g_va = NULL;
}

static int nat_reader(void *unused)
{
	unsigned long a;

	while (!kthread_should_stop()) {
		a = (unsigned long)g_va + 64;
		rcu_read_lock();
		/* loop to stretch the RCU critical section */
		for (int i = 0; i < 32; i++)
			(void)gen_pool_virt_to_phys(g_pool, a + i * 16);
		rcu_read_unlock();
	}
	return 0;
}

static int nat_builder(void *unused)
{
	int cycles = 0;

	while (!kthread_should_stop()) {
		msleep(20);
		if (!g_pool)
			continue;
		gen_pool_destroy(g_pool);	/* vfree chunks, no grace period */
		g_pool = gen_pool_create(POOL_ORDER, -1);
		if (!g_pool)
			break;
		if (gen_pool_add_virt(g_pool, (unsigned long)g_va, g_pa,
				      g_size, -1)) {
			gen_pool_destroy(g_pool);
			g_pool = NULL;
			break;
		}
		cycles++;
	}
	pr_info("nat builder cycles=%d\n", cycles);
	return 0;
}

static int __init genrace_init(void)
{
	int ret;

	ret = pool_build();
	if (ret) {
		pr_err("pool build failed %d\n", ret);
		return ret;
	}
	pr_info("pool=%px va=%px pa=%pa size=%zu\n",
		g_pool, g_va, &g_pa, g_size);

	if (!strcmp(mode, "ctl")) {
		ret = register_kprobe(&kp_vtp);
		if (ret) {
			pr_err("register_kprobe(gen_pool_virt_to_phys) failed %d\n",
			       ret);
			pool_teardown();
			return ret;
		}
		init_completion(&reader_done);
		/* pin the reader away from CPU0: the main thread busy-waits */
		reader_th = kthread_create(reader_once, NULL, "genrd");
		if (IS_ERR(reader_th)) {
			unregister_kprobe(&kp_vtp);
			pool_teardown();
			return PTR_ERR(reader_th);
		}
		kthread_bind(reader_th, 1);
		wake_up_process(reader_th);
		/* wait until the reader is parked inside the stall */
		while (atomic_read(&stall_seen) != 1)
			cpu_relax();
		pr_warn("reader stalled; destroying pool NOW (no grace period expected)\n");
		gen_pool_destroy(g_pool);	/* chunks vfree'd under the reader */
		g_pool = NULL;
		/* reader resumes on freed chunks here */
		wait_for_completion(&reader_done);
		kthread_stop(reader_th);
		unregister_kprobe(&kp_vtp);
		pr_info("ctl done; watch for KASAN vmalloc-use-after-free above\n");
		if (g_va)
			vfree(g_va);
	} else {
		struct task_struct *b;
		struct task_struct *r[8];
		int i;

		for (i = 0; i < nat_readers && i < 8; i++)
			r[i] = kthread_run(nat_reader, NULL, "genrd%d", i);
		b = kthread_run(nat_builder, NULL, "genbd");
		msleep(nat_secs * 1000);
		for (i = 0; i < nat_readers && i < 8; i++)
			kthread_stop(r[i]);
		kthread_stop(b);
		pr_info("nat done\n");
		pool_teardown();
	}
	return 0;
}

static void __exit genrace_exit(void)
{
	pr_info("removed\n");
}

module_init(genrace_init);
module_exit(genrace_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("API-1 gen_pool_destroy RCU-grace-period gap reproducer");

                 reply	other threads:[~2026-10-30 11:47 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

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=4aae234b.1ae67.1a18ca32d72.Coremail.xuhx@hust.edu.cn \
    --to=xuhx@hust.edu.cn \
    --cc=akpm@linux-foundation.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-usb@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®