프로세스 주소 공간(process address space)을 access하는 커널 API
가장 잘 알려진 다음 API 두 개를 알아본다.
- copy_from_user()
- copy_to_user()
그리고 유저 프로세스 공간으로의 안전한 접근이 가능한지 여부를 체크하는 다음 API를 알아본다.
- access_ok()
유저 프로세스 공간으로 부터 커널로 데이터 복사
copy_from_user()
include/linux/uaccess.h
static __always_inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n) { if (likely(check_copy_size(to, n, false))) n = _copy_from_user(to, from, n); return n; }
유저 가상 주소 @from에서 @n 바이트만큼 커널 가상 주소 @to로 데이터를 복사한다.
_copy_from_user()
include/linux/uaccess.h
#ifdef INLINE_COPY_FROM_USER static inline unsigned long _copy_from_user(void *to, const void __user *from, unsigned long n) { unsigned long res = n; might_fault(); if (likely(access_ok(from, n))) { kasan_check_write(to, n); res = raw_copy_from_user(to, from, n); } if (unlikely(res)) memset(to + (n - res), 0, res); return res; } #endif
이 함수는 유저 application에서 사용될 때에는 INLINE_COPY_FROM_USER 옵션이 적용되어 인라인 함수로 제공된다. 그렇지 않고 커널을 통해 호출되는 경우 라이브러리를 통해 제공된다.
아키텍처별 raw_copy_from_user()
다음 아키텍처별로 권한 설정을 아키텍처 고유의 방법을 사용한다.
- ARM32
- 도메인 접근 제어 레지스터를 사용하여 제어한다.
- ARM64
- 커널에서의 유저 액세스 권한 제어 플래그를 사용하여 제어한다.
raw_copy_from_user() – ARM64
arch/arm64/include/asm/uaccess.h
#define raw_copy_from_user(to, from, n) \ ({ \ __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
ENTRY(__arch_copy_from_user) uaccess_enable_not_uao x3, x4, x5 add end, x0, x2 #include "copy_template.S" uaccess_disable_not_uao x3, x4 mov x0, #0 // Nothing to copy ret ENDPROC(__arch_copy_from_user) 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
static inline unsigned long __must_check raw_copy_from_user(void *to, const void __user *from, unsigned long n) { unsigned int __ua_flags; __ua_flags = uaccess_save_and_enable(); n = arm_copy_from_user(to, from, n); uaccess_restore(__ua_flags); return n; }
유저 가상 주소 @from(r1)에서 @n 바이트(r2)만큼 커널 가상 주소 @to(r0)로 복사한다.
다음 그림은 ARM32 시스템에서 copy_from_user() 함수의 처리 흐름을 보여준다.
arm_copy_from_user()
lib/copy_from_user.S
ENTRY(arm_copy_from_user) #ifdef CONFIG_CPU_SPECTRE get_thread_info r3 ldr r3, [r3, #TI_ADDR_LIMIT] uaccess_mask_range_ptr r1, r2, r3, ip #endif #include "copy_template.S" ENDPROC(arm_copy_from_user)
- copy_template.S 에는 어셈블리로 복사 코드가 담겨있다.
유저 프로세스 공간으로 커널 데이터 복사
copy_to_user()
include/linux/uaccess.h
static __always_inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n) { if (likely(check_copy_size(from, n, true))) n = _copy_to_user(to, from, n); return n; }
커널 가상 주소 @from에서 @n 바이트만큼 유저 가상 주소 @to로 데이터를 복사한다.
_copy_to_user()
include/linux/uaccess.h
#ifdef INLINE_COPY_TO_USER static inline unsigned long _copy_to_user(void __user *to, const void *from, unsigned long n) { might_fault(); if (access_ok(to, n)) { kasan_check_read(from, n); n = raw_copy_to_user(to, from, n); } return n; } #endif
이 함수는 유저 application에서 사용될 때에는 INLINE_COPY_TO_USER 옵션이 적용되어 인라인 함수로 제공된다. 그렇지 않고 커널을 통해 호출되는 경우 라이브러리를 통해 제공된다.
아키텍처별 raw_copy_to_user()
다음 아키텍처별로 권한 설정을 아키텍처 고유의 방법을 사용한다.
- ARM32
- 도메인 접근 제어 레지스터를 사용하여 제어한다.
- ARM64
- 커널에서의 유저 액세스 권한 제어 플래그를 사용하여 제어한다.
raw_copy_to_user() – ARM64
arch/arm64/include/asm/uaccess.h
#define raw_copy_to_user(to, from, n) \ ({ \ __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
ENTRY(__arch_copy_to_user) uaccess_enable_not_uao x3, x4, x5 add end, x0, x2 #include "copy_template.S" uaccess_disable_not_uao x3, x4 mov x0, #0 ret ENDPROC(__arch_copy_to_user) 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
static inline unsigned long __must_check raw_copy_to_user(void __user *to, const void *from, unsigned long n) { #ifndef CONFIG_UACCESS_WITH_MEMCPY unsigned int __ua_flags; __ua_flags = uaccess_save_and_enable(); n = arm_copy_to_user(to, from, n); uaccess_restore(__ua_flags); return n; #else return arm_copy_to_user(to, from, n); #endif }
커널 가상 주소 @from(r1)에서 @n 바이트(r2)만큼 유저 가상 주소 @to(r0)로 복사한다.
arm_copy_to_user()
lib/copy_to_user.S
ENTRY(__copy_to_user_std) WEAK(arm_copy_to_user) #ifdef CONFIG_CPU_SPECTRE get_thread_info r3 ldr r3, [r3, #TI_ADDR_LIMIT] uaccess_mask_range_ptr r0, r2, r3, ip #endif #include "copy_template.S" ENDPROC(arm_copy_to_user)
- copy_template.S 에는 어셈블리로 복사 코드가 담겨있다.
User 공간 접근 제어 – ARM64
보안을 향상시킬 목적으로 커널에서 유저 공간 액세스를 컨트롤하기 위해 다음 두 가지 방법 중 하나를 사용할 수 있다. 이러한 기능이 적용되지 않는 경우 기본적으로 커널에서 유저 공간의 접근이 언제나 허용된다.
- ARMv8.1-PAN 기능을 사용한 제어 (HW 기법)
- 커널 옵션: CONFIG_ARM64_PAN
- 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
.macro uaccess_enable_not_uao, tmp1, tmp2, tmp3 uaccess_ttbr0_enable \tmp1, \tmp2, \tmp3 alternative_if ARM64_ALT_PAN_NOT_UAO SET_PSTATE_PAN(0) alternative_else_nop_endif .endm
커널에서 유저 데이터에 접근할 수 있도록 허용한다.
- 코드 라인 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
.macro uaccess_ttbr0_enable, tmp1, tmp2, tmp3 alternative_if_not ARM64_HAS_PAN save_and_disable_irq \tmp3 // avoid preemption __uaccess_ttbr0_enable \tmp1, \tmp2 restore_irq \tmp3 alternative_else_nop_endif .endm
ARMv8.1 아키텍처 이상에서 지원되는 PAN 기능이 지원되지 않는 경우 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 허용하게 한다.
__uaccess_ttbr0_enable() 매크로
.macro __uaccess_ttbr0_enable, tmp1, tmp2 get_thread_info \tmp1 ldr \tmp1, [\tmp1, #TSK_TI_TTBR0] // load saved TTBR0_EL1 mrs \tmp2, ttbr1_el1 extr \tmp2, \tmp2, \tmp1, #48 ror \tmp2, \tmp2, #16 msr ttbr1_el1, \tmp2 // set the active ASID isb msr ttbr0_el1, \tmp1 // set the non-PAN TTBR0_EL1 isb .endm
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
.macro uaccess_disable_not_uao, tmp1, tmp2 uaccess_ttbr0_disable \tmp1, \tmp2 alternative_if ARM64_ALT_PAN_NOT_UAO SET_PSTATE_PAN(1) alternative_else_nop_endif .endm
커널에서 유저 데이터에 접근하지 못하게 금지한다.
- 코드 라인 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
.macro uaccess_ttbr0_enable, tmp1, tmp2, tmp3 alternative_if_not ARM64_HAS_PAN save_and_disable_irq \tmp3 // avoid preemption __uaccess_ttbr0_enable \tmp1, \tmp2 restore_irq \tmp3 alternative_else_nop_endif .endm
ARMv8.1 아키텍처 이상에서 지원되는 PAN 기능이 지원되지 않는 경우 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 금지하게 한다.
__uaccess_ttbr0_disable() 매크로
arch/arm64/include/asm/asm-uaccess.h
.macro __uaccess_ttbr0_disable, tmp1 mrs \tmp1, ttbr1_el1 // swapper_pg_dir bic \tmp1, \tmp1, #TTBR_ASID_MASK sub \tmp1, \tmp1, #RESERVED_TTBR0_SIZE // reserved_ttbr0 just before swapper_pg_dir msr ttbr0_el1, \tmp1 // set reserved TTBR0_EL1 isb add \tmp1, \tmp1, #RESERVED_TTBR0_SIZE msr ttbr1_el1, \tmp1 // set reserved ASID isb .endm
SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 금지하도록 ttbr을 설정한다.
- 커널 페이지 테이블을 가리키는 TTBR1의 asid를 클리어한다.
- 유저 페이지 테이블을 가리키는 TTBR0에 reserved_ttbr0 라는 이름의 빈 페이지 테이블을 가리키게한다.
- CONFIG_ARM64_SW_TTBR0_PAN 커널 옵션을 사용하는 경우 swapper_pg_dir 이전에 reserved_ttbr0 라는 이름의 빈 페이지 테이블이 구성되어 있다.
Process State(PSTATE) 설정
/* * Instructions for modifying PSTATE fields. * As per Arm ARM for v8-A, Section "C.5.1.3 op0 == 0b00, architectural hints, * barriers and CLREX, and PSTATE access", ARM DDI 0487 C.a, system instructions * for accessing PSTATE fields have the following encoding: * Op0 = 0, CRn = 4 * Op1, Op2 encodes the PSTATE field modified and defines the constraints. * CRm = Imm4 for the instruction. * Rt = 0x1f */
#define pstate_field(op1, op2) ((op1) << Op1_shift | (op2) << Op2_shift) #define PSTATE_Imm_shift CRm_shift #define PSTATE_PAN pstate_field(0, 4) #define PSTATE_UAO pstate_field(0, 3) #define PSTATE_SSBS pstate_field(3, 1) #define SET_PSTATE_PAN(x) __emit_inst(0xd500401f | PSTATE_PAN | ((!!x) << PSTATE_Imm_ss hift)) #define SET_PSTATE_UAO(x) __emit_inst(0xd500401f | PSTATE_UAO | ((!!x) << PSTATE_Imm_ss hift)) #define SET_PSTATE_SSBS(x) __emit_inst(0xd500401f | PSTATE_SSBS | ((!!x) << PSTATE_Imm__ shift))
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
static inline void uaccess_enable_not_uao(void) { __uaccess_enable(ARM64_ALT_PAN_NOT_UAO); }
커널에서 유저 데이터에 접근할 수 있도록 잠시 허용한다.
__uaccess_enable()
arch/arm64/include/asm/uaccess.h
#define __uaccess_enable(alt) \ do { \ if (!uaccess_ttbr0_enable()) \ asm(ALTERNATIVE("nop", SET_PSTATE_PAN(0), alt, \ CONFIG_ARM64_PAN)); \ } while (0)
커널에서 유저 액세스가 가능하도록 허용한다.
- 코드 라인 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
static inline bool uaccess_ttbr0_enable(void) { if (!system_uses_ttbr0_pan()) return false; __uaccess_ttbr0_enable(); return true; }
커널에서 유저 데이터에 접근할 수 있도록 허용한다.
- 코드 라인 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
static inline bool system_uses_ttbr0_pan(void) { return IS_ENABLED(CONFIG_ARM64_SW_TTBR0_PAN) && !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
static inline void __uaccess_ttbr0_enable(void) { unsigned long flags, ttbr0, ttbr1; /* * Disable interrupts to avoid preemption between reading the 'ttbr0' * variable and the MSR. A context switch could trigger an ASID * roll-over and an update of 'ttbr0'. */ local_irq_save(flags); ttbr0 = READ_ONCE(current_thread_info()->ttbr0); /* Restore active ASID */ ttbr1 = read_sysreg(ttbr1_el1); ttbr1 &= ~TTBR_ASID_MASK; /* safety measure */ ttbr1 |= ttbr0 & TTBR_ASID_MASK; write_sysreg(ttbr1, ttbr1_el1); isb(); /* Restore user page table */ write_sysreg(ttbr0, ttbr0_el1); isb(); 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
static inline void uaccess_disable_not_uao(void) { __uaccess_disable(ARM64_ALT_PAN_NOT_UAO); }
다시 커널에서 유저 데이터에 접근하지 못하게 한다.
__uaccess_disable()
arch/arm64/include/asm/uaccess.h
#define __uaccess_disable(alt) \ do { \ if (!uaccess_ttbr0_disable()) \ asm(ALTERNATIVE("nop", SET_PSTATE_PAN(1), alt, \ CONFIG_ARM64_PAN)); \ } while (0)
커널에서 유저 데이터에 접근하지 못하게 금지한다.
- 코드 라인 3에서 ARMv8.1 아키텍처 이상에서 지원되는 PAN 기능이 지원되지 않는 경우 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 금지하게 한다.
- 코드 라인 4~6에서 PAN 기능이 지원되면 ARMv8.2 아키텍처 이상에서만 지원하는 UAO 기능에 의지 하지 않고 PAN 기능을 enable하여 커널에서 유저 공간에 접근하지 못하게 금지한다.
uaccess_ttbr0_disable()
arch/arm64/include/asm/uaccess.h
static inline bool uaccess_ttbr0_disable(void) { if (!system_uses_ttbr0_pan()) return false; __uaccess_ttbr0_disable(); return true; }
커널에서 유저 데이터에 접근할 수 없도록 금지한다.
- 코드 라인 2에서 ARMv8.1 아키텍처 이상에서 지원되는 PAN 기능이 지원되지 않는 경우 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 금지하게 한다.
- 코드 라인 3~5에서 PAN 기능이 지원되면 ARMv8.2 아키텍처 이상에서만 지원하는 UAO 기능에 의지 하지 않고 PAN 기능을 enable하여 커널에서 유저 공간에 접근하지 못하게 금지한다.
__uaccess_ttbr0_disable()
arch/arm64/include/asm/uaccess.h
static inline void __uaccess_ttbr0_disable(void) { unsigned long flags, ttbr; local_irq_save(flags); ttbr = read_sysreg(ttbr1_el1); ttbr &= ~TTBR_ASID_MASK; /* reserved_ttbr0 placed before swapper_pg_dir */ write_sysreg(ttbr - RESERVED_TTBR0_SIZE, ttbr0_el1); isb(); /* Set reserved ASID */ write_sysreg(ttbr, ttbr1_el1); isb(); 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
/* * These two functions allow hooking accesses to userspace to increase * system integrity by ensuring that the kernel can not inadvertantly * perform such accesses (eg, via list poison values) which could then * be exploited for priviledge escalation. */
static inline unsigned int uaccess_save_and_enable(void) { #ifdef CONFIG_CPU_SW_DOMAIN_PAN unsigned int old_domain = get_domain(); /* Set the current domain access to permit user accesses */ set_domain((old_domain & ~domain_mask(DOMAIN_USER)) | domain_val(DOMAIN_USER, DOMAIN_CLIENT)); return old_domain; #else return 0; #endif }
유저 도메인의 값을 반환하고, 클라이언트 권한(페이지 테이블에 설정된 permission 대로 동작하기)을 부여한다.
uaccess_restore()
arch/arm/include/asm/uaccess.h
static inline void uaccess_restore(unsigned int flags) { #ifdef CONFIG_CPU_SW_DOMAIN_PAN /* Restore the user access mask */ set_domain(flags); #endif }
유저 도메인의 값을 원래 값(flags)으로 복구한다.
도메인별 permission 권한 설정하기
set_domain()
arch/arm/include/asm/domain.h
static inline void set_domain(unsigned val) { asm volatile( "mcr p15, 0, %0, c3, c0 @ set domain" : : "r" (val) : "memory"); isb(); }
DACR(Domain Access Control Register)을 사용하여 16개의 도메인을 지정한다.
- ARM32 커널은 16개의 도메인 중 커널, 유저, IO 및 벡터 도메인을 지정하여 총 4개의 도메인을 사용한다.
- 각 도메인 마다 2비트를 사용하여 다음과 같은 permission 사용 여부를 지정할 수 있다.
- 0=no access
- 항상 permission 에러가 발생한다.
- 1=client
- 페이지 변환 테이블에 지정된 permission 비트에 해당하는 체크를 수행한다.
- 2=reserved
- 3=manager
- 페이지 변환 테이블에 지정된 permission 비트를 무시한다.
- 0=no access
uaccess_mask_range_ptr() 매크로
include/asm/assembler.h
.macro uaccess_mask_range_ptr, addr:req, size:req, limit:req, tmp:req #ifdef CONFIG_CPU_SPECTRE sub \tmp, \limit, #1 subs \tmp, \tmp, \addr @ tmp = limit - 1 - addr addhs \tmp, \tmp, #1 @ if (tmp >= 0) { subhss \tmp, \tmp, \size @ tmp = limit - (addr + size) } movlo \addr, #0 @ if (tmp < 0) addr = NULL csdb #endif .endm
uaccess_save 매크로
arch/arm/include/asm/assembler.h
.macro uaccess_save, tmp #ifdef CONFIG_CPU_SW_DOMAIN_PAN mrc p15, 0, \tmp, c3, c0, 0 str \tmp, [sp, #SVC_DACR] #endif .endm
보안을 위해 커널에서 user 영역에 접근을 하지 못하게 제한한다. 그리고 이전 유저 도메인에 대한 도메인 타입을 스택이 가리키고 있는 pt_regs의 멤버 dacr에 백업한다.
uaccess_restore 매크로
arch/arm/include/asm/assembler.h
.macro uaccess_restore #ifdef CONFIG_CPU_SW_DOMAIN_PAN ldr r0, [sp, #SVC_DACR] mcr p15, 0, r0, c3, c0, 0 #endif .endm
보안을 위해 커널에서 user 영역에 접근에 대한 여부를 기록하는데, 백업해 두었던 스택에 위치한 pt_regs의 멤버 dacr을 읽은 값을 기록한다.
uaccess_disable 매크로
arch/arm/include/asm/assembler.h
.macro uaccess_disable, tmp, isb=1 #ifdef CONFIG_CPU_SW_DOMAIN_PAN /* * Whenever we re-enter userspace, the domains should always be * set appropriately. */ mov \tmp, #DACR_UACCESS_DISABLE mcr p15, 0, \tmp, c3, c0, 0 @ Set domain register .if \isb instr_sync .endif #endif .endm
보안을 위해 커널에서 user 영역에 접근을 하지 못하게 제한한다.
- 코드 라인 7~8에서 커널에서 user 영역에 접근을 제한한다.
- 코드 라인 9~11에서 @isb=1인 경우 instruction 베리어를 수행한다.
uaccess_enable 매크로
arch/arm/include/asm/assembler.h
.macro uaccess_enable, tmp, isb=1 #ifdef CONFIG_CPU_SW_DOMAIN_PAN /* * Whenever we re-enter userspace, the domains should always be * set appropriately. */ mov \tmp, #DACR_UACCESS_ENABLE mcr p15, 0, \tmp, c3, c0, 0 .if \isb instr_sync .endif #endif .endm
보안을 위해 커널에서 user 영역에 접근을 하지 못하게 제한한 것을 허용하게 한다.
- 코드 라인 7~8에서 커널에서 user 영역에 접근하도록 허용한다.
- 코드 라인 9~11에서 @isb=1인 경우 instruction 베리어를 수행한다.
DACR_UACCESS_DISABLE
arch/arm/include/asm/domain.h
#define DACR_UACCESS_DISABLE \ (__DACR_DEFAULT | domain_val(DOMAIN_USER, DOMAIN_NOACCESS))
유저 도메인의 타입을 noaccess로 지정한다. (커널에서 유저 영역의 액세스 금지)
DACR_UACCESS_ENABLE
arch/arm/include/asm/domain.h
#define DACR_UACCESS_DISABLE \ (__DACR_DEFAULT | domain_val(DOMAIN_USER, DOMAIN_NOACCESS)) #define DACR_UACCESS_ENABLE \ (__DACR_DEFAULT | domain_val(DOMAIN_USER, DOMAIN_CLIENT))
유저 도메인의 타입을 client로 지정한다. (커널에서 유저 영역의 액세스 허용)
arch/arm/include/asm/domain.h
#define __DACR_DEFAULT \ domain_val(DOMAIN_KERNEL, DOMAIN_CLIENT) | \ domain_val(DOMAIN_IO, DOMAIN_CLIENT) | \ domain_val(DOMAIN_VECTORS, DOMAIN_CLIENT)
#define domain_val(dom,type) ((type) << (2 * (dom)))
16개의 도메인 중 요청한 도메인 @dom에 도메인 @type을 지정한다.
- DACR(Domain Access Control Register) 레지스터는 16개의 도메인에 2비트씩 도메인 타입을 지정할 수 있다.
도메인 타입
#define DOMAIN_NOACCESS 0 #define DOMAIN_CLIENT 1 #ifdef CONFIG_CPU_USE_DOMAINS #define DOMAIN_MANAGER 3 #else #define DOMAIN_MANAGER 1 #endif
유효한 유저 프로세스 주소 사용 여부 체크
access_ok() – ARM64
arch/arm64/include/asm/uaccess.h
#define access_ok(addr, size) __range_ok(addr, size)
@addr 주소에서 @size 만큼의 영역이 유효한 유저 프로세스 주소인지 여부를 체크한다. 유효하지 않는 경우 0을 반환한다.
__range_ok() – ARM64
arch/arm64/include/asm/uaccess.h
/* * Test whether a block of memory is a valid user space address. * Returns 1 if the range is valid, 0 otherwise. * * This is equivalent to the following test: * (u65)addr + (u65)size <= (u65)current->addr_limit + 1 */
static inline unsigned long __range_ok(const void __user *addr, unsigned long size) { unsigned long ret, limit = current_thread_info()->addr_limit; __chk_user_ptr(addr); asm volatile( // A + B <= C + 1 for all A,B,C, in four easy steps: // 1: X = A + B; X' = X % 2^64 " adds %0, %3, %2\n" // 2: Set C = 0 if X > 2^64, to guarantee X' > C in step 4 " csel %1, xzr, %1, hi\n" // 3: Set X' = ~0 if X >= 2^64. For X == 2^64, this decrements X' // to compensate for the carry flag being set in step 4. For // X > 2^64, X' merely has to remain nonzero, which it does. " csinv %0, %0, xzr, cc\n" // 4: For X < 2^64, this gives us X' - C - 1 <= 0, where the -1 // comes from the carry in being clear. Otherwise, we are // testing X' - C == 0, subject to the previous adjustments. " sbcs xzr, %0, %1\n" " cset %0, ls\n" : "=&r" (ret), "+r" (limit) : "Ir" (size), "0" (addr) : "cc"); return ret; }
access_ok() – ARM32
arch/arm/include/asm/uaccess.h
#define access_ok(addr, size) (__range_ok(addr, size) == 0)
@addr 주소에서 @size 만큼의 영역이 유효한 유저 프로세스 주소인지 여부를 체크한다. 유효하지 않는 경우 0을 반환한다.
__range_ok() – ARM32
arch/arm/include/asm/uaccess.h
/* We use 33-bit arithmetic here... */ #define __range_ok(addr, size) ({ \ unsigned long flag, roksum; \ __chk_user_ptr(addr); \ __asm__("adds %1, %2, %3; sbcccs %1, %1, %0; movcc %0, #0" \ : "=&r" (flag), "=&r" (roksum) \ : "r" (addr), "Ir" (size), "0" (current_thread_info()->addr_limit) \ : "cc"); \ flag; })
커널에서 유저 영역의 값(1, 2, 4, 8 바이트) 읽어오기
get_user() API는 아키텍처별로 약간의 다른 구현을 사용한다.
- arm64
- exception 테이블을 활용하여 읽기를 시도한다.
- arm32
- MMU를 사용하는 ARMv7 아키텍처에 한해 exception 테이블 사용없이 미리 체크를 한 후 읽기를 시도한다.
- MMU를 사용하지 않는 경우 exception 테이블을 활용하여 읽기를 시도한다.
get_user() – ARM64
arch/arm64/include/asm/uaccess.h
#define get_user __get_user
커널에서 유저 영역의 주소 @p에 담긴 데이터를 @p 타입 사이즈만큼 읽어 커널 영역의 주소 @x에 대입한다. 만일 읽을 수 없으면 -EFAULT 에러를 반환한다.
__get_user() – ARM64
arch/arm64/include/asm/uaccess.h
#define __get_user(x, ptr) \ ({ \ int __gu_err = 0; \ __get_user_check((x), (ptr), __gu_err); \ __gu_err; \ })
__get_user_check() – ARM64
arch/arm64/include/asm/uaccess.h
#define __get_user_check(x, ptr, err) \ ({ \ __typeof__(*(ptr)) __user *__p = (ptr); \ might_fault(); \ if (access_ok(__p, sizeof(*__p))) { \ __p = uaccess_mask_ptr(__p); \ __get_user_err((x), __p, (err)); \ } else { \ (x) = 0; (err) = -EFAULT; \ } \ })
__get_user_err() – ARM64
arch/arm64/include/asm/uaccess.h
#define __get_user_err(x, ptr, err) \ do { \ unsigned long __gu_val; \ __chk_user_ptr(ptr); \ uaccess_enable_not_uao(); \ switch (sizeof(*(ptr))) { \ case 1: \ __get_user_asm("ldrb", "ldtrb", "%w", __gu_val, (ptr), \ (err), ARM64_HAS_UAO); \ break; \ case 2: \ __get_user_asm("ldrh", "ldtrh", "%w", __gu_val, (ptr), \ (err), ARM64_HAS_UAO); \ break; \ case 4: \ __get_user_asm("ldr", "ldtr", "%w", __gu_val, (ptr), \ (err), ARM64_HAS_UAO); \ break; \ case 8: \ __get_user_asm("ldr", "ldtr", "%x", __gu_val, (ptr), \ (err), ARM64_HAS_UAO); \ break; \ default: \ BUILD_BUG(); \ } \ uaccess_disable_not_uao(); \ (x) = (__force __typeof__(*(ptr)))__gu_val; \ } while (0)
__get_user_asm() – ARM64
arch/arm64/include/asm/uaccess.h
/* * The "__xxx" versions of the user access functions do not verify the address * space - it must have been done previously with a separate "access_ok()" * call. * * The "__xxx_error" versions set the third argument to -EFAULT if an error * occurs, and leave it unchanged on success. */
#define __get_user_asm(instr, alt_instr, reg, x, addr, err, feature) \ asm volatile( \ "1:"ALTERNATIVE(instr " " reg "1, [%2]\n", \ alt_instr " " reg "1, [%2]\n", feature) \ "2:\n" \ " .section .fixup, \"ax\"\n" \ " .align 2\n" \ "3: mov %w0, %3\n" \ " mov %1, #0\n" \ " b 2b\n" \ " .previous\n" \ _ASM_EXTABLE(1b, 3b) \ : "+r" (err), "=&r" (x) \ : "r" (addr), "i" (-EFAULT))
get_user() – ARM32
arch/arm/include/asm/uaccess.h
#define get_user(x, p) \ ({ \ might_fault(); \ __get_user_check(x, p); \ })
커널에서 유저 영역의 주소 @p에 담긴 데이터를 @p 타입 사이즈만큼 읽어 커널 영역의 주소 @x에 대입한다. 만일 읽을 수 없으면 -EFAULT 에러를 반환한다.
__get_user_check() – ARM32
arch/arm/include/asm/uaccess.h
#define __get_user_check(x, p) \ ({ \ unsigned long __limit = current_thread_info()->addr_limit - 1; \ register typeof(*(p)) __user *__p asm("r0") = (p); \ register __inttype(x) __r2 asm("r2"); \ register unsigned long __l asm("r1") = __limit; \ register int __e asm("r0"); \ unsigned int __ua_flags = uaccess_save_and_enable(); \ switch (sizeof(*(__p))) { \ case 1: \ if (sizeof((x)) >= 8) \ __get_user_x_64t(__r2, __p, __e, __l, 1); \ else \ __get_user_x(__r2, __p, __e, __l, 1); \ break; \ case 2: \ if (sizeof((x)) >= 8) \ __get_user_x_64t(__r2, __p, __e, __l, 2); \ else \ __get_user_x(__r2, __p, __e, __l, 2); \ break; \ case 4: \ if (sizeof((x)) >= 8) \ __get_user_x_64t(__r2, __p, __e, __l, 4); \ else \ __get_user_x(__r2, __p, __e, __l, 4); \ break; \ case 8: \ if (sizeof((x)) < 8) \ __get_user_x_32t(__r2, __p, __e, __l, 4); \ else \ __get_user_x(__r2, __p, __e, __l, 8); \ break; \ default: __e = __get_user_bad(); break; \ } \ uaccess_restore(__ua_flags); \ x = (typeof(*(p))) __r2; \ __e; \ })
__get_user_x() – ARM32
arch/arm/include/asm/uaccess.h
#define __get_user_x(__r2, __p, __e, __l, __s) \ __asm__ __volatile__ ( \ __asmeq("%0", "r0") __asmeq("%1", "r2") \ __asmeq("%3", "r1") \ "bl __get_user_" #__s \ : "=&r" (__e), "=r" (__r2) \ : "0" (__p), "r" (__l) \ : __GUP_CLOBBER_##__s)
사이즈(__s)에 따라 커널이 유저 영역을 읽는 함수를 호출한다.
- 1, 2, 4, 8 바이트에 따른 호출함수가 라이브러리에 따로 준비되어 있다.
__get_user_1() – ARM32
arch/arm/lib/getuser.S
ENTRY(__get_user_1) check_uaccess r0, 1, r1, r2, __get_user_bad 1: TUSER(ldrb) r2, [r0] mov r0, #0 ret lr ENDPROC(__get_user_1) _ASM_NOKPROBE(__get_user_1)
커널에서 유저 영역의 1바이트를 읽어낸다. 만일 읽을 수 없으면 -EFAULT 에러를 반환한다.
check_uaccess() 매크로 – ARM32
arch/arm/include/asm/assembler.h
.macro check_uaccess, addr:req, size:req, limit:req, tmp:req, bad:req #ifndef CONFIG_CPU_USE_DOMAINS adds \tmp, \addr, #\size - 1 sbcccs \tmp, \tmp, \limit bcs \bad #ifdef CONFIG_CPU_SPECTRE movcs \addr, #0 csdb #endif #endif .endm
커널에서 유저 데이터 엑세스에 문제가 없는지 확인한다. 에러 발생 시 -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
__get_user_bad8: mov r3, #0 __get_user_bad: mov r2, #0 mov r0, #-EFAULT ret lr ENDPROC(__get_user_bad) ENDPROC(__get_user_bad8) _ASM_NOKPROBE(__get_user_bad) _ASM_NOKPROBE(__get_user_bad8)
커널에서 유저 데이터 액세스에 문제가 발생했을 때 처리할 함수이다. -EFAULT 에러를 r0 레지스터를 통해 반환한다.
참고
- sort_main_extable() | 문c
- Exception Table (extable) | 문c
안녕하세요.
copy_to_user() 에 대해서 궁금한 점이 생겨서 질문 남겨드립니다.
위 ‘copy_to_user’ 함수에서 대한 설명을 보면 kernel은 application으로 전달받은 메모리에 접근하기 위해 kernel address space에 해당 메모리를 매핑하여 copy 하고 매핑한 메모리에 대해서 해제하는것으로 이해가 되었습니다.
그리고 모기향책 2판을 보고 있는 중이며, 1.5장 mmu 에서 아래와 같은 글을 보았습니다.
” 변환 테이블의 시작 주소는 TTBRO_ELl(Translation Table Base Register)과 TTBRl_ELl에 지정된다. MMU는 접근한 가상 주소의 상위 비트가 모두 0이면 TTBRO가 가리키는 변환 테이블을 사
용하고, 상위 비트가 모두 l이면 TTBRl이 가리키는 변환 테이블을 사용한다.”
이 책의 내용에 따르면 kernel에서 복사를 할 때 굳히 메모리 매핑을 할 필요 없이 유저영역의 주소(0x00000000_00000000 ~ 0x0000FFFF_FFFFFFFF)에 접근하면서 자연스럽게 TTBR0_EL1을 통해 주소변환이 일어나서 copy operation이 일어날 것으로 보입니다. 혹시 커널에서 kernel address space에 user로 부터 전달받은 메모리 주소를 매핑시켜야 할 이유가 따로 있을까요?
안녕하세요?
생각하시는 것처럼 매핑하지 않습니다. 그냥 memcpy()와 동일하게 동작하지만,
최근 커널의 경우 보안 때문에 커널 조차도 곧바로 memcpy를 하지 못할 뿐입니다.
따라서 잠깐 보안을 풀고 memcpy() 한 후 다시 보안을 겁니다.