mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Yury Norov <ynorov@nvidia.com>
To: Nickolai Zeldovich <nickolai@csail.mit.edu>
Cc: Paul Walmsley <pjw@kernel.org>,
	Palmer Dabbelt <palmer@dabbelt.com>,
	Albert Ou <aou@eecs.berkeley.edu>,
	Alexandre Ghiti <alex@ghiti.fr>,
	Charlie Jenkins <charlie@rivosinc.com>,
	Yury Norov <yury.norov@gmail.com>,
	linux-riscv@lists.infradead.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH v1] riscv: Fix lost icache flush requests in set_icache_stale_mask()
Date: Wed, 28 Oct 2026 11:43:38 -0400	[thread overview]
Message-ID: <auIYKkk_J0lskudq@yury> (raw)
In-Reply-To: <20261013171238.777119-1-nickolai@csail.mit.edu>

On Tue, Oct 13, 2026 at 01:12:37PM -0400, Nickolai Zeldovich wrote:
> set_icache_stale_mask() marks every other hart's icache as stale for the
> current mm by reading the current hart's bit, storing an all-ones mask and
> then writing the current hart's bit back as it was read:
> 
> 	stale_cpu = cpumask_test_cpu(cpu, mask);
> 	cpumask_setall(mask);
> 	__assign_cpu(cpu, mask, stale_cpu);
> 
> mm->context.icache_stale_mask is shared by all harts running the mm and
> is updated concurrently without a lock: flush_icache_mm() sets every bit
> with cpumask_setall() (a plain store) whenever a thread flushes the
> icache, prepare_sync_core_cmd() does the same for
> membarrier(MEMBARRIER_CMD_PRIVATE_EXPEDITED_SYNC_CORE), and
> flush_icache_deferred() clears the current hart's bit with an atomic
> test-and-clear from switch_mm(). Those are safe with each other because
> each of them either only sets bits or atomically clears its own hart's
> bit, and a bit set one time too many only costs a redundant fence.i.
> set_icache_stale_mask() is the one writer that stores a value it read
> earlier: the current hart's bit as returned by cpumask_test_cpu(). If
> another hart sets that bit between the read and the write-back, the
> write-back erases it. The erased bit is a pending deferred icache flush
> for the current hart.
> 
> Consider a process with two threads on different harts:
> 
>   - Thread T1, on hart C, writes new instructions and calls
>     riscv_flush_icache(start, end, SYS_RISCV_FLUSH_ICACHE_LOCAL).
>     flush_icache_mm(mm, true) flushes hart C and only sets the other
>     harts' stale bits, without IPIs: hart A is to flush its icache the
>     next time it switches to this mm.
> 
>   - Thread T2, on hart A, calls prctl(PR_RISCV_SET_ICACHE_FLUSH_CTX,
>     PR_RISCV_CTX_SW_FENCEI_OFF, PR_RISCV_SCOPE_PER_THREAD) at the same
>     time. set_icache_stale_mask() reads hart A's bit as clear, T1's
>     cpumask_setall() sets it, and __assign_cpu() clears it again.
> 
> Hart A then runs another process, and T1 migrates to hart A. switch_mm()
> finds hart A's bit clear and does not flush, so T1 executes the old
> instructions still in hart A's icache. For a JIT this is a wrong result
> or a crash on a mix of old and new code. The guarantee of
> membarrier(SYNC_CORE), which relies on the same bits, is lost the same
> way. The window is the handful of instructions between the load and the
> store, but a thread that calls the prctl while another thread of the
> process flushes its icache hits it often, see the numbers below.
> 
> Set the other harts' bits one at a time with cpumask_set_cpu(), which
> only ever sets a bit, and leave the current hart's bit alone: it already
> holds the value the old code tried to preserve. Commit 6b9391b581fd
> ("riscv: Include riscv_set_icache_flush_ctx prctl") kept the current
> hart's bit to cope with concurrent callers of this function; that is
> preserved. Commit e5bf9a4b68fa ("riscv: switch set_icache_stale_mask()
> to using non-atomic assign_cpu()") noted that the sequence is not atomic
> as a whole; it does not need to be, it only must never clear a bit.
> 
> Reproduced on a StarFive JH7110 (VisionFive 2, non-coherent icache) with
> a test program that runs the sequence above in a loop: T1 executes the
> code on hart A to fill its icache, moves to hart C, rewrites the code and
> flushes with SYS_RISCV_FLUSH_ICACHE_LOCAL while T2 hammers the prctl on
> hart A, lets another process run on hart A, moves back to hart A and
> executes the code again. A result computed from the old code is a stale
> fetch. Each line is 50000 iterations on a pair of harts (A, C):
> 
>   v7.3-rc7 unpatched:               13177 and 14362 stale
>   v7.3-rc7 + 100 us delay in the
>     read-modify-write window:       49993 and 47675 stale
>   v7.3-rc7 + this patch:                0 and     0 stale
>   v7.3-rc7 + this patch + delay:        0 and     0 stale
> 
> Control runs with T2 idle never read stale code on any kernel, so the
> deferred flush itself works; only its request gets lost.
> 
> Found while formally verifying set_icache_stale_mask() against the
> kernel binary.
> 
> Fixes: 6b9391b581fd ("riscv: Include riscv_set_icache_flush_ctx prctl")
> Assisted-by: LLM
> Signed-off-by: Nickolai Zeldovich <nickolai@csail.mit.edu>
> ---
> 
> Notes:
>     The reproducer used for the numbers in the commit message is below (nolibc,
>     build with "make headers_install" done:
>     clang --target=riscv64-linux-gnu -Os -static -nostdlib -nostdinc
>       -I tools/include/nolibc -I usr/include -include nolibc.h
>       -o icache_stale_mask icache_stale_mask.c
>     and run as "icache_stale_mask <hartA> <hartC> 50000" for the race and with a
>     fifth argument for the control run). It needs hardware with a non-coherent
>     icache; QEMU TCG keeps translated code coherent and never shows the symptom.
>     
>     Build checks: riscv defconfig Image with clang, W=1 on cacheflush.o, and
>     allnoconfig (CONFIG_SMP=n, the function is not compiled), all clean.
>     checkpatch --strict reports only the missing Signed-off-by.
>     
>     /*
>      * icache_stale_mask.c: a thread's deferred icache flush is lost when
>      * prctl(PR_RISCV_SET_ICACHE_FLUSH_CTX, PR_RISCV_CTX_SW_FENCEI_OFF, ...) on
>      * one hart races with riscv_flush_icache(SYS_RISCV_FLUSH_ICACHE_LOCAL) on
>      * another hart of the same process.
>      *
>      * Build with the kernel's nolibc (after "make headers_install"):
>      *   clang --target=riscv64-linux-gnu -Os -static -nostdlib -nostdinc \
>      *     -I tools/include/nolibc -I usr/include -include nolibc.h \
>      *     -o icache_stale_mask icache_stale_mask.c
>      * Usage: icache_stale_mask <hartA> <hartC> [iterations] [control]
>      *
>      * Three actors:
>      *   T1 (the main thread): writes new code at P from hart C and calls
>      *      riscv_flush_icache(P, P + 4096, SYS_RISCV_FLUSH_ICACHE_LOCAL), which
>      *      flushes hart C and marks every other hart's icache stale for this mm.
>      *   T2 (a thread of the same process, pinned to hart A): calls the prctl in
>      *      a loop while T1 flushes.  Its set_icache_stale_mask() reads hart A's
>      *      stale bit (clear), then stores the whole mask back with the bit as
>      *      read, erasing the bit T1 set in between.
>      *   Q  (another process, pinned to hart A): runs on hart A after T2 blocks,
>      *      so that T1's arrival on hart A is a switch_mm() into the process,
>      *      the point where the kernel performs the deferred flush iff the bit
>      *      is set.
>      * Each iteration: T1 runs the old code on hart A (hart A's icache now holds
>      * it), moves to hart C, writes new code, flushes LOCAL while T2 hammers the
>      * prctl, lets Q run on hart A, moves back to hart A and runs the code.  A
>      * result computed from the old code is a stale fetch.  With "control" T2
>      * never runs: every iteration must then read the new code.
>      */
>     #define N 1000	/* li a0,0; N x addi a0,a0,v; ret: returns N*v */
>     #define PAGE 4096
>     
>     #ifndef SYS_RISCV_FLUSH_ICACHE_LOCAL
>     #define SYS_RISCV_FLUSH_ICACHE_LOCAL 1
>     #endif
>     #ifndef __NR_riscv_flush_icache
>     #define __NR_riscv_flush_icache 259
>     #endif
>     #ifndef PR_RISCV_SET_ICACHE_FLUSH_CTX
>     #define PR_RISCV_SET_ICACHE_FLUSH_CTX 71
>     #define PR_RISCV_CTX_SW_FENCEI_ON 0
>     #define PR_RISCV_CTX_SW_FENCEI_OFF 1
>     #define PR_RISCV_SCOPE_PER_PROCESS 0
>     #define PR_RISCV_SCOPE_PER_THREAD 1
>     #endif
>     
>     static unsigned int *rw;	/* the code page, writable mapping */
>     static unsigned int *rx;	/* the same page, executable mapping */
>     static volatile int go;		/* T2 hammers the prctl while set */
>     static int t1_to_t2[2], t2_to_t1[2], t1_to_q[2], q_to_t1[2];
>     static int hart_a, hart_c;
>     static char t2_stack[65536] __attribute__((aligned(16)));
>     
>     /* clone a thread (shared mm) running fn(arg) on the given stack */
>     long __clone_thread(long flags, void *stack_top, int (*fn)(void *), void *arg);
>     asm(".text\n"
>         ".globl __clone_thread\n"
>         "__clone_thread:\n"
>         "	addi	a1, a1, -16\n"
>         "	sd	a2, 0(a1)\n"		/* fn  */
>         "	sd	a3, 8(a1)\n"		/* arg */
>         "	li	a2, 0\n"
>         "	li	a3, 0\n"
>         "	li	a4, 0\n"
>         "	li	a7, 220\n"		/* __NR_clone */
>         "	ecall\n"
>         "	bnez	a0, 1f\n"
>         "	ld	a2, 0(sp)\n"
>         "	ld	a0, 8(sp)\n"
>         "	jalr	a2\n"
>         "	li	a7, 93\n"		/* __NR_exit */
>         "	ecall\n"
>         "1:	ret\n");
>     
>     static void pin(int cpu)
>     {
>     	unsigned long mask = 1UL << cpu;
>     
>     	if (syscall(__NR_sched_setaffinity, 0, sizeof(mask), &mask))
>     		printf("sched_setaffinity(%d) failed\n", cpu);
>     }
>     
>     static void write_text(unsigned int v)
>     {
>     	int i;
>     
>     	rw[0] = 0x00000513;
>     	for (i = 1; i <= N; i++)
>     		rw[i] = 0x00050513 | (v << 20);
>     	rw[N + 1] = 0x00008067;
>     }
>     
>     static long run_text(void)
>     {
>     	return ((long (*)(void))rx)();
>     }
>     
>     static int t2_main(void *arg)
>     {
>     	char c;
>     	long r;
>     
>     	pin(hart_a);
>     	while (read(t1_to_t2[0], &c, 1) == 1) {
>     		write(t2_to_t1[1], "", 1);	/* about to hammer the prctl */
>     		while (go) {
>     			r = syscall(__NR_prctl, PR_RISCV_SET_ICACHE_FLUSH_CTX,
>     				    PR_RISCV_CTX_SW_FENCEI_OFF,
>     				    PR_RISCV_SCOPE_PER_THREAD, 0, 0);
>     			if (r) {
>     				printf("prctl failed: %ld\n", r);
>     				break;
>     			}
>     		}
>     		write(t2_to_t1[1], "", 1);
>     	}
>     	return 0;
>     }
>     
>     int main(int argc, char **argv)
>     {
>     	int iters = argc > 3 ? atoi(argv[3]) : 1000;
>     	int control = argc > 4;
>     	int i, fd, stale = 0, old_only = 0, bad = 0;
>     	long got;
>     	char c;
>     
>     	if (argc < 3) {
>     		printf("usage: %s <hartA> <hartC> [iterations] [control]\n", argv[0]);
>     		return 2;
>     	}
>     	hart_a = atoi(argv[1]);
>     	hart_c = atoi(argv[2]);
>     
>     	fd = memfd_create("text", 0);
>     	ftruncate(fd, PAGE);
>     	rw = mmap(NULL, PAGE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
>     	rx = mmap(NULL, PAGE, PROT_READ | PROT_EXEC, MAP_SHARED, fd, 0);
>     	pipe(t1_to_t2);
>     	pipe(t2_to_t1);
>     	pipe(t1_to_q);
>     	pipe(q_to_t1);
>     
>     	if (!fork()) {			/* Q: another mm, pinned to hart A */
>     		close(t1_to_q[1]);
>     		close(t1_to_t2[1]);
>     		pin(hart_a);
>     		while (read(t1_to_q[0], &c, 1) == 1)
>     			write(q_to_t1[1], "", 1);
>     		_exit(0);
>     	}
>     	__clone_thread(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND |
>     		       CLONE_THREAD | CLONE_SYSVSEM,
>     		       t2_stack + sizeof(t2_stack), t2_main, NULL);
>     
>     	for (i = 0; i < iters; i++) {
>     		unsigned int old = 1 + (i & 1), new = 2 - (i & 1);
>     
>     		pin(hart_a);
>     		write_text(old);
>     		syscall(__NR_riscv_flush_icache, 0, 0, 0);	/* all harts */
>     		got = run_text();		/* hart A caches the old code */
>     		if (got != (long)N * old) {
>     			bad++;
>     			continue;
>     		}
>     
>     		pin(hart_c);
>     		if (!control) {
>     			go = 1;
>     			write(t1_to_t2[1], "", 1);	/* T2 hammers the prctl on A */
>     			read(t2_to_t1[0], &c, 1);	/* ... from now on */
>     		}
>     		write_text(new);
>     		/* flush hart C now, mark hart A's icache stale for this mm */
>     		syscall(__NR_riscv_flush_icache, rx, (char *)rx + PAGE,
>     			SYS_RISCV_FLUSH_ICACHE_LOCAL);
>     		if (!control) {
>     			go = 0;
>     			read(t2_to_t1[0], &c, 1);	/* T2 stops and blocks */
>     		}
>     		write(t1_to_q[1], "", 1);	/* Q runs on hart A: A leaves our mm */
>     		read(q_to_t1[0], &c, 1);
>     
>     		pin(hart_a);			/* switch_mm into our mm on hart A */
>     		got = run_text();
>     		if (got != (long)N * new) {
>     			stale++;
>     			if (got == (long)N * old)
>     				old_only++;
>     		}
>     	}
>     	close(t1_to_t2[1]);
>     	close(t1_to_q[1]);
>     	wait(NULL);
>     	printf("harts A=%d C=%d %s: %d of %d iterations ran stale code (%d entirely old, %d warm-up failures)\n",
>     	       hart_a, hart_c, control ? "control" : "race", stale, iters,
>     	       old_only, bad);
>     	return !!stale;
>     }
> 
>  arch/riscv/mm/cacheflush.c | 17 ++++++++---------
>  1 file changed, 8 insertions(+), 9 deletions(-)
> 
> diff --git a/arch/riscv/mm/cacheflush.c b/arch/riscv/mm/cacheflush.c
> index f8ead7cb7c7d6..22d6520a05417 100644
> --- a/arch/riscv/mm/cacheflush.c
> +++ b/arch/riscv/mm/cacheflush.c
> @@ -180,20 +180,19 @@ void __init riscv_init_cbo_blocksizes(void)
>  static void set_icache_stale_mask(void)
>  {
>  	int cpu = get_cpu();
> +	unsigned int other;
>  	cpumask_t *mask;
> -	bool stale_cpu;
>  
>  	/*
> -	 * Mark every other hart's icache as needing a flush for
> -	 * this MM. Maintain the previous value of the current
> -	 * cpu to handle the case when this function is called
> -	 * concurrently on different harts.
> +	 * Mark every other hart's icache as needing a flush for this MM.
> +	 * Set the bits one at a time: flush_icache_mm() and
> +	 * flush_icache_deferred() update this mask concurrently, and a
> +	 * read-modify-write of the whole mask would discard their updates.
>  	 */
>  	mask = &current->mm->context.icache_stale_mask;
> -	stale_cpu = cpumask_test_cpu(cpu, mask);
> -
> -	cpumask_setall(mask);
> -	__assign_cpu(cpu, mask, stale_cpu);
> +	for_each_possible_cpu(other)
> +		if (other != cpu)
> +			cpumask_set_cpu(other, mask);

Not judging the motivation, but the implementation should avoid the
for-loop:

        
	cpumask_setall(mask);
        __cpumask_clear_cpu(cpu);

Or I misunderstood your intention?

Thanks,
Yury

>  	put_cpu();
>  }
>  #endif
> 
> base-commit: 24a19b953e2a3d0ea5a8c4951a0ab8f32720c907
> -- 
> 2.56.0

  reply	other threads:[~2026-10-28 15:43 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-13 17:12 Nickolai Zeldovich
2026-10-28 15:43 ` Yury Norov [this message]
2026-10-28 17:54   ` Nickolai Zeldovich

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=auIYKkk_J0lskudq@yury \
    --to=ynorov@nvidia.com \
    --cc=alex@ghiti.fr \
    --cc=aou@eecs.berkeley.edu \
    --cc=charlie@rivosinc.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-riscv@lists.infradead.org \
    --cc=nickolai@csail.mit.edu \
    --cc=palmer@dabbelt.com \
    --cc=pjw@kernel.org \
    --cc=yury.norov@gmail.com \
    /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®