kernel이 user pointer를 받았다고 그 주소를 평범한 memcpy()의 입력으로 사용할 수는 없습니다. 주소가 유효하지 않을 수 있고, copy 도중 mapping이 바뀔 수 있으며, 아직 backing이 없어 fault가 필요할 수도 있습니다. uaccess는 이런 접근을 복구 가능한 instruction으로 수행하는 interface입니다.
access_ok는 mapping reservation이 아닙니다
__user는 정적 분석을 돕는 annotation이며 runtime lock이 아닙니다. access_ok()도 지정 범위가 user address로 허용되는지 확인하는 초기 검사입니다. 그 주소에 VMA와 resident page가 계속 존재한다는 보장은 아닙니다. copy가 시작된 뒤 page fault와 접근 실패가 여전히 가능합니다.
wrapper의 __must_check는 반환값을 무시했을 때 compiler 진단을 받을 수 있게 하는 표식입니다. raw wrapper의 cast에 쓰인 __force는 address-space 정적 분석을 위한 명시적 변환이며, runtime 접근 권한이나 fault 복구를 새로 부여하지 않습니다. 이 두 annotation도 반환값 검사와 올바른 uaccess 진입을 대신하지 않습니다.
copy_from_user(to, from, n)와 copy_to_user()는 복사하지 못한 byte 수를 반환합니다. 0이면 전체 성공이고 양수면 short copy입니다. 이 값은 negative errno가 아니므로 syscall의 error 반환으로 그대로 쓰지 않습니다. caller는 부분 전달을 허용할지와 -EFAULT 처리 등을 자신의 API에 맞게 결정해야 합니다.
zero padding은 양방향 공통 동작이 아닙니다
일반 copy_from_user()는 short copy에서 kernel destination의 미복사 tail을 zero-pad합니다. raw copy와 __copy_from_user()·inatomic variant는 같은 보장을 하지 않습니다. copy_to_user()가 남은 user destination을 자동 zeroing한다는 뜻도 아닙니다. 반환값 검사를 생략해도 된다는 결론은 어느 경우에도 나오지 않습니다.
object-size 검사, instrumentation과 usercopy fault injection은 wrapper 계층에 있습니다. might_fault()는 fault 가능한 문맥을 debug하는 표식이지 page를 미리 준비하거나 spinlock 아래 sleep을 허용하는 기능이 아닙니다. raw routine은 wrapper가 검사한 계약을 전제로 하므로 편의를 위해 직접 호출하는 대체 API가 아닙니다.
instruction pointer로 복구 지점을 찾습니다
x86 copy routine은 CPU 기능에 따라 적합한 assembly implementation을 선택합니다. 핵심은 copy instruction과 exception fixup entry가 함께 정의된다는 점입니다. fault를 정상 demand paging으로 해결하면 같은 instruction을 재개합니다. 해결할 수 없으면 exception table에서 faulting instruction의 entry를 찾아 미복사량을 돌려주는 경로로 register 상태를 고칩니다.
exception table의 key는 잘못 접근한 user address 그 자체가 아니라 faulting kernel instruction 위치입니다. entry는 instruction·fixup 주소를 상대 offset으로 보관할 수 있으며 binary search와 변환 helper가 실제 주소를 복원합니다. kernel, module과 BPF의 table 검색 범위도 고려합니다.
복구할 수 있는 fault와 oops의 경계
kernelmode_fixup_or_oops() 계열은 복구 entry가 있는지 확인합니다. 등록된 uaccess instruction의 잘못된 user 접근은 copy 실패로 돌아올 수 있지만, 임의 kernel pointer dereference에 같은 보호가 붙지는 않습니다. interrupt·atomic context에서 일반 user copy가 필요로 하는 fault 대기를 수행할 수 있는지도 별도 제약입니다.
uaccess는 여러 byte를 복사하는 동안 user data를 snapshot으로 고정하지 않습니다. caller가 header와 payload의 일관성을 요구하면 별도의 검증·복사 전략이 필요합니다. copy_to_user_page()처럼 kernel alias를 통해 user folio를 복사하며 cache maintenance를 수행하는 API는 일반 user-pointer copy와도 다른 계약입니다. x86에서 일부 hook이 no-op이라는 사실을 모든 architecture에 적용하지 않습니다.
두 wrapper와 실제 copy 경로를 이어 읽습니다
아래 code는 official Linux v6.0의 전체 explicit definition입니다. Historical layout reference이며 책 전체의 exact tag를 증명한 것은 아닙니다. 원래 copyright notice와 COPYING, GPL-2.0를 보존했습니다. Kernel context가 필요한 source로, standalone build나 runtime 검증은 하지 않았습니다.
/* SPDX-License-Identifier: GPL-2.0 */
/* Upstream Linux v6.0: include/linux/uaccess.h:148-154
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static __always_inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (check_copy_size(to, n, false))
n = _copy_from_user(to, from, n);
return n;
}
/* SPDX-License-Identifier: GPL-2.0 */
/* Upstream Linux v6.0: include/linux/uaccess.h:156-162
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (check_copy_size(from, n, true))
n = _copy_to_user(to, from, n);
return n;
}
// SPDX-License-Identifier: GPL-2.0
/* Upstream Linux v6.0: lib/usercopy.c:10-21
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
unsigned long _copy_from_user(void *to, const void __user *from, unsigned long n)
{
unsigned long res = n;
might_fault();
if (!should_fail_usercopy() && likely(access_ok(from, n))) {
instrument_copy_from_user(to, from, n);
res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
memset(to + (n - res), 0, res);
return res;
}
// SPDX-License-Identifier: GPL-2.0
/* Upstream Linux v6.0: lib/usercopy.c:26-36
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
unsigned long _copy_to_user(void __user *to, const void *from, unsigned long n)
{
might_fault();
if (should_fail_usercopy())
return n;
if (likely(access_ok(to, n))) {
instrument_copy_to_user(to, from, n);
n = raw_copy_to_user(to, from, n);
}
return n;
}
_copy_from_user()는 실제 미복사량 res를 기준으로 kernel buffer tail을 지웁니다. 반면 _copy_to_user()에는 대응 memset이 없습니다. Size 검사를 통과하지 못한 바깥 wrapper, raw variant까지 모두 같은 zero-fill 계약이라고 일반화해서도 안 됩니다. Fault injection으로 실패했을 수 있으므로 양수 반환을 반드시 hardware page fault가 발생했다는 관찰로 해석하지 않겠습니다.
/* SPDX-License-Identifier: GPL-2.0 */
/* Upstream Linux v6.0: arch/x86/include/asm/uaccess_64.h:49-53
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static __always_inline __must_check unsigned long
raw_copy_from_user(void *dst, const void __user *src, unsigned long size)
{
return copy_user_generic(dst, (__force void *)src, size);
}
/* SPDX-License-Identifier: GPL-2.0 */
/* Upstream Linux v6.0: arch/x86/include/asm/uaccess_64.h:55-59
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static __always_inline __must_check unsigned long
raw_copy_to_user(void __user *dst, const void *src, unsigned long size)
{
return copy_user_generic((__force void *)dst, src, size);
}
/* SPDX-License-Identifier: GPL-2.0 */
/* Upstream Linux v6.0: arch/x86/include/asm/uaccess_64.h:27-47
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static __always_inline __must_check unsigned long
copy_user_generic(void *to, const void *from, unsigned len)
{
unsigned ret;
/*
* If CPU has ERMS feature, use copy_user_enhanced_fast_string.
* Otherwise, if CPU has rep_good feature, use copy_user_generic_string.
* Otherwise, use copy_user_generic_unrolled.
*/
alternative_call_2(copy_user_generic_unrolled,
copy_user_generic_string,
X86_FEATURE_REP_GOOD,
copy_user_enhanced_fast_string,
X86_FEATURE_ERMS,
ASM_OUTPUT2("=a" (ret), "=D" (to), "=S" (from),
"=d" (len)),
"1" (to), "2" (from), "3" (len)
: "memory", "rcx", "r8", "r9", "r10", "r11");
return ret;
}
Assembly 함수의 끝에도 recovery 경로가 있습니다
copy_user_generic()의 alternative call은 CPU feature에 맞는 implementation을 선택합니다. 아래 assembly 정의는 SYM_FUNC_START부터 SYM_FUNC_END까지 보존했습니다. 그러나 함수 밖의 short-copy·tail label, assembler macro와 linker가 필요하므로 이것만 별도로 assemble할 수 있는 완성 program은 아닙니다.
copy_user_enhanced_fast_string.S
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Copyright 2008 Vitaly Mayatskikh <vmayatsk@redhat.com>
* Copyright 2002 Andi Kleen, SuSE Labs.
*
* Functions to copy from and to user space.
*/
/* Upstream Linux v6.0: arch/x86/lib/copy_user_64.S:161-175
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
SYM_FUNC_START(copy_user_enhanced_fast_string)
ASM_STAC
/* CPUs without FSRM should avoid rep movsb for short copies */
ALTERNATIVE "cmpl $64, %edx; jb copy_user_short_string", "", X86_FEATURE_FSRM
movl %edx,%ecx
1: rep movsb
xorl %eax,%eax
ASM_CLAC
RET
12: movl %ecx,%edx /* ecx is zerorest also */
jmp .Lcopy_user_handle_tail
_ASM_EXTABLE_CPY(1b, 12b)
SYM_FUNC_END(copy_user_enhanced_fast_string)
/* SPDX-License-Identifier: GPL-2.0 */
/* Upstream Linux v6.0: arch/x86/include/asm/asm.h:135-141
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
# define _ASM_EXTABLE_TYPE(from, to, type) \
.pushsection "__ex_table","a" ; \
.balign 4 ; \
.long (from) - . ; \
.long (to) - . ; \
.long type ; \
.popsection
rep movsb의 정상 완료와 exception fixup은 반환 register를 다르게 준비합니다. Exception table의 상대 offset은 각 field 위치를 기준으로 계산합니다. User buffer 범위를 table에 허용 목록으로 적는 구조가 아닙니다. 다음 두 helper의 기준 주소가 각각 &x->insn, &x->fixup인 점을 보시면 구별할 수 있습니다.
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Derived from arch/ppc/mm/extable.c and arch/i386/mm/extable.c.
*
* Copyright (C) 2004 Paul Mackerras, IBM Corp.
*/
/* Upstream Linux v6.0: lib/extable.c:18-21
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static inline unsigned long ex_to_insn(const struct exception_table_entry *x)
{
return (unsigned long)&x->insn + x->insn;
}
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: arch/x86/mm/extable.c:26-30
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static inline unsigned long
ex_fixup_addr(const struct exception_table_entry *x)
{
return (unsigned long)&x->fixup + x->fixup;
}
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 1995 Linus Torvalds
* Copyright (C) 2001, 2002 Andi Kleen, SuSE Labs.
* Copyright (C) 2008-2009, Red Hat Inc., Ingo Molnar
*/
/* Upstream Linux v6.0: arch/x86/mm/fault.c:711-761
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
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);
}
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: arch/x86/mm/extable.c:205-277
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
int fixup_exception(struct pt_regs *regs, int trapnr, unsigned long error_code,
unsigned long fault_addr)
{
const struct exception_table_entry *e;
int type, reg, imm;
#ifdef CONFIG_PNPBIOS
if (unlikely(SEGMENT_IS_PNP_CODE(regs->cs))) {
extern u32 pnp_bios_fault_eip, pnp_bios_fault_esp;
extern u32 pnp_bios_is_utter_crap;
pnp_bios_is_utter_crap = 1;
printk(KERN_CRIT "PNPBIOS fault.. attempting recovery.\n");
__asm__ volatile(
"movl %0, %%esp\n\t"
"jmp *%1\n\t"
: : "g" (pnp_bios_fault_esp), "g" (pnp_bios_fault_eip));
panic("do_trap: can't hit this");
}
#endif
e = search_exception_tables(regs->ip);
if (!e)
return 0;
type = FIELD_GET(EX_DATA_TYPE_MASK, e->data);
reg = FIELD_GET(EX_DATA_REG_MASK, e->data);
imm = FIELD_GET(EX_DATA_IMM_MASK, e->data);
switch (type) {
case EX_TYPE_DEFAULT:
case EX_TYPE_DEFAULT_MCE_SAFE:
return ex_handler_default(e, regs);
case EX_TYPE_FAULT:
case EX_TYPE_FAULT_MCE_SAFE:
return ex_handler_fault(e, regs, trapnr);
case EX_TYPE_UACCESS:
return ex_handler_uaccess(e, regs, trapnr);
case EX_TYPE_COPY:
return ex_handler_copy(e, regs, trapnr);
case EX_TYPE_CLEAR_FS:
return ex_handler_clear_fs(e, regs);
case EX_TYPE_FPU_RESTORE:
return ex_handler_fprestore(e, regs);
case EX_TYPE_BPF:
return ex_handler_bpf(e, regs);
case EX_TYPE_WRMSR:
return ex_handler_msr(e, regs, true, false, reg);
case EX_TYPE_RDMSR:
return ex_handler_msr(e, regs, false, false, reg);
case EX_TYPE_WRMSR_SAFE:
return ex_handler_msr(e, regs, true, true, reg);
case EX_TYPE_RDMSR_SAFE:
return ex_handler_msr(e, regs, false, true, reg);
case EX_TYPE_WRMSR_IN_MCE:
ex_handler_msr_mce(regs, true);
break;
case EX_TYPE_RDMSR_IN_MCE:
ex_handler_msr_mce(regs, false);
break;
case EX_TYPE_POP_REG:
regs->sp += sizeof(long);
fallthrough;
case EX_TYPE_IMM_REG:
return ex_handler_imm_reg(e, regs, reg, imm);
case EX_TYPE_FAULT_SGX:
return ex_handler_sgx(e, regs, trapnr);
case EX_TYPE_UCOPY_LEN:
return ex_handler_ucopy_len(e, regs, trapnr, reg, imm);
case EX_TYPE_ZEROPAD:
return ex_handler_zeropad(e, regs, fault_addr);
}
BUG();
}
Book listing에서 빠진 exception type·signal 관련 분기까지 공식 source 그대로 포함했습니다. 복구가 허용된 instruction인지와 어떤 register fixup을 적용할지는 구분된 판단입니다. 단순히 “kernel fault도 복구된다”만 기억하면 임의 user pointer dereference를 정당화하게 됩니다.
Source: v6.0 uaccess.h, lib/usercopy.c, uaccess_64.h, copy_user_64.S, asm.h, fault.c, extable.c, lib/extable.c. 아래 v6.18은 latest나 book exact tag가 아닌 별도 비교 자료입니다.
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 8 §8.1–§8.1.1. 반환값과 padding은 Linux v6.18 include/linux/uaccess.h, 복구는 arch/x86/mm/extable.c, lib/extable.c에 있습니다.