OP-TEE OS · source analysis

OP-TEE thread pool과 secure stack

thread_init_primary의 실제 역할과 별도의 thread pool 준비, standard SMC의 FREE·ACTIVE·SUSPENDED 전이를 구분합니다.

OP-TEE OS 4.10.0은 코드 분석 기준입니다. 제품 적용 전 공식 OP-TEE 변경 기록과 플랫폼 보안 공지를 확인하세요.

Series
05 / 12
Baseline
4.10.0
Commit
753afbbee168
Source
core/arch/arm/kernel/thread.c:584-672 (발췌 범위)

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()의 비가상화 분기다.

CPU별 포인터와 전역 slot

그림 1. core-local과 thread_ctx의 소유 관계
PER-CPUthread_core_localACTIVE: curr_thread=nSUSPENDED: INVALIDCPU는 slot 영구 소유 안 함GLOBALthreads[n]state · regs · flagsstack_va_end · user_mapTSD/session과 연계RPC RESUME같은 slot nSUSPENDED → ACTIVEmap/args 조건부 복원saved regs 재개suspend는 slot을 보존하고 CPU의 curr_thread만 비운다

SUSPENDED slot은 CPU를 점유하지 않지만 saved regs와 stack 관련 상태를 보존한다.

새 SMC와 RPC 재개의 상태 전이

그림 2. FREE·ACTIVE·SUSPENDED 상태 기계
NEW SMCFREE 탐색빈 slot 없으면 returnALLOCATEACTIVElock 해제 후 CPU 설정RPCSUSPENDEDregs·map 보존RETURN_FROM_RPCACTIVE 재개ID·state 검사FINALFREEflags·curr_thread 정리RPC가 없으면 ACTIVE → FREE. 재개 함수는 ID 범위와 SUSPENDED state만 검사하며 암호학적 token 검증을 뜻하지 않는다.추가 RPC

빈 slot이 없으면 할당 함수는 return한다. 재개 함수는 thread ID 범위와 SUSPENDED 상태를 확인한다.

전이마다 바뀌는 필드

전이조건slot/CPU 상태근거
FREE → ACTIVEglobal lock 아래 FREE 탐색state=ACTIVE; curr_thread=n; regs 초기화thread.c L220–262
빈 slot 없음탐색 실패slot 변경 없이 할당 함수에서 returnL243–244
ACTIVE → SUSPENDEDRPC/foreign interruptsaved cpsr·pc·flags, 조건부 user map; curr_thread=INVALIDL516–567
SUSPENDED → ACTIVEn 범위와 SUSPENDED 상태 검사map·RPC 인자 조건부 복원, saved regs 재개L379–428
ACTIVE → FREE최종 처리 뒤 freeflags 초기화, curr_thread=INVALIDL465–487

상태 갱신에는 global lock이 쓰이지만 이 표만으로 외부 request의 모든 유효성 검사를 증명할 수 없다.

스레드 상태와 CPU의 관계

스레드 풀 준비와 실행 중인 스레드의 상태 변화를 구분한다.

그림 3. pool 준비부터 재개까지

pool 준비는 thread_init_primary()가 아니라 primary_late의 비가상화 분기다.

01primary_late: pool
02new SMC
03ACTIVE
04RPC: SUSPENDED
05resume / FREE
그림 4. ACTIVE와 SUSPENDED RPC thread

같은 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

원본 코드

아래 코드는 OP-TEE OS 4.10.0의 core/arch/arm/kernel/thread.c에서 584-672줄을 그대로 가져온 것이다. 설명을 위해 실제 코드를 가짜 의사 코드로 바꾸지 않았다.

commit753afbbee1682f5d16fd30e87b31058a4fd4f4b8anchor · L608void thread_init_primary(void)
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

대표 발췌와 실제 상태 함수

첫 코드 상자 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 함수를 함께 읽는다.

함께 읽어야 하는 원본 코드

첫 코드 조각만으로 동작이 완성되지 않는 경우 호출 매크로, 객체 정의와 실제 실행 목록을 같은 페이지에 묶었다. 각 조각은 같은 기준 commit에서 가져왔다.

01. 빈 thread를 골라 SMC register context로 실행

core/arch/arm/kernel/thread.c 255-325줄이다.

