요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0
======================================================
Control-flow Enforcement Technology (CET) Shadow Stack
======================================================
CET Background
==============
Control-flow Enforcement Technology (CET) covers several related x86 processor
features that provide protection against control flow hijacking attacks. CET
can protect both applications and the kernel.
CET introduces shadow stack and indirect branch tracking (IBT). A shadow stack
is a secondary stack allocated from memory which cannot be directly modified by
applications. When executing a CALL instruction, the processor pushes the
return address to both the normal stack and the shadow stack. Upon
function return, the processor pops the shadow stack copy and compares it
to the normal stack copy. If the two differ, the processor raises a
control-protection fault. IBT verifies indirect CALL/JMP targets are intended
as marked by the compiler with 'ENDBR' opcodes. Not all CPU's have both Shadow
Stack and Indirect Branch Tracking. Today in the 64-bit kernel, only userspace
shadow stack and kernel IBT are supported.
Requirements to use Shadow Stack
================================
To use userspace shadow stack you need HW that supports it, a kernel
configured with it and userspace libraries compiled with it.
The kernel Kconfig option is X86_USER_SHADOW_STACK. When compiled in, shadow
stacks can be disabled at runtime with the kernel parameter: nousershstk.
To build a user shadow stack enabled kernel, Binutils v2.29 or LLVM v6 or later
are required.
At run time, /proc/cpuinfo shows CET features if the processor supports
CET. "user_shstk" means that userspace shadow stack is supported on the current
kernel and HW.
Application Enabling
====================
An application's CET capability is marked in its ELF note and can be verified
from readelf/llvm-readelf output::
readelf -n <application> | grep -a SHSTK
properties: x86 feature: SHSTK
The kernel does not process these applications markers directly. Applications
or loaders must enable CET features using the interface described in section 4.
Typically this would be done in dynamic loader or static runtime objects, as is
the case in GLIBC.
Enabling arch_prctl()'s
=======================
Elf features should be enabled by the loader using the below arch_prctl's. They
are only supported in 64 bit user applications. These operate on the features
on a per-thread basis. The enablement status is inherited on clone, so if the
feature is enabled on the first thread, it will propagate to all the thread's
in an app.
arch_prctl(ARCH_SHSTK_ENABLE, unsigned long feature)
Enable a single feature specified in 'feature'. Can only operate on
one feature at a time.
arch_prctl(ARCH_SHSTK_DISABLE, unsigned long feature)
Disable a single feature specified in 'feature'. Can only operate on
one feature at a time.
arch_prctl(ARCH_SHSTK_LOCK, unsigned long features)
Lock in features at their current enabled or disabled status. 'features'
is a mask of all features to lock. All bits set are processed, unset bits
are ignored. The mask is ORed with the existing value. So any feature bits
set here cannot be enabled or disabled afterwards.
arch_prctl(ARCH_SHSTK_UNLOCK, unsigned long features)
Unlock features. 'features' is a mask of all features to unlock. All
bits set are processed, unset bits are ignored. Only works via ptrace.
arch_prctl(ARCH_SHSTK_STATUS, unsigned long addr)
Copy the currently enabled features to the address passed in addr. The
features are described using the bits passed into the others in
'features'.
The return values are as follows. On success, return 0. On error, errno can
be::
-EPERM if any of the passed feature are locked.
-ENOTSUPP if the feature is not supported by the hardware or
kernel.
-EINVAL arguments (non existing feature, etc)
-EFAULT if could not copy information back to userspace
The feature's bits supported are::
ARCH_SHSTK_SHSTK - Shadow stack
ARCH_SHSTK_WRSS - WRSS
Currently shadow stack and WRSS are supported via this interface. WRSS
can only be enabled with shadow stack, and is automatically disabled
if shadow stack is disabled.
Proc Status
===========
To check if an application is actually running with shadow stack, the
user can read the /proc/$PID/status. It will report "wrss" or "shstk"
depending on what is enabled. The lines look like this::
x86_Thread_features: shstk wrss
x86_Thread_features_locked: shstk wrss
Implementation of the Shadow Stack
==================================
Shadow Stack Size
-----------------
A task's shadow stack is allocated from memory to a fixed size of
MIN(RLIMIT_STACK, 4 GB). In other words, the shadow stack is allocated to
the maximum size of the normal stack, but capped to 4 GB. In the case
of the clone3 syscall, there is a stack size passed in and shadow stack
uses this instead of the rlimit.
Signal
------
The main program and its signal handlers use the same shadow stack. Because
the shadow stack stores only return addresses, a large shadow stack covers
the condition that both the program stack and the signal alternate stack run
out.
When a signal happens, the old pre-signal state is pushed on the stack. When
shadow stack is enabled, the shadow stack specific state is pushed onto the
shadow stack. Today this is only the old SSP (shadow stack pointer), pushed
in a special format with bit 63 set. On sigreturn this old SSP token is
verified and restored by the kernel. The kernel will also push the normal
restorer address to the shadow stack to help userspace avoid a shadow stack
violation on the sigreturn path that goes through the restorer.
So the shadow stack signal frame format is as follows::
|1...old SSP| - Pointer to old pre-signal ssp in sigframe token format
(bit 63 set to 1)
| ...| - Other state may be added in the future
32 bit ABI signals are not supported in shadow stack processes. Linux prevents
32 bit execution while shadow stack is enabled by the allocating shadow stacks
outside of the 32 bit address space. When execution enters 32 bit mode, either
via far call or returning to userspace, a #GP is generated by the hardware
which, will be delivered to the process as a segfault. When transitioning to
userspace the register's state will be as if the userspace ip being returned to
caused the segfault.
Fork
----
The shadow stack's vma has VM_SHADOW_STACK flag set; its PTEs are required
to be read-only and dirty. When a shadow stack PTE is not RO and dirty, a
shadow access triggers a page fault with the shadow stack access bit set
in the page fault error code.
When a task forks a child, its shadow stack PTEs are copied and both the
parent's and the child's shadow stack PTEs are cleared of the dirty bit.
Upon the next shadow stack access, the resulting shadow stack page fault
is handled by page copy/re-use.
When a pthread child is created, the kernel allocates a new shadow stack
for the new thread. New shadow stack creation behaves like mmap() with respect
to ASLR behavior. Similarly, on thread exit the thread's shadow stack is
disabled.
Exec
----
On exec, shadow stack features are disabled by the kernel. At which point,
userspace can choose to re-enable, or lock them.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
CET shadow stack 배경과 요구사항
1-40이 문서는 `SPDX-License-Identifier: GPL-2.0`으로 배포됩니다. Control-flow Enforcement Technology(CET)는 control-flow hijacking attack을 막는 여러 x86 processor feature를 포함하며 application과 kernel을 모두 보호할 수 있습니다.
CET는 shadow stack과 indirect branch tracking(IBT)을 도입합니다. shadow stack은 application이 직접 수정할 수 없는 secondary stack입니다. `CALL` 실행 시 processor가 return address를 normal stack과 shadow stack에 모두 push하고, function return 시 shadow copy를 pop해 normal copy와 비교합니다. 다르면 control-protection fault를 발생시킵니다.
IBT는 indirect `CALL`/`JMP` target이 compiler가 `ENDBR` opcode로 표시한 의도된 위치인지 검증합니다. 모든 CPU가 Shadow Stack과 IBT를 둘 다 갖는 것은 아닙니다. 현재 64-bit kernel은 userspace shadow stack과 kernel IBT만 지원합니다.
userspace shadow stack에는 이를 지원하는 hardware, 해당 기능으로 구성한 kernel, 기능을 넣어 compile한 userspace library가 필요합니다. kernel Kconfig option은 `X86_USER_SHADOW_STACK`이며 runtime에는 `nousershstk` kernel parameter로 비활성화할 수 있습니다.
shadow-stack kernel build에는 Binutils v2.29 또는 LLVM v6 이상이 필요합니다. runtime에 `/proc/cpuinfo`는 processor의 CET feature를 표시하며 `user_shstk`는 현재 kernel과 hardware가 userspace shadow stack을 지원한다는 뜻입니다.
application의 CET 활성화
41-54application의 CET capability는 ELF note에 표시되며 `readelf` 또는 `llvm-readelf` output에서 확인할 수 있습니다.
readelf -n <application> | grep -a SHSTK
properties: x86 feature: SHSTK
kernel은 application marker를 직접 처리하지 않습니다. application 또는 loader가 다음 section의 interface로 CET feature를 활성화해야 합니다. 일반적으로 GLIBC처럼 dynamic loader나 static runtime object가 수행합니다.
shadow-stack arch_prctl interface
55-104loader는 다음 `arch_prctl()`로 ELF feature를 활성화해야 합니다. 64-bit user application에서만 지원되며 thread별로 작동합니다. enable 상태는 `clone`으로 상속되므로 첫 thread에서 켜면 application의 모든 thread로 전파됩니다.
| operation | 동작 |
|---|---|
| `arch_prctl(ARCH_SHSTK_ENABLE, feature)` | `feature`로 지정한 단일 feature를 활성화합니다. |
| `arch_prctl(ARCH_SHSTK_DISABLE, feature)` | `feature`로 지정한 단일 feature를 비활성화합니다. |
| `arch_prctl(ARCH_SHSTK_LOCK, features)` | mask에 설정된 feature의 현재 enable/disable 상태를 lock합니다. 기존 mask와 OR되며 이후 변경할 수 없습니다. |
| `arch_prctl(ARCH_SHSTK_UNLOCK, features)` | mask의 feature를 unlock합니다. `ptrace`를 통해서만 동작합니다. |
| `arch_prctl(ARCH_SHSTK_STATUS, addr)` | 현재 활성화된 feature bit를 `addr`이 가리키는 userspace address로 복사합니다. |
성공하면 0을 반환하며 오류의 errno는 다음과 같습니다.
| errno | 조건 |
|---|---|
| `-EPERM` | 전달한 feature 중 하나라도 lock되어 있습니다. |
| `-ENOTSUPP` | hardware 또는 kernel이 feature를 지원하지 않습니다. |
| `-EINVAL` | 존재하지 않는 feature 등 argument가 잘못되었습니다. |
| `-EFAULT` | userspace로 정보를 복사하지 못했습니다. |
| feature bit | 의미 |
|---|---|
| `ARCH_SHSTK_SHSTK` | Shadow stack |
| `ARCH_SHSTK_WRSS` | WRSS |
현재 shadow stack과 WRSS를 지원합니다. WRSS는 shadow stack과 함께 있을 때만 활성화할 수 있고 shadow stack을 끄면 자동으로 비활성화됩니다.
process의 활성 상태 확인
105-113application이 실제로 shadow stack을 사용 중인지 확인하려면 `/proc/$PID/status`를 읽습니다. 활성화된 기능에 따라 `wrss` 또는 `shstk`가 다음처럼 표시됩니다.
x86_Thread_features: shstk wrss
x86_Thread_features_locked: shstk wrss
크기와 signal frame 구현
114-155task의 shadow stack은 `MIN(RLIMIT_STACK, 4 GB)`의 고정 크기로 memory에서 allocate합니다. normal stack의 최대 크기와 같지만 4GB로 제한됩니다. `clone3` syscall은 전달받은 stack size를 rlimit 대신 사용합니다.
main program과 signal handler는 같은 shadow stack을 사용합니다. shadow stack에는 return address만 저장되므로 큰 shadow stack 하나로 program stack과 signal alternate stack이 모두 소진되는 상황을 포괄합니다.
signal 발생 시 pre-signal state를 stack에 push합니다. shadow stack이 활성화되어 있으면 shadow-stack-specific state도 shadow stack에 push합니다. 현재는 이전 SSP(shadow stack pointer)만 bit 63을 설정한 special format으로 push합니다.
`sigreturn`에서 kernel이 이전 SSP token을 검증하고 restore합니다. restorer를 거치는 sigreturn path에서 userspace shadow-stack violation을 피하도록 normal restorer address도 shadow stack에 push합니다.
현재 token과 향후 확장 가능한 state의 배치를 구조화했습니다.
shadow-stack process는 32-bit ABI signal을 지원하지 않습니다. Linux는 shadow stack을 32-bit address space 밖에 allocate해 활성 상태의 32-bit 실행을 막습니다. far call 또는 userspace 복귀로 32-bit mode에 들어가면 hardware가 `#GP`를 발생시키고 process에는 segfault로 전달합니다. userspace 전환 시 register state는 복귀 대상 userspace IP가 segfault를 일으킨 것처럼 보입니다.
fork·thread 생성·exec
156-179shadow stack VMA에는 `VM_SHADOW_STACK` flag가 설정되고 PTE는 read-only이면서 dirty여야 합니다. RO와 dirty 조건을 충족하지 않은 shadow-stack PTE에 access하면 page-fault error code의 shadow-stack access bit가 설정된 page fault가 발생합니다.
task가 child를 fork하면 shadow-stack PTE를 복사하고 parent와 child 양쪽 PTE에서 dirty bit를 지웁니다. 다음 shadow-stack access에서 생기는 page fault는 page copy 또는 reuse로 처리합니다.
pthread child를 만들면 kernel이 새 thread용 shadow stack을 allocate합니다. 새 shadow stack 생성의 ASLR 동작은 `mmap()`과 같습니다. thread exit 시 해당 shadow stack을 비활성화합니다.
`exec`에서는 kernel이 shadow-stack feature를 비활성화합니다. 이후 userspace가 다시 활성화하거나 lock할 수 있습니다.
요약과 해설
shstk.rst:1-179shadow stack은 CALL의 return address를 application이 직접 수정할 수 없는 별도 stack에 복제하고 return 시 normal stack과 비교해 control-flow hijack을 탐지합니다.
64-bit loader가 `ARCH_SHSTK_*` arch_prctl로 thread별 기능을 관리하며 signal SSP token, copy-on-write PTE, pthread별 stack과 exec 시 reset 규칙을 따라야 합니다.