CONFIG_USERSPACE=y, 사용자 모드를 지원하는 아키텍처를 전제로 한다. 그림은 소스 경로를 다시 그린 설명도이며 실기 보안 검증이나 성능 측정값이 아니다. 사용자 코드 밖의 커널·슈퍼바이저 코드와 빌드 구성을 신뢰하는 모델이다.1. 주소 접근과 객체 조작은 별개로 판정한다
사용자 스레드가 일반 데이터 주소를 직접 읽고 쓰려면 현재 스레드에 적용된 메모리 보호 설정이 그 주소를 허용해야 한다. 반면 k_sem_take()에 세마포어 포인터를 넘길 때는 시스템 호출 검증이 그 포인터가 알려진 세마포어 객체인지, 호출 스레드에 권한이 있는지, 초기화되었는지를 확인한다. 객체 권한은 해당 객체 구조체의 커널 메모리를 직접 읽도록 열어 주는 권한이 아니다.
① 데이터 주소 접근 · 메모리 보호
사용자 스레드 → 코드·읽기 전용 데이터·스택·허용된 버퍼
코드·읽기 전용 데이터와 스택은 기본 접근 범위다. 추가 읽기·쓰기 RAM은 구성별 사용자 영역 또는 현재 메모리 도메인 파티션에 따라 달라진다.
같은 도메인의 다른 사용자 스택도 기본적으로 접근 가능하며, 지원 아키텍처의 isolated-stacks 구성이 이를 더 제한할 수 있다.
② 커널 서비스 호출 · 객체 검증
사용자 스레드 → syscall → 커널 객체
객체 포인터의 등록 여부·종류·호출 스레드 권한·초기화 상태를 verifier가 확인한다.
예: k_sem_take(&sem, ...)은 세마포어 객체 권한이 필요하다.
메모리 도메인에 데이터 버퍼를 넣어도 세마포어 객체 사용 권한이 자동으로 생기지 않는다. 반대로 k_object_access_grant()는 임의의 RAM 범위를 사용자 주소 공간에 매핑하지 않는다. 스택 간 접근은 같은 도메인인지, 아키텍처가 스택 격리를 지원·활성화했는지도 봐야 한다.
| 하려는 일 | 주요 경계 | 권한을 주는 단위 |
|---|---|---|
| 코드 실행·읽기 전용 데이터 접근 | 부트 시 사용자용 코드·읽기 전용 영역 | 실행·읽기 권한. 일반 쓰기 버퍼로 취급하지 않음 |
| 스택의 바이트 읽기·쓰기 | 현재 스레드와 같은 메모리 도메인, 선택적 스택 격리 | 자기 스택은 보장. 같은 도메인의 다른 사용자 스택은 기본 접근 가능하며 isolated-stacks 구성에서 달라짐 |
| 추가 사용자 버퍼의 바이트 읽기·쓰기 | 현재 스레드의 메모리 보호 설정 | 구성별 사용자 RW 영역 또는 메모리 도메인 파티션 |
k_sem_take()로 세마포어 조작 | syscall verifier의 커널 객체 검사 | 세마포어 객체에 대한 스레드 권한 |
| 세마포어 구조체를 사용자 포인터로 직접 역참조 | 커널 메모리 보호 | 객체 권한이 있어도 직접 접근은 허용되지 않음 |
근거: 커널 객체 문서, 부트 시 사용자 영역, 스택·도메인·선택적 격리.
2. 사용자 스레드는 사용자용 스택과 진입 경로가 필요하다
K_USER 옵션으로 스레드를 만들면 사용자 권한으로 실행한다. 이때 K_THREAD_STACK_DEFINE 같은 사용자 모드 가능 스택을 써야 한다. v3.7.0의 z_setup_new_thread()는 K_USER 스레드에 kernel-only 스택이 들어오면 assertion으로 막고, 새 스레드 객체에 자기 자신에 대한 접근 권한을 준다. 기존 슈퍼바이저 스레드는 k_thread_user_mode_enter()로 낮은 권한에 진입할 수 있지만, 이 API는 돌아오지 않는 단방향 전환이다.
새 스레드는 생성자의 메모리 도메인 구성을 이어받는다. 커널 객체 권한까지 복사하는 것은 별도 K_INHERIT_PERMS 옵션이다. v3.7.0의 실제 복사 루프는 부모가 권한을 가진 객체를 자식에 부여하되 부모 스레드 객체 자체는 제외한다. 따라서 도메인을 물려받았다는 사실만으로 부모의 세마포어 사용 권한까지 생겼다고 해석하면 안 된다.
근거: 스레드 문서, thread.c 541–550행, 630–635행, 775–801행, userspace.c 588–610행.
3. 사용자 호출은 구현 전에 인수를 검증한다
__syscall로 선언된 API의 사용자 경로는 트랩과 생성된 marshaling 코드를 거쳐 z_vrfy_* 검증 함수에 도착한다. 검증이 통과하면 z_impl_* 구현을 실행한다. 슈퍼바이저 호출은 보통 직접 구현 경로로 가므로 같은 사용자 검증 단계를 거친다고 그리면 틀리다. 실제 k_sem_take() 검증 함수는 K_OOPS(K_SYSCALL_OBJ(sem, K_OBJ_SEM))을 실행한 뒤 구현을 호출한다.
k_sem_take()의 사용자 호출 경로와 실패 분기각 칸은 코드상의 단계이며 일정한 실행 시간이나 일반 API 반환 순서를 뜻하지 않는다.
k_sem_take(sem, timeout)호출자의 포인터는 아직 신뢰할 수 없다.
인수를 커널의 사용자 호출 경로로 전달한다.
z_vrfy_k_sem_take()K_SYSCALL_OBJ: 알려진 객체·세마포어 종류·권한·초기화 상태
z_impl_k_sem_take()세마포어 count와 대기 처리로 진행한다.
K_OOPSfatal 오류 경로. 검증 내부의 -EPERM을 일반 API 반환으로 읽지 않는다.
아래 두 칸은 검증 통과/실패의 양 갈래다. 버퍼 포인터를 받는 다른 syscall은 별도의 읽기·쓰기 범위 검증도 할 수 있다.
4. 객체 포인터를 알고 있어도 사용 허가는 따로 필요하다
사용자 스레드가 세마포어의 주소를 계산할 수 있다는 것과 그 세마포어를 사용할 수 있다는 것은 다르다. v3.7.0의 k_object_validate()는 알려진 객체와 기대 종류를 먼저 검사하고, 현재 스레드의 permission bit를 확인한 뒤, 요청된 초기화 상태를 검사한다. 일반 K_SYSCALL_OBJ는 초기화된 객체를 요구한다. 객체마다 실제 syscall verifier가 요구하는 조건을 확인해야 하며 모든 API가 똑같은 객체 인수를 받는 것은 아니다.
| 내부 검사 | 통과 조건 | 실패 시 내부 코드 |
|---|---|---|
| 객체·종류 | 알려진 객체이고 요구 타입과 일치 | -EBADF |
| 스레드 권한 | 현재 스레드 permission bit 또는 public 객체 | -EPERM |
| 초기화 상태 | 일반 사용 API는 초기화된 객체 | -EINVAL 등, 검사 모드에 따라 다름 |
k_object_access_grant()는 특정 스레드에 객체 접근을 부여한다. 슈퍼바이저에서 호출할 수 있고, 사용자 스레드가 호출한다면 부여할 객체와 대상 스레드 객체 양쪽에 대한 권한이 필요하다. 정적 스레드에는 K_THREAD_ACCESS_GRANT를 사용할 수 있다. K_INHERIT_PERMS는 생성 시 복사 선택이다. k_object_access_all_grant()로 public이 된 객체는 현재와 미래의 모든 스레드가 사용할 수 있으며 그 공개 권한은 철회할 수 없다. 따라서 필요한 객체에만 필요한 스레드 권한을 주는 범위 선택이 중요하다.
5. 메모리 도메인은 사용자 데이터 주소 범위를 나눈다
k_mem_domain은 파티션 목록을 담고, 각 k_mem_partition은 시작 주소·크기·접근 속성을 지정한다. 슈퍼바이저가 도메인과 파티션을 구성하고 스레드를 배정한다. 스레드는 한 시점에 하나의 도메인에 속하며, k_mem_domain_add_thread()는 대상이 다른 도메인에 있다면 기존 목록에서 빼고 새 도메인에 넣는다. 생성 시에는 부모의 도메인을 상속한다. 파티션의 정렬, 크기, 허용 개수와 실제 보호는 보드의 MPU·MMU 및 아키텍처 구현에 좌우된다.
스택은 별도 주의가 필요하다. 자기 스택 접근은 보장되지만, 같은 도메인의 다른 사용자 스택도 기본적으로 접근 가능하다. 아키텍처가 CONFIG_ARCH_MEM_DOMAIN_SUPPORTS_ISOLATED_STACKS를 지원하고 관련 격리가 활성화되면 스택별 접근이 더 제한될 수 있다. 다른 도메인의 사용자 스택에는 접근할 수 없다.
메모리 영역 할당 방식 자체는 기존 Zephyr Memory Management 글에서 다룬다. 여기서는 할당된 바이트의 소유자보다 사용자 스레드의 접근 가능 범위를 본다.
6. 거부는 일반 오류 반환과 같지 않다
k_sem_take()의 사용자 검증이 실패하면 K_OOPS가 아키텍처 syscall oops로 진행한다. 커널 메모리를 직접 건드린 경우에는 CPU의 메모리 보호 예외가 발생할 수 있다. 두 경우 모두 fatal 처리 정책을 보아야 한다. v3.7.0의 기본 k_sys_fatal_error_handler()는 시스템을 중단한다. 애플리케이션이 사용자 정의 handler를 제공하고 그 함수가 반환할 때에야 z_fatal_error()의 뒤쪽에서 현재 스레드 abort를 시도하며, ISR·panic·테스트 구성에는 별도 조건이 있다.
| 상황 | 직접 확인할 경로 | 결과를 단정하면 안 되는 이유 |
|---|---|---|
잘못된 k_sem_take() 객체 | z_vrfy_k_sem_take() → K_OOPS | 검증 내부의 -EBADF/-EPERM은 호출자에게 반환하는 정상 API 오류가 아니다. |
| 허용되지 않은 주소 직접 접근 | 아키텍처 메모리 보호 예외·fatal 처리 | 정확한 예외 보고는 아키텍처와 구성에 의존한다. |
| 기본 fatal handler | arch_system_halt() | “사용자 스레드만 종료되고 앱은 계속 실행”이 기본 계약은 아니다. |
| 반환하는 사용자 정의 fatal handler | z_fatal_error() 후반의 abort 경로 | 스레드 문맥·ISR·panic 등 복구 정책의 제약을 함께 봐야 한다. |
근거: sem.c 188–194행, fatal.c 36–44행, 85–178행.
7. 접근 실패를 재현할 때의 점검 순서
- 최종
.config에서CONFIG_USERSPACE와 보드의 사용자 모드 지원을 확인한다. - 스레드가
K_USER인지, 사용자 모드 가능 스택인지, 생성 시K_INHERIT_PERMS를 썼는지 확인한다. - 실패한 대상이 데이터 버퍼인지 커널 객체인지 먼저 나눈다. 버퍼면 도메인·파티션과 아키텍처 제약을, 객체면 등록·종류·권한·초기화를 확인한다.
- 해당 API의 실제
z_vrfy_*를 읽고, fatal handler가 기본 구현인지 사용자 정의 구현인지 확인한다.
8. 고정 버전 근거와 읽는 범위
문서는 Zephyr v3.7.0, 코드는 동일 태그를 고정해 비교했다. 아래 링크는 이 글의 설명을 재검사할 시작점이다. 공식 문서의 개요와 소스의 분기 사이에 차이가 있으면 실제 빌드 구성과 해당 아키텍처 경로를 더 확인해야 한다.
| 주제 | 공식 문서 | v3.7.0 코드 |
|---|---|---|
| 사용자 모드·객체 | User Mode · Kernel Objects | kernel/userspace.c |
| 스레드·상속 | Threads | kernel/thread.c |
| 시스템 호출 | System Calls | kernel/sem.c · syscall_handler.h |
| 메모리 도메인 | Memory Protection Design | kernel/mem_domain.c |
| 오류 정책 | User Mode Overview | kernel/fatal.c |