filecore/arch/arm/kernel/thread.canchor · L267void thread_alloc_and_run
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줄이다.

filecore/arch/arm/kernel/thread.canchor · L516int thread_state_suspend(uint32_t flags
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;

새 호출과 재개의 차이

01

새 standard SMC

__thread_alloc_and_run()은 FREE slot을 lock 아래 ACTIVE로 바꾼 뒤 CPU별 current-thread 번호와 시작 regs를 설정한다.

02

RPC suspend

thread_state_suspend()는 cpsr·pc·flags를 저장하고 user map이 활성일 때만 저장·제거한다. slot은 SUSPENDED로 남는다.

03

RPC resume

thread_resume_from_rpc()는 n 범위와 SUSPENDED 상태를 확인한다. 이 함수 자체가 암호학적 token을 검증한다는 근거는 없다.

저장과 조건부 복원

01

CPU와 slot

thread_core_local.curr_thread는 CPU별 현재 실행 slot이다. suspend 동안 slot은 남지만 CPU는 다른 요청을 맡을 수 있다.

02

user map과 인자

user mapping이 활성일 때만 저장·복원한다. a0–a3를 saved regs로 복사하는 일은 THREAD_FLAGS_COPY_ARGS_ON_RETURN가 있을 때만 한다.

03

최종 FREE

thread_state_free()는 ACTIVE를 확인하고 FREE로 전환해 flags와 CPU별 current-thread 값을 비운다. RPC suspend와 구분한다.

다섯 개의 상태 경계

그림 5. thread slot의 상태 계약
01FREE slot

global lock 탐색

02ACTIVE

regs·curr_thread

03SUSPENDED

cpsr·pc/map 저장

04ACTIVE resume

ID·state 검사

05FREE

flags 정리

RPC가 없는 호출은 03–04를 거치지 않고, 빈 slot·잘못된 ID는 별도 return 경로다.

조건실제 분기확인
CFG_NS_VIRTUALIZATIONearly boot에서 thread pool을 guest별 경로로 미룸early primary에 pool이 없다는 사실만으로 실패 판정하지 않음
CFG_CORE_UNMAP_CORE_AT_EL0init_user_kcode()의 mapping 변수 계산대표 발췌 L579–611
CFG_WITH_PAGERsuspend/free 중 stack physical page release 가능virtual slot과 physical page를 구분
user map 활성suspend 저장·resume 복원kernel-only call과 구분
THREAD_FLAGS_COPY_ARGS_ON_RETURNRPC a0–a3 복사모든 resume에 NS 인자를 받아들이지 않음

slot과 CPU 상태의 수명

대상활성 구간경계
threads[n].stateFREE → ACTIVE → 선택적 SUSPENDED → FREEglobal lock 아래 전이
threads[n].regs새 SMC에서 초기화, suspend 동안 보존resume은 saved regs로 계속
curr_thread해당 CPU에서 ACTIVE 실행 중 nsuspend/free 뒤 INVALID
thread stackslot에 연결pager면 사용하지 않는 physical page release 가능
user_map활성일 때만 저장resume 때 조건부 복원

할당 실패와 잘못된 재개

조건이 함수의 동작추가 확인
FREE slot 없음할당 함수가 return상위 SMC 경로의 응답값을 별도로 확인
resume ID 범위 밖·slot이 SUSPENDED 아님재개 함수가 return상위 request parser 확인
stack canary 훼손thread_check_canaries()에서 탐지 가능stack과 빌드 설정 기록
최종 free 미도달slot이 ACTIVE에 남을 수 있음최종 응답의 thread_state_free() 도달 확인

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.elftext/data 배치
상태 계측CPU index, n, state, curr_threadRPC 전후 같은 slot과 CPU 소유권
Linux 측dmesg | grep -i opteesecure 내부 state와 별도 해석

상태 전이 계측

  1. 01
    slot 고갈

    동시 call을 늘리며 found_thread와 각 state를 기록하고 상위 응답을 확인한다.

  2. 02
    RPC 왕복

    같은 n의 cpsr·pc, have_user_map, CPU별 curr_thread를 비교한다.

  3. 03
    최종 free

    정상·오류 응답 각각에서 thread_state_free() 도달 여부를 본다.

원문과 다음 글