프로세스 주소 공간(process address space)을 access하는 커널 API
가장 잘 알려진 다음 API 두 개를 알아본다.
- copy_from_user()
- copy_to_user()
그리고 유저 프로세스 공간으로의 안전한 접근이 가능한지 여부를 체크하는 다음 API를 알아본다.
유저 프로세스 공간으로 부터 커널로 데이터 복사
copy_from_user()
include/linux/uaccess.h
1 | static __always_inline unsigned long __must_check |
2 | copy_from_user( void *to, const void __user *from, unsigned long n) |
4 | if (likely(check_copy_size(to, n, false ))) |
5 | n = _copy_from_user(to, from, n); |
유저 가상 주소 @from에서 @n 바이트만큼 커널 가상 주소 @to로 데이터를 복사한다.
_copy_from_user()
include/linux/uaccess.h
01 | #ifdef INLINE_COPY_FROM_USER |
02 | static inline unsigned long |
03 | _copy_from_user( void *to, const void __user *from, unsigned long n) |
05 | unsigned long res = n; |
07 | if (likely(access_ok(from, n))) { |
08 | kasan_check_write(to, n); |
09 | res = raw_copy_from_user(to, from, n); |
12 | memset (to + (n - res), 0, res); |
이 함수는 유저 application에서 사용될 때에는 INLINE_COPY_FROM_USER 옵션이 적용되어 인라인 함수로 제공된다. 그렇지 않고 커널을 통해 호출되는 경우 라이브러리를 통해 제공된다.
아키텍처별 raw_copy_from_user()
다음 아키텍처별로 권한 설정을 아키텍처 고유의 방법을 사용한다.
- ARM32
- 도메인 접근 제어 레지스터를 사용하여 제어한다.
- ARM64
- 커널에서의 유저 액세스 권한 제어 플래그를 사용하여 제어한다.
raw_copy_from_user() – ARM64
arch/arm64/include/asm/uaccess.h
1 | #define raw_copy_from_user(to, from, n) \ |
3 | __arch_copy_from_user((to), __uaccess_mask_ptr(from), (n)); \ |
유저 가상 주소 @from(x1)에서 @n 바이트(x2)만큼 커널 가상 주소 @to(x0)로 복사한다.
다음 그림은 ARM64 시스템에서 copy_from_user() 함수의 처리 흐름을 보여준다.

__arch_copy_from_user() – ARM64
arch/arm64/lib/copy_from_user.S
1 | ENTRY(__arch_copy_from_user) |
2 | uaccess_enable_not_uao x3, x4, x5 |
4 | #include "copy_template.S" |
5 | uaccess_disable_not_uao x3, x4 |
8 | ENDPROC(__arch_copy_from_user) |
9 | EXPORT_SYMBOL(__arch_copy_from_user) |
커널이 유저 데이터에 접근할 수 있도록 잠시 허용한 후 유저 가상 주소 src(x1)에서 n 바이트(x2)만큼 커널 가상 주소 dest(x0)로 복사한다. 그런 후 커널이 유저 데이터에 접근하지 못하게 막는다.
- copy_template.S 에는 어셈블리로 복사 코드가 담겨있다.
raw_copy_from_user() – ARM32
arch/arm/include/asm/uaccess.h
01 | static inline unsigned long __must_check |
02 | raw_copy_from_user( void *to, const void __user *from, unsigned long n) |
04 | unsigned int __ua_flags; |
06 | __ua_flags = uaccess_save_and_enable(); |
07 | n = arm_copy_from_user(to, from, n); |
08 | uaccess_restore(__ua_flags); |
유저 가상 주소 @from(r1)에서 @n 바이트(r2)만큼 커널 가상 주소 @to(r0)로 복사한다.
다음 그림은 ARM32 시스템에서 copy_from_user() 함수의 처리 흐름을 보여준다.

arm_copy_from_user()
lib/copy_from_user.S
01 | ENTRY(arm_copy_from_user) |
02 | #ifdef CONFIG_CPU_SPECTRE |
04 | ldr r3, [r3, #TI_ADDR_LIMIT] |
05 | uaccess_mask_range_ptr r1, r2, r3, ip |
08 | #include "copy_template.S" |
10 | ENDPROC(arm_copy_from_user) |
- copy_template.S 에는 어셈블리로 복사 코드가 담겨있다.
유저 프로세스 공간으로 커널 데이터 복사
copy_to_user()
include/linux/uaccess.h
1 | static __always_inline unsigned long __must_check |
2 | copy_to_user( void __user *to, const void *from, unsigned long n) |
4 | if (likely(check_copy_size(from, n, true ))) |
5 | n = _copy_to_user(to, from, n); |
커널 가상 주소 @from에서 @n 바이트만큼 유저 가상 주소 @to로 데이터를 복사한다.
_copy_to_user()
include/linux/uaccess.h
01 | #ifdef INLINE_COPY_TO_USER |
02 | static inline unsigned long |
03 | _copy_to_user( void __user *to, const void *from, unsigned long n) |
06 | if (access_ok(to, n)) { |
07 | kasan_check_read(from, n); |
08 | n = raw_copy_to_user(to, from, n); |
이 함수는 유저 application에서 사용될 때에는 INLINE_COPY_TO_USER 옵션이 적용되어 인라인 함수로 제공된다. 그렇지 않고 커널을 통해 호출되는 경우 라이브러리를 통해 제공된다.
아키텍처별 raw_copy_to_user()
다음 아키텍처별로 권한 설정을 아키텍처 고유의 방법을 사용한다.
- ARM32
- 도메인 접근 제어 레지스터를 사용하여 제어한다.
- ARM64
- 커널에서의 유저 액세스 권한 제어 플래그를 사용하여 제어한다.
raw_copy_to_user() – ARM64
arch/arm64/include/asm/uaccess.h
1 | #define raw_copy_to_user(to, from, n) \ |
3 | __arch_copy_to_user(__uaccess_mask_ptr(to), (from), (n)); \ |
커널 가상 주소 @from(x1)에서 @n 바이트(x2)만큼 유저 가상 주소 @to(x0)로 복사한다.
__arch_copy_to_user()
arch/arm64/lib/copy_to_user.S
1 | ENTRY(__arch_copy_to_user) |
2 | uaccess_enable_not_uao x3, x4, x5 |
4 | #include "copy_template.S" |
5 | uaccess_disable_not_uao x3, x4 |
8 | ENDPROC(__arch_copy_to_user) |
9 | EXPORT_SYMBOL(__arch_copy_to_user) |
커널이 유저 데이터에 접근할 수 있도록 잠시 허용한 후 커널 가상 주소 src(x1)에서 n 바이트(x2)만큼 유저 가상 주소 dest(x0)로 복사한다. 그런 후 커널이 유저 데이터에 접근하지 못하게 막는다.
- copy_template.S 에는 어셈블리로 복사 코드가 담겨있다.
raw_copy_to_user() – ARM32
arch/arm/include/asm/uaccess.h
01 | static inline unsigned long __must_check |
02 | raw_copy_to_user( void __user *to, const void *from, unsigned long n) |
04 | #ifndef CONFIG_UACCESS_WITH_MEMCPY |
05 | unsigned int __ua_flags; |
06 | __ua_flags = uaccess_save_and_enable(); |
07 | n = arm_copy_to_user(to, from, n); |
08 | uaccess_restore(__ua_flags); |
11 | return arm_copy_to_user(to, from, n); |
커널 가상 주소 @from(r1)에서 @n 바이트(r2)만큼 유저 가상 주소 @to(r0)로 복사한다.
arm_copy_to_user()
lib/copy_to_user.S
01 | ENTRY(__copy_to_user_std) |
03 | #ifdef CONFIG_CPU_SPECTRE |
05 | ldr r3, [r3, #TI_ADDR_LIMIT] |
06 | uaccess_mask_range_ptr r0, r2, r3, ip |
09 | #include "copy_template.S" |
11 | ENDPROC(arm_copy_to_user) |
- copy_template.S 에는 어셈블리로 복사 코드가 담겨있다.
User 공간 접근 제어 – ARM64
보안을 향상시킬 목적으로 커널에서 유저 공간 액세스를 컨트롤하기 위해 다음 두 가지 방법 중 하나를 사용할 수 있다. 이러한 기능이 적용되지 않는 경우 기본적으로 커널에서 유저 공간의 접근이 언제나 허용된다.
- ARMv8.1-PAN 기능을 사용한 제어 (HW 기법)
- SW_TTBR0_PAN 기능을 사용한 제어 (SW 기법)
- TTBR0_EL1을 빈 페이지 테이블에 연결하고, TTBR1_EL1의 ASID를 클리어하는 SW 에뮬레이션 방법을 사용하여 커널에서 유저 접근을 금지시킬 수 있다.
- 커널 옵션:CONFIG_ARM64_SW_TTBR0_PAN
그 외 – UAO 기능을 사용한 커널에서의 유저 공간 접근 권한 Override
커널과 유저 데이터 교환 함수들(copy_from_user(), copy_to_user()…)에서 유저 공간에 접근할 때 ldr/str 대신 유저 페이지 테이블에 기록된 권한 설정들을 override 하여 사용하는 ldtr/sttr 명령을 사용할 수 있다.
- ARMv8.2-UAO
- 커널에서 유저 주소에 접근할 때 ldr/str 명령으로 접근하는 경우 유저 페이지 테이블에 기록된 권한 설정들을 사용하지 못한다.
- 그러나 UAO 기능이 지원되는 아키텍처에서 ldtr/sttr 명령을 사용 시 유저 페이지 테이블에 기록된 권한 설정들을 Override 할 수 있다.
- 커널 옵션: CONFIG_ARM64_UAO
1) 유저 공간 접근 허용 – for ARM64 Assembly
uaccess_enable_not_uao() 매크로
arch/arm64/include/asm/asm-uaccess.h
1 | .macro uaccess_enable_not_uao, tmp1, tmp2, tmp3 |
2 | uaccess_ttbr0_enable \tmp1, \tmp2, \tmp3 |
3 | alternative_if ARM64_ALT_PAN_NOT_UAO |
5 | alternative_else_nop_endif |
커널에서 유저 데이터에 접근할 수 있도록 허용한다.
- 코드 라인 2에서 ARMv8.1 아키텍처 이상에서 지원되는 PAN 기능이 지원되지 않는 경우 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 허용하게 한다.
- 코드 라인 3~5에서 PAN 기능이 지원되면 ARMv8.2 아키텍처 이상에서만 지원하는 UAO 기능에 의지 하지 않고 PAN 기능을 disable하여 커널에서 유저 공간에 접근할 수 있도록 허용한다.
uaccess_ttbr0_enable() 매크로
arch/arm64/include/asm/asm-uaccess.h
1 | .macro uaccess_ttbr0_enable, tmp1, tmp2, tmp3 |
2 | alternative_if_not ARM64_HAS_PAN |
3 | save_and_disable_irq \tmp3 |
4 | __uaccess_ttbr0_enable \tmp1, \tmp2 |
6 | alternative_else_nop_endif |
ARMv8.1 아키텍처 이상에서 지원되는 PAN 기능이 지원되지 않는 경우 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 허용하게 한다.
__uaccess_ttbr0_enable() 매크로
01 | .macro __uaccess_ttbr0_enable, tmp1, tmp2 |
03 | ldr \tmp1, [\tmp1, #TSK_TI_TTBR0] |
05 | extr \tmp2, \tmp2, \tmp1, #48 |
SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 허락하도록 ttbr을 설정한다.
- 유저 페이지 테이블을 위해 사용되는 TTBR0의 asid를 커널 페이지 테이블을 위해 사용되는 TTBR1에 복사하여 커널이 유저 액세스가 가능하도록 허용한다.
- 유저 페이지 테이블을 위해 사용되는 TTBR0의 asid를 읽어 커널 페이지 테이블을 위해 사용되는 TTBR1에 asid 필드만을 기록하여 커널에서 유저 영역에 접근할 수 있게 허용한다.
2) 유저 공간 접근 금지 – for ARM64 Assembly
uaccess_disable_not_uao() 매크로
arch/arm64/include/asm/asm-uaccess.h
1 | .macro uaccess_disable_not_uao, tmp1, tmp2 |
2 | uaccess_ttbr0_disable \tmp1, \tmp2 |
3 | alternative_if ARM64_ALT_PAN_NOT_UAO |
5 | alternative_else_nop_endif |
커널에서 유저 데이터에 접근하지 못하게 금지한다.
- 코드 라인 2에서 ARMv8.1 아키텍처 이상에서 지원되는 PAN 기능이 지원되지 않는 경우 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 금지하게 한다.
- 코드 라인 3~5에서 PAN 기능이 지원되면 ARMv8.2 아키텍처 이상에서만 지원하는 UAO 기능에 의지 하지 않고 PAN 기능을 enable하여 커널에서 유저 공간에 접근하지 못하게 금지한다.
uaccess_ttbr0_disable() 매크로
arch/arm64/include/asm/asm-uaccess.h
1 | .macro uaccess_ttbr0_enable, tmp1, tmp2, tmp3 |
2 | alternative_if_not ARM64_HAS_PAN |
3 | save_and_disable_irq \tmp3 |
4 | __uaccess_ttbr0_enable \tmp1, \tmp2 |
6 | alternative_else_nop_endif |
ARMv8.1 아키텍처 이상에서 지원되는 PAN 기능이 지원되지 않는 경우 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 금지하게 한다.
__uaccess_ttbr0_disable() 매크로
arch/arm64/include/asm/asm-uaccess.h
01 | .macro __uaccess_ttbr0_disable, tmp1 |
03 | bic \tmp1, \tmp1, #TTBR_ASID_MASK |
04 | sub \tmp1, \tmp1, #RESERVED_TTBR0_SIZE |
07 | add \tmp1, \tmp1, #RESERVED_TTBR0_SIZE |
SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 금지하도록 ttbr을 설정한다.
- 커널 페이지 테이블을 가리키는 TTBR1의 asid를 클리어한다.
- 유저 페이지 테이블을 가리키는 TTBR0에 reserved_ttbr0 라는 이름의 빈 페이지 테이블을 가리키게한다.
- CONFIG_ARM64_SW_TTBR0_PAN 커널 옵션을 사용하는 경우 swapper_pg_dir 이전에 reserved_ttbr0 라는 이름의 빈 페이지 테이블이 구성되어 있다.
Process State(PSTATE) 설정
02 | * Instructions for modifying PSTATE fields. |
03 | * As per Arm ARM for v8-A, Section "C.5.1.3 op0 == 0b00, architectural hints, |
04 | * barriers and CLREX, and PSTATE access", ARM DDI 0487 C.a, system instructions |
05 | * for accessing PSTATE fields have the following encoding: |
07 | * Op1, Op2 encodes the PSTATE field modified and defines the constraints. |
08 | * CRm = Imm4 for the instruction. |
01 | #define pstate_field(op1, op2) ((op1) << Op1_shift | (op2) << Op2_shift) |
02 | #define PSTATE_Imm_shift CRm_shift |
04 | #define PSTATE_PAN pstate_field(0, 4) |
05 | #define PSTATE_UAO pstate_field(0, 3) |
06 | #define PSTATE_SSBS pstate_field(3, 1) |
08 | #define SET_PSTATE_PAN(x) __emit_inst(0xd500401f | PSTATE_PAN | ((!!x) << PSTATE_Imm_ss |
10 | #define SET_PSTATE_UAO(x) __emit_inst(0xd500401f | PSTATE_UAO | ((!!x) << PSTATE_Imm_ss |
12 | #define SET_PSTATE_SSBS(x) __emit_inst(0xd500401f | PSTATE_SSBS | ((!!x) << PSTATE_Imm__ |
MSR을 사용하고 immediate 접근을 통해 PSTATE 필드 들 중 다음의 필드들을 설정할 수 있다.
- SET_PSTATE_PAN()
- PSTATE 중 PAN(Privileged Access Never) 비트를 설정한다.
- ARMv8.1-PAN 이 구현된 아키텍처만 사용가능하다.
- SET_PSTATE_UAO()
- PSTATE 중 UAO(User Access Override) 비트를 설정한다.
- ARMv8.2-UAO가 구현된 아키텍처만 사용가능하다.
다음 그림은 MSR의 immediate 접근을 통해 PSTATE 관련 몇 개의 플래그를 설정하기 위해 사용되는 op1, CRm, op2의 위치를 보여준다.
- CRm 값을 지정하기 위해 필요한 CRm_shift 값은 8이다.
- op1 값을 지정하기 위해 필요한 op1_shift 값은 16이다.
- op2 값을 지정하기 위해 필요한 op2_shift 값은 5이다.

3) 유저 공간 접근 허용 – for ARM64 C
uaccess_enable_not_uao()
arch/arm64/include/asm/uaccess.h
1 | static inline void uaccess_enable_not_uao( void ) |
3 | __uaccess_enable(ARM64_ALT_PAN_NOT_UAO); |
커널에서 유저 데이터에 접근할 수 있도록 잠시 허용한다.
__uaccess_enable()
arch/arm64/include/asm/uaccess.h
1 | #define __uaccess_enable(alt) \ |
3 | if (!uaccess_ttbr0_enable()) \ |
4 | asm(ALTERNATIVE( "nop" , SET_PSTATE_PAN(0), alt, \ |
커널에서 유저 액세스가 가능하도록 허용한다.
- 코드 라인 3에서 ARMv8.1 아키텍처 이상에서 지원되는 PAN 기능이 지원되지 않는 경우 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 허용하게 한다.
- 코드 라인 4~5에서 PAN 기능 지원 여부에 따라 ARMv8.2 아키텍처 이상에서만 지원하는 UAO 기능에 의지 하지 않고 PAN 기능을 disable하여 커널에서 유저 공간에 접근할 수 있도록 허용한다.
uaccess_ttbr0_enable() – ARM64
arch/arm64/include/asm/uaccess.h
1 | static inline bool uaccess_ttbr0_enable( void ) |
3 | if (!system_uses_ttbr0_pan()) |
5 | __uaccess_ttbr0_enable(); |
커널에서 유저 데이터에 접근할 수 있도록 허용한다.
- 코드 라인 2에서 ARMv8.1 아키텍처 이상에서 지원되는 PAN 기능이 지원되지 않는 경우 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 허용하게 한다.
- 코드 라인 3~5에서 PAN 기능이 지원되면 ARMv8.2 아키텍처 이상에서만 지원하는 UAO 기능에 의지 하지 않고 PAN 기능을 enable하여 커널에서 유저 공간에 접근할 수 있도록 허용한다.
system_uses_ttbr0_pan() – ARM64
arch/arm64/include/asm/cpufeature.h
1 | static inline bool system_uses_ttbr0_pan( void ) |
3 | return IS_ENABLED(CONFIG_ARM64_SW_TTBR0_PAN) && |
4 | !cpus_have_const_cap(ARM64_HAS_PAN); |
현재 동작 중인 ARM64 커널에 CONFIG_ARM64_SW_TTBR0_PAN 커널 옵션이 설정되어 있지만 아키텍처가 PAN(Previlidge Access Never) 기능을 가지고 있지 않으면 TTBR0을 사용하여 PAN을 에뮬레이션을 하기 위해 1을 반환한다.
__uaccess_ttbr0_enable() – ARM64
arch/arm64/include/asm/uaccess.h
01 | static inline void __uaccess_ttbr0_enable( void ) |
03 | unsigned long flags, ttbr0, ttbr1; |
10 | local_irq_save(flags); |
11 | ttbr0 = READ_ONCE(current_thread_info()->ttbr0); |
14 | ttbr1 = read_sysreg(ttbr1_el1); |
15 | ttbr1 &= ~TTBR_ASID_MASK; |
16 | ttbr1 |= ttbr0 & TTBR_ASID_MASK; |
17 | write_sysreg(ttbr1, ttbr1_el1); |
21 | write_sysreg(ttbr0, ttbr0_el1); |
23 | local_irq_restore(flags); |
SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 허락하도록 ttbr을 설정한다.
- TTBR0의 asid를 TTBR1에 복사하여 커널이 유저 액세스가 가능하도록 허용한다.
- 유저 페이지 테이블을 위해 사용되는 ttbr0의 asid를 읽어 커널 페이지 테이블을 위해 사용되는 ttbr1에 asid 필드만을 기록하여 커널에서 유저 영역에 접근할 수 있게 허용한다.
4) 유저 공간 접근 금지 – for ARM64 C
uaccess_disable_not_uao()
arch/arm64/include/asm/uaccess.h
1 | static inline void uaccess_disable_not_uao( void ) |
3 | __uaccess_disable(ARM64_ALT_PAN_NOT_UAO); |
다시 커널에서 유저 데이터에 접근하지 못하게 한다.
__uaccess_disable()
arch/arm64/include/asm/uaccess.h
1 | #define __uaccess_disable(alt) \ |
3 | if (!uaccess_ttbr0_disable()) \ |
4 | asm(ALTERNATIVE( "nop" , SET_PSTATE_PAN(1), alt, \ |
커널에서 유저 데이터에 접근하지 못하게 금지한다.
- 코드 라인 3에서 ARMv8.1 아키텍처 이상에서 지원되는 PAN 기능이 지원되지 않는 경우 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 금지하게 한다.
- 코드 라인 4~6에서 PAN 기능이 지원되면 ARMv8.2 아키텍처 이상에서만 지원하는 UAO 기능에 의지 하지 않고 PAN 기능을 enable하여 커널에서 유저 공간에 접근하지 못하게 금지한다.
uaccess_ttbr0_disable()
arch/arm64/include/asm/uaccess.h
1 | static inline bool uaccess_ttbr0_disable( void ) |
3 | if (!system_uses_ttbr0_pan()) |
5 | __uaccess_ttbr0_disable(); |
커널에서 유저 데이터에 접근할 수 없도록 금지한다.
- 코드 라인 2에서 ARMv8.1 아키텍처 이상에서 지원되는 PAN 기능이 지원되지 않는 경우 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 금지하게 한다.
- 코드 라인 3~5에서 PAN 기능이 지원되면 ARMv8.2 아키텍처 이상에서만 지원하는 UAO 기능에 의지 하지 않고 PAN 기능을 enable하여 커널에서 유저 공간에 접근하지 못하게 금지한다.
__uaccess_ttbr0_disable()
arch/arm64/include/asm/uaccess.h
01 | static inline void __uaccess_ttbr0_disable( void ) |
03 | unsigned long flags, ttbr; |
05 | local_irq_save(flags); |
06 | ttbr = read_sysreg(ttbr1_el1); |
07 | ttbr &= ~TTBR_ASID_MASK; |
09 | write_sysreg(ttbr - RESERVED_TTBR0_SIZE, ttbr0_el1); |
12 | write_sysreg(ttbr, ttbr1_el1); |
14 | local_irq_restore(flags); |
SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 금지하도록 ttbr을 설정한다.
- TTBR1의 asid를 클리어하여 다시 TTBR1에 기록한다.
- TTBR0에 reserved_ttbr0 라는 이름의 빈 페이지 테이블을 가리키게한다.
- CONFIG_ARM64_SW_TTBR0_PAN 커널 옵션을 사용하는 경우 swapper_pg_dir 이전에 reserved_ttbr0 라는 이름의 빈 페이지 테이블이 구성되어 있다.
유저 도메인의 권한 설정 및 복원 – ARM32
uaccess_save_and_enable()
arch/arm/include/asm/uaccess.h
2 | * These two functions allow hooking accesses to userspace to increase |
3 | * system integrity by ensuring that the kernel can not inadvertantly |
4 | * perform such accesses (eg, via list poison values) which could then |
5 | * be exploited for priviledge escalation. |
01 | static inline unsigned int uaccess_save_and_enable( void ) |
03 | #ifdef CONFIG_CPU_SW_DOMAIN_PAN |
04 | unsigned int old_domain = get_domain(); |
07 | set_domain((old_domain & ~domain_mask(DOMAIN_USER)) | |
08 | domain_val(DOMAIN_USER, DOMAIN_CLIENT)); |
유저 도메인의 값을 반환하고, 클라이언트 권한(페이지 테이블에 설정된 permission 대로 동작하기)을 부여한다.
uaccess_restore()
arch/arm/include/asm/uaccess.h
1 | static inline void uaccess_restore(unsigned int flags) |
3 | #ifdef CONFIG_CPU_SW_DOMAIN_PAN |
유저 도메인의 값을 원래 값(flags)으로 복구한다.
도메인별 permission 권한 설정하기
set_domain()
arch/arm/include/asm/domain.h
1 | static inline void set_domain(unsigned val) |
4 | "mcr p15, 0, %0, c3, c0 @ set domain" |
5 | : : "r" (val) : "memory" ); |
DACR(Domain Access Control Register)을 사용하여 16개의 도메인을 지정한다.
- ARM32 커널은 16개의 도메인 중 커널, 유저, IO 및 벡터 도메인을 지정하여 총 4개의 도메인을 사용한다.
- 각 도메인 마다 2비트를 사용하여 다음과 같은 permission 사용 여부를 지정할 수 있다.
- 0=no access
- 1=client
- 페이지 변환 테이블에 지정된 permission 비트에 해당하는 체크를 수행한다.
- 2=reserved
- 3=manager
- 페이지 변환 테이블에 지정된 permission 비트를 무시한다.
uaccess_mask_range_ptr() 매크로
include/asm/assembler.h
01 | .macro uaccess_mask_range_ptr, addr:req, size:req, limit:req, tmp:req |
02 | #ifdef CONFIG_CPU_SPECTRE |
04 | subs \tmp, \tmp, \addr @ tmp = limit - 1 - addr |
05 | addhs \tmp, \tmp, #1 @ if (tmp >= 0) { |
06 | subhss \tmp, \tmp, \size @ tmp = limit - (addr + size) } |
07 | movlo \addr, #0 @ if (tmp < 0) addr = NULL |
uaccess_save 매크로
arch/arm/include/asm/assembler.h
1 | .macro uaccess_save, tmp |
2 | #ifdef CONFIG_CPU_SW_DOMAIN_PAN |
3 | mrc p15, 0, \tmp, c3, c0, 0 |
4 | str \tmp, [sp, #SVC_DACR] |
보안을 위해 커널에서 user 영역에 접근을 하지 못하게 제한한다. 그리고 이전 유저 도메인에 대한 도메인 타입을 스택이 가리키고 있는 pt_regs의 멤버 dacr에 백업한다.
uaccess_restore 매크로
arch/arm/include/asm/assembler.h
2 | #ifdef CONFIG_CPU_SW_DOMAIN_PAN |
3 | ldr r0, [sp, #SVC_DACR] |
4 | mcr p15, 0, r0, c3, c0, 0 |
보안을 위해 커널에서 user 영역에 접근에 대한 여부를 기록하는데, 백업해 두었던 스택에 위치한 pt_regs의 멤버 dacr을 읽은 값을 기록한다.
uaccess_disable 매크로
arch/arm/include/asm/assembler.h
01 | .macro uaccess_disable, tmp, isb=1 |
02 | #ifdef CONFIG_CPU_SW_DOMAIN_PAN |
07 | mov \tmp, #DACR_UACCESS_DISABLE |
08 | mcr p15, 0, \tmp, c3, c0, 0 @ Set domain register |
보안을 위해 커널에서 user 영역에 접근을 하지 못하게 제한한다.
- 코드 라인 7~8에서 커널에서 user 영역에 접근을 제한한다.
- 코드 라인 9~11에서 @isb=1인 경우 instruction 베리어를 수행한다.
uaccess_enable 매크로
arch/arm/include/asm/assembler.h
01 | .macro uaccess_enable, tmp, isb=1 |
02 | #ifdef CONFIG_CPU_SW_DOMAIN_PAN |
07 | mov \tmp, #DACR_UACCESS_ENABLE |
08 | mcr p15, 0, \tmp, c3, c0, 0 |
보안을 위해 커널에서 user 영역에 접근을 하지 못하게 제한한 것을 허용하게 한다.
- 코드 라인 7~8에서 커널에서 user 영역에 접근하도록 허용한다.
- 코드 라인 9~11에서 @isb=1인 경우 instruction 베리어를 수행한다.
DACR_UACCESS_DISABLE
arch/arm/include/asm/domain.h
1 | #define DACR_UACCESS_DISABLE \ |
2 | (__DACR_DEFAULT | domain_val(DOMAIN_USER, DOMAIN_NOACCESS)) |
유저 도메인의 타입을 noaccess로 지정한다. (커널에서 유저 영역의 액세스 금지)
DACR_UACCESS_ENABLE
arch/arm/include/asm/domain.h
1 | #define DACR_UACCESS_DISABLE \ |
2 | (__DACR_DEFAULT | domain_val(DOMAIN_USER, DOMAIN_NOACCESS)) |
3 | #define DACR_UACCESS_ENABLE \ |
4 | (__DACR_DEFAULT | domain_val(DOMAIN_USER, DOMAIN_CLIENT)) |
유저 도메인의 타입을 client로 지정한다. (커널에서 유저 영역의 액세스 허용)
arch/arm/include/asm/domain.h
1 | #define __DACR_DEFAULT \ |
2 | domain_val(DOMAIN_KERNEL, DOMAIN_CLIENT) | \ |
3 | domain_val(DOMAIN_IO, DOMAIN_CLIENT) | \ |
4 | domain_val(DOMAIN_VECTORS, DOMAIN_CLIENT) |
1 | #define domain_val(dom,type) ((type) << (2 * (dom))) |
16개의 도메인 중 요청한 도메인 @dom에 도메인 @type을 지정한다.
- DACR(Domain Access Control Register) 레지스터는 16개의 도메인에 2비트씩 도메인 타입을 지정할 수 있다.
도메인 타입
1 | #define DOMAIN_NOACCESS 0 |
3 | #ifdef CONFIG_CPU_USE_DOMAINS |
4 | #define DOMAIN_MANAGER 3 |
6 | #define DOMAIN_MANAGER 1 |
유효한 유저 프로세스 주소 사용 여부 체크
access_ok() – ARM64
arch/arm64/include/asm/uaccess.h
1 | #define access_ok(addr, size) __range_ok(addr, size) |
@addr 주소에서 @size 만큼의 영역이 유효한 유저 프로세스 주소인지 여부를 체크한다. 유효하지 않는 경우 0을 반환한다.
__range_ok() – ARM64
arch/arm64/include/asm/uaccess.h
2 | * Test whether a block of memory is a valid user space address. |
3 | * Returns 1 if the range is valid, 0 otherwise. |
5 | * This is equivalent to the following test: |
6 | * (u65)addr + (u65)size <= (u65)current->addr_limit + 1 |
01 | static inline unsigned long __range_ok( const void __user *addr, unsigned long size) |
03 | unsigned long ret, limit = current_thread_info()->addr_limit; |
11 | " csel %1, xzr, %1, hi\n" |
15 | " csinv %0, %0, xzr, cc\n" |
21 | : "=&r" (ret), "+r" (limit) : "Ir" (size), "0" (addr) : "cc" ); |
access_ok() – ARM32
arch/arm/include/asm/uaccess.h
1 | #define access_ok(addr, size) (__range_ok(addr, size) == 0) |
@addr 주소에서 @size 만큼의 영역이 유효한 유저 프로세스 주소인지 여부를 체크한다. 유효하지 않는 경우 0을 반환한다.
__range_ok() – ARM32
arch/arm/include/asm/uaccess.h
1 | /* We use 33-bit arithmetic here... */ |
2 | #define __range_ok(addr, size) ({ \ |
3 | unsigned long flag, roksum; \ |
4 | __chk_user_ptr(addr); \ |
5 | __asm__("adds %1, %2, %3; sbcccs %1, %1, %0; movcc %0, #0" \ |
6 | : "=&r" (flag), "=&r" (roksum) \ |
7 | : "r" (addr), "Ir" (size), "0" (current_thread_info()->addr_limit) \ |
커널에서 유저 영역의 값(1, 2, 4, 8 바이트) 읽어오기
get_user() API는 아키텍처별로 약간의 다른 구현을 사용한다.
- arm64
- exception 테이블을 활용하여 읽기를 시도한다.
- arm32
- MMU를 사용하는 ARMv7 아키텍처에 한해 exception 테이블 사용없이 미리 체크를 한 후 읽기를 시도한다.
- MMU를 사용하지 않는 경우 exception 테이블을 활용하여 읽기를 시도한다.
get_user() – ARM64
arch/arm64/include/asm/uaccess.h
1 | #define get_user __get_user |
커널에서 유저 영역의 주소 @p에 담긴 데이터를 @p 타입 사이즈만큼 읽어 커널 영역의 주소 @x에 대입한다. 만일 읽을 수 없으면 -EFAULT 에러를 반환한다.
__get_user() – ARM64
arch/arm64/include/asm/uaccess.h
1 | #define __get_user(x, ptr) \ |
4 | __get_user_check((x), (ptr), __gu_err); \ |
__get_user_check() – ARM64
arch/arm64/include/asm/uaccess.h
01 | #define __get_user_check(x, ptr, err) \ |
03 | __typeof__(*(ptr)) __user *__p = (ptr); \ |
05 | if (access_ok(__p, sizeof (*__p))) { \ |
06 | __p = uaccess_mask_ptr(__p); \ |
07 | __get_user_err((x), __p, (err)); \ |
09 | (x) = 0; (err) = -EFAULT; \ |
__get_user_err() – ARM64
arch/arm64/include/asm/uaccess.h
01 | #define __get_user_err(x, ptr, err) \ |
03 | unsigned long __gu_val; \ |
04 | __chk_user_ptr(ptr); \ |
05 | uaccess_enable_not_uao(); \ |
06 | switch ( sizeof (*(ptr))) { \ |
08 | __get_user_asm( "ldrb" , "ldtrb" , "%w" , __gu_val, (ptr), \ |
09 | (err), ARM64_HAS_UAO); \ |
12 | __get_user_asm( "ldrh" , "ldtrh" , "%w" , __gu_val, (ptr), \ |
13 | (err), ARM64_HAS_UAO); \ |
16 | __get_user_asm( "ldr" , "ldtr" , "%w" , __gu_val, (ptr), \ |
17 | (err), ARM64_HAS_UAO); \ |
20 | __get_user_asm( "ldr" , "ldtr" , "%x" , __gu_val, (ptr), \ |
21 | (err), ARM64_HAS_UAO); \ |
26 | uaccess_disable_not_uao(); \ |
27 | (x) = (__force __typeof__(*(ptr)))__gu_val; \ |
__get_user_asm() – ARM64
arch/arm64/include/asm/uaccess.h
2 | * The "__xxx" versions of the user access functions do not verify the address |
3 | * space - it must have been done previously with a separate "access_ok()" |
6 | * The "__xxx_error" versions set the third argument to -EFAULT if an error |
7 | * occurs, and leave it unchanged on success. |
01 | #define __get_user_asm(instr, alt_instr, reg, x, addr, err, feature) \ |
03 | "1:" ALTERNATIVE(instr " " reg "1, [%2]\n" , \ |
04 | alt_instr " " reg "1, [%2]\n" , feature) \ |
06 | " .section .fixup, \"ax\"\n" \ |
12 | _ASM_EXTABLE(1b, 3b) \ |
13 | : "+r" (err), "=&r" (x) \ |
14 | : "r" (addr), "i" (-EFAULT)) |
get_user() – ARM32
arch/arm/include/asm/uaccess.h
1 | #define get_user(x, p) \ |
4 | __get_user_check(x, p); \ |
커널에서 유저 영역의 주소 @p에 담긴 데이터를 @p 타입 사이즈만큼 읽어 커널 영역의 주소 @x에 대입한다. 만일 읽을 수 없으면 -EFAULT 에러를 반환한다.
__get_user_check() – ARM32
arch/arm/include/asm/uaccess.h
01 | #define __get_user_check(x, p) \ |
03 | unsigned long __limit = current_thread_info()->addr_limit - 1; \ |
04 | register typeof(*(p)) __user *__p asm( "r0" ) = (p); \ |
05 | register __inttype(x) __r2 asm( "r2" ); \ |
06 | register unsigned long __l asm( "r1" ) = __limit; \ |
07 | register int __e asm( "r0" ); \ |
08 | unsigned int __ua_flags = uaccess_save_and_enable(); \ |
09 | switch ( sizeof (*(__p))) { \ |
11 | if ( sizeof ((x)) >= 8) \ |
12 | __get_user_x_64t(__r2, __p, __e, __l, 1); \ |
14 | __get_user_x(__r2, __p, __e, __l, 1); \ |
17 | if ( sizeof ((x)) >= 8) \ |
18 | __get_user_x_64t(__r2, __p, __e, __l, 2); \ |
20 | __get_user_x(__r2, __p, __e, __l, 2); \ |
23 | if ( sizeof ((x)) >= 8) \ |
24 | __get_user_x_64t(__r2, __p, __e, __l, 4); \ |
26 | __get_user_x(__r2, __p, __e, __l, 4); \ |
29 | if ( sizeof ((x)) < 8) \ |
30 | __get_user_x_32t(__r2, __p, __e, __l, 4); \ |
32 | __get_user_x(__r2, __p, __e, __l, 8); \ |
34 | default : __e = __get_user_bad(); break ; \ |
36 | uaccess_restore(__ua_flags); \ |
37 | x = (typeof(*(p))) __r2; \ |
__get_user_x() – ARM32
arch/arm/include/asm/uaccess.h
1 | #define __get_user_x(__r2, __p, __e, __l, __s) \ |
2 | __asm__ __volatile__ ( \ |
3 | __asmeq( "%0" , "r0" ) __asmeq( "%1" , "r2" ) \ |
5 | "bl __get_user_" #__s \ |
6 | : "=&r" (__e), "=r" (__r2) \ |
7 | : "0" (__p), "r" (__l) \ |
사이즈(__s)에 따라 커널이 유저 영역을 읽는 함수를 호출한다.
- 1, 2, 4, 8 바이트에 따른 호출함수가 라이브러리에 따로 준비되어 있다.
__get_user_1() – ARM32
arch/arm/lib/getuser.S
2 | check_uaccess r0, 1, r1, r2, __get_user_bad |
7 | _ASM_NOKPROBE(__get_user_1) |
커널에서 유저 영역의 1바이트를 읽어낸다. 만일 읽을 수 없으면 -EFAULT 에러를 반환한다.
check_uaccess() 매크로 – ARM32
arch/arm/include/asm/assembler.h
01 | .macro check_uaccess, addr:req, size:req, limit:req, tmp:req, bad:req |
02 | #ifndef CONFIG_CPU_USE_DOMAINS |
03 | adds \tmp, \addr, #\size - 1 |
04 | sbcccs \tmp, \tmp, \limit |
06 | #ifdef CONFIG_CPU_SPECTRE |
커널에서 유저 데이터 엑세스에 문제가 없는지 확인한다. 에러 발생 시 -EFAULT를 반환한다.
- MMU를 enable 시킨 ARMv7 아키텍처를 사용하면 CONFIG_CPU_SPECTRE 커널 옵션을 디폴트로 사용한다.
- csdb 는 Consumption of Speculative Data Barrier를 의미한다.
__get_user_bad() & __get_user_bad8() – ARM32
arch/arm/lib/getuser.S
07 | ENDPROC(__get_user_bad) |
08 | ENDPROC(__get_user_bad8) |
09 | _ASM_NOKPROBE(__get_user_bad) |
10 | _ASM_NOKPROBE(__get_user_bad8) |
커널에서 유저 데이터 액세스에 문제가 발생했을 때 처리할 함수이다. -EFAULT 에러를 r0 레지스터를 통해 반환한다.
참고