프로세스 주소 공간(process address space)을 access하는 커널 API
가장 잘 알려진 다음 API 두 개를 알아본다.
- copy_from_user()
- copy_to_user()
그리고 유저 프로세스 공간으로의 안전한 접근이 가능한지 여부를 체크하는 다음 API를 알아본다.
유저 프로세스 공간으로 부터 커널로 데이터 복사
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 기법)
- 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
- 1=client
- 페이지 변환 테이블에 지정된 permission 비트에 해당하는 체크를 수행한다.
- 2=reserved
- 3=manager
- 페이지 변환 테이블에 지정된 permission 비트를 무시한다.
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 레지스터를 통해 반환한다.
참고