[PATCH] LoongArch: fix silent hang on kernel exec into an NX page

Song Hu posted 1 patch 1 week, 2 days ago
arch/loongarch/mm/fault.c | 24 ++++++++++++++++++++----
1 file changed, 20 insertions(+), 4 deletions(-)
[PATCH] LoongArch: fix silent hang on kernel exec into an NX page
Posted by Song Hu 1 week, 2 days ago
spurious_fault() returns 1 for any non-write fault on a present page
without checking the access permission, so a kernel instruction fetch into
a present NX page (TLBNX) is treated as spurious and the faulting
instruction re-executes forever -- a silent hang that bypasses
fixup_exception() and the oops path.

Recover the access type from the exception code: an exec fault (TLBNX) is
spurious only if the page is executable, a read fault only if it is
readable.  Fold the pmd/pte check together and add pud_leaf/p4d_leaf.

Fixes: 09cfefb7fa70c ("LoongArch: Add memory management")
Signed-off-by: Song Hu <husong@kylinos.cn>
---
 arch/loongarch/mm/fault.c | 24 ++++++++++++++++++++----
 1 file changed, 20 insertions(+), 4 deletions(-)

diff --git a/arch/loongarch/mm/fault.c b/arch/loongarch/mm/fault.c
index 2c93d33356e5..591a9c7060f2 100644
--- a/arch/loongarch/mm/fault.c
+++ b/arch/loongarch/mm/fault.c
@@ -38,6 +38,7 @@ static int __kprobes spurious_fault(unsigned long write, unsigned long address)
 	pud_t *pud;
 	pmd_t *pmd;
 	pte_t *pte;
+	unsigned long prot;
 
 	if (!(address & __UA_LIMIT))
 		return 0;
@@ -58,15 +59,30 @@ static int __kprobes spurious_fault(unsigned long write, unsigned long address)
 	if (!pmd_present(pmdp_get(pmd)))
 		return 0;
 
