* lib/genalloc: gen_pool_destroy() frees chunks with no RCU grace period while every library reader is an RCU reader
@ 2026-10-30 11:47 徐晗翔
0 siblings, 0 replies; only message in thread
From: 徐晗翔 @ 2026-10-30 11:47 UTC (permalink / raw)
To: akpm; +Cc: linux-kernel, linux-usb
[-- 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");
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2026-10-30 11:47 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-30 11:47 lib/genalloc: gen_pool_destroy() frees chunks with no RCU grace period while every library reader is an RCU reader 徐晗翔
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®