사용자 공간 복사 (Usercopy)
개요 (Overview)
사용자 공간 복사(Usercopy)는 커널과 사용자 공간 간의 메모리 복사 시 안전성을 검증하는 하드닝 메커니즘입니다. CONFIG_HARDENED_USERCOPY 설정을 통해 활성화되며, copy_to_user()와 copy_from_user() 호출 시 커널 메모리가 부적절하게 노출되거나 오버플로우되는 것을 방지합니다. 마치 도서관에서 책을 빌려줄 때 중요한 서류가 섞여 있지 않은지 확인하는 것과 같습니다.
이 메커니즘은 PAX_USERCOPY에서 파생되었으며, 커널에서 사용자 공간으로 복사되는 데이터가 허용된 범위 내에 있는지 확인합니다. 스택 객체, 힙 객체, 커널 텍스트 영역에 대한 검증을 수행하며, 위반 시 BUG()를 발생시켜 보안 취약점을 차단합니다. 커널 메모리에서 민감한 정보(암호화 키, 포인터 등)가 사용자 공간으로 새어나가는 것을 막는 보안 게이트와 같습니다.
소스 파일 경로:
├── mm/usercopy.c ← 하드닝 검증 핵심 구현
├── include/linux/ucopysize.h ← check_copy_size/check_object_size
├── include/linux/uaccess.h ← copy_to_user/copy_from_user 인터페이스
├── include/linux/fault-inject-usercopy.h ← fault injection 지원
├── mm/slub.c:8044 ← __check_heap_object (SLAB 힙 검증)
└── include/linux/slab.h:412 ← kmem_cache_create_usercopy
빠른 점검 명령
# HARDENED_USERCOPY 활성화 여부 확인
zcat /proc/config.gz 2>/dev/null | grep HARDENED_USERCOPY || \
grep HARDENED_USERCOPY /boot/config-$(uname -r) 2>/dev/null || echo "확인 불가"
# hardened_usercopy 부팅 파라미터 확인
cat /proc/cmdline | grep -o "hardened_usercopy[= ]" || echo "기본값 사용"
# HARDENED_USERCOPY 관련 심볼 확인
cat /proc/kallsyms | grep -E "check_object_size|__check_object_size|usercopy_abort" | head -10
# 하드닝 위반 로그 확인
dmesg | grep -i "usercopy\|Kernel memory.*attempt" | head -10
# SLAB 사용자 복사 영역 확인 (kmem_cache_create_usercopy 사용 캐시)
cat /proc/slabinfo | head -2
slabtop -o | head -20
# Fault injection 활성화 여부 확인
zcat /proc/config.gz 2>/dev/null | grep FAULT_INJECTION_USERCOPY
# 커널에서 usercopy 관련 함수 사용처 확인
grep -r "copy_to_user\|copy_from_user" \
/home/ryotta205/Chamber_linux/linux-7.0-source/mm/ 2>/dev/null | wc -l
# 아키텍처별 uaccess 구현 확인
ls /home/ryotta205/Chamber_linux/linux-7.0-source/arch/*/include/asm/uaccess.h 2>/dev/null
# copy_struct_from_user 사용 예시 확인
grep -r "copy_struct_from_user\|copy_struct_to_user" \
/home/ryotta205/Chamber_linux/linux-7.0-source/ 2>/dev/null | head -10
# 사용자 복사 위반 아키텍처별 지원 확인
cat /proc/kallsyms | grep -E "arch_within_stack_frames" | head -5
# HARDENED_USERCOPY 기본값 확인
zcat /proc/config.gz 2>/dev/null | grep HARDENED_USERCOPY_DEFAULT_ON || \
grep HARDENED_USERCOPY_DEFAULT_ON /boot/config-$(uname -r) 2>/dev/null || echo "확인 불가"
# SLAB usercopy 캐시 목록 확인
cat /proc/slabinfo | grep -i "usercopy\|copy" || echo "해당 없음"
# 커널 로그에서 recent usercopy 위반 확인
journalctl -k | grep -i "usercopy" | tail -10
핵심 자료구조
1. `check_stack_object` 반환값 (mm/usercopy.c:37)
/* 스택 객체 검증 결과 분기 */
static noinline int check_stack_object(const void *obj, unsigned long len)
{
const void * const stack = task_stack_page(current); // 현재 프로세스 스택 시작
const void * const stackend = stack + THREAD_SIZE; // 스택 끝
int ret;
/* 객체가 스택에 전혀 없음 */
if (obj + len <= stack || stackend <= obj)
return NOT_STACK; // 스택 외부 객체
/* 객체가 스택과 부분적으로 겹침 (오류) */
if (obj < stack || stackend < obj + len)
return BAD_STACK;
/* 유효한 스택 프레임 내에 있는지 확인 */
ret = arch_within_stack_frames(stack, stackend, obj, len);
if (ret)
return ret;
/* 스택 깊이 검증 (아키텍처 지원 시) */
#ifdef CONFIG_ARCH_HAS_CURRENT_STACK_POINTER
if (IS_ENABLED(CONFIG_STACK_GROWSUP)) {
if ((void *)current_stack_pointer < obj + len)
return BAD_STACK;
} else {
if (obj < (void *)current_stack_pointer)
return BAD_STACK;
}
#endif
return GOOD_STACK; // 스택 내 안전 영역
}
// 반환값: NOT_STACK(0), GOOD_FRAME(1), GOOD_STACK(2), BAD_STACK(3)
// mm/usercopy.c:37-72
2. `__check_object_size` (mm/usercopy.c:223)
/* 사용자 복사 전 객체 안전성 검증 메인 함수 */
void __check_object_size(const void *ptr, unsigned long n, bool to_user)
{
/* 크기 0이면 모든 검증 건너뜀 */
if (!n)
return;
/* 1. 주소 유효성 검증 (래퍼, NULL, 제로 할당) */
check_bogus_address((const unsigned long)ptr, n, to_user);
/* 2. 스택 객체 검증 */
switch (check_stack_object(ptr, n)) {
case NOT_STACK:
/* 스택 외부 객체 - 계속 검증 */
break;
case GOOD_FRAME:
case GOOD_STACK:
/* 스택 내 안전 영역 - 검증 완료 */
return;
default:
/* BAD_STACK - 스택 오버플로우 의심 */
usercopy_abort("process stack", NULL, to_user,
(void *)current_stack_pointer - ptr, n);
}
/* 3. 힙 객체 검증 (SLAB, vmalloc, kmap) */
check_heap_object(ptr, n, to_user);
/* 4. 커널 텍스트 영역 노출 방지 */
check_kernel_text_object((const unsigned long)ptr, n, to_user);
}
// mm/usercopy.c:223-263
3. `__check_heap_object` (mm/slub.c:8044)
/* SLAB 힙 객체 사용자 복사 영역 검증 */
void __check_heap_object(const void *ptr, unsigned long n,
const struct slab *slab, bool to_user)
{
struct kmem_cache *s;
unsigned int offset;
bool is_kfence = is_kfence_address(ptr);
ptr = kasan_reset_tag(ptr); // KASAN 태그 제거
/* SLAB 캐시 찾기 */
s = slab->slab_cache;
/* 불가능한 포인터 거부 */
if (ptr < slab_address(slab))
usercopy_abort("SLUB object not in SLUB page?!", NULL,
to_user, 0, n);
/* 객체 내 오프셋 계산 */
if (is_kfence)
offset = ptr - kfence_object_start(ptr); // KFENCE 객체
else
offset = (ptr - slab_address(slab)) % s->size; // 일반 SLAB
/* Red zone 영역 검증 (디버그 모드) */
if (!is_kfence && kmem_cache_debug_flags(s, SLAB_RED_ZONE)) {
if (offset < s->red_left_pad)
usercopy_abort("SLUB object in left red zone",
s->name, to_user, offset, n);
offset -= s->red_left_pad;
}
/* 사용자 복사 허용 영역(whitelist) 검증 */
if (offset >= s->useroffset &&
offset - s->useroffset <= s->usersize &&
n <= s->useroffset - offset + s->usersize)
return; // 허용 영역 내
/* 허용 영역 외 접근 시 중단 */
usercopy_abort("SLUB object", s->name, to_user, offset, n);
}
// mm/slub.c:8044-8082
4. `usercopy_abort` (mm/usercopy.c:86)
/* 사용자 복사 위반 시 중단 함수 */
void __noreturn usercopy_abort(const char *name, const char *detail,
bool to_user, unsigned long offset,
unsigned long len)
{
/* 위반 유형별 로그 출력 */
pr_emerg("Kernel memory %s attempt detected %s %s%s%s%s (offset %lu, size %lu)!\n",
to_user ? "exposure" : "overwrite", // 노출 vs 오버라이트
to_user ? "from" : "to",
name ? : "unknown?!",
detail ? " '" : "", detail ? : "", detail ? "'" : "",
offset, len);
/* BUG()로 커널 패닉 유도 (보안 위반 시) */
BUG();
}
// mm/usercopy.c:86-103
5. `check_copy_size` (include/linux/ucopysize.h:44)
/* 복사 크기 컴파일타임/런타임 검증 */
static __always_inline __must_check bool
check_copy_size(const void *addr, size_t bytes, bool is_source)
{
/* 컴파일타임 객체 크기 검증 */
int sz = __builtin_object_size(addr, 0);
if (unlikely(sz >= 0 && sz < bytes)) {
if (!__builtin_constant_p(bytes))
copy_overflow(sz, bytes); // 런타임 오버플로우
else if (is_source)
__bad_copy_from(); // 소스 크기 부족
else
__bad_copy_to(); // 대상 크기 부족
return false;
}
/* INT_MAX 초과 검증 */
if (WARN_ON_ONCE(bytes > INT_MAX))
return false;
/* HARDENED_USERCOPY 런타임 검증 */
check_object_size(addr, bytes, is_source);
return true;
}
// include/linux/ucopysize.h:44-61
핵심 함수
1. `copy_from_user` (include/linux/uaccess.h:216)
static __always_inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
{
/* 크기 검증 (컴파일타임 + HARDENED_USERCOPY) */
if (!check_copy_size(to, n, false))
return n;
/* 아키텍처별 실제 복사 수행 */
#ifdef INLINE_COPY_FROM_USER
return _inline_copy_from_user(to, from, n);
#else
return _copy_from_user(to, from, n);
#endif
}
// copy_from_user(): 사용자空間 → 커널 공간 복사
// 실패 시 대상 버퍼를 0으로 패딩
2. `copy_to_user` (include/linux/uaccess.h:228)
static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
/* 크기 검증 (컴파일타임 + HARDENED_USERCOPY) */
if (!check_copy_size(from, n, true))
return n;
/* 아키텍처별 실제 복사 수행 */
#ifdef INLINE_COPY_TO_USER
return _inline_copy_to_user(to, from, n);
#else
return _copy_to_user(to, from, n);
#endif
}
// copy_to_user(): 커널 공간 → 사용자空間 복사
// 실패 시 패딩 없음 (호출자가 반드시 반환값 확인 필요)
3. `check_heap_object` (mm/usercopy.c:162)
static inline void check_heap_object(const void *ptr, unsigned long n,
bool to_user)
{
unsigned long addr = (unsigned long)ptr;
unsigned long offset;
struct page *page;
struct slab *slab;
/* kmap 영역 검증 */
if (is_kmap_addr(ptr)) {
offset = offset_in_page(ptr);
if (n > PAGE_SIZE - offset)
usercopy_abort("kmap", NULL, to_user, offset, n);
return;
}
/* vmalloc 영역 검증 */
if (is_vmalloc_addr(ptr) && !pagefault_disabled()) {
struct vmap_area *area = find_vmap_area(addr);
if (!area)
usercopy_abort("vmalloc", "no area", to_user, 0, n);
if (n > area->va_end - addr) {
offset = addr - area->va_start;
usercopy_abort("vmalloc", NULL, to_user, offset, n);
}
return;
}
/* 유효한 물리 주소가 아니면 검증 불가 */
if (!virt_addr_valid(ptr))
return;
/* SLAB 객체 검증 */
page = virt_to_page(ptr);
slab = page_slab(page);
if (slab) {
__check_heap_object(ptr, n, slab, to_user);
} else if (PageCompound(page)) {
/* Compund 페이지 (대용량 할당) 검증 */
page = compound_head(page);
offset = ptr - page_address(page);
if (n > page_size(page) - offset)
usercopy_abort("page alloc", NULL, to_user, offset, n);
}
// 일반 페이지는 대용량 할당의 일부일 수 있어 검증 생략
}
// mm/usercopy.c:162-210
4. `parse_hardened_usercopy` (mm/usercopy.c:268)
/* 부팅 파라미터 hardened_usercopy= 파서 */
static int __init parse_hardened_usercopy(char *str)
{
if (kstrtobool(str, &enable_checks))
pr_warn("Invalid option string for hardened_usercopy: '%s'\n", str);
return 1;
}
__setup("hardened_usercopy=", parse_hardened_usercopy);
/* 초기화 함수에서 정적 분기 활성화/비활성화 */
static int __init set_hardened_usercopy(void)
{
if (enable_checks)
static_branch_enable(&validate_usercopy_range); // 검증 활성화
else
static_branch_disable(&validate_usercopy_range); // 검증 비활성화
return 1;
}
late_initcall(set_hardened_usercopy);
// mm/usercopy.c:268-287
// 커널 부팅 시 hardened_usercopy=on/off로 제어 가능
5. `copy_struct_from_user` (include/linux/uaccess.h:394)
/* 구조체 사용자 복사 (호환성 보장) */
static __always_inline __must_check int
copy_struct_from_user(void *dst, size_t ksize, const void __user *src,
size_t usize)
{
size_t size = min(ksize, usize); // 공통 크기
size_t rest = max(ksize, usize) - size; // 나머지 바이트
/* 커널 크기 검증 */
if (WARN_ON_ONCE(ksize > __builtin_object_size(dst, 1)))
return -E2BIG;
/* 나머지 바이트 처리 */
if (usize < ksize) {
/* 구버전 사용자: 남은 바이트를 0으로 패딩 */
memset(dst + size, 0, rest);
} else if (usize > ksize) {
/* 신버전 사용자: 남은 바이트가 0인지 확인 */
int ret = check_zeroed_user(src + size, rest);
if (ret <= 0)
return ret ?: -E2BIG;
}
/* 공통 부분 복사 */
if (copy_from_user(dst, src, size))
return -EFAULT;
return 0;
}
// include/linux/uaccess.h:394-417
// 구조체 확장 시 하위 호환성 보장 메커니즘
호출 흐름
copy_from_user 호출 흐름
copy_from_user(to, from, n)
└── check_copy_size(to, n, false) ← 크기 검증
├── __builtin_object_size() 검증 ← 컴파일타임
├── copy_overflow() ← 오버플로우 시
└── check_object_size() ← HARDENED_USERCOPY
└── __check_object_size(ptr, n, to_user)
├── check_bogus_address() ← 래퍼/NULL 주소 거부
├── check_stack_object() ← 스택 객체 검증
│ ├── NOT_STACK → 계속
│ ├── GOOD_FRAME/GOOD_STACK → 반환
│ └── BAD_STACK → usercopy_abort()
├── check_heap_object() ← 힙 객체 검증
│ ├── is_kmap_addr() → kmap 범위 검증
│ ├── is_vmalloc_addr() → vmalloc 범위 검증
│ ├── __check_heap_object() → SLAB whitelist 검증
│ └── PageCompound() → compound 페이지 검증
└── check_kernel_text_object() ← 커널 텍스트 노출 방지
└── _copy_from_user() / _inline_copy_from_user()
└── raw_copy_from_user() ← 아키텍처별 실제 복사
copy_to_user 호출 흐름
copy_to_user(to, from, n)
└── check_copy_size(from, n, true) ← 크기 검증 (소스 기준)
└── __check_object_size() 검증
└── (위와 동일한 검증 체인)
└── _copy_to_user() / _inline_copy_to_user()
└── raw_copy_to_user() ← 아키텍처별 실제 복사
조건별 비교
사용자 복사 검증 대상 비교
| 검증 대상 | 검증 함수 | 검증 내용 | 위반 시 결과 |
| 스택 객체 | `check_stack_object()` | 프레임 경계, 스택 깊이 | BAD_STACK → usercopy_abort() |
| SLAB 힙 | `__check_heap_object()` | useroffset/usersize whitelist | 허용 영역 외 → usercopy_abort() |
| vmalloc | `check_heap_object()` | vmap_area 범위 | 범위 외 → usercopy_abort() |
| kmap | `check_heap_object()` | 페이지 경계 | 페이지 경계 외 → usercopy_abort() |
| 커널 텍스트 | `check_kernel_text_object()` | _stext~_etext 범위 | 텍스트 영역 → usercopy_abort() |
| 래퍼 주소 | `check_bogus_address()` | NULL/제로 할당/래퍼 | 위반 → usercopy_abort() |
복사 함수별 동작 비교
| 함수 | 소스 | 대상 | 크기 검증 | 실패 시 패딩 | 사용 시점 |
| `copy_from_user()` | 사용자 | 커널 | O (check_copy_size) | O (0 패딩) | 일반 복사 |
| `copy_to_user()` | 커널 | 사용자 | O (check_copy_size) | X | 일반 복사 |
| `__copy_from_user()` | 사용자 | 커널 | X (호출자 검증) | X | 원자적 복사 불가 시 |
| `__copy_to_user()` | 커널 | 사용자 | X (호출자 검증) | X | 원자적 복사 불가 시 |
| `copy_struct_from_user()` | 사용자 | 커널 | O (구조체 크기) | O (남은 바이트 0 패딩) | 시스템 콜 구조체 |
하드닝 검증 단계 비교
| 단계 | 검증 내용 | 검증 시점 | 비활성화 가능 |
| 컴파일타임 | `__builtin_object_size()` | 컴파일 시 | X (자동) |
| 런타임 | `check_object_size()` | 복사 호출 시 | `hardened_usercopy=off` |
| Fault injection | `should_fail_usercopy()` | 복사 호출 시 | `CONFIG_FAULT_INJECTION_USERCOPY` |
다이어그램
호출 흐름
자료구조 관계도
관련 문서
00-overview.html — 메모리 관리 개요
02-slab.html — SLUB 할당자 (__check_heap_object)
27-debug.html — 디버그 도구 (KASAN/KFENCE)
61-page_table_check.html — 페이지 테이블 검증
참고 사항
보안 영향: hardened_usercopy=off로 비활성화 가능하지만 보안 취약점 노출
성능 영향: 스택 프레임 검증은 arch_within_stack_frames() 의존
아키텍처 차이: x86_64는 스택 프레임 검증 지원, 일부 아키텍처는 GOOD_STACK만 반환
KFENCE 연동: KFENCE 할당된 객체는 별도 오프셋 계산 (kfence_object_start())
KASAN 연동: kasan_reset_tag()로 메모리 태그 제거 후 검증