From: Andy Chiu <tchiu@tenstorrent.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>, Andy Chiu <andybnac@gmail.com>,
Deepak Gupta <debug@rivosinc.com>,
linux-riscv@lists.infradead.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH v1] riscv: signal: mark the FP state clean before restoring it from the frame
Date: Fri, 30 Oct 2026 16:44:59 -0500 [thread overview]
Message-ID: <auUP2yrVl7lwkNkC@Tao-Chiu's-Mac> (raw)
In-Reply-To: <20261016160708.790397-1-nickolai@csail.mit.edu>
Hi Nickolai,
On Fri, Oct 16, 2026 at 12:07:08PM -0400, Nickolai Zeldovich wrote:
> restore_fp_state() copies the FP image of the signal frame into
> current->thread.fstate with __copy_from_user() and then calls
> fstate_restore(), which loads thread.fstate into the FP registers when
> regs->status.FS is not Off and marks the state Clean.
>
> regs->status is still the sstatus saved when the signal handler entered
> the kernel for rt_sigreturn(); restore_sigcontext() only overwrites the
> general registers. If the handler wrote any FP register, FS is Dirty.
> rt_sigreturn() runs with interrupts and preemption enabled, and the copy
> sleeps when the frame page is not resident. A context switch anywhere
> between the first store of the copy and fstate_restore() goes through
> __switch_to_fpu(), whose fstate_save() sees FS == Dirty and stores the
> live FP registers, which hold the handler's values, over the image that
> was just copied from the frame:
>
> rt_sigreturn(), FS == Dirty scheduler
> ---------------------------------------- -----------------------------
> restore_fp_state()
> __copy_from_user(&thread.fstate, ...)
> <preempted, or sleeping on a fault>
> __switch_to_fpu(prev = task)
> fstate_save(): FS == Dirty,
> live f0-f31, fcsr ->
> thread.fstate; FS = Clean
> ...
> __switch_to_fpu(next = task)
> fstate_restore():
> thread.fstate -> live
> <copy completes>
> fstate_restore(): thread.fstate -> live registers
>
> The interrupted user code resumes with the signal handler's FP registers
> (the handler's values in the part of the image copied before the switch
> and its own in the rest) instead of the ones it had when the signal
> arrived.
>
> Any program whose signal handler executes a floating-point instruction
> is exposed: when the handler returns while a reschedule is pending, or
> when the signal frame has been paged out in the meantime, the
> interrupted code silently continues with corrupted f0-f31 and fcsr.
> With tick preemption the window is the 264-byte copy, so the corruption
> is rare and shows up as sporadic wrong floating-point results: a test
> program that loads a pattern into f0-f31, sends itself SIGUSR1 and
> compares the registers afterwards, with a busy thread on the same CPU,
> saw the handler's values in 110 of 200000 sigreturns on a CONFIG_PREEMPT
> kernel under QEMU. The corruption is deterministic when the copy sleeps:
> the same program with the frame on a userfaultfd-registered page that
> the handler drops, so that the copy faults 128 bytes into the image,
> gets the handler's values back in f0-f15 on every sigreturn.
>
> The copy-then-restore order dates from the initial port, commit
> e2c0cdfba7f6 ("RISC-V: User-facing API"), so every kernel since v4.15
> is affected. The vector state had the same problem in __restore_v_state()
> and commit c27fa53b858b ("riscv: Fix vector state restore in
> rt_sigreturn()") fixed it by marking the live vector state clean before
> the copy.
>
> Do the same for the FP state: mark it Clean before the copy when it is
> not Off. The live registers belong to the handler and are discarded by
> sigreturn anyway; fstate_save() then skips them on a context switch, and
> fstate_restore() still loads the image, both when the task is switched
> back in and at the end of restore_fp_state(). With the change the test
> program sees the correct registers in 20 of 20 userfaultfd runs and in
> 200000 of 200000 preemption runs.
>
> Found while formally verifying rt_sigreturn() against the kernel binary.
>
> Fixes: e2c0cdfba7f6 ("RISC-V: User-facing API")
> Assisted-by: LLM
> Signed-off-by: Nickolai Zeldovich <nickolai@csail.mit.edu>
> ---
>
> Notes:
> The pending "riscv: signal: protect regs->status RMW from concurrent
> preemption" (Guobin Zhang, 2026-08-07, with follow-ups on 2026-09-25 and
> 2026-10-08) addresses a different problem in the same functions: a lost
> update on regs->status when the read-modify-write in __fstate_clean() is
> preempted and the scheduler writes the VS bits in between. It keeps the
> copy-then-restore order in restore_fp_state(), so it does not cover this
> bug. The two patches touch different hunks and apply in either order; the
> preempt_disable() it puts around __fstate_clean() would cover the new call
> as well.
>
> The guard mirrors fstate_restore(): with FS == Off nothing is restored
> today and the patch keeps that. A context switch before the first store
> of the copy is harmless with or without the patch, because fstate_save()
> then runs before the image is written; the riscv scalar usercopy stores
> in 64-byte strides, which is why the deterministic reproducer puts the
> page boundary 128 bytes into the image.
>
> The deterministic reproducer, as run (exit 1 before the patch with f0-f7 and
> f10-f15 reported, exit 0 after):
>
> // Reproducer: riscv64 rt_sigreturn() loses the FP state it restores when the copy of the
> // FP image sleeps (here: a userfaultfd page fault) while the handler left sstatus.FS dirty.
> // Build: riscv64-linux-gnu-gcc -static -O2 -pthread -o fpstate_sigreturn fpstate_sigreturn.c
> // Run as root (userfaultfd) on a preemptible kernel with CONFIG_FPU and CONFIG_USERFAULTFD.
> // Exit 1: FP registers corrupted after the signal; 0: clean; 2: could not set up.
> #define _GNU_SOURCE
> #include <fcntl.h>
> #include <pthread.h>
> #include <signal.h>
> #include <stdint.h>
> #include <stdio.h>
> #include <string.h>
> #include <sys/ioctl.h>
> #include <sys/mman.h>
> #include <sys/syscall.h>
> #include <ucontext.h>
> #include <unistd.h>
> #include <linux/userfaultfd.h>
>
> #define PAGE 4096UL
> #define SPLIT 128 /* page boundary this far into the 264-byte FP image */
> #define F(op, i, r) #op " f" #i ", " #i "*8(%[" #r "])\n"
> #define ALL(op, r) F(op, 0, r) F(op, 1, r) F(op, 2, r) F(op, 3, r) F(op, 4, r) F(op, 5, r) \
> F(op, 6, r) F(op, 7, r) F(op, 8, r) F(op, 9, r) F(op, 10, r) F(op, 11, r) \
> F(op, 12, r) F(op, 13, r) F(op, 14, r) F(op, 15, r)
> #define CLOBBERS "memory", "ft0", "ft1", "ft2", "ft3", "ft4", "ft5", "ft6", "ft7", \
> "fs0", "fs1", "fa0", "fa1", "fa2", "fa3", "fa4", "fa5"
>
> static uint64_t pat[16], hpat[16], out[16];
> static unsigned char saved[PAGE] __attribute__((aligned(PAGE)));
> static volatile int placed;
>
> static void handler(int sig, siginfo_t *si, void *ctx)
> {
> unsigned long hi = (unsigned long)&((ucontext_t *)ctx)->uc_mcontext.__fpregs + SPLIT;
>
> placed = !(hi & (PAGE - 1));
> if (placed) { /* drop the page holding bytes SPLIT.. of the FP image */
> memcpy(saved, (void *)hi, PAGE);
> if (madvise((void *)hi, PAGE, MADV_DONTNEED))
> _exit(2);
> }
> /* leave FS dirty, with the handler's values in the registers */
> asm volatile(ALL(fld, h) "csrw fcsr, zero\n" : : [h]"r"(hpat) : CLOBBERS);
> }
>
> static void *serve(void *arg) /* repopulate the dropped page when rt_sigreturn faults on it */
> {
> struct uffdio_copy cp = { .src = (unsigned long)saved, .len = PAGE };
> struct uffd_msg msg;
> int fd = (int)(long)arg;
>
> while (read(fd, &msg, sizeof(msg)) == sizeof(msg)) {
> cp.dst = msg.arg.pagefault.address & ~(PAGE - 1);
> cp.copy = 0;
> ioctl(fd, UFFDIO_COPY, &cp);
> }
> return NULL;
> }
>
> int main(void)
> {
> struct sigaction sa = { .sa_sigaction = handler, .sa_flags = SA_SIGINFO | SA_ONSTACK };
> struct uffdio_api api = { .api = UFFD_API };
> struct uffdio_register reg = { .mode = UFFDIO_REGISTER_MODE_MISSING };
> stack_t ss = { .ss_size = 65536 };
> long pid = getpid(), tid = syscall(__NR_gettid);
> unsigned long fcsr = 0, shift;
> pthread_t th;
> int fd, i, bad = 0;
>
> for (i = 0; i < 16; i++) {
> pat[i] = 0x4010000000000000ULL + i;
> hpat[i] = 0xbff0000000000000ULL + i;
> }
> ss.ss_sp = mmap(NULL, ss.ss_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> memset(ss.ss_sp, 0, ss.ss_size); /* populate: only the dropped page will fault */
> reg.range.start = (unsigned long)ss.ss_sp;
> reg.range.len = ss.ss_size;
> fd = syscall(__NR_userfaultfd, O_CLOEXEC);
> if (fd < 0 || ioctl(fd, UFFDIO_API, &api) || ioctl(fd, UFFDIO_REGISTER, ®)) {
> perror("userfaultfd");
> return 2;
> }
> pthread_create(&th, NULL, serve, (void *)(long)fd);
> sigaction(SIGUSR1, &sa, NULL);
>
> /* move the frame down 16 bytes at a time until the FP image crosses a page at SPLIT */
> for (shift = 0; shift < PAGE && !placed; shift += 16) {
> ss.ss_size = 65536 - shift;
> sigaltstack(&ss, NULL);
> register long a0 asm("a0") = pid, a1 asm("a1") = tid;
> register long a2 asm("a2") = SIGUSR1, a7 asm("a7") = __NR_tgkill;
> fcsr = 0x1f;
> /* load the pattern, send ourselves SIGUSR1, store what the sigreturn left behind */
> asm volatile(ALL(fld, in) "csrw fcsr, %[fc]\n ecall\n csrr %[fc], fcsr\n" ALL(fsd, o)
> : "+r"(a0), [fc]"+r"(fcsr)
> : "r"(a1), "r"(a2), "r"(a7), [in]"r"(pat), [o]"r"(out) : CLOBBERS);
> }
> if (!placed)
> return 2;
> for (i = 0; i < 16; i++) { /* f8/f9 are callee-saved: the handler's epilogue reloads them */
> if (i == 8 || i == 9 || out[i] == pat[i])
> continue;
> printf("f%d = %016lx, expected %016lx\n", i, out[i], pat[i]);
> bad++;
> }
> if (fcsr != 0x1f) {
> printf("fcsr = %lx, expected 1f\n", fcsr);
> bad++;
> }
> printf("%s\n", bad ? "FAIL: FP state corrupted across rt_sigreturn" : "PASS");
> return bad ? 1 : 0;
> }
>
I would prefer adding this into riscv selftest just to keep future
developments intact.
> arch/riscv/kernel/signal.c | 7 +++++++
> 1 file changed, 7 insertions(+)
>
> diff --git a/arch/riscv/kernel/signal.c b/arch/riscv/kernel/signal.c
> index 59784dc117e4..3058c89470e8 100644
> --- a/arch/riscv/kernel/signal.c
> +++ b/arch/riscv/kernel/signal.c
> @@ -47,6 +47,13 @@ static long restore_fp_state(struct pt_regs *regs,
> long err;
> struct __riscv_d_ext_state __user *state = &sc_fpregs->d;
>
> + /*
> + * Mark the live FP state clean before the copy, so a context switch
> + * does not save the handler's registers over the restored image.
> + */
> + if ((regs->status & SR_FS) != SR_FS_OFF)
> + __fstate_clean(regs);
> +
With the v2 patch from Guobin [1], consider something like:
if ((regs->status & SR_FS) != SR_FS_OFF) {
preempt_disable();
__fstate_clean(regs);
preempt_enable();
> err = __copy_from_user(¤t->thread.fstate, state, sizeof(*state));
> if (unlikely(err))
> return err;
>
> base-commit: 90201351a862182bdabfc1ad1c97b1e1b6ed5931
> --
> 2.53.0
>
[1]: https://lore.kernel.org/all/20261020-vector_fpu_regs_status_rmw_fix-v2-1-474a59155e96@intel.com/
Thanks,
Andy
prev parent reply other threads:[~2026-10-30 21:45 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-16 16:07 Nickolai Zeldovich
2026-10-30 21:44 ` Andy Chiu [this message]
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='auUP2yrVl7lwkNkC@Tao-Chiu'\''s-Mac' \
--to=tchiu@tenstorrent.com \
--cc=alex@ghiti.fr \
--cc=andybnac@gmail.com \
--cc=aou@eecs.berkeley.edu \
--cc=debug@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 \
/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®