copy_from_user()

 

프로세스 주소 공간(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 비트를 무시한다.

 

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

 


유효한 유저 프로세스 주소 사용 여부 체크

 

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 레지스터를 통해 반환한다.

 

참고

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다