[PATCH v3 1/2] ARM: mm: fix use-after-free in __do_user_fault() under CONFIG_DEBUG_USER
Qi Xi
xiqi2 at huawei.com
Fri Jun 26 18:39:33 PDT 2026
Hi Russell,
Thank you for the review. I understand the general concern about
taking locks in fault paths, but I would like to clarify the specific
case here.
__do_user_fault() with CONFIG_DEBUG_USER is not a kernel-dying path.
After show_pte() prints debug info, the kernel calls
force_sig_fault(SIGSEGV) and returns to user space. The system
continues running normally. Without this fix, a concurrent munmap can
cause show_pte() to trigger a secondary kernel fault, turning a
harmless SIGSEGV into a kernel panic.
Regarding your concern about the mm lock being already held: I have
verified that all three callers of __do_user_fault() (do_page_fault
-> bad_area, do_bad_area user path, and do_kernel_address_page_fault
user path) release mmap_read_lock or never hold it before entering
__do_user_fault(). So the lock is not held here.
It is also worth noting that we did NOT modify the paths where the
kernel is already dying (die_kernel_fault, __do_kernel_fault). Those
paths remain unchanged and continue to call show_pte() without any
lock, just as they always have.
On 26/06/2026 17:44, Russell King wrote:
> On Fri, Jun 26, 2026 at 03:30:47PM +0800, Qi Xi wrote:
>> When CONFIG_DEBUG_USER is enabled with user_debug=31 on 32-bit ARM,
>> a user page fault triggers show_pte() via __do_user_fault() after
>> do_page_fault() has already released mmap_read_lock. If another
>> thread concurrently calls munmap(), the page table pages can be
>> freed while show_pte() is still reading them, causing a
>> use-after-free in show_pte().
>>
>> The race can be reproduced on multi_v7_defconfig with:
>> CONFIG_DEBUG_USER=y
>> CONFIG_ARM_LPAE=y
>> kernel command line: user_debug=31
>>
>> A delay inserted in show_pte() for testing widens the race window and
>> makes the UAF reliably reproducible. On LPAE, the race works as
>> follows:
>>
>> CPU 0 (fault path) CPU 1 (munmap)
>> munmap(page 0) -> clears PTE[0]
>> PTE/PMD pages remain
>>
>> read page 0 -> page fault
>> -> do_DataAbort()
>> -> do_page_fault()
>> -> lock_mm_and_find_vma() -> no VMA
>> (mmap_read_lock released)
>> -> __do_user_fault()
>> -> show_pte(tsk->mm, addr)
>> -> *pgd (valid)
>> -> p4d/pud checks pass
>>
>> -> [delay] munmap(page 1)
>> -> clears PTE[1]
>> -> PTE/PMD pages freed
>> -> PGD cleared
>>
>> -> pmd_offset(pud, addr)
>> -> *pud=0 -> __va(0)
>> -> dereference
>> -> secondary data abort (kernel)
>>
>> Fix by taking mmap_read_lock() around show_pte() in __do_user_fault().
>> __do_user_fault() is called from process context with interrupts
>> enabled, so the context can sleep and mmap_read_lock() is safe here.
> This is a fault path which should only be called when something is
> already wrong, the mm lock may already be held (e.g. a kernel
> fault while already holding the mmap lock.) We can't take any locks
> here.
>
More information about the linux-arm-kernel
mailing list