-	if (pmd_leaf(*pmd)) {
-		return write ? pmd_write(pmdp_get(pmd)) : 1;
+	if (p4d_leaf(p4dp_get(p4d))) {
+		prot = p4d_val(p4dp_get(p4d));
+	} else if (pud_leaf(pudp_get(pud))) {
+		prot = pud_val(pudp_get(pud));
+	} else if (pmd_leaf(*pmd)) {
+		prot = pmd_val(pmdp_get(pmd));
 	} else {
 		pte = pte_offset_kernel(pmd, address);
 		if (!pte_present(ptep_get(pte)))
 			return 0;
-
-		return write ? pte_write(ptep_get(pte)) : 1;
+		prot = pte_val(ptep_get(pte));
 	}
+
+	/*
+	 * The TLB-protect handler passes write=0 for read (TLBNR), exec (TLBNX)
+	 * and privilege (TLBPE) faults alike, so recover the real cause from the
+	 * exception code; otherwise an exec into a present NX page (or a read of
+	 * a NO_READ page) is wrongly treated as spurious and loops forever.
+	 */
+	if (write)
+		return prot & _PAGE_WRITE;
+	if (read_csr_excode() == EXCCODE_TLBNX)
+		return !(prot & _PAGE_NO_EXEC);
+	return !(prot & _PAGE_NO_READ);
 }
 
 static void __kprobes no_context(struct pt_regs *regs,
-- 
2.43.0
Re: [PATCH] LoongArch: fix silent hang on kernel exec into an NX page
Posted by Huacai Chen 1 week, 2 days ago
Hi, Song,

On Thu, Jul 16, 2026 at 11:02 AM Song Hu <husong@kylinos.cn> wrote:
>
> spurious_fault() returns 1 for any non-write fault on a present page
> without checking the access permission, so a kernel instruction fetch into
> a present NX page (TLBNX) is treated as spurious and the faulting
> instruction re-executes forever -- a silent hang that bypasses
> fixup_exception() and the oops path.
Can you give a method to reproduce this hang?

>
> Recover the access type from the exception code: an exec fault (TLBNX) is
> spurious only if the page is executable, a read fault only if it is
> readable.  Fold the pmd/pte check together and add pud_leaf/p4d_leaf.
>
> Fixes: 09cfefb7fa70c ("LoongArch: Add memory management")
> Signed-off-by: Song Hu <husong@kylinos.cn>
> ---
>  arch/loongarch/mm/fault.c | 24 ++++++++++++++++++++----
>  1 file changed, 20 insertions(+), 4 deletions(-)
>
> diff --git a/arch/loongarch/mm/fault.c b/arch/loongarch/mm/fault.c
> index 2c93d33356e5..591a9c7060f2 100644
> --- a/arch/loongarch/mm/fault.c
> +++ b/arch/loongarch/mm/fault.c
> @@ -38,6 +38,7 @@ static int __kprobes spurious_fault(unsigned long write, unsigned long address)
>         pud_t *pud;
>         pmd_t *pmd;
>         pte_t *pte;
> +       unsigned long prot;
>
>         if (!(address & __UA_LIMIT))
>                 return 0;
> @@ -58,15 +59,30 @@ static int __kprobes spurious_fault(unsigned long write, unsigned long address)
>         if (!pmd_present(pmdp_get(pmd)))
>                 return 0;
>
> -       if (pmd_leaf(*pmd)) {
> -               return write ? pmd_write(pmdp_get(pmd)) : 1;
> +       if (p4d_leaf(p4dp_get(p4d))) {
> +               prot = p4d_val(p4dp_get(p4d));
> +       } else if (pud_leaf(pudp_get(pud))) {
> +               prot = pud_val(pudp_get(pud));
> +       } else if (pmd_leaf(*pmd)) {
> +               prot = pmd_val(pmdp_get(pmd));
P4D doesn't exist on LoongArch, PUD huge page isn't supported on
LoongArch, so what are you doing?

Huacai

>         } else {
>                 pte = pte_offset_kernel(pmd, address);
>                 if (!pte_present(ptep_get(pte)))
>                         return 0;
> -
> -               return write ? pte_write(ptep_get(pte)) : 1;
> +               prot = pte_val(ptep_get(pte));
>         }
> +
> +       /*
> +        * The TLB-protect handler passes write=0 for read (TLBNR), exec (TLBNX)
> +        * and privilege (TLBPE) faults alike, so recover the real cause from the
> +        * exception code; otherwise an exec into a present NX page (or a read of
> +        * a NO_READ page) is wrongly treated as spurious and loops forever.
> +        */
> +       if (write)
> +               return prot & _PAGE_WRITE;
> +       if (read_csr_excode() == EXCCODE_TLBNX)
> +               return !(prot & _PAGE_NO_EXEC);
> +       return !(prot & _PAGE_NO_READ);
>  }
>
>  static void __kprobes no_context(struct pt_regs *regs,
> --
> 2.43.0
>
Re: [PATCH] LoongArch: fix silent hang on kernel exec into an NX page
Posted by Song Hu 1 week, 2 days ago
在 2026/7/16 14:24, Huacai Chen 写道:
> Hi, Song,
>
> On Thu, Jul 16, 2026 at 11:02 AM Song Hu <husong@kylinos.cn> wrote:
>> spurious_fault() returns 1 for any non-write fault on a present page
>> without checking the access permission, so a kernel instruction fetch into
>> a present NX page (TLBNX) is treated as spurious and the faulting
>> instruction re-executes forever -- a silent hang that bypasses
>> fixup_exception() and the oops path.
> Can you give a method to reproduce this hang?
Hi Huacai,

Thanks for the review.

A late_initcall that allocates a page-table-backed executable page, flips it
NX, then calls into it -- the TLBNX loops forever in spurious_fault() on the
unpatched kernel.  Built as obj-y:

  static int __init repro_init(void)
  {
      void (*fn)(void) = __vmalloc_node_range(PAGE_SIZE, PAGE_SIZE,
                      VMALLOC_START, VMALLOC_END, GFP_KERNEL,
                      PAGE_KERNEL_EXEC, VM_FLUSH_RESET_PERMS,
                      NUMA_NO_NODE, __builtin_return_address(0));
      if (!fn)
          return 0;
      set_memory_nx((unsigned long)fn, 1);
      fn();   /* TLBNX -> spurious_fault()=1 -> infinite fault loop */
      return 0;
  }
  late_initcall(repro_init);

The page must be page-table-backed (vmalloc range); an alloc_page()/DMW
linear-map address isn't in the kernel page tables, so the walk in
spurious_fault() never finds it.

Tested in a qemu-system-loongarch64 guest (-machine virt -accel kvm -cpu
la464) on a Loongson-3A6000 host :
unpatched, the initcall never returns -- silent infinite TLBNX loop, no oops;
with the fix, it oopses ("Unable to handle kernel paging request ... era ==
<addr>").
>> Recover the access type from the exception code: an exec fault (TLBNX) is
>> spurious only if the page is executable, a read fault only if it is
>> readable.  Fold the pmd/pte check together and add pud_leaf/p4d_leaf.
>>
>> Fixes: 09cfefb7fa70c ("LoongArch: Add memory management")
>> Signed-off-by: Song Hu <husong@kylinos.cn>
>> ---
>>  arch/loongarch/mm/fault.c | 24 ++++++++++++++++++++----
>>  1 file changed, 20 insertions(+), 4 deletions(-)
>>
>> diff --git a/arch/loongarch/mm/fault.c b/arch/loongarch/mm/fault.c
>> index 2c93d33356e5..591a9c7060f2 100644
>> --- a/arch/loongarch/mm/fault.c
>> +++ b/arch/loongarch/mm/fault.c
>> @@ -38,6 +38,7 @@ static int __kprobes spurious_fault(unsigned long write, unsigned long address)
>>         pud_t *pud;
>>         pmd_t *pmd;
>>         pte_t *pte;
>> +       unsigned long prot;
>>
>>         if (!(address & __UA_LIMIT))
>>                 return 0;
>> @@ -58,15 +59,30 @@ static int __kprobes spurious_fault(unsigned long write, unsigned long address)
>>         if (!pmd_present(pmdp_get(pmd)))
>>                 return 0;
>>
>> -       if (pmd_leaf(*pmd)) {
>> -               return write ? pmd_write(pmdp_get(pmd)) : 1;
>> +       if (p4d_leaf(p4dp_get(p4d))) {
>> +               prot = p4d_val(p4dp_get(p4d));
>> +       } else if (pud_leaf(pudp_get(pud))) {
>> +               prot = pud_val(pudp_get(pud));
>> +       } else if (pmd_leaf(*pmd)) {
>> +               prot = pmd_val(pmdp_get(pmd));
> P4D doesn't exist on LoongArch, PUD huge page isn't supported on
> LoongArch, so what are you doing?
my mistake.  p4d is folded here and there is no PUD huge page
on LoongArch, so p4d_leaf()/pud_leaf() are always false in this path -- they
were unnecessary.  I'll drop both in v2 and keep only the pmd_leaf()/pte
paths.

Thanks,
Song
> Huacai
>
>>         } else {
>>                 pte = pte_offset_kernel(pmd, address);
>>                 if (!pte_present(ptep_get(pte)))
>>                         return 0;
>> -
>> -               return write ? pte_write(ptep_get(pte)) : 1;
>> +               prot = pte_val(ptep_get(pte));
>>         }
>> +
>> +       /*
>> +        * The TLB-protect handler passes write=0 for read (TLBNR), exec (TLBNX)
>> +        * and privilege (TLBPE) faults alike, so recover the real cause from the
>> +        * exception code; otherwise an exec into a present NX page (or a read of
>> +        * a NO_READ page) is wrongly treated as spurious and loops forever.
>> +        */
>> +       if (write)
>> +               return prot & _PAGE_WRITE;
>> +       if (read_csr_excode() == EXCCODE_TLBNX)
>> +               return !(prot & _PAGE_NO_EXEC);
>> +       return !(prot & _PAGE_NO_READ);
>>  }
>>
>>  static void __kprobes no_context(struct pt_regs *regs,
>> --
>> 2.43.0
>>