x86/mm: Update ptep/pmdp_set_wrprotect() for _PAGE_SAVED_DIRTY
authorRick Edgecombe <rick.p.edgecombe@intel.com>
Tue, 13 Jun 2023 00:10:37 +0000 (17:10 -0700)
committerRick Edgecombe <rick.p.edgecombe@intel.com>
Tue, 11 Jul 2023 21:12:19 +0000 (14:12 -0700)
When shadow stack is in use, Write=0,Dirty=1 PTE are preserved for
shadow stack. Copy-on-write PTEs then have Write=0,SavedDirty=1.

When a PTE goes from Write=1,Dirty=1 to Write=0,SavedDirty=1, it could
become a transient shadow stack PTE in two cases:

1. Some processors can start a write but end up seeing a Write=0 PTE by
   the time they get to the Dirty bit, creating a transient shadow stack
   PTE. However, this will not occur on processors supporting shadow
   stack, and a TLB flush is not necessary.

2. When _PAGE_DIRTY is replaced with _PAGE_SAVED_DIRTY non-atomically, a
   transient shadow stack PTE can be created as a result.

Prevent the second case when doing a write protection and Dirty->SavedDirty
shift at the same time with a CMPXCHG loop. The first case

Note, in the PAE case CMPXCHG will need to operate on 8 byte, but
try_cmpxchg() will not use CMPXCHG8B, so it cannot operate on a full PAE
PTE. However the exiting logic is not operating on a full 8 byte region
either, and relies on the fact that the Write bit is in the first 4
bytes when doing the clear_bit(). Since both the Dirty, SavedDirty and
Write bits are in the first 4 bytes, casting to a long will be similar to
the existing behavior which also casts to a long.

Dave Hansen, Jann Horn, Andy Lutomirski, and Peter Zijlstra provided many
insights to the issue. Jann Horn provided the CMPXCHG solution.

Co-developed-by: Yu-cheng Yu <yu-cheng.yu@intel.com>
Signed-off-by: Yu-cheng Yu <yu-cheng.yu@intel.com>
Signed-off-by: Rick Edgecombe <rick.p.edgecombe@intel.com>
Signed-off-by: Dave Hansen <dave.hansen@linux.intel.com>
Acked-by: Mike Rapoport (IBM) <rppt@kernel.org>
Tested-by: Pengfei Xu <pengfei.xu@intel.com>
Tested-by: John Allen <john.allen@amd.com>
Tested-by: Kees Cook <keescook@chromium.org>
Link: https://lore.kernel.org/all/20230613001108.3040476-12-rick.p.edgecombe%40intel.com
arch/x86/include/asm/pgtable.h

index 0c0747c..b48d07a 100644 (file)
@@ -1190,7 +1190,17 @@ static inline pte_t ptep_get_and_clear_full(struct mm_struct *mm,
 static inline void ptep_set_wrprotect(struct mm_struct *mm,
                                      unsigned long addr, pte_t *ptep)
 {
-       clear_bit(_PAGE_BIT_RW, (unsigned long *)&ptep->pte);
+       /*
+        * Avoid accidentally creating shadow stack PTEs
+        * (Write=0,Dirty=1).  Use cmpxchg() to prevent races with
+        * the hardware setting Dirty=1.
+        */
+       pte_t old_pte, new_pte;
+
+       old_pte = READ_ONCE(*ptep);
+       do {
+               new_pte = pte_wrprotect(old_pte);
+       } while (!try_cmpxchg((long *)&ptep->pte, (long *)&old_pte, *(long *)&new_pte));
 }
 
 #define flush_tlb_fix_spurious_fault(vma, address, ptep) do { } while (0)
@@ -1242,7 +1252,17 @@ static inline pud_t pudp_huge_get_and_clear(struct mm_struct *mm,
 static inline void pmdp_set_wrprotect(struct mm_struct *mm,
                                      unsigned long addr, pmd_t *pmdp)
 {
-       clear_bit(_PAGE_BIT_RW, (unsigned long *)pmdp);
+       /*
+        * Avoid accidentally creating shadow stack PTEs
+        * (Write=0,Dirty=1).  Use cmpxchg() to prevent races with
+        * the hardware setting Dirty=1.
+        */
+       pmd_t old_pmd, new_pmd;
+
+       old_pmd = READ_ONCE(*pmdp);
+       do {
+               new_pmd = pmd_wrprotect(old_pmd);
+       } while (!try_cmpxchg((long *)pmdp, (long *)&old_pmd, *(long *)&new_pmd));
 }
 
 #ifndef pmdp_establish