fault handler의 결과를 성공과 실패 두 가지로만 읽으면 lock을 두 번 해제하거나 이미 버린 VMA를 다시 쓰기 쉽습니다. VM_FAULT_RETRY와 VM_FAULT_COMPLETED는 작업 결과뿐 아니라 다음에 누가 어떤 상태에서 이어 가야 하는지를 전달합니다.
공통 handler에 들어가기 전
v6.0의 mmap-lock 경로는 먼저 mmap_read_trylock()을 시도합니다. 실패했고 fault가 kernel mode에서 발생했다면 search_exception_tables(regs->ip)로 그 instruction이 복구 가능한 uaccess인지 검사합니다. 등록되지 않은 kernel instruction이 이미 자신이 가진 lock을 기다리는 상황을 막기 위해서입니다. trylock 성공 뒤에도 잠들 수 있는 context인지 검사하는 might_sleep()가 남습니다.
find_vma()가 돌려주는 것은 주소를 포함한 VMA라는 보장이 아니라 vm_end > address인 첫 VMA입니다. 따라서 vm_start도 확인해야 하며, 시작보다 아래라면 VM_GROWSDOWN 확장을 통과해야 합니다. 이어지는 access_error()는 write에 VM_WRITE가 없는 경우, read protection fault, 접근 가능한 flag가 전혀 없는 VMA를 배제합니다. 전체 정의에는 pkey·SGX·architecture permission 검사도 들어 있습니다. CoW 해결은 VMA의 금지된 접근을 허가하는 수단이 아닙니다.
v6.18 x86는 user fault에서 lock_vma_under_rcu()의 per-VMA read-lock 경로를 먼저 사용할 수 있습니다. 실패하거나 retry가 필요하면 lock_mm_and_find_vma()를 사용하는 mmap-lock 경로로 내려갑니다. 따라서 아래 v6.0의 함수 전체를 최신 lock 획득 순서라고 읽으면 안 됩니다. 현재 shadow-stack access를 WRITE로 처리하는 분기와 register 상태로 USER를 정하는 규칙도 v6.18 do_user_addr_fault()에 따로 있습니다.
user address에 대한 fault라도 kernel의 user-code 실행 시도, reserved page-table bit 오류, fault 처리가 금지된 atomic context는 정상적인 demand paging으로 해결할 수 없습니다. mm이 없는 context도 별도 처리합니다. handler는 write·instruction·user-origin 정보를 fault flags로 바꾸고 VMA를 찾아 접근 권한을 확인합니다.
VMA lookup이 주소보다 뒤의 VMA를 반환했다고 주소가 그 안에 있는 것은 아닙니다. vm_start <= address < vm_end가 성립해야 합니다. 아래쪽 hole이라면 growth 가능한 stack인지와 확장 조건을 확인합니다. VMA가 write를 허용하지 않으면 read-only PTE의 write를 CoW로 해결할 수 없습니다.
RETRY는 같은 pointer로 다시 호출하라는 뜻이 아닙니다
file fault가 folio lock이나 I/O를 기다리면서 mmap_lock을 놓으면 경쟁이 줄어듭니다. 대신 VMA가 사라지거나 속성이 바뀔 수 있습니다. RETRY 결과를 받은 caller는 반환 계약에 따라 lock을 다시 얻고 주소를 다시 찾습니다. 이전 VMA pointer나 최초 permission 판정을 그대로 사용하는 것은 올바른 retry가 아닙니다.
FAULT_FLAG_ALLOW_RETRY와 FAULT_FLAG_TRIED는 최초 시도와 후속 대기의 정책을 구분합니다. 이를 모든 fault가 정확히 두 번만 실행된다는 횟수 보장으로 읽으면 안 됩니다. fault의 재경쟁, GUP의 바깥 loop와 version별 per-VMA lock fallback은 다른 재시도를 만들 수 있습니다.
COMPLETED는 해당 경로가 fault를 완료했고 lock 처리도 맡았다는 결과입니다. caller가 평범한 성공처럼 다시 unlock하면 안 됩니다. 보통의 성공은 필요한 lock을 caller가 정리하는 계약과 구별됩니다. signal이 pending이면 retry보다 signal 처리를 우선할 수도 있습니다. kernel-origin access에는 fatal signal의 복구 처리도 필요합니다.
아래 표는 v6.0 do_user_addr_fault()의 mmap-lock 경로에서 위에서 아래로 먼저 일치하는 조건을 적용한 결과입니다. 다른 caller나 per-VMA lock 경로의 일반 규칙으로 확장하지 않습니다.
| 먼저 확인하는 결과 | 이 caller가 하는 일 | lock/VMA에 대한 의미 |
|---|---|---|
fault_signal_pending()이 참 |
필요하면 kernel-origin fixup을 수행하고 return | RETRY가 결합된 경로이므로 여기서 보통의 unlock을 반복하지 않습니다. |
VM_FAULT_COMPLETED |
즉시 return | 하위 경로가 fault와 lock 정리를 끝냈습니다. |
VM_FAULT_RETRY |
TRIED를 기록하고 lookup부터 재시도 | 놓은 lock을 다시 얻고 VMA를 새로 찾습니다. |
| 위 조건이 모두 거짓 | mmap_read_unlock() 후 결과 판정 |
이 경로에서 caller가 lock을 반환합니다. |
| unlock 뒤 error mask가 없음 | return | 보통의 처리 완료입니다. |
| unlock 뒤 error mask가 있음 | OOM, SIGBUS/HWPOISON, SIGSEGV 등에 따라 분기 | kernel-origin fatal signal·OOM에는 user signal과 다른 복구 분기도 있습니다. |
정보 bit와 error mask
VM_FAULT_NOPAGE는 caller에게 일반 vmf->page 결과를 넘기지 않는다는 계약입니다. callback이 직접 PTE를 채운 경우나 경쟁에서 이미 해결된 경우를 포함하며, 이 bit만으로 특정 page가 설치됐다고 단정하지 않습니다. VM_FAULT_DONE_COW는 callback이 CoW까지 처리한 결과이고, VM_FAULT_NEEDDSYNC는 DAX synchronous fault에서 후속 동기화가 필요한 결과입니다. 둘 다 ordinary page를 반환받은 것처럼 finish_fault()를 반복하는 신호가 아닙니다.
VM_FAULT_OOM 자체를 “이미 mmap lock을 풀었다”는 뜻으로 읽어서도 안 됩니다. 아래 v6.0 caller는 RETRY·COMPLETED를 먼저 배제한 뒤 lock을 풀고 OOM을 처리합니다. user-mode fault는 pagefault_out_of_memory() 경로로, kernel-mode uaccess는 exception fixup 또는 oops 경로로 갑니다. 실제 kill의 필요 여부는 memcg OOM 처리와 동시 진행 상황을 포함한 후속 정책의 문제입니다.
VM_FAULT_MAJOR는 handler가 major fault로 accounting했다는 정보입니다. 한 instruction이 정확히 한 번의 storage I/O를 수행했다는 측정값은 아닙니다. cache, readahead와 동시 fault 때문에 fault 수와 I/O 양은 일치하지 않습니다. VM_FAULT_LOCKED는 반환 folio의 lock 상태를, VM_FAULT_NOPAGE는 일반적인 page 결과를 사용할 수 없거나 entry 설치를 별도로 해결한 경로를 나타낼 수 있습니다.
OOM, SIGBUS, SIGSEGV, hardware-poison 관련 결과는 error mask로 묶지만 처리 방법은 다릅니다. OOM 결과가 모든 caller에서 즉시 같은 victim을 죽인다는 뜻은 아닙니다. VM_FAULT_FALLBACK은 huge fault가 base-page 경로로 물러날 때 쓰며 일반 architecture 최종 반환의 모든 위치에서 허용되는 결과는 아닙니다. 과거의 VM_FAULT_WRITE 같은 bit도 tag에 따라 없을 수 있습니다.
signal 이름보다 원인을 먼저 봅니다
bad-area helper 중에는 mmap lock을 반환하는 wrapper와 처음부터 lock을 소유하지 않는 _nosemaphore 경로가 있습니다. 최종 helper는 register가 user mode여도 hardware USER bit가 없는 implicit access를 별도로 검사합니다. 일반 user fault에서는 IRQ를 허용한 상태에서 prefetch·AMD erratum·VDSO fixup을 살피고, 남은 오류에 대해 signal 정보를 기록합니다. 이 순서는 signal handler가 실행되는 시점과도 다릅니다.
sanitize_error_code()는 kernel address에 대한 user 접근을 항상 protection fault처럼 보고하여 kernel page-table 배치를 노출하지 않게 합니다. 따라서 log의 PROT bit를 원래 hardware 상태의 무가공 값으로 해석하면 안 됩니다. show_signal_msg()도 unhandled SIGSEGV와 printk rate limit을 통과할 때에만 comm·pid·fault address·IP·SP·error와 관련 VMA·opcode를 출력합니다. fault 주소와 error는 task_struct->thread의 architecture state에도 기록합니다. 원본 설명의 task_sched가 아니라 task_struct입니다.
mapping 부재는 보통 SEGV_MAPERR, 존재하지만 접근이 금지된 VMA는 SEGV_ACCERR로 이어질 수 있습니다. protection key에는 별도 정보가 있습니다. VMA 안의 file page가 backing EOF를 완전히 넘으면 SIGBUS가 가능합니다. 주소 공간의 유효성과 backing 공급 가능성은 다른 조건입니다.
bad-area 처리는 user가 kernel page-table 배치를 추론하지 못하도록 error 정보를 정리하고, VDSO exception 같은 특수 복구를 확인하며, 설정과 rate limit에 따라 진단을 출력합니다. thread의 fault 상태를 보관하고 signal을 전달한 뒤에도 architecture entry가 요구하는 IRQ 상태를 맞추어야 합니다. fault는 PTE를 고치는 함수 하나가 아니라 exception 진입·재시작 protocol 전체입니다.
Linux v6.0의 user fault와 bad-area 전체 경로
lock을 얻는 지점에서 retry label, signal 분기와 각 return까지 이어서 확인할 수 있도록 함수 전체를 싣습니다. 이 코드는 per-VMA lock 도입 전의 구현이며 위에서 설명한 v6.18 경로와 구별합니다.
아래는 Linux v6.0의 전체 함수·자료형 정의입니다. kernel의 header·configuration·호출 규약에 의존하는 코드이며 독립 실행 프로그램은 아닙니다. 본문 해설과는 별도로 각 source의 SPDX license를 적용합니다. 원래 copyright notice는 source notice에, GPL version 2 전문은 license 파일에 보존합니다. 표시상의 tab을 space로 바꾼 것 외에 함수 내부의 문장이나 configuration 분기는 제거하지 않았습니다.
do_user_addr_fault()
Linux v6.0 arch/x86/mm/fault.c, SPDX GPL-2.0입니다.
static inline
void do_user_addr_fault(struct pt_regs *regs,
unsigned long error_code,
unsigned long address)
{
struct vm_area_struct *vma;
struct task_struct *tsk;
struct mm_struct *mm;
vm_fault_t fault;
unsigned int flags = FAULT_FLAG_DEFAULT;
tsk = current;
mm = tsk->mm;
if (unlikely((error_code & (X86_PF_USER | X86_PF_INSTR)) == X86_PF_INSTR)) {
/*
* Whoops, this is kernel mode code trying to execute from
* user memory. Unless this is AMD erratum #93, which
* corrupts RIP such that it looks like a user address,
* this is unrecoverable. Don't even try to look up the
* VMA or look for extable entries.
*/
if (is_errata93(regs, address))
return;
page_fault_oops(regs, error_code, address);
return;
}
/* kprobes don't want to hook the spurious faults: */
if (WARN_ON_ONCE(kprobe_page_fault(regs, X86_TRAP_PF)))
return;
/*
* Reserved bits are never expected to be set on
* entries in the user portion of the page tables.
*/
if (unlikely(error_code & X86_PF_RSVD))
pgtable_bad(regs, error_code, address);
/*
* If SMAP is on, check for invalid kernel (supervisor) access to user
* pages in the user address space. The odd case here is WRUSS,
* which, according to the preliminary documentation, does not respect
* SMAP and will have the USER bit set so, in all cases, SMAP
* enforcement appears to be consistent with the USER bit.
*/
if (unlikely(cpu_feature_enabled(X86_FEATURE_SMAP) &&
!(error_code & X86_PF_USER) &&
!(regs->flags & X86_EFLAGS_AC))) {
/*
* No extable entry here. This was a kernel access to an
* invalid pointer. get_kernel_nofault() will not get here.
*/
page_fault_oops(regs, error_code, address);
return;
}
/*
* If we're in an interrupt, have no user context or are running
* in a region with pagefaults disabled then we must not take the fault
*/
if (unlikely(faulthandler_disabled() || !mm)) {
bad_area_nosemaphore(regs, error_code, address);
return;
}
/*
* It's safe to allow irq's after cr2 has been saved and the
* vmalloc fault has been handled.
*
* User-mode registers count as a user access even for any
* potential system fault or CPU buglet:
*/
if (user_mode(regs)) {
local_irq_enable();
flags |= FAULT_FLAG_USER;
} else {
if (regs->flags & X86_EFLAGS_IF)
local_irq_enable();
}
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, address);
if (error_code & X86_PF_WRITE)
flags |= FAULT_FLAG_WRITE;
if (error_code & X86_PF_INSTR)
flags |= FAULT_FLAG_INSTRUCTION;
#ifdef CONFIG_X86_64
/*
* Faults in the vsyscall page might need emulation. The
* vsyscall page is at a high address (>PAGE_OFFSET), but is
* considered to be part of the user address space.
*
* The vsyscall page does not have a "real" VMA, so do this
* emulation before we go searching for VMAs.
*
* PKRU never rejects instruction fetches, so we don't need
* to consider the PF_PK bit.
*/
if (is_vsyscall_vaddr(address)) {
if (emulate_vsyscall(error_code, regs, address))
return;
}
#endif
/*
* Kernel-mode access to the user address space should only occur
* on well-defined single instructions listed in the exception
* tables. But, an erroneous kernel fault occurring outside one of
* those areas which also holds mmap_lock might deadlock attempting
* to validate the fault against the address space.
*
* Only do the expensive exception table search when we might be at
* risk of a deadlock. This happens if we
* 1. Failed to acquire mmap_lock, and
* 2. The access did not originate in userspace.
*/
if (unlikely(!mmap_read_trylock(mm))) {
if (!user_mode(regs) && !search_exception_tables(regs->ip)) {
/*
* Fault from code in kernel from
* which we do not expect faults.
*/
bad_area_nosemaphore(regs, error_code, address);
return;
}
retry:
mmap_read_lock(mm);
} else {
/*
* The above down_read_trylock() might have succeeded in
* which case we'll have missed the might_sleep() from
* down_read():
*/
might_sleep();
}
vma = find_vma(mm, address);
if (unlikely(!vma)) {
bad_area(regs, error_code, address);
return;
}
if (likely(vma->vm_start <= address))
goto good_area;
if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
bad_area(regs, error_code, address);
return;
}
if (unlikely(expand_stack(vma, address))) {
bad_area(regs, error_code, address);
return;
}
/*
* Ok, we have a good vm_area for this memory access, so
* we can handle it..
*/
good_area:
if (unlikely(access_error(error_code, vma))) {
bad_area_access_error(regs, error_code, address, vma);
return;
}
/*
* If for any reason at all we couldn't handle the fault,
* make sure we exit gracefully rather than endlessly redo
* the fault. Since we never set FAULT_FLAG_RETRY_NOWAIT, if
* we get VM_FAULT_RETRY back, the mmap_lock has been unlocked.
*
* Note that handle_userfault() may also release and reacquire mmap_lock
* (and not return with VM_FAULT_RETRY), when returning to userland to
* repeat the page fault later with a VM_FAULT_NOPAGE retval
* (potentially after handling any pending signal during the return to
* userland). The return to userland is identified whenever
* FAULT_FLAG_USER|FAULT_FLAG_KILLABLE are both set in flags.
*/
fault = handle_mm_fault(vma, address, flags, regs);
if (fault_signal_pending(fault, regs)) {
/*
* Quick path to respond to signals. The core mm code
* has unlocked the mm for us if we get here.
*/
if (!user_mode(regs))
kernelmode_fixup_or_oops(regs, error_code, address,
SIGBUS, BUS_ADRERR,
ARCH_DEFAULT_PKEY);
return;
}
/* The fault is fully completed (including releasing mmap lock) */
if (fault & VM_FAULT_COMPLETED)
return;
/*
* If we need to retry the mmap_lock has already been released,
* and if there is a fatal signal pending there is no guarantee
* that we made any progress. Handle this case first.
*/
if (unlikely(fault & VM_FAULT_RETRY)) {
flags |= FAULT_FLAG_TRIED;
goto retry;
}
mmap_read_unlock(mm);
if (likely(!(fault & VM_FAULT_ERROR)))
return;
if (fatal_signal_pending(current) && !user_mode(regs)) {
kernelmode_fixup_or_oops(regs, error_code, address,
0, 0, ARCH_DEFAULT_PKEY);
return;
}
if (fault & VM_FAULT_OOM) {
/* Kernel mode? Handle exceptions or die: */
if (!user_mode(regs)) {
kernelmode_fixup_or_oops(regs, error_code, address,
SIGSEGV, SEGV_MAPERR,
ARCH_DEFAULT_PKEY);
return;
}
/*
* We ran out of memory, call the OOM killer, and return the
* userspace (which will retry the fault, or kill us if we got
* oom-killed):
*/
pagefault_out_of_memory();
} else {
if (fault & (VM_FAULT_SIGBUS|VM_FAULT_HWPOISON|
VM_FAULT_HWPOISON_LARGE))
do_sigbus(regs, error_code, address, fault);
else if (fault & VM_FAULT_SIGSEGV)
bad_area_nosemaphore(regs, error_code, address);
else
BUG();
}
}
access_error()
Linux v6.0 arch/x86/mm/fault.c, SPDX GPL-2.0입니다.
static inline int
access_error(unsigned long error_code, struct vm_area_struct *vma)
{
/* This is only called for the current mm, so: */
bool foreign = false;
/*
* Read or write was blocked by protection keys. This is
* always an unconditional error and can never result in
* a follow-up action to resolve the fault, like a COW.
*/
if (error_code & X86_PF_PK)
return 1;
/*
* SGX hardware blocked the access. This usually happens
* when the enclave memory contents have been destroyed, like
* after a suspend/resume cycle. In any case, the kernel can't
* fix the cause of the fault. Handle the fault as an access
* error even in cases where no actual access violation
* occurred. This allows userspace to rebuild the enclave in
* response to the signal.
*/
if (unlikely(error_code & X86_PF_SGX))
return 1;
/*
* Make sure to check the VMA so that we do not perform
* faults just to hit a X86_PF_PK as soon as we fill in a
* page.
*/
if (!arch_vma_access_permitted(vma, (error_code & X86_PF_WRITE),
(error_code & X86_PF_INSTR), foreign))
return 1;
if (error_code & X86_PF_WRITE) {
/* write, present and write, not present: */
if (unlikely(!(vma->vm_flags & VM_WRITE)))
return 1;
return 0;
}
/* read, present: */
if (unlikely(error_code & X86_PF_PROT))
return 1;
/* read, not present: */
if (unlikely(!vma_is_accessible(vma)))
return 1;
return 0;
}
__bad_area_nosemaphore()
Linux v6.0 arch/x86/mm/fault.c, SPDX GPL-2.0입니다.
static void
__bad_area_nosemaphore(struct pt_regs *regs, unsigned long error_code,
unsigned long address, u32 pkey, int si_code)
{
struct task_struct *tsk = current;
if (!user_mode(regs)) {
kernelmode_fixup_or_oops(regs, error_code, address,
SIGSEGV, si_code, pkey);
return;
}
if (!(error_code & X86_PF_USER)) {
/* Implicit user access to kernel memory -- just oops */
page_fault_oops(regs, error_code, address);
return;
}
/*
* User mode accesses just cause a SIGSEGV.
* It's possible to have interrupts off here:
*/
local_irq_enable();
/*
* Valid to do another page fault here because this one came
* from user space:
*/
if (is_prefetch(regs, error_code, address))
return;
if (is_errata100(regs, address))
return;
sanitize_error_code(address, &error_code);
if (fixup_vdso_exception(regs, X86_TRAP_PF, error_code, address))
return;
if (likely(show_unhandled_signals))
show_signal_msg(regs, error_code, address, tsk);
set_signal_archinfo(address, error_code);
if (si_code == SEGV_PKUERR)
force_sig_pkuerr((void __user *)address, pkey);
else
force_sig_fault(SIGSEGV, si_code, (void __user *)address);
local_irq_disable();
}
sanitize_error_code()
Linux v6.0 arch/x86/mm/fault.c, SPDX GPL-2.0입니다.
static void sanitize_error_code(unsigned long address,
unsigned long *error_code)
{
/*
* To avoid leaking information about the kernel page
* table layout, pretend that user-mode accesses to
* kernel addresses are always protection faults.
*
* NB: This means that failed vsyscalls with vsyscall=none
* will have the PROT bit. This doesn't leak any
* information and does not appear to cause any problems.
*/
if (address >= TASK_SIZE_MAX)
*error_code |= X86_PF_PROT;
}
show_signal_msg()
Linux v6.0 arch/x86/mm/fault.c, SPDX GPL-2.0입니다.
static inline void
show_signal_msg(struct pt_regs *regs, unsigned long error_code,
unsigned long address, struct task_struct *tsk)
{
const char *loglvl = task_pid_nr(tsk) > 1 ? KERN_INFO : KERN_EMERG;
if (!unhandled_signal(tsk, SIGSEGV))
return;
if (!printk_ratelimit())
return;
printk("%s%s[%d]: segfault at %lx ip %px sp %px error %lx",
loglvl, tsk->comm, task_pid_nr(tsk), address,
(void *)regs->ip, (void *)regs->sp, error_code);
print_vma_addr(KERN_CONT " in ", regs->ip);
printk(KERN_CONT "\n");
show_opcodes(regs, loglvl);
}
fault_signal_pending()
Linux v6.0 include/linux/sched/signal.h, SPDX GPL-2.0입니다.
static inline bool fault_signal_pending(vm_fault_t fault_flags,
struct pt_regs *regs)
{
return unlikely((fault_flags & VM_FAULT_RETRY) &&
(fatal_signal_pending(current) ||
(user_mode(regs) && signal_pending(current))));
}
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 6 §6.1.2–§6.1.5, §6.11. Linux v6.18 arch/x86/mm/fault.c, fault flag 정의, Process Addresses를 참고하실 수 있습니다.