사용자 공간 복사 (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()로 메모리 태그 제거 후 검증