Re: [PATCH 0/6] alpha: fix stale TLB translations breaking copy-on-write and writeback
From: Matt Turner
Date: Sun Aug 09 2026 - 22:35:51 EST
On Sun, Aug 9, 2026 at 4:55 AM Magnus Lindholm <linmag7@xxxxxxxxx> 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: stable@xxxxxxxxxxxxxxx on all of these commits.
I'll send comments in reply to the patches individually.