read()가 user buffer에 data를 복사하다 page fault를 만났다고 해서 kernel bug가 발생한 것은 아닙니다. CPU가 kernel mode로 실행 중이라는 사실과 faulting address가 kernel 영역이라는 사실은 다릅니다. 이 두 좌표를 먼저 분리해야 정상적인 demand paging과 복구할 수 없는 잘못된 접근을 구별할 수 있습니다.
주소의 소유 영역과 실행 mode
VMA는 주소 범위와 접근 정책을 먼저 확보하고, PTE와 실제 backing은 나중에 준비할 수 있습니다. MMU가 translation이나 permission 문제를 발견하면 fault handler가 그 접근의 정당성을 판단합니다. 정당한 접근이면 필요한 translation을 만든 뒤 원래 instruction을 다시 실행합니다. anonymous fault는 새 backing을 공급할 수 있고 file fault는 이미 존재하는 page cache를 연결할 수도 있으므로, 모든 fault가 새 data page allocation이라는 뜻은 아닙니다.
미리 population하는 경우에도 read와 write를 구별해야 합니다. MAP_POPULATE를 무조건 write fault와 동일시할 수는 없습니다. private writable VMA에서는 CoW를 미리 해소하려는 write 의도가 사용되지만, shared나 read-only mapping까지 같은 방식으로 처리하지는 않습니다. 또한 population 실패와 mmap() 자체의 성공 여부가 같은 계약은 아닙니다. 이 구별은 v6.18 mm/gup.c의 populate_vma_page_range()를 기준으로 합니다.
x86-64의 hardware exception은 fault address와 error code, 당시 register 상태를 handler에 전달합니다. error code에는 write인지 instruction fetch인지, protection 위반인지, reserved bit 문제인지 같은 정보가 있습니다. architecture entry는 이 정보를 공통 memory-management fault 처리로 연결합니다.
kernel address의 fault는 일반적인 user demand paging 대상이 아닙니다. 반면 user address의 fault는 application load/store뿐 아니라 kernel의 uaccess에서도 발생합니다. 후자는 VMA와 page table을 정상적으로 준비하면 원래 copy instruction으로 돌아갈 수 있습니다. legacy vsyscall 영역은 주소 숫자만으로 단순 분류할 수 없는 별도 처리 대상입니다.
주소와 실행 mode를 다음처럼 나누어 읽으시면 분기 기준을 놓치지 않습니다. 이 표는 v6.0 x86의 대표 경로이며 instrumentation과 architecture 예외는 아래 전체 함수에서 확인합니다.
| fault address | 당시 실행 상태 | 먼저 확인할 조건 | 가능한 다음 경로 |
|---|---|---|---|
| 일반 user 영역 | user mode | VMA 포함 여부, stack 성장, VMA access permission | 공통 fault 처리 또는 user signal |
| 일반 user 영역 | kernel mode의 uaccess | fault 허용 context와 exception-table 복구 가능성, VMA permission | 공통 fault 처리 또는 fixup/oops |
| kernel 영역 | kernel mode | 이미 허용된 permission의 stale TLB인지, 복구 entry가 있는지 | 특수 복구 또는 oops |
| kernel 영역 | user mode | 허용된 legacy vsyscall 예외인지 | 예외 처리 또는 bad-area signal |
stale protection과 실제 금지 접근
여기서 TLB는 Translation Lookaside Buffer입니다. permission을 RO에서 RW로, NX에서 executable로 완화했는데 CPU에 더 엄격한 과거 entry가 남아 있으면 fault를 계기로 그 상태를 갱신할 수 있습니다. 반대로 permission을 줄인 변경을 같은 논리로 지연해도 된다는 뜻은 아닙니다. v6.0의 spurious_kernel_fault()는 error code가 WRITE|PROT 또는 INSTR|PROT와 정확히 맞는 경우만 받으며, non-present나 reserved-bit fault를 이 성공 경로에 포함하지 않습니다.
일부 kernel fault는 page table이 이미 허용하도록 바뀌었는데 TLB에 이전 restrictive permission이 남아서 발생할 수 있습니다. spurious_kernel_fault()는 이 가능성을 확인합니다. PGD부터 PTE까지 present 여부를 확인하고 huge leaf에서는 해당 level에서 멈추며, write·execute permission이 실제로 허용되는지 봅니다.
이 처리는 임의의 kernel fault를 무시하는 기능이 아닙니다. table에서도 read-only인데 write했다면 복구할 근거가 없습니다. 상위 level과 leaf permission이 일치하는지도 중요합니다. stale TLB는 translation cache의 문제이며 file data cache가 오래됐다는 뜻이 아닙니다.
복구 경로가 등록된 instruction
kernel address의 fault에서 무작정 mmap_lock을 얻지 않는 것도 복구 규칙의 일부입니다. 이미 lock을 가진 kernel 경로나 prefetch 관련 예외에서 다시 잠들면 deadlock을 만들 수 있습니다. 아래 v6.0 전체 정의에 들어 있는 x86-32 vmalloc_fault() 분기는 master page table의 변경을 동기화하는 별도 경우이며, x86-64의 일반 demand allocation으로 읽어서는 안 됩니다. kmmio와 kprobes도 정상 fault dispatch보다 먼저 처리하거나 우회하는 hook입니다.
일반 user 주소를 잘못 읽었다면 fixup_exception()이 exception table을 검색해 복구할 수 있습니다. uaccess copy routine은 fault 가능한 instruction과 fixup destination을 명시합니다. 복구 가능한 copy는 나머지 byte를 복사하지 못했다는 결과로 돌아오며, 임의의 C pointer dereference에는 이런 계약이 자동으로 붙지 않습니다.
복구 entry가 없고 허용된 특수 fault도 아니면 oops 경로로 갑니다. vmalloc kernel stack의 guard-page overflow, EFI firmware 관련 예외, KFENCE의 비 present 감시 등은 별도 진단 기회를 가집니다. prefetch와 특정 CPU errata에 관한 예외도 모든 protection 위반에 일반화할 수 없습니다.
oops와 process signal을 구별합니다
fixup_exception()이 성공하면 interrupt context에서는 즉시 복귀합니다. task context에서는 sig_on_uaccess_err와 전달할 signal이 모두 있는 경우에만 architecture fault 정보를 정리하고 signal을 강제합니다. exception fixup에 실패한 경우에도 AMD prefetch erratum 검사는 oops보다 먼저입니다. 따라서 fixup 가능 여부와 signal 전송 여부는 같은 boolean이 아닙니다.
page_fault_oops()의 VMAP_STACK 처리는 단순히 guard를 보고 log를 찍는 데서 끝나지 않습니다. 남은 stack이 부족한 상태에서 다시 double fault를 내며 진단 출력까지 잃지 않도록 별도 stack에서 overflow 처리를 수행합니다. EFI firmware fault와 non-present KFENCE fault도 일반 oops 이전의 분기입니다. user-mode register 상태에서 발생한 implicit kernel access는 이 kernel-stack·firmware 검사를 건너뛸 수 있습니다.
oops는 kernel이 내부 불변식이 깨졌다고 판단한 진단입니다. 실행 중 task 종료로 이어질 수 있지만 항상 그 task 하나만 끝나는 것은 아닙니다. interrupt context, kernel thread나 panic 정책 등에 따라 system 전체를 멈출 수도 있습니다. application의 잘못된 접근에 보내는 보통의 SIGSEGV와 같은 error channel이 아닙니다.
실제 분석에서는 fault address, instruction pointer, user/kernel mode, error code와 exception-table 복구 가능성을 함께 보아야 합니다. “page fault가 났으니 RAM 부족”이나 “kernel mode이니 oops”라는 설명은 이 분기들을 모두 놓칩니다.
Linux v6.0의 kernel-address fault 전체 경로
주소 분류, stale-permission 검사와 fixup 뒤 oops까지의 함수를 함께 읽으시면 정상 복구와 오류 종료의 경계가 보입니다. x86-32와 instrumentation configuration 분기도 함수 안에서 생략하지 않았습니다.
아래는 Linux v6.0의 전체 함수·자료형 정의입니다. kernel의 header·configuration·호출 규약에 의존하는 코드이며 독립 실행 프로그램은 아닙니다. 본문 해설과는 별도로 각 source의 SPDX license를 적용합니다. 원래 copyright notice는 source notice에, GPL version 2 전문은 license 파일에 보존합니다. 표시상의 tab을 space로 바꾼 것 외에 함수 내부의 문장이나 configuration 분기는 제거하지 않았습니다.
handle_page_fault()
Linux v6.0 arch/x86/mm/fault.c, SPDX GPL-2.0입니다.
static __always_inline void
handle_page_fault(struct pt_regs *regs, unsigned long error_code,
unsigned long address)
{
trace_page_fault_entries(regs, error_code, address);
if (unlikely(kmmio_fault(regs, address)))
return;
/* Was the fault on kernel-controlled part of the address space? */
if (unlikely(fault_in_kernel_space(address))) {
do_kern_addr_fault(regs, error_code, address);
} else {
do_user_addr_fault(regs, error_code, address);
/*
* User address page fault handling might have reenabled
* interrupts. Fixing up all potential exit points of
* do_user_addr_fault() and its leaf functions is just not
* doable w/o creating an unholy mess or turning the code
* upside down.
*/
local_irq_disable();
}
}
fault_in_kernel_space()
Linux v6.0 arch/x86/mm/fault.c, SPDX GPL-2.0입니다.
bool fault_in_kernel_space(unsigned long address)
{
/*
* On 64-bit systems, the vsyscall page is at an address above
* TASK_SIZE_MAX, but is not considered part of the kernel
* address space.
*/
if (IS_ENABLED(CONFIG_X86_64) && is_vsyscall_vaddr(address))
return false;
return address >= TASK_SIZE_MAX;
}
do_kern_addr_fault()
Linux v6.0 arch/x86/mm/fault.c, SPDX GPL-2.0입니다.
static void
do_kern_addr_fault(struct pt_regs *regs, unsigned long hw_error_code,
unsigned long address)
{
/*
* Protection keys exceptions only happen on user pages. We
* have no user pages in the kernel portion of the address
* space, so do not expect them here.
*/
WARN_ON_ONCE(hw_error_code & X86_PF_PK);
#ifdef CONFIG_X86_32
/*
* We can fault-in kernel-space virtual memory on-demand. The
* 'reference' page table is init_mm.pgd.
*
* NOTE! We MUST NOT take any locks for this case. We may
* be in an interrupt or a critical region, and should
* only copy the information from the master page table,
* nothing more.
*
* Before doing this on-demand faulting, ensure that the
* fault is not any of the following:
* 1. A fault on a PTE with a reserved bit set.
* 2. A fault caused by a user-mode access. (Do not demand-
* fault kernel memory due to user-mode accesses).
* 3. A fault caused by a page-level protection violation.
* (A demand fault would be on a non-present page which
* would have X86_PF_PROT==0).
*
* This is only needed to close a race condition on x86-32 in
* the vmalloc mapping/unmapping code. See the comment above
* vmalloc_fault() for details. On x86-64 the race does not
* exist as the vmalloc mappings don't need to be synchronized
* there.
*/
if (!(hw_error_code & (X86_PF_RSVD | X86_PF_USER | X86_PF_PROT))) {
if (vmalloc_fault(address) >= 0)
return;
}
#endif
if (is_f00f_bug(regs, hw_error_code, address))
return;
/* Was the fault spurious, caused by lazy TLB invalidation? */
if (spurious_kernel_fault(hw_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;
/*
* Note, despite being a "bad area", there are quite a few
* acceptable reasons to get here, such as erratum fixups
* and handling kernel code that can fault, like get_user().
*
* Don't take the mm semaphore here. If we fixup a prefetch
* fault we could otherwise deadlock:
*/
bad_area_nosemaphore(regs, hw_error_code, address);
}
spurious_kernel_fault_check()
Linux v6.0 arch/x86/mm/fault.c, SPDX GPL-2.0입니다.
static int spurious_kernel_fault_check(unsigned long error_code, pte_t *pte)
{
if ((error_code & X86_PF_WRITE) && !pte_write(*pte))
return 0;
if ((error_code & X86_PF_INSTR) && !pte_exec(*pte))
return 0;
return 1;
}
spurious_kernel_fault()
Linux v6.0 arch/x86/mm/fault.c, SPDX GPL-2.0입니다.
static noinline int
spurious_kernel_fault(unsigned long error_code, unsigned long address)
{
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
int ret;
/*
* Only writes to RO or instruction fetches from NX may cause
* spurious faults.
*
* These could be from user or supervisor accesses but the TLB
* is only lazily flushed after a kernel mapping protection
* change, so user accesses are not expected to cause spurious
* faults.
*/
if (error_code != (X86_PF_WRITE | X86_PF_PROT) &&
error_code != (X86_PF_INSTR | X86_PF_PROT))
return 0;
pgd = init_mm.pgd + pgd_index(address);
if (!pgd_present(*pgd))
return 0;
p4d = p4d_offset(pgd, address);
if (!p4d_present(*p4d))
return 0;
if (p4d_large(*p4d))
return spurious_kernel_fault_check(error_code, (pte_t *) p4d);
pud = pud_offset(p4d, address);
if (!pud_present(*pud))
return 0;
if (pud_large(*pud))
return spurious_kernel_fault_check(error_code, (pte_t *) pud);
pmd = pmd_offset(pud, address);
if (!pmd_present(*pmd))
return 0;
if (pmd_large(*pmd))
return spurious_kernel_fault_check(error_code, (pte_t *) pmd);
pte = pte_offset_kernel(pmd, address);
if (!pte_present(*pte))
return 0;
ret = spurious_kernel_fault_check(error_code, pte);
if (!ret)
return 0;
/*
* Make sure we have permissions in PMD.
* If not, then there's a bug in the page tables:
*/
ret = spurious_kernel_fault_check(error_code, (pte_t *) pmd);
WARN_ONCE(!ret, "PMD has incorrect permission bits\n");
return ret;
}
kernelmode_fixup_or_oops()
Linux v6.0 arch/x86/mm/fault.c, SPDX GPL-2.0입니다.
static noinline void
kernelmode_fixup_or_oops(struct pt_regs *regs, unsigned long error_code,
unsigned long address, int signal, int si_code,
u32 pkey)
{
WARN_ON_ONCE(user_mode(regs));
/* Are we prepared to handle this kernel fault? */
if (fixup_exception(regs, X86_TRAP_PF, error_code, address)) {
/*
* Any interrupt that takes a fault gets the fixup. This makes
* the below recursive fault logic only apply to a faults from
* task context.
*/
if (in_interrupt())
return;
/*
* Per the above we're !in_interrupt(), aka. task context.
*
* In this case we need to make sure we're not recursively
* faulting through the emulate_vsyscall() logic.
*/
if (current->thread.sig_on_uaccess_err && signal) {
sanitize_error_code(address, &error_code);
set_signal_archinfo(address, error_code);
if (si_code == SEGV_PKUERR) {
force_sig_pkuerr((void __user *)address, pkey);
} else {
/* XXX: hwpoison faults will set the wrong code. */
force_sig_fault(signal, si_code, (void __user *)address);
}
}
/*
* Barring that, we can do the fixup and be happy.
*/
return;
}
/*
* AMD erratum #91 manifests as a spurious page fault on a PREFETCH
* instruction.
*/
if (is_prefetch(regs, error_code, address))
return;
page_fault_oops(regs, error_code, address);
}
page_fault_oops()
Linux v6.0 arch/x86/mm/fault.c, SPDX GPL-2.0입니다.
static noinline void
page_fault_oops(struct pt_regs *regs, unsigned long error_code,
unsigned long address)
{
#ifdef CONFIG_VMAP_STACK
struct stack_info info;
#endif
unsigned long flags;
int sig;
if (user_mode(regs)) {
/*
* Implicit kernel access from user mode? Skip the stack
* overflow and EFI special cases.
*/
goto oops;
}
#ifdef CONFIG_VMAP_STACK
/*
* Stack overflow? During boot, we can fault near the initial
* stack in the direct map, but that's not an overflow -- check
* that we're in vmalloc space to avoid this.
*/
if (is_vmalloc_addr((void *)address) &&
get_stack_guard_info((void *)address, &info)) {
/*
* We're likely to be running with very little stack space
* left. It's plausible that we'd hit this condition but
* double-fault even before we get this far, in which case
* we're fine: the double-fault handler will deal with it.
*
* We don't want to make it all the way into the oops code
* and then double-fault, though, because we're likely to
* break the console driver and lose most of the stack dump.
*/
call_on_stack(__this_cpu_ist_top_va(DF) - sizeof(void*),
handle_stack_overflow,
ASM_CALL_ARG3,
, [arg1] "r" (regs), [arg2] "r" (address), [arg3] "r" (&info));
unreachable();
}
#endif
/*
* Buggy firmware could access regions which might page fault. If
* this happens, EFI has a special OOPS path that will try to
* avoid hanging the system.
*/
if (IS_ENABLED(CONFIG_EFI))
efi_crash_gracefully_on_page_fault(address);
/* Only not-present faults should be handled by KFENCE. */
if (!(error_code & X86_PF_PROT) &&
kfence_handle_page_fault(address, error_code & X86_PF_WRITE, regs))
return;
oops:
/*
* Oops. The kernel tried to access some bad page. We'll have to
* terminate things with extreme prejudice:
*/
flags = oops_begin();
show_fault_oops(regs, error_code, address);
if (task_stack_end_corrupted(current))
printk(KERN_EMERG "Thread overran stack, or stack corrupted\n");
sig = SIGKILL;
if (__die("Oops", regs, error_code))
sig = 0;
/* Executive summary in case the body of the oops scrolled away */
printk(KERN_DEFAULT "CR2: %016lx\n", address);
oops_end(flags, regs, sig);
}
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 6 §6.1–§6.1.1. 현재 x86 경계 처리는 Linux v6.18 arch/x86/mm/fault.c, uaccess 복구는 arch/x86/mm/extable.c를 참고하실 수 있습니다.