Re: [PATCH 0/6] alpha: fix stale TLB translations breaking copy-on-write and writeback
Matt Turner <[email protected]>
| Newsgroups | gmane.linux.ports.alpha,gmane.linux.kernel |
|---|---|
| Message-ID | <CAEdQ38F_pP1a2ZC3DKSWx5mdEjdoPWc92ojXvaO61tMyBLLjRw@mail.gmail.com> |
On Sun, Aug 9, 2026 at 4:55 AM Magnus Lindholm <[email protected]> wrote: > > On Alpha, stale TLB translations can break copy-on-write and shared-mapping > writeback: a multi-threaded process can lose stores to its own private > memory, read data belonging to its own child, and lose data written through > a shared file mapping. The copy-on-write failures need more than one CPU; > the writeback failure also happens on a uniprocessor. > > Three related problems are fixed here. > > Patch 1 is stranded deferred-ASN bookkeeping. check_mmu_context() clears > asn_lock and acts on need_new_asn, but it runs only as the tail of > switch_to(), after alpha_switch_to() returns. A newly forked task never > gets there: its first context switch resumes at ret_from_fork instead. > asn_lock is left set and the task goes on to run user space with it set and > interrupts enabled, so a shootdown IPI arriving in that window takes the > deferred path, and the need_new_asn handshake meant to cover it never runs. > finish_task_switch() calls finish_arch_post_lock_switch() with preemption > disabled, on the CPU that ran switch_mm(), which is where that bookkeeping > can be completed. > > kthread_use_mm() and sched_force_init_mm() also reach the same hook, > outside the scheduler's preemption-disabled switch tail. > check_mmu_context() acts on per-CPU state, so it can only complete this > bookkeeping while still on the CPU that ran switch_mm(). The hook > therefore tests preemptible() directly: where migration is possible it > does nothing, while where preemption remains disabled, or is not > configured, completing the bookkeeping is safe. > > The second problem is a targeted tbi() issued against the wrong context. > tbi() acts on the address space context currently loaded on a CPU, so it > reaches an mm's translations only when a thread of that mm is current. > current->active_mm is not sufficient: under lazy TLB an idle or kernel task > keeps an mm as its active_mm while a different ASN is loaded, so the > invalidate hits the wrong context, and nothing retires the old ASN - > mm->context[cpu] is still valid, so the resuming thread reuses it. Patch 2 > fixes the shootdown IPI handler, patch 3 the local side of > flush_tlb_page(), patch 4 the uniprocessor flush_tlb_page(). > > The third is an omitted caller-side invalidate, covered by patches 3, 5 > and 6: when the target mm is not the calling CPU's active_mm, nothing > invalidates that CPU at all, because smp_call_function() does not call back > into the caller. The UP flush_tlb_mm() and flush_icache_user_page() already > contain exactly the missing branch. Their active_mm tests are left alone: > both load a fresh context through __load_new_mm_context() rather than a > targeted tbi() against whatever ASN happened to be loaded, and only the > targeted tbi() depends on which context is loaded. > > No user-space data race is involved in the reproducers: every slot is > written and read by one thread only, and the main thread inspects them only > after joining the workers. The other writes come from forked children with > their own address space, so observing one of those in the parent is the bug. > > Deferred-path behaviour after the fixes, counted with the counters reset > before each 6-second run of the lost-store reproducer: > > lost stores runs entering the window > no fixes 11 of 40 13 of 40, 11 of them lost > patched 0 of 400 15 of 400, none lost > > Where the remaining patches are reached, counted the same way: > > thread of mm lazily > current borrowing > writeback of a shared mapping 51577 50688 > reclaim under memory pressure 17609 14827 > anonymous COW / fork 144237 0 > > About half the calls during writeback, and none at all on anonymous > memory, which is why patches 1 and 2 did not cover it. flush_tlb_mm() was > entered with the mm not this CPU's active_mm 2934 times over a fork-heavy > run and 632 times while otherwise idle. > > Reproducing it. Two self-contained tests were written for this. Source for > both can be made available on request. > > alpha-cow-smoketest.c covers patches 1 and 2 (pthreads only, ~40s, needs > more than one CPU; confined to one with taskset -c 0 it does not fail). A > failing run reports: > > stale read after COW fault FAIL > thread 2 read 0xdeadbeefcafebabe, expected 0x1000002 <- the CHILD's value > > Two details in it matter, because getting either wrong hides the bug: > slots are 128 bytes apart so several threads share a page, and a thread > writes its slot once then reads it many times, since a thread that keeps > writing refreshes its own translation. Its lost-stores check fires in at > best a quarter of runs; the stale-read check is the reliable one. > > mkclean4.c covers patches 3 and 4 (needs root), and fails on both SMP and > UP. A single thread writes a small MAP_SHARED file while background > writeback cleans it, and the file is then compared against the mapping; > every round loses data. It forces two conditions that are rare in normal > operation on SMP: the flusher kworker and the writer on the same CPU, and > a working set small enough to stay resident in the data TLB. That is also > why this is hard to hit in the field - any faulting write from any CPU > repairs the dirty state. On a uniprocessor the first condition holds by > construction, so no pinning is needed there. > > Patches 5 and 6 have no reproducer for the bug they fix; they are > justified by the contract of the functions, by the UP implementations > already having the missing branch, and by the counts above. Patch 6's > path was exercised for regressions by driving gdb to set and clear a > breakpoint several hundred times, reaching copy_to_user_page() -> > flush_icache_user_page(). > > Originally found as intermittent heap corruption in glibc's > malloc/tst-malloc-fork-deadlock-malloc-check. glibc is not at fault: with > MALLOC_CHECK_=3 it is simply a very effective detector, and every fork() > runs __malloc_fork_unlock_child() in the child, which writes to allocator > state. > > Testing. ES40, EV68AL (21264C) Tsunami, 3 CPUs. Generated against > v7.2-rc6; the results below are from v7.2-rc2, and the series was > re-tested on a clean v7.2-rc1 tree with the same outcome. > > before after > smoke test, stale-read check 7 of 9 rounds 0 of 9 > lost stores 11 of 40 0 of 400 > writeback (mkclean4), SMP 10 of 10 0 of 10 > writeback (mkclean4), UP every round 0 of 8 > tst-malloc-fork-deadlock- > malloc-check 8 of 10 fail 25/25 pass > ptrace breakpoint exerciser - result matches > > Also 10/10 pass each for tst-malloc-fork-deadlock, tst-malloc-check, > tst-tcfree1-malloc-check and tst-tcfree2-malloc-check. No measurable > cost: 1470/1038/206 forks per second with 0/2/8 sibling threads against > 1472/1043/199 unpatched. Patch 5 changes a function none of the > reproducers exercise, so it is covered for regressions only. > > Also run with CONFIG_COMPACTION and CONFIG_MIGRATION enabled, with > compaction forced continuously underneath the tests: 368522 folios > migrated during the run, no failures. > > Also built and tested with CONFIG_ALPHA_GENERIC and CONFIG_SMP=n. That is > how patch 4 was found: arch/alpha/kernel/smp.c is not built there, so > patches 2, 3, 5 and 6 are absent and patch 1 is inert, and mkclean4.c > failed on every round because the uniprocessor flush_tlb_page() carries > the same defect. With patch 4 it passes 8 of 8, and the rest of the tests > above pass there too. > > Magnus Lindholm (6): > alpha: run check_mmu_context() from finish_arch_post_lock_switch() > alpha: only use a targeted tbi() when the target mm is really current > alpha: fix the local TLB invalidate in flush_tlb_page() > alpha: only use a targeted tbi() when the target mm is really current > (UP) > alpha: invalidate the local context in flush_tlb_mm() > alpha: invalidate the local context in flush_icache_user_page() > > arch/alpha/include/asm/mmu_context.h | 28 +++++++++++++++++++ > arch/alpha/include/asm/tlbflush.h | 9 +++++- > arch/alpha/kernel/smp.c | 41 ++++++++++++++++++++++++++-- > 3 files changed, 75 insertions(+), 3 deletions(-) > > -- > 2.53.0 Nice work! I think we should have a Cc: [email protected] on all of these commits. I'll send comments in reply to the patches individually.