리눅스 커널 7.4, x86에서 __get_user()·__put_user() 제거 — SMAP이 바꾼 사용자 공간 접근 비용

리눅스 커널 7.4에서 x86의 __get_user()와 __put_user()가 사라집니다. 두 매크로는 오랫동안 “검사를 한 번만 하고 빠르게 접근하는” 최적화 수단이었습니다. 하지만 SMAP(Supervisor Mode Access Prevention)이 보편화된 뒤로는 x86에서 오히려 더 느려졌습니다.

이번 정리는 리누스 토발즈가 직접 작성한 48개짜리 패치 시리즈입니다. 토발즈가 큰 규모의 코드를 직접 쓰는 일은 드뭅니다. 그는 LLM을 “좀 더 똑똑한 코시넬(Coccinelle) 스크립트”처럼 썼다고 밝혔습니다. 이 글에서는 세 가지를 정리합니다. 두 매크로가 필요 없어진 이유, 대신 쓸 API, 실제 커널 트리의 변화입니다.

검사를 아끼려던 매크로

커널이 사용자 공간 메모리의 변수 하나를 읽고 쓸 때는 get_user()와 put_user()를 씁니다. 두 매크로는 접근할 때마다 주소가 사용자 공간 범위에 있는지 검사합니다. 구조체 필드 세 개를 읽으면 검사도 세 번 합니다. 실제로 필요한 검사는 첫 번째 한 번뿐입니다.

API범위 검사원래 의도
get_user() / put_user()호출마다단일 변수 접근의 기본 API
access_ok() + __get_user() / __put_user()처음 한 번여러 번 접근할 때 반복 검사 비용 절약

SMAP이 바꾼 비용 구조

지금의 CPU는 대부분 SMAP을 지원합니다. SMAP이 켜져 있으면 커널 모드에서 사용자 공간 메모리에 접근할 수 없습니다. 커널 버그나 익스플로잇을 막는 데 효과적인 기능입니다. 그래서 커널은 사용자 공간에 접근하기 직전에 SMAP을 끄고, 접근한 뒤 다시 켜야 합니다. x86에서는 stac/clac 명령이 이 일을 합니다. 이 한 쌍의 비용은 범위 검사보다 훨씬 큽니다.

__get_user()도 접근할 때마다 SMAP을 껐다 켭니다. 그래서 범위 검사를 생략해도 이득이 거의 없습니다. 토발즈의 제거 커밋은 x86에서는 오히려 더 느리다고 설명합니다.

모든 사용자 공간 접근을 각각 켜고 꺼야 하게 된 뒤로 이 매크로는 아무 이득도 주지 못했다. x86에서 __get_user()는 lfence를 실행한다. get_user()는 평범한 명령 몇 개로 주소를 검사할 뿐이라, __get_user()가 더 느려졌다. — 커밋 f748af39e335 메시지에서

지금 권장하는 방법: scoped user access

여러 번 접근할 때 빠르게 하려면 SMAP을 처음에 한 번 끄고 모든 접근이 끝난 뒤 한 번 켜야 합니다. 예전에는 user_read_access_begin()과 user_read_access_end()를 직접 짝지어 썼습니다. 오류 경로에서 end 호출을 빠뜨려 SMAP이 꺼진 채로 남는 버그가 실제로 여러 번 있었습니다.

2025년 6.18 커널에서 토마스 글라이크스너(Thomas Gleixner)가 scope guard 기반 매크로를 추가했습니다. 블록을 어떻게 빠져나가든 컴파일러가 접근 종료 코드를 실행합니다.

scoped_user_read_access(ptr, efault) {
	unsafe_get_user(dest1, &ptr->field1, efault);
	unsafe_get_user(dest2, &ptr->field2, efault);
	unsafe_get_user(dest3, &ptr->field3, efault);
}
return 0;
efault:
	return -EFAULT;

ptr은 블록 시작에서 한 번 검사합니다. 검사나 접근이 실패하면 efault 레이블로 점프합니다. 매크로 정의는 include/linux/uaccess.h에 있습니다. __cleanup() 속성을 쓰는 for 루프 트릭으로 구현돼 있습니다.

