Skip to content

page_cache_ext: fix use-after-free in list sampling + a latent lock-pairing hazard - #6

Open
anadav wants to merge 2 commits into
cache-ext:cache_extfrom
anadav:fix/sample-scoring-uaf
Open

page_cache_ext: fix use-after-free in list sampling + a latent lock-pairing hazard#6
anadav wants to merge 2 commits into
cache-ext:cache_extfrom
anadav:fix/sample-scoring-uaf

Conversation

@anadav

@anadav anadav commented Sep 4, 2026

Copy link
Copy Markdown

Two synchronization fixes in mm/page_cache_ext_ds.c, found while porting cache_ext to Linux 6.18 and benchmarking it:

1. Use-after-free in __bpf_cache_ext_list_sample(). The snip-then-score-unlocked optimization races with valid_folios_del(), which kfrees the cache_ext_list_node wrapper after dropping the registry write_lock. Nodes sitting in the per-CPU sample_folios_arr[] can therefore be freed while score_fn() dereferences them; the LIST_POISON check in __putback_list_nodes() catches only some cases and itself reads freed memory. The fix holds the registry write_lock across snip + score + putback (score_fn is non-sleepable, so calling it under rwlock_t is safe).

Honest caveat: this serializes sampling against all list updates and measurably slows sampling-based policies under memory pressure — on our 6.18 port we measured ~30% throughput loss for the LHD policy (YCSB-C on LevelDB, 4 GiB cgroup, victim quality unchanged; the loss is lock serialization in the reclaim path — disk utilization drops from saturated to ~75%). A performance-preserving fix would refcount the node wrapper instead; we took correctness first and want to flag the trade-off to you rather than hide it.

2. Latent lock-pairing hazard in cache_ext_list_iterate_extended(). The lock is chosen by comparing iterate modes against CACHE_EXT_ITERATE_SKIP, but released by comparing against CACHE_EXT_CONTINUE_ITER — a return code from a different enum. This is benign today only because both constants happen to be 0; reordering either enum would silently pair read_lock with write_unlock and corrupt the rwlock. The fix uses the same condition on both sides.

🤖 Generated with Claude Code

anadav and others added 2 commits September 4, 2026 11:37
The snip-then-score-unlocked optimization in __bpf_cache_ext_list_sample()
races with valid_folios_del(): the cache_ext_list_node wrapper is kfreed
after that path drops the registry write_lock, so nodes sitting in the
per-CPU sample_folios_arr[] can be freed while score_fn() dereferences
them.  The LIST_POISON check in __putback_list_nodes() catches only some
of these, and itself reads freed memory.

Hold the registry write_lock across snip + score + putback.  score_fn is
non-sleepable, so calling it under rwlock_t is safe.

Note: this serializes sampling against all list updates and measurably
slows sampling-based policies under memory pressure (we measured ~30%
throughput loss for the LHD policy on YCSB-C/LevelDB with a 4 GiB cgroup,
on a 6.18 port of this code); a perf-preserving fix would refcount the
node wrapper.  Correctness first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed with

cache_ext_list_iterate_extended() chooses read_lock vs write_lock by
comparing the iterate modes against CACHE_EXT_ITERATE_SKIP, but releases
by comparing against CACHE_EXT_CONTINUE_ITER -- an iterate *return code*
from a different enum.  This works today only because both constants
happen to be 0; reordering either enum would silently pair read_lock
with write_unlock and corrupt the rwlock.  Use the same condition on
both sides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant