mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Jisheng Zhang <jszhang@kernel.org>
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>,
	Deepak Gupta <debug@rivosinc.com>, Zong Li <zong.li@sifive.com>,
	Andrew Morton <akpm@linux-foundation.org>,
	linux-riscv@lists.infradead.org, linux-mm@kvack.org,
	linux-kernel@vger.kernel.org
Subject: Re: [PATCH v1] riscv: mm: Do not make execute-only PTEs readable in pte_wrprotect()
Date: Tue, 20 Oct 2026 09:28:55 +0800	[thread overview]
Message-ID: <atbD14yBffqVLQ3Z@xhacker> (raw)
In-Reply-To: <20261017162229.796020-1-nickolai@csail.mit.edu>

On Sat, Oct 17, 2026 at 12:22:28PM -0400, Nickolai Zeldovich wrote:
> On riscv, a user page mapped execute-only becomes readable when the
> kernel write-protects its PTE. An unprivileged process that writes to
> a MAP_SHARED file mapping, changes it to PROT_EXEC with mprotect() and
> then has the page written back (msync(), or any writeback of the file)
> can read the page afterwards, although every load from it faulted
> before the writeback; the same happens to an execute-only anonymous
> THP in the parent and the child of a fork(). The mapping stays
> readable until the next mprotect(). Nothing else breaks, but
> execute-only memory loses its point: code that relies on it not being
> readable from user space can be read. This affects every kernel since
> v7.0.
> 
> Since commit 540de7ade1e1 ("riscv/mm: update write protect to work on
> shadow stacks"), pte_wrprotect() sets _PAGE_READ unconditionally, so
> that a shadow stack PTE (XWR = 010) becomes XWR = 001 rather than the
> non-leaf encoding 000. That commit assumed that every other PTE it is
> applied to already has _PAGE_READ. Execute-only PTEs (XWR = 100,
> PAGE_EXEC, used for PROT_EXEC mappings) do not, and pte_wrprotect() is
> applied to PTEs that are not writable: the writeback path
> folio_mkclean() -> page_vma_mkclean_one() write-protects every present
> PTE that is dirty or writable, and mprotect() keeps _PAGE_DIRTY in the
> PTE (_PAGE_CHG_MASK), so a written page that was made execute-only is
> turned into XWR = 101. copy_huge_pmd() write protects an anonymous huge
> PMD whether or not it is writable, through pmdp_set_wrprotect() ->
> ptep_set_wrprotect() for the parent, which open-codes the same
> expression, and pmd_wrprotect() for the child.
> 
> Set _PAGE_READ only when the PTE is writable, which is the shadow stack
> case the bit was added for (a writable PTE without _PAGE_READ is only
> ever a shadow stack PTE), and use pte_wrprotect() in
> ptep_set_wrprotect() instead of repeating the expression. For a PTE
> that is not writable pte_wrprotect() is a no-op again, as it was before
> 540de7ade1e1.
> 
> Tested under QEMU (riscv64 virt, defconfig with an ext4 disk): after
> write, mprotect(PROT_EXEC) and msync(MS_SYNC) a load from the shared
> file page succeeds without this patch and faults with it; with
> CONFIG_TRANSPARENT_HUGEPAGE the execute-only anonymous THP is readable
> after fork() without this patch and not with it.
> 
> Fixes: 540de7ade1e1 ("riscv/mm: update write protect to work on shadow stacks")
> Assisted-by: LLM
> Signed-off-by: Nickolai Zeldovich <nickolai@csail.mit.edu>

Reviewed-by: Jisheng Zhang <jszhang@kernel.org>

> ---
> 
> Notes:
>     The second hunk (ptep_set_wrprotect()) conflicts with "riscv: mm: Make
>     ptep_set_wrprotect() atomic again" (posted 2026-10-16), which already
>     computes the new PTE with pte_wrprotect(); if that patch is applied
>     first, only the pte_wrprotect() hunk of this one is needed, and the
>     fork/THP case is fixed through it.
>     
>     Reproducer (riscv64; the file case needs a block-backed filesystem such
>     as ext4, the THP case CONFIG_TRANSPARENT_HUGEPAGE; no privileges):
>     
>         /* An execute-only mapping must not become readable through writeback or fork. */
>         #include <fcntl.h>
>         #include <setjmp.h>
>         #include <signal.h>
>         #include <stdio.h>
>         #include <sys/mman.h>
>         #include <sys/wait.h>
>         #include <unistd.h>
>     
>         static sigjmp_buf env;
>     
>         static void segv(int sig)
>         {
>         	siglongjmp(env, 1);
>         }
>     
>         /* 1 if a user load from p succeeds */
>         static int readable(volatile char *p)
>         {
>         	if (sigsetjmp(env, 1))
>         		return 0;
>         	(void)*p;
>         	return 1;
>         }
>     
>         int main(int argc, char **argv)
>         {
>         	const char *path = argc > 1 ? argv[1] : "/mnt/xom";
>         	size_t thp = 2UL << 20;
>         	int fd, bad = 0;
>         	char *p, *a;
>     
>         	signal(SIGSEGV, segv);
>     
>         	/* Shared file mapping: write, make it execute-only, write back. */
>         	fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600);
>         	if (fd < 0 || ftruncate(fd, 4096))
>         		return perror(path), 2;
>         	p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
>         	if (p == MAP_FAILED)
>         		return perror("mmap"), 2;
>         	p[0] = 1;
>         	mprotect(p, 4096, PROT_EXEC);
>         	printf("file: before msync readable=%d\n", readable(p));
>         	msync(p, 4096, MS_SYNC);
>         	printf("file: after msync  readable=%d\n", readable(p));
>         	bad |= readable(p);
>     
>         	/* Anonymous THP: write, make it execute-only, fork. */
>         	a = mmap(NULL, 2 * thp, PROT_READ | PROT_WRITE,
>         		 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
>         	a = (char *)(((unsigned long)a + thp - 1) & ~(thp - 1));
>         	madvise(a, thp, MADV_HUGEPAGE);
>         	a[0] = 1;
>         	mprotect(a, thp, PROT_EXEC);
>         	printf("thp:  before fork  readable=%d\n", readable(a));
>         	if (fork() == 0)
>         		_exit(0);
>         	wait(NULL);
>         	printf("thp:  after fork   readable=%d\n", readable(a));
>         	bad |= readable(a);
>     
>         	printf("%s\n", bad ? "FAIL: execute-only page became readable" : "PASS");
>         	return bad;
>         }
>     
>         riscv64-linux-gnu-gcc -O2 -static -o xom xom_min.c
>         ./xom /mnt/ext4/xom
>     
>     Before (mainline 70dd4ee72eb2, defconfig + THP, QEMU virt):
>         file: before msync readable=0
>         file: after msync  readable=1
>         thp:  before fork  readable=0
>         thp:  after fork   readable=1
>         FAIL: execute-only page became readable
>     
>     After:
>         file: before msync readable=0
>         file: after msync  readable=0
>         thp:  before fork  readable=0
>         thp:  after fork   readable=0
>         PASS
> 
>  arch/riscv/include/asm/pgtable.h | 9 ++++++---
>  1 file changed, 6 insertions(+), 3 deletions(-)
> 
> diff --git a/arch/riscv/include/asm/pgtable.h b/arch/riscv/include/asm/pgtable.h
> index 40b1ed4f3ea8..872b8b6a944b 100644
> --- a/arch/riscv/include/asm/pgtable.h
> +++ b/arch/riscv/include/asm/pgtable.h
> @@ -406,7 +406,11 @@ static inline int pte_special(pte_t pte)
>  
>  static inline pte_t pte_wrprotect(pte_t pte)
>  {
> -	return __pte((pte_val(pte) & ~(_PAGE_WRITE)) | (_PAGE_READ));
> +	/* Shadow stack is XWR = 010: keep a leaf by making it readable */
> +	if (pte_val(pte) & _PAGE_WRITE)
> +		pte = __pte(pte_val(pte) | _PAGE_READ);
> +
> +	return __pte(pte_val(pte) & ~(_PAGE_WRITE));
>  }
>  
>  #ifdef CONFIG_HAVE_ARCH_USERFAULTFD_WP
> @@ -692,8 +696,7 @@ static inline void ptep_set_wrprotect(struct mm_struct *mm,
>  	 * encoding 000b which is wrong encoding with V = 1. This should lead to page fault
>  	 * but we dont want this wrong configuration to be set in page tables.
>  	 */
> -	atomic_long_set((atomic_long_t *)ptep,
> -			((pte_val(read_pte) & ~(unsigned long)_PAGE_WRITE) | _PAGE_READ));
> +	atomic_long_set((atomic_long_t *)ptep, pte_val(pte_wrprotect(read_pte)));
>  }
>  
>  #define __HAVE_ARCH_PTEP_CLEAR_YOUNG_FLUSH
> 
> base-commit: 70dd4ee72eb2d5793c62e71c14e3aba78e5a4e56
> -- 
> 2.53.0
> 
> 
> _______________________________________________
> linux-riscv mailing list
> linux-riscv@lists.infradead.org
> http://lists.infradead.org/mailman/listinfo/linux-riscv

      reply	other threads:[~2026-10-20  1:49 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-17 16:22 Nickolai Zeldovich
2026-10-20  1:28 ` Jisheng Zhang [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=atbD14yBffqVLQ3Z@xhacker \
    --to=jszhang@kernel.org \
    --cc=akpm@linux-foundation.org \
    --cc=alex@ghiti.fr \
    --cc=aou@eecs.berkeley.edu \
    --cc=debug@rivosinc.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=linux-riscv@lists.infradead.org \
    --cc=nickolai@csail.mit.edu \
    --cc=palmer@dabbelt.com \
    --cc=pjw@kernel.org \
    --cc=zong.li@sifive.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®