B=https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/plain
curl -s -o uaccess.h "$B/include/linux/uaccess.h"
grep -n "define scoped_user_.*_access(" uaccess.h
839:#define scoped_user_read_access(usrc, elbl)				\
862:#define scoped_user_write_access(udst, elbl)				\
885:#define scoped_user_rw_access(uptr, elbl)				\

48개 패치가 바꾼 호출 지점

두 매크로를 지우려면 남은 호출 지점을 모두 고쳐야 했습니다. 대부분은 단순 치환이었지만, 원래 용도와 다르게 쓰던 곳도 있었습니다.

기존 사용 형태바꾼 방법
단일 변수 접근일반 get_user() / put_user()
여러 필드 연속 접근(시그널 코드 등)scoped_user_*_access() + unsafe_get_user() / unsafe_put_user()
폴트 처리가 필요해서 커널 주소에 사용get_kernel_nofault()
원자적 컨텍스트에서 조용히 실패하기를 기대페이지 폴트를 명시적으로 비활성화
사용자·커널 주소가 모두 올 수 있는 포인터경우마다 개별 패치로 분리

마지막 커밋이 x86의 매크로와 그 뒤에 있던 __{get,put}_user_nocheck_N 어셈블리 헬퍼를 지웠습니다. 3개 파일에서 10줄이 추가되고 166줄이 삭제됐습니다. 7.3 태그와 현재 메인라인의 x86 헤더를 비교하면 차이가 보입니다.

F=arch/x86/include/asm/uaccess.h
curl -s -o v73.h    "$B/$F?h=v7.3"
curl -s -o master.h "$B/$F"
grep -n "define __get_user(\|define __put_user(" v73.h master.h
v73.h:131:#define __get_user(x,ptr) do_get_user_call(get_user_nocheck,x,ptr)
v73.h:230:#define __put_user(x, ptr) do_put_user_call(put_user_nocheck,x,ptr)

메인라인 헤더에는 정의가 없습니다. 다른 아키텍처는 각자의 정의를 그대로 둡니다. arm64의 __get_user()는 처음부터 get_user()와 같은 동작이라 서두를 이유가 없습니다. UML은 asm-generic의 접근 함수를 쓰므로 영향이 없습니다.

LLM을 리팩터링 도구로

토발즈는 이 작업에 LLM을 “좀 더 똑똑한 코시넬 스크립트”로 썼다고 밝혔습니다. 무엇을 바꿀지는 사람이 정하고, 수십 곳의 호출 지점을 고치고 빌드를 돌리는 일은 LLM이 맡는 방식입니다. LWN 댓글에서도 비슷한 경험이 이어졌습니다. “반쯤 끝난 API 전환”을 마무리하는 데 맞다는 평가가 많았습니다. 공통 경로만 새 API로 옮기고 오래된 코드에 옛 API가 남은 상태를 말합니다. 한 개발자는 LLM에게 코시넬 스크립트를 직접 쓰게 해서 변환을 돌렸다고 했습니다.

댓글에서는 기술적인 지적도 나왔습니다. access_ok()에는 크기 인자가 있습니다. 그러니 “첫 검사만 필요하다”는 설명은 구조체가 여러 페이지에 걸치지 않을 때만 맞다는 지적입니다. 조너선 코벳은 access_ok()가 실제로는 주소가 사용자 영역에 있는지만 본다고 답했습니다. 벤 허친스(Ben Hutchings)는 이 설명이 x86_64에서만 맞다고 덧붙였습니다. 32비트에서는 사용자 주소와 커널 주소 사이에 빈 공간이 없기 때문입니다.

정리

한때 모범 사례였던 최적화가 하드웨어 보안 기능 때문에 의미를 잃었습니다. 그 뒤로도 코드에서 완전히 빠지기까지 수년이 걸렸습니다. 이번에는 LLM 덕분에 남은 호출 지점을 한 번에 정리할 수 있었습니다.

x86용 out-of-tree 모듈을 관리한다면 7.4로 올리기 전에 호출 지점을 찾으십시오. grep -rn "__get_user\|__put_user"면 충분합니다. 단일 접근은 get_user()로 바꾸면 됩니다. 연속 접근은 scoped_user_read_access() 블록으로 옮기는 것이 정석입니다. 커널 주소에 쓰고 있었다면 get_kernel_nofault()가 맞는 API입니다.

참고

답글 남기기