QUESTION
OP-TEE thread slot은 언제 CPU에서 떨어지는가?
thread_ctx는 Linux task와 1:1 대응하지 않는다. 새 standard SMC는 FREE slot을 ACTIVE로 바꾸고 RPC로 나갈 때 같은 slot을 SUSPENDED로 보존한다. 그 사이 CPU별 curr_thread는 INVALID가 된다.
대표 발췌의 thread_init_primary()는 init_user_kcode()만 호출한다. thread pool 생성은 boot_init_primary_late()의 비가상화 분기다.
STRUCTURE
CPU별 포인터와 전역 slot
SUSPENDED slot은 CPU를 점유하지 않지만 saved regs와 stack 관련 상태를 보존한다.
STATE PATH
새 SMC와 RPC 재개의 상태 전이
빈 slot이 없으면 할당 함수는 return한다. 재개 함수는 thread ID 범위와 SUSPENDED 상태를 확인한다.
STATE LEDGER
전이마다 바뀌는 필드
| 전이 | 조건 | slot/CPU 상태 | 근거 |
|---|---|---|---|
| FREE → ACTIVE | global lock 아래 FREE 탐색 | state=ACTIVE; curr_thread=n; regs 초기화 | thread.c L220–262 |
| 빈 slot 없음 | 탐색 실패 | slot 변경 없이 할당 함수에서 return | L243–244 |
| ACTIVE → SUSPENDED | RPC/foreign interrupt | saved cpsr·pc·flags, 조건부 user map; curr_thread=INVALID | L516–567 |
| SUSPENDED → ACTIVE | n 범위와 SUSPENDED 상태 검사 | map·RPC 인자 조건부 복원, saved regs 재개 | L379–428 |
| ACTIVE → FREE | 최종 처리 뒤 free | flags 초기화, curr_thread=INVALID | L465–487 |
상태 갱신에는 global lock이 쓰이지만 이 표만으로 외부 request의 모든 유효성 검사를 증명할 수 없다.
VISUAL WALKTHROUGH
스레드 상태와 CPU의 관계
스레드 풀 준비와 실행 중인 스레드의 상태 변화를 구분한다.
pool 준비는 thread_init_primary()가 아니라 primary_late의 비가상화 분기다.
같은 thread_ctx의 서로 다른 상태다.
ACTIVE
01CPU curr_thread=n
02secure stack 실행
03현재 register 문맥
04lock 아래 state 전이
SUSPENDED
01saved cpsr·pc 유지
02slot·stack 보존
03user map 조건부 저장
04CPU curr_thread=INVALID
UPSTREAM SOURCE
원본 코드
아래 코드는 OP-TEE OS 4.10.0의 core/arch/arm/kernel/thread.c에서 584-672줄을 그대로 가져온 것이다. 설명을 위해 실제 코드를 가짜 의사 코드로 바꾸지 않았다.
584
585 thread_user_kcode_va = ROUNDDOWN(v, CORE_MMU_USER_CODE_SIZE);
586 ve = ROUNDUP(ve, CORE_MMU_USER_CODE_SIZE);
587 thread_user_kcode_size = ve - thread_user_kcode_va;
588
589 core_mmu_get_user_va_range(&v, NULL);
590 thread_user_kcode_offset = thread_user_kcode_va - v;
591
592 set_core_local_kcode_offset(thread_core_local,
593 thread_user_kcode_offset);
594#if defined(CFG_CORE_WORKAROUND_SPECTRE_BP_SEC) && defined(ARM64)
595 set_core_local_kcode_offset((void *)thread_user_kdata_page,
596 thread_user_kcode_offset);
597 /*
598 * When transitioning to EL0 subtract SP with this much to point to
599 * this special kdata page instead. SP is restored by add this much
600 * while transitioning back to EL1.
601 */
602 v += thread_user_kcode_size;
603 thread_user_kdata_sp_offset = (vaddr_t)thread_core_local - v;
604#endif
605#endif /*CFG_CORE_UNMAP_CORE_AT_EL0*/
606}
607
608void thread_init_primary(void)
609{
610 init_user_kcode();
611}
612
613static uint32_t __maybe_unused get_midr_implementer(uint32_t midr)
614{
615 return (midr >> MIDR_IMPLEMENTER_SHIFT) & MIDR_IMPLEMENTER_MASK;
616}
617
618static uint32_t __maybe_unused get_midr_primary_part(uint32_t midr)
619{
620 return (midr >> MIDR_PRIMARY_PART_NUM_SHIFT) &
621 MIDR_PRIMARY_PART_NUM_MASK;
622}
623
624static uint32_t __maybe_unused get_midr_variant(uint32_t midr)
625{
626 return (midr >> MIDR_VARIANT_SHIFT) & MIDR_VARIANT_MASK;
627}
628
629static uint32_t __maybe_unused get_midr_revision(uint32_t midr)
630{
631 return (midr >> MIDR_REVISION_SHIFT) & MIDR_REVISION_MASK;
632}
633
634#ifdef CFG_CORE_WORKAROUND_SPECTRE_BP_SEC
635#ifdef ARM64
636static bool probe_workaround_available(uint32_t wa_id)
637{
638 int32_t r;
639
640 r = thread_smc(SMCCC_VERSION, 0, 0, 0);
641 if (r < 0)
642 return false;
643 if (r < 0x10001) /* compare with version 1.1 */
644 return false;
645
646 /* Version >= 1.1, so SMCCC_ARCH_FEATURES is available */
647 r = thread_smc(SMCCC_ARCH_FEATURES, wa_id, 0, 0);
648 return r >= 0;
649}
650
651static vaddr_t __maybe_unused select_vector_wa_spectre_v2(void)
652{
653 if (probe_workaround_available(SMCCC_ARCH_WORKAROUND_1)) {
654 DMSG("SMCCC_ARCH_WORKAROUND_1 (%#08" PRIx32 ") available",
655 SMCCC_ARCH_WORKAROUND_1);
656 DMSG("SMC Workaround for CVE-2017-5715 used");
657 return (vaddr_t)thread_excp_vect_wa_spectre_v2;
658 }
659
660 DMSG("SMCCC_ARCH_WORKAROUND_1 (%#08" PRIx32 ") unavailable",
661 SMCCC_ARCH_WORKAROUND_1);
662 DMSG("SMC Workaround for CVE-2017-5715 not needed (if ARM-TF is up to date)");
663 return (vaddr_t)thread_excp_vect;
664}
665#else
666static vaddr_t __maybe_unused select_vector_wa_spectre_v2(void)
667{
668 return (vaddr_t)thread_excp_vect_wa_spectre_v2;
669}
670#endif
671#endif
672
READING CONTRACT
대표 발췌와 실제 상태 함수
첫 코드 상자 thread.c L584–672의 L608–611은 thread_init_primary() → init_user_kcode()뿐이다. 주변은 CFG_CORE_UNMAP_CORE_AT_EL0와 Spectre vector helper다. 이를 thread pool 생성 본문으로 읽지 않는다.
상태 전이는 아래 관련 원문의 할당·suspend 함수와 고정 commit 전체 파일 L379–487의 resume/free 함수를 함께 읽는다.
RELATED SOURCE
함께 읽어야 하는 원본 코드
첫 코드 조각만으로 동작이 완성되지 않는 경우 호출 매크로, 객체 정의와 실제 실행 목록을 같은 페이지에 묶었다. 각 조각은 같은 기준 commit에서 가져왔다.
01. 빈 thread를 골라 SMC register context로 실행
core/arch/arm/kernel/thread.c 255-325줄이다.
255 threads[n].regs.apiakey_hi = threads[n].keys.apia_hi;
256 threads[n].regs.apiakey_lo = threads[n].keys.apia_lo;
257#endif
258
259 thread_lazy_save_ns_vfp();
260
261 l->flags &= ~THREAD_CLF_TMP;
262 thread_resume(&threads[n].regs);
263 /*NOTREACHED*/
264 panic();
265}
266
267void thread_alloc_and_run(uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3,
268 uint32_t a4, uint32_t a5)
269{
270 __thread_alloc_and_run(a0, a1, a2, a3, a4, a5, 0, 0,
271 thread_std_smc_entry, 0);
272}
273
274#ifdef CFG_SECURE_PARTITION
275void thread_sp_alloc_and_run(struct thread_smc_args *args __maybe_unused)
276{
277 __thread_alloc_and_run(args->a0, args->a1, args->a2, args->a3, args->a4,
278 args->a5, args->a6, args->a7,
279 spmc_sp_thread_entry, THREAD_FLAGS_FFA_ONLY);
280}
281#endif
282
283#ifdef ARM32
284static void copy_a0_to_a3(struct thread_ctx_regs *regs, uint32_t a0,
285 uint32_t a1, uint32_t a2, uint32_t a3)
286{
287 /*
288 * Update returned values from RPC, values will appear in
289 * r0-r3 when thread is resumed.
290 */
291 regs->r0 = a0;
292 regs->r1 = a1;
293 regs->r2 = a2;
294 regs->r3 = a3;
295}
296#endif /*ARM32*/
297
298#ifdef ARM64
299static void copy_a0_to_a3(struct thread_ctx_regs *regs, uint32_t a0,
300 uint32_t a1, uint32_t a2, uint32_t a3)
301{
302 /*
303 * Update returned values from RPC, values will appear in
304 * x0-x3 when thread is resumed.
305 */
306 regs->x[0] = a0;
307 regs->x[1] = a1;
308 regs->x[2] = a2;
309 regs->x[3] = a3;
310}
311#endif /*ARM64*/
312
313#ifdef ARM32
314static bool is_from_user(uint32_t cpsr)
315{
316 return (cpsr & ARM32_CPSR_MODE_MASK) == ARM32_CPSR_MODE_USR;
317}
318#endif
319
320#ifdef ARM64
321static bool is_from_user(uint32_t cpsr)
322{
323 if (cpsr & (SPSR_MODE_RW_32 << SPSR_MODE_RW_SHIFT))
324 return true;
325 if (((cpsr >> SPSR_64_MODE_EL_SHIFT) & SPSR_64_MODE_EL_MASK) ==
02. ACTIVE thread를 SUSPENDED로 바꾸고 context를 보존
core/arch/arm/kernel/thread.c 504-602줄이다.
504 vaddr_t base = thr->stack_va_end - STACK_THREAD_SIZE;
505 size_t len = sp - base;
506
507 tee_pager_release_phys((void *)base, len);
508}
509#else
510static void release_unused_kernel_stack(struct thread_ctx *thr __unused,
511 uint32_t cpsr __unused)
512{
513}
514#endif
515
516int thread_state_suspend(uint32_t flags, uint32_t cpsr, vaddr_t pc)
517{
518 struct thread_core_local *l = thread_get_core_local();
519 int ct = l->curr_thread;
520
521 assert(ct != THREAD_ID_INVALID);
522
523 if (core_mmu_user_mapping_is_active())
524 ftrace_suspend();
525
526 thread_check_canaries();
527
528 release_unused_kernel_stack(threads + ct, cpsr);
529
530 if (is_from_user(cpsr)) {
531 thread_user_save_vfp();
532 tee_ta_update_session_utime_suspend();
533 tee_ta_gprof_sample_pc(pc);
534 }
535 thread_lazy_restore_ns_vfp();
536
537 thread_lock_global();
538
539 assert(threads[ct].state == THREAD_STATE_ACTIVE);
540 threads[ct].flags |= flags;
541 threads[ct].regs.cpsr = cpsr;
542 threads[ct].regs.pc = pc;
543 threads[ct].state = THREAD_STATE_SUSPENDED;
544
545 threads[ct].have_user_map = core_mmu_user_mapping_is_active();
546 if (threads[ct].have_user_map) {
547 if (threads[ct].flags & THREAD_FLAGS_EXIT_ON_FOREIGN_INTR)
548 tee_ta_ftrace_update_times_suspend();
549 core_mmu_get_user_map(&threads[ct].user_map);
550 core_mmu_set_user_map(NULL);
551 }
552
553 if (IS_ENABLED(CFG_SECURE_PARTITION)) {
554 struct ts_session *ts_sess =
555 TAILQ_FIRST(&threads[ct].tsd.sess_stack);
556
557 spmc_sp_set_to_preempted(ts_sess);
558 }
559
560 l->curr_thread = THREAD_ID_INVALID;
561
562 if (IS_ENABLED(CFG_NS_VIRTUALIZATION))
563 virt_unset_guest();
564
565 thread_unlock_global();
566
567 return ct;
568}
569
570static void __maybe_unused
571set_core_local_kcode_offset(struct thread_core_local *cls, long offset)
572{
573 size_t n = 0;
574
575 for (n = 0; n < CFG_TEE_CORE_NB_CORE; n++)
576 cls[n].kcode_offset = offset;
577}
578
579static void init_user_kcode(void)
580{
581#ifdef CFG_CORE_UNMAP_CORE_AT_EL0
582 vaddr_t v = (vaddr_t)thread_excp_vect;
583 vaddr_t ve = (vaddr_t)thread_excp_vect_end;
584
585 thread_user_kcode_va = ROUNDDOWN(v, CORE_MMU_USER_CODE_SIZE);
586 ve = ROUNDUP(ve, CORE_MMU_USER_CODE_SIZE);
587 thread_user_kcode_size = ve - thread_user_kcode_va;
588
589 core_mmu_get_user_va_range(&v, NULL);
590 thread_user_kcode_offset = thread_user_kcode_va - v;
591
592 set_core_local_kcode_offset(thread_core_local,
593 thread_user_kcode_offset);
594#if defined(CFG_CORE_WORKAROUND_SPECTRE_BP_SEC) && defined(ARM64)
595 set_core_local_kcode_offset((void *)thread_user_kdata_page,
596 thread_user_kcode_offset);
597 /*
598 * When transitioning to EL0 subtract SP with this much to point to
599 * this special kdata page instead. SP is restored by add this much
600 * while transitioning back to EL1.
601 */
602 v += thread_user_kcode_size;
DETAILS
새 호출과 재개의 차이
새 standard SMC
__thread_alloc_and_run()은 FREE slot을 lock 아래 ACTIVE로 바꾼 뒤 CPU별 current-thread 번호와 시작 regs를 설정한다.
RPC suspend
thread_state_suspend()는 cpsr·pc·flags를 저장하고 user map이 활성일 때만 저장·제거한다. slot은 SUSPENDED로 남는다.
RPC resume
thread_resume_from_rpc()는 n 범위와 SUSPENDED 상태를 확인한다. 이 함수 자체가 암호학적 token을 검증한다는 근거는 없다.
IMPLEMENTATION NOTES
저장과 조건부 복원
CPU와 slot
thread_core_local.curr_thread는 CPU별 현재 실행 slot이다. suspend 동안 slot은 남지만 CPU는 다른 요청을 맡을 수 있다.
user map과 인자
user mapping이 활성일 때만 저장·복원한다. a0–a3를 saved regs로 복사하는 일은 THREAD_FLAGS_COPY_ARGS_ON_RETURN가 있을 때만 한다.
최종 FREE
thread_state_free()는 ACTIVE를 확인하고 FREE로 전환해 flags와 CPU별 current-thread 값을 비운다. RPC suspend와 구분한다.
DEEP ANALYSIS
다섯 개의 상태 경계
global lock 탐색
regs·curr_thread
cpsr·pc/map 저장
ID·state 검사
flags 정리
RPC가 없는 호출은 03–04를 거치지 않고, 빈 slot·잘못된 ID는 별도 return 경로다.
| 조건 | 실제 분기 | 확인 |
|---|---|---|
CFG_NS_VIRTUALIZATION | early boot에서 thread pool을 guest별 경로로 미룸 | early primary에 pool이 없다는 사실만으로 실패 판정하지 않음 |
CFG_CORE_UNMAP_CORE_AT_EL0 | init_user_kcode()의 mapping 변수 계산 | 대표 발췌 L579–611 |
CFG_WITH_PAGER | suspend/free 중 stack physical page release 가능 | virtual slot과 physical page를 구분 |
| user map 활성 | suspend 저장·resume 복원 | kernel-only call과 구분 |
THREAD_FLAGS_COPY_ARGS_ON_RETURN | RPC a0–a3 복사 | 모든 resume에 NS 인자를 받아들이지 않음 |
OBJECT LIFETIME
slot과 CPU 상태의 수명
| 대상 | 활성 구간 | 경계 |
|---|---|---|
threads[n].state | FREE → ACTIVE → 선택적 SUSPENDED → FREE | global lock 아래 전이 |
threads[n].regs | 새 SMC에서 초기화, suspend 동안 보존 | resume은 saved regs로 계속 |
curr_thread | 해당 CPU에서 ACTIVE 실행 중 n | suspend/free 뒤 INVALID |
| thread stack | slot에 연결 | pager면 사용하지 않는 physical page release 가능 |
user_map | 활성일 때만 저장 | resume 때 조건부 복원 |
FAILURE PATH
할당 실패와 잘못된 재개
| 조건 | 이 함수의 동작 | 추가 확인 |
|---|---|---|
| FREE slot 없음 | 할당 함수가 return | 상위 SMC 경로의 응답값을 별도로 확인 |
| resume ID 범위 밖·slot이 SUSPENDED 아님 | 재개 함수가 return | 상위 request parser 확인 |
| stack canary 훼손 | thread_check_canaries()에서 탐지 가능 | stack과 빌드 설정 기록 |
| 최종 free 미도달 | slot이 ACTIVE에 남을 수 있음 | 최종 응답의 thread_state_free() 도달 확인 |
EVIDENCE
ELF와 secure console
같은 빌드의 ELF를 읽고 CPU index·slot n·state를 secure console에서 계측한다. nm은 C 선언이 아닌 심볼을 출력한다.
| 목적 | 읽기 전용 명령·관찰 | 판정 |
|---|---|---|
| 심볼 | ${CROSS_COMPILE64}nm -an out/arm/core/tee.elf | grep -E " (thread_init_primary|thread_resume_from_rpc|thread_state_suspend)$" | 링크 주소 확인 |
| section | ${CROSS_COMPILE64}readelf -W -lS out/arm/core/tee.elf | text/data 배치 |
| 상태 계측 | CPU index, n, state, curr_thread | RPC 전후 같은 slot과 CPU 소유권 |
| Linux 측 | dmesg | grep -i optee | secure 내부 state와 별도 해석 |
LAB
상태 전이 계측
- 01slot 고갈
동시 call을 늘리며
found_thread와 각 state를 기록하고 상위 응답을 확인한다. - 02RPC 왕복
같은 n의 cpsr·pc,
have_user_map, CPU별 curr_thread를 비교한다. - 03최종 free
정상·오류 응답 각각에서
thread_state_free()도달 여부를 본다.
PRIMARY REFERENCES