← Architecture 비교

Linux 6.18.37 LTS · Architecture comparison 12/21

시그널 프레임과 사용자 실행 상태 복원

kernel이 pending signal을 선택한 뒤 user stack에 rt_sigframe을 쓰고 handler로 PC를 바꾸며, rt_sigreturn이 register와 extended state를 복원하는 과정을 비교합니다.

비교 대상
arm64 / x86-64 / RISC-V
실제 원본
파일 3개 · 원문 229줄
기준 태그
Linux v6.18.37
분석 축
상태 · 접근 순서 · 객체 수명 · 지연 시간

01 · QUESTION

무엇을 확인할 것인가

비동기 signal이 syscall 또는 user instruction을 중단했을 때 원래 PC, stack, mask, FPU/vector state는 어디에 보관되고 어떤 검증 뒤 신뢰되는가?

exit-to-user path가 deliverable signal을 고르고 architecture setup_rt_frame()이 user stack 또는 altstack에 ABI frame을 만든다. handler return trampoline은 rt_sigreturn syscall로 들어와 frame을 copy/validate한 뒤 pt_regs와 signal mask를 복원한다.

kernel-owned pt_regs에서 user-writable signal frame으로 상태를 복사하는 순간 신뢰 경계가 바뀐다. sigreturn은 user가 수정한 frame을 읽으므로 privilege bit, canonical address, reserved field를 반드시 검증한다.

지연 시간 관점fixed integer context 외에 SVE/SME, XSAVE/AMX, RISC-V vector처럼 가변 크기 state를 frame에 materialize하면 signal delivery와 sigreturn 시간이 state byte 수에 비례해 증가한다.

02 · CONTRACT

공통 규약과 아키텍처별 구현

architecture핵심 mechanism실패 형태확인할 상태
arm64rt_sigframe과 extensible context record chainrecord size overflow, duplicate magic, 잘못된 VL 또는 PSTATE privilege bit를 허용하면 out-of-bounds read와 privilege corruption 위험이 생긴다.SP 16-byte alignment, frame record magic/size, FPSIMD/SVE VL, PC/LR, PSTATE와 sigmask를 확인한다.
x86-64ucontext, sigcontext와 XSAVE fpstate pointernoncanonical RIP/RSP, 잘못된 CS/SS, forged xfeatures 또는 shadow-stack token 불일치는 sigreturn fault로 막아야 한다.frame rsp, uc_mcontext, fpstate magic/size/xfeatures, CS/SS/RFLAGS, CET SSP와 restorer 주소를 본다.
RISC-Vrt_sigframe의 sigcontext와 vector state datap pointerVLEN 기반 size overflow, vector datap pointer가 frame 밖을 가리키는 경우 또는 SSTATUS.SPP 조작을 검증하지 않으면 memory corruption과 privilege 문제가 생긴다.EPC/SP/RA, a0-a2, sigmask, fp fcsr, vector vlenb/vl/vtype/vstart와 SSTATUS를 확인한다.

03 · DIAGRAMS

분기, 저장 필드, 오류 경계

그림 1. handler 진입까지의 세 분기왼쪽부터 실제 표시 함수의 조건과 레지스터 변경 시점을 따라갑니다.

arm64

배치
FP/SIMD 상태 저장 → get_sigframe()(L1545–1548)
프레임
uc_stack·sigcontext·mask, SA_SIGINFO이면 siginfo(L1553–1559)
진입
err == 0일 때만 setup_return(); PC/SP/x0 등 세부 설정은 signal.c:1475–1506

x86-64

조건
SA_RESTORER와 사용자 프레임 접근 확인(L171–179)
프레임
ucontext·sigcontext·mask를 쓰고, 조건부 siginfo와 shadow stack을 준비(L181–199)
진입
이후 RDI=signal, RSI=&info, RDX=&uc, RIP=handler, RSP=frame(L201–212)

RISC-V native

배치
frame 크기 계산 → SP 차감·16바이트 정렬 → access_ok()(L307–325)
프레임
siginfo·ucontext·sigcontext·mask 쓰기 결과를 err에 모음(L327–336)
진입
MMU별 RA를 정한 후 EPC/SP/a0–a2 설정(L339–369)
그림 2. 사용자 프레임에 저장하는 것주소상의 물리적 순서도가 아니라 각 표시 코드가 쓰는 필드와 조건의 지도입니다.
중단된 사용자 문맥 → 쓰기 가능한 사용자 프레임pt_regs와 복원용 mask를 사용자 메모리로 내보낸 뒤 처리기용 레지스터를 따로 설정합니다. 이 프레임은 사용자 코드가 변경할 수 있으므로 복원은 별도 경계입니다.
arm64uc_stack + setup_sigframe()가 저장하는 레지스터·masksiginfo는 SA_SIGINFO일 때만 복사합니다(L1556–1559). 확장 record의 실제 쓰기는 signal.c:1271–1402.
x86-64pretcode + uc_mcontext + uc_sigmasksiginfo는 SA_SIGINFO 조건부입니다(L188–195). FP 저장영역 포인터는 unsafe_put_sigcontext()에 전달됩니다(L189).
RISC-Vsiginfo + uc_stack + uc_mcontext + uc_sigmask이 구현은 siginfo를 항상 복사합니다(L327–334). 선택적 FP/vector 저장은 signal.c:268–289.
복원은 다음 단계arm64 복원 함수 signal.c:1013–1071와 시스템 콜 signal.c:1126–1163 · x86-64 signal_64.c:246–278 · RISC-V signal.c:225–265은 발췌 바깥의 rt_sigreturn 경로입니다.
그림 3. frame 작성 실패가 끊는 지점모든 오류가 같은 사유나 같은 후속 처리라고 가정하지 않습니다.
arm64 배치get_sigframe() 실패(L1547)→setup_rt_frame()이 1을 반환(L1548)→handler용 setup_return() 호출 전
arm64 쓰기프레임 쓰기에서 err 누적(L1553–1559)→err == 0이 아니면 진입 준비 생략(L1561–1562)→기존 접근 상태 복원(L1569–1574)
x86-64restorer 없음·프레임 접근 실패(L171–179)→-EFAULT; 사용자 쓰기 오류는 signal_64.c:238–240→handler 레지스터 설정 전
RISC-V주소 검사·프레임 복사·무MMU 코드 쓰기 실패(L324–349)→-EFAULT→EPC/SP/a0–a2 설정 전(L365–369)

04 · SOURCE

Linux 6.18.37 원본 코드와 주요 구문 해설

소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 원문 전체 발췌를 보존하고, 개별 해설은 근거가 있는 구문에만 붙였습니다.

arm64 · Linux 6.18.37

rt_sigframe과 extensible context record chain

sigcontext 뒤 FPSIMD, ESR, SVE, ZA, ZT 같은 _aarch64_ctx record를 magic/size로 연결한다. setup_rt_frame()은 VL에 따라 frame 크기를 계산하고 handler x0-x2와 PC/LR/SP를 설정한다.

원본 코드: arch/arm64/kernel/signal.c:1529-1609

1529		task_smstop_sm(current);
1530		current->thread.svcr &= ~SVCR_ZA_MASK;
1531		write_sysreg_s(0, SYS_TPIDR2_EL0);
1532	}
1533 
1534	return 0;
1535}
1536 
1537static int setup_rt_frame(int usig, struct ksignal *ksig, sigset_t *set,
1538			  struct pt_regs *regs)
1539{
1540	struct rt_sigframe_user_layout user;
1541	struct rt_sigframe __user *frame;
1542	struct user_access_state ua_state = {};
1543	int err = 0;
1544 
1545	fpsimd_save_and_flush_current_state();
1546 
1547	if (get_sigframe(&user, ksig, regs))
1548		return 1;
1549 
1550	save_reset_user_access_state(&ua_state);
1551	frame = user.sigframe;
1552 
1553	__put_user_error(0, &frame->uc.uc_flags, err);
1554	__put_user_error(NULL, &frame->uc.uc_link, err);
1555 
1556	err |= __save_altstack(&frame->uc.uc_stack, regs->sp);
1557	err |= setup_sigframe(&user, regs, set, &ua_state);
1558	if (ksig->ka.sa.sa_flags & SA_SIGINFO)
1559		err |= copy_siginfo_to_user(&frame->info, &ksig->info);
1560 
1561	if (err == 0)
1562		err = setup_return(regs, ksig, &user, usig);
1563 
1564	/*
1565	 * We must not fail if setup_return() succeeded - see comment at the
1566	 * beginning of setup_return().
1567	 */
1568 
1569	if (err == 0)
1570		set_handler_user_access_state();
1571	else
1572		restore_user_access_state(&ua_state);
1573 
1574	return err;
1575}
1576 
1577static void setup_restart_syscall(struct pt_regs *regs)
1578{
1579	if (is_compat_task())
1580		compat_setup_restart_syscall(regs);
1581	else
1582		regs->regs[8] = __NR_restart_syscall;
1583}
1584 
1585/*
1586 * OK, we're invoking a handler
1587 */
1588static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
1589{
1590	sigset_t *oldset = sigmask_to_save();
1591	int usig = ksig->sig;
1592	int ret;
1593 
1594	rseq_signal_deliver(ksig, regs);
1595 
1596	/*
1597	 * Set up the stack frame
1598	 */
1599	if (is_compat_task()) {
1600		if (ksig->ka.sa.sa_flags & SA_SIGINFO)
1601			ret = compat_setup_rt_frame(usig, ksig, oldset, regs);
1602		else
1603			ret = compat_setup_frame(usig, ksig, oldset, regs);
1604	} else {
1605		ret = setup_rt_frame(usig, ksig, oldset, regs);
1606	}
1607 
1608	/*
1609	 * Check that the resulting registers are actually sane.

주요 구문 해설

원본 81줄은 그대로 표시하며, 검토한 45개 구문에 설명을 붙였습니다.

L1529 task_smstop_sm(current);

저장된 태스크 상태에 SMSTOP SM의 효과를 적용하여 신호 처리기가 SME streaming 모드 밖에서 시작하게 합니다. streaming 모드였다면 벡터 상태 등을 규정된 값으로 바꾸고 SM 비트를 지웁니다.

L1530 current->thread.svcr &= ~SVCR_ZA_MASK;

저장된 SVCR에서 ZA 활성 비트를 지웁니다. 중단된 코드가 ZA를 사용했어도 신호 처리기는 ZA가 비활성인 상태로 시작해야 합니다.

L1531 write_sysreg_s(0, SYS_TPIDR2_EL0);

TPIDR2_EL0를 0으로 만들어 신호 처리기가 중단된 코드의 SME 지연 저장 상태 포인터를 그대로 사용하지 않게 합니다. 앞에서 원래 상태를 신호 프레임에 보관한 뒤 처리기 진입 상태를 준비하는 부분입니다.

L1534 return 0;

신호 처리기 진입용 레지스터와 CPU 기능 상태 준비를 성공했음을 반환합니다.

L1537static int setup_rt_frame(int usig, struct ksignal *ksig, sigset_t *set,

신호 번호 usig, 처리기 설정 ksig와 복원할 신호 마스크 set으로 AArch64 신호 프레임을 만듭니다.

L1538 struct pt_regs *regs)

중단된 사용자 레지스터를 regs로 받습니다. 원래 상태는 프레임에 저장하고 regs 자체는 처리기 진입용으로 바꿉니다.

L1540 struct rt_sigframe_user_layout user;

기본 신호 프레임과 확장 문맥이 사용자 스택의 어느 위치에 놓일지 담는 배치 정보입니다.

L1541 struct rt_sigframe __user *frame;

사용자 스택의 기본 rt_sigframe을 가리킬 포인터입니다. __user 표식은 일반 커널 포인터처럼 직접 접근해서는 안 됨을 나타냅니다.

L1542 struct user_access_state ua_state = {};

프레임을 쓰기 위해 임시로 바꾸는 사용자 접근 권한의 원래 값을 보관합니다. 빈 초기값에서 지원되는 POE 상태를 채웁니다.

L1543 int err = 0;

여러 사용자 메모리 쓰기의 오류를 모을 변수입니다. 모두 성공하면 0이 유지되어 처리기 진입 준비를 계속합니다.

L1545 fpsimd_save_and_flush_current_state();

현재 태스크의 FP·SIMD 등 상태를 메모리에 저장하고 CPU에 남아 있는 상태와의 연결을 정리합니다. 이후 신호 프레임이 최신 값을 담을 수 있게 하는 작업이며, 일반 데이터 캐시를 비운다는 뜻은 아닙니다.

L1547 if (get_sigframe(&user, ksig, regs))

스택 위치와 확장 상태 크기를 반영한 프레임 배치를 구합니다. 실패하면 불완전한 주소로 프레임을 쓰기 전에 중단합니다.

L1548 return 1;

신호 프레임 배치를 구하지 못했으므로 비영(1) 실패를 반환합니다. 호출자가 신호 설정 실패로 처리합니다.

L1550 save_reset_user_access_state(&ua_state);

POE를 지원하면 기존 POR_EL0 접근 권한을 ua_state에 저장하고 신호 프레임을 쓸 수 있도록 임시로 권한을 엽니다. 사용자 보호 키 설정이 프레임 작성 자체를 가로막지 않게 준비합니다.

L1551 frame = user.sigframe;

계산한 배치 중 기본 신호 프레임 주소를 꺼냅니다. 다음 줄부터 이 주소에 ucontext와 siginfo를 기록합니다.

L1553 __put_user_error(0, &frame->uc.uc_flags, err);

사용자 신호 프레임의 uc_flags를 0으로 초기화합니다. __put_user_error는 쓰기 오류를 err에 누적하므로, 이 필드를 쓰지 못했으면 뒤에서 정상 처리기 진입을 중단할 수 있습니다.

L1554 __put_user_error(NULL, &frame->uc.uc_link, err);

신호 프레임의 uc_link를 NULL로 기록해 연결된 다음 사용자 문맥이 없음을 나타냅니다. 사용자 메모리 쓰기 실패는 앞선 오류와 함께 err에 남깁니다.

L1556 err |= __save_altstack(&frame->uc.uc_stack, regs->sp);

중단 당시 SP를 기준으로 대체 신호 스택의 설정과 사용 상태를 uc_stack에 저장합니다. 신호 처리기가 끝난 뒤 sigreturn에서 원래 스택 상태를 복구할 자료입니다.

L1557 err |= setup_sigframe(&user, regs, set, &ua_state);

중단된 레지스터, 신호 마스크와 확장 상태를 계산해 둔 user 프레임 배치에 저장합니다. ua_state에는 임시 변경 전 사용자 접근 권한이 담겨 있으므로 그 상태도 복귀 때 되살릴 수 있게 전달합니다.

L1558 if (ksig->ka.sa.sa_flags & SA_SIGINFO)

처리기가 세 인자 형식의 SA_SIGINFO를 요청했는지 검사합니다. 요청했을 때만 발생 원인 등의 siginfo를 프레임에 복사합니다.

L1559 err |= copy_siginfo_to_user(&frame->info, &ksig->info);

SA_SIGINFO 처리기에 전달할 신호 번호·발생 원인 등의 siginfo를 사용자 프레임에 복사합니다. 접근 오류가 있으면 err에 반영하여 불완전한 프레임으로 처리기를 실행하지 않게 합니다.

L1561 if (err == 0)

프레임의 모든 사용자 메모리 쓰기가 성공했는지 확인합니다. 실패가 누적됐다면 PC·SP를 처리기용으로 바꾸지 않습니다.

L1562 err = setup_return(regs, ksig, &user, usig);

프레임 작성에 오류가 없을 때 PC·SP·인자·복귀 주소 등 regs를 신호 처리기 진입 상태로 바꿉니다. usig는 처리기에 전달할 신호 번호이며, 설정에 실패하면 err를 통해 알립니다.

L1569 if (err == 0)

프레임 작성과 처리기 진입 준비까지 성공했는지 확인합니다. 성공이면 처리기용 접근 권한을 적용하고, 실패면 원래 권한으로 복구합니다.

L1570 set_handler_user_access_state();

프레임과 처리기 진입 준비에 성공했으므로 POR_EL0를 신호 처리기용 기본 접근 권한으로 설정합니다. 이 시점 이후에는 임시 권한으로 사용자 메모리를 더 접근하지 않습니다.

L1571 else

신호 설정이 실패한 경로입니다. 임시로 완화했던 사용자 접근 권한을 원래 값으로 되돌립니다.

L1572 restore_user_access_state(&ua_state);

신호 프레임을 만들지 못한 경우 ua_state에 보관한 기존 사용자 접근 권한을 되돌립니다. 성공 경로와 달리 처리기용 권한으로 바꾸지 않고, 중단된 코드의 상태를 유지합니다.

L1574 return err;

신호 프레임 준비의 성공 0 또는 누적된 실패값을 반환합니다. 이 결과로 상위 신호 코드가 실제 전달 가능 여부를 판단합니다.

L1577static void setup_restart_syscall(struct pt_regs *regs)

재시작이 필요한 시스템 콜을 restart_syscall로 다시 호출하도록 번호 레지스터를 준비합니다. 사용자 ABI에 따라 번호를 놓는 위치가 다릅니다.

L1579 if (is_compat_task())

현재 태스크가 32비트 호환 ABI인지 검사합니다. AArch32이면 호환 번호와 레지스터 규약을 사용합니다.

L1580 compat_setup_restart_syscall(regs);

32비트 호환 태스크에 맞는 레지스터에 restart_syscall 번호를 준비합니다. 다시 시작해야 하는 시스템 콜이 AArch64의 x8 규약이 아니라 호환 ABI의 시스템 콜 규약을 따르도록 합니다.

L1581 else

64비트 AArch64 태스크의 경로입니다. 시스템 콜 번호를 x8에 넣는 규약을 따릅니다.

L1582 regs->regs[8] = __NR_restart_syscall;

복귀 후 재실행할 시스템 콜 번호를 x8의 저장값에 넣습니다. 원래 번호 대신 restart_syscall이 재시작용 상태를 처리하게 합니다.

L1588static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)

선택된 신호를 사용자 처리기에 전달할 준비를 합니다. rseq 처리, ABI별 프레임 작성, 레지스터 유효성 확인과 단일 단계 상태 반영을 연결합니다.

L1590 sigset_t *oldset = sigmask_to_save();

신호 복귀 때 복원할 차단 신호 집합을 고릅니다. 보통 current->blocked를 쓰지만 임시 신호 마스크를 되돌려야 하는 상황이면 saved_sigmask를 선택하므로, 처리 시점의 마스크와 항상 같지는 않습니다.

L1591 int usig = ksig->sig;

전달할 Linux 신호 번호를 usig에 보관합니다. ABI별 프레임 작성과 처리기 첫 인자에 같은 번호를 사용합니다.

L1592 int ret;

사용자 시그널 프레임 구성의 성공·실패를 담을 ret를 선언합니다. 뒤에서 프레임 생성 결과와 사용자 레지스터 유효성 검사 결과를 합쳐 signal_setup_done에 전달합니다.

L1594 rseq_signal_deliver(ksig, regs);

등록된 rseq 실행 구간이 신호로 중단됐음을 처리합니다. 필요한 경우 복귀 PC를 해당 구간의 중단 처리 지점으로 옮긴 뒤 그 레지스터 상태로 신호 프레임을 구성하게 합니다.

L1599 if (is_compat_task()) {

AArch32 호환 태스크이면 32비트 형식으로 프레임을 만듭니다. 그렇지 않으면 아래 AArch64 rt 프레임 경로를 사용합니다.

L1600 if (ksig->ka.sa.sa_flags & SA_SIGINFO)

호환 태스크의 처리기가 SA_SIGINFO를 요구하는지 검사합니다. 요구하면 확장 rt 프레임을, 아니면 전통적인 호환 프레임을 선택합니다.

L1601 ret = compat_setup_rt_frame(usig, ksig, oldset, regs);

32비트 호환 태스크가 SA_SIGINFO를 요청했으므로 siginfo와 ucontext가 포함된 실시간 시그널 프레임을 구성합니다. 기존 시그널 마스크·레지스터를 저장하고 핸들러 인자를 준비한 결과를 ret에 받습니다.

L1602 else

호환 태스크가 SA_SIGINFO를 요청하지 않은 경우입니다. 옛 arm32 처리기가 기대하는 기본 신호 프레임을 만듭니다.

L1603 ret = compat_setup_frame(usig, ksig, oldset, regs);

SA_SIGINFO를 쓰지 않는 32비트 호환 태스크에 기존 형식의 시그널 프레임을 만듭니다. 저장 문맥과 복귀 코드를 준비하고 핸들러로 들어갈 레지스터를 설정한 성공·실패를 기록합니다.

L1604 } else {

native AArch64 태스크의 경로입니다. SA_SIGINFO 여부와 무관하게 AArch64 rt 프레임 형식을 사용합니다.

L1605 ret = setup_rt_frame(usig, ksig, oldset, regs);

64비트 태스크의 사용자 스택에 실시간 시그널 프레임을 구성합니다. 원래 문맥과 시그널 마스크를 저장하고 핸들러로 진입할 사용자 레지스터를 설정하며, 오류 여부를 ret에 기록합니다.

x86-64 · Linux 6.18.37

ucontext, sigcontext와 XSAVE fpstate pointer

x64 rt frame은 pretcode, ucontext, siginfo와 fpstate area를 user stack에 둔다. x64_setup_rt_frame()은 pt_regs를 sigcontext에 복사하고 handler RDI/RSI/RDX와 RIP/RSP를 설정한다.

원본 코드: arch/x86/kernel/signal_64.c:156-228

156		flags = UC_SIGCONTEXT_SS;
157 
158	if (likely(user_64bit_mode(regs)))
159		flags |= UC_STRICT_RESTORE_SS;
160 
161	return flags;
162}
163 
164int x64_setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs)
165{
166	sigset_t *set = sigmask_to_save();
167	struct rt_sigframe __user *frame;
168	void __user *fp = NULL;
169	unsigned long uc_flags;
170 
171	/* x86-64 should always use SA_RESTORER. */
172	if (!(ksig->ka.sa.sa_flags & SA_RESTORER))
173		return -EFAULT;
174 
175	frame = get_sigframe(ksig, regs, sizeof(struct rt_sigframe), &fp);
176	uc_flags = frame_uc_flags(regs);
177 
178	if (!user_access_begin(frame, sizeof(*frame)))
179		return -EFAULT;
180 
181	/* Create the ucontext.  */
182	unsafe_put_user(uc_flags, &frame->uc.uc_flags, Efault);
183	unsafe_put_user(0, &frame->uc.uc_link, Efault);
184	unsafe_save_altstack(&frame->uc.uc_stack, regs->sp, Efault);
185 
186	/* Set up to return from userspace.  If provided, use a stub
187	   already in userspace.  */
188	unsafe_put_user(ksig->ka.sa.sa_restorer, &frame->pretcode, Efault);
189	unsafe_put_sigcontext(&frame->uc.uc_mcontext, fp, regs, set, Efault);
190	unsafe_put_sigmask(set, frame, Efault);
191	user_access_end();
192 
193	if (ksig->ka.sa.sa_flags & SA_SIGINFO) {
194		if (copy_siginfo_to_user(&frame->info, &ksig->info))
195			return -EFAULT;
196	}
197 
198	if (setup_signal_shadow_stack(ksig))
199		return -EFAULT;
200 
201	/* Set up registers for signal handler */
202	regs->di = ksig->sig;
203	/* In case the signal handler was declared without prototypes */
204	regs->ax = 0;
205 
206	/* This also works for non SA_SIGINFO handlers because they expect the
207	   next argument after the signal number on the stack. */
208	regs->si = (unsigned long)&frame->info;
209	regs->dx = (unsigned long)&frame->uc;
210	regs->ip = (unsigned long) ksig->ka.sa.sa_handler;
211 
212	regs->sp = (unsigned long)frame;
213 
214	/*
215	 * Set up the CS and SS registers to run signal handlers in
216	 * 64-bit mode, even if the handler happens to be interrupting
217	 * 32-bit or 16-bit code.
218	 *
219	 * SS is subtle.  In 64-bit mode, we don't need any particular
220	 * SS descriptor, but we do need SS to be valid.  It's possible
221	 * that the old SS is entirely bogus -- this can happen if the
222	 * signal we're trying to deliver is #GP or #SS caused by a bad
223	 * SS value.  We also have a compatibility issue here: DOSEMU
224	 * relies on the contents of the SS register indicating the
225	 * SS value at the time of the signal, even though that code in
226	 * DOSEMU predates sigreturn's ability to restore SS.  (DOSEMU
227	 * avoids relying on sigreturn to restore SS; instead it uses
228	 * a trampoline.)  So we do our best: if the old SS was valid,

주요 구문 해설

원본 73줄은 그대로 표시하며, 검토한 35개 구문에 설명을 붙였습니다.

L156 flags = UC_SIGCONTEXT_SS;

XSAVE 확장 상태를 제공하지 않는 경우에도 SS 레지스터 문맥이 포함됨을 UC_SIGCONTEXT_SS로 표시합니다.

L158 if (likely(user_64bit_mode(regs)))

신호가 실제 64비트 사용자 코드 실행 중에 발생했는지 검사합니다. 그 경우 복귀 때 SS를 엄격하게 복원하도록 추가 표시합니다.

L159 flags |= UC_STRICT_RESTORE_SS;

64비트 문맥에서 저장한 SS의 엄격한 복원을 요구합니다. 오래된 32·16비트 문맥과의 호환 복원 규칙을 적용하지 않게 합니다.

L161 return flags;

확장 FP 상태와 SS 복원 규칙을 나타내는 비트들을 반환합니다. 이 값이 사용자 ucontext의 uc_flags에 기록됩니다.

L164int x64_setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs)

x86-64 신호 처리기용 rt 프레임과 진입 레지스터를 준비합니다. 원래 사용자 상태를 저장하고 처리기에서 sigreturn으로 돌아올 수 있게 합니다.

L166 sigset_t *set = sigmask_to_save();

신호 처리기에서 복귀한 뒤 적용할 신호 마스크를 선택합니다. 임시로 변경한 마스크의 복원이 예약되어 있으면 saved_sigmask를, 아니면 현재 blocked 집합을 프레임에 저장합니다.

L167 struct rt_sigframe __user *frame;

사용자 스택에 만들 rt_sigframe의 주소를 담습니다. get_sigframe이 정한 위치를 이후 접근 검사와 쓰기에 사용합니다.

L168 void __user *fp = NULL;

프레임에 연계되는 사용자 FP 상태 저장 영역의 주소입니다. 처음 NULL로 두고 get_sigframe이 저장 위치를 정하면 갱신합니다.

L169 unsigned long uc_flags;

프레임이 담는 확장 상태와 SS 복원 규칙을 표시할 비트 묶음입니다. frame_uc_flags가 계산하여 uc_flags 필드에 기록합니다.

L172 if (!(ksig->ka.sa.sa_flags & SA_RESTORER))

처리기가 복귀용 sa_restorer를 제공했는지 검사합니다. x86-64 신호 ABI는 이 사용자 복귀 코드가 있어야 rt_sigreturn을 요청할 수 있습니다.

L173 return -EFAULT;

필수 복귀 코드가 없으므로 신호 프레임 설정을 -EFAULT로 실패 처리합니다.

L175 frame = get_sigframe(ksig, regs, sizeof(struct rt_sigframe), &fp);

red-zone, altstack, fpstate 크기를 반영해 새 user RSP를 계산한다.

L176 uc_flags = frame_uc_flags(regs);

이 프레임에 확장 FP 상태와 SS 복구 정보를 어떻게 해석할지 나타내는 uc_flags를 만듭니다. XSAVE 지원 여부와 중단된 코드가 64비트 모드인지에 따라 UC_FP_XSTATE·UC_STRICT_RESTORE_SS 등을 정합니다.

L178 if (!user_access_begin(frame, sizeof(*frame)))

프레임 전체가 허용된 사용자 주소 범위인지 검사하고 쓰기 구간을 엽니다. 실제 페이지 쓰기 오류는 이어지는 unsafe 접근의 Efault 경로에서도 처리합니다.

L179 return -EFAULT;

프레임 주소 범위로 사용자 접근을 시작할 수 없으므로 -EFAULT를 반환합니다.

L182 unsafe_put_user(uc_flags, &frame->uc.uc_flags, Efault);

계산한 uc_flags를 사용자 프레임에 기록합니다. 앞의 user_access_begin으로 연 접근 구간에서 수행하며, 오류가 나면 Efault로 이동해 프레임 작성 실패를 처리합니다.

L183 unsafe_put_user(0, &frame->uc.uc_link, Efault);

uc_link에 0을 써 연결된 다음 사용자 문맥이 없음을 표시합니다. 쓰기 도중 사용자 페이지 접근이 실패하면 Efault 경로로 빠집니다.

L184 unsafe_save_altstack(&frame->uc.uc_stack, regs->sp, Efault);

중단 당시 SP를 바탕으로 대체 신호 스택의 설정과 상태를 uc_stack에 기록합니다. sigreturn이 원래 신호 스택 상태를 복구할 수 있도록 하며, 기록 실패는 Efault로 처리합니다.

L187 already in userspace. */

사용자 공간에 이미 있는 복귀용 stub을 사용한다는 앞줄 주석의 끝입니다. 다음 줄에서 sa_restorer를 프레임의 반환 주소로 기록합니다.

L188 unsafe_put_user(ksig->ka.sa.sa_restorer, &frame->pretcode, Efault);

사용자가 등록한 sa_restorer 주소를 프레임의 복귀 주소에 넣습니다. 신호 처리기가 반환하면 이 사용자 코드가 rt_sigreturn을 요청하게 하며, 커널이 여기서 restorer를 직접 호출하는 것은 아닙니다.

L189 unsafe_put_sigcontext(&frame->uc.uc_mcontext, fp, regs, set, Efault);

현재 integer register와 trap metadata를 user-visible sigcontext로 복사한다.

L190 unsafe_put_sigmask(set, frame, Efault);

복귀 때 되살릴 신호 차단 집합 set을 프레임의 uc_sigmask에 기록합니다. 64비트 신호 집합을 사용자 메모리에 쓰다가 오류가 나면 Efault로 이동합니다.

L191 user_access_end();

신호 프레임 필드들을 쓰기 위해 열었던 사용자 메모리 접근 구간을 닫습니다. 이후의 siginfo 복사와 shadow stack 준비가 이 unsafe 접근 구간 밖에서 진행되도록 경계를 정합니다.

L193 if (ksig->ka.sa.sa_flags & SA_SIGINFO) {

SA_SIGINFO 처리기일 때만 siginfo 내용을 복사합니다. 번호 하나만 받는 처리기에는 이 추가 자료의 전달을 요구하지 않습니다.

L194 if (copy_siginfo_to_user(&frame->info, &ksig->info))

사용자 프레임에 신호 발생 정보를 복사하고 실패 여부를 검사합니다. 쓰기 실패면 처리기로 진입하지 않습니다.

L195 return -EFAULT;

siginfo 복사를 완료하지 못했으므로 -EFAULT로 신호 설정 실패를 반환합니다.

L198 if (setup_signal_shadow_stack(ksig))

사용 중인 사용자 shadow stack에도 신호 호출과 복귀에 필요한 상태를 준비합니다. 실패하면 일반 스택만 준비된 채 처리기를 실행하지 않습니다.

L199 return -EFAULT;

shadow stack 준비가 실패했으므로 신호 전달 설정을 -EFAULT로 중단합니다.

L202 regs->di = ksig->sig;

신호 번호를 저장된 RDI에 넣습니다. x86-64 함수 호출 규약의 첫 정수 인자로 처리기에 전달됩니다.

L204 regs->ax = 0;

프로토타입 없는 처리기 호출과의 호환성을 위해 RAX를 0으로 둡니다. 이 호출에서 인자로 전달하는 벡터 레지스터가 없다는 상태도 맞춥니다.

L207 next argument after the signal number on the stack. */

SA_SIGINFO를 쓰지 않는 처리기와도 이 인자 준비가 호환된다는 주석의 끝입니다. 아래 RSI·RDX에는 추가 정보 주소를 두지만 한 인자 처리기는 사용하지 않습니다.

L208 regs->si = (unsigned long)&frame->info;

두 번째 인자 RSI에 사용자 프레임의 siginfo 주소를 넣습니다. SA_SIGINFO 처리기가 신호 발생 원인을 읽는 포인터입니다.

L209 regs->dx = (unsigned long)&frame->uc;

세 번째 인자 RDX에 ucontext 주소를 넣습니다. 처리기는 중단된 레지스터와 신호 마스크 등의 문맥을 이 자료에서 볼 수 있습니다.

L210 regs->ip = (unsigned long) ksig->ka.sa.sa_handler;

IRET/SYSRET 이후 signal handler를 실행하도록 saved instruction pointer를 바꾼다.

L212 regs->sp = (unsigned long)frame;

사용자 SP를 새 신호 프레임 시작으로 바꿉니다. 사용자 모드 복귀 후 처리기가 이 스택과 저장된 restorer 주소로 실행됩니다.

RISC-V · Linux 6.18.37

rt_sigframe의 sigcontext와 vector state datap pointer

integer pt_regs와 FP state를 frame에 저장하고 vector 사용 task는 vstate header와 datap 영역을 정렬해 추가한다. handler a0-a2와 RA trampoline, EPC/SP를 새 값으로 바꾼다.

원본 코드: arch/riscv/kernel/signal.c:307-381

307	sp = sigsp(sp, ksig) - framesize;
308 
309	/* Align the stack frame. */
310	sp &= ~0xfUL;
311 
312	return (void __user *)sp;
313}
314 
315static int setup_rt_frame(struct ksignal *ksig, sigset_t *set,
316	struct pt_regs *regs)
317{
318	struct rt_sigframe __user *frame;
319	long err = 0;
320	unsigned long __maybe_unused addr;
321	size_t frame_size = get_rt_frame_size(false);
322 
323	frame = get_sigframe(ksig, regs, frame_size);
324	if (!access_ok(frame, frame_size))
325		return -EFAULT;
326 
327	err |= copy_siginfo_to_user(&frame->info, &ksig->info);
328 
329	/* Create the ucontext. */
330	err |= __put_user(0, &frame->uc.uc_flags);
331	err |= __put_user(NULL, &frame->uc.uc_link);
332	err |= __save_altstack(&frame->uc.uc_stack, regs->sp);
333	err |= setup_sigcontext(frame, regs);
334	err |= __copy_to_user(&frame->uc.uc_sigmask, set, sizeof(*set));
335	if (err)
336		return -EFAULT;
337 
338	/* Set up to return from userspace. */
339#ifdef CONFIG_MMU
340	regs->ra = (unsigned long)VDSO_SYMBOL(
341		current->mm->context.vdso, rt_sigreturn);
342#else
343	/*
344	 * For the nommu case we don't have a VDSO.  Instead we push two
345	 * instructions to call the rt_sigreturn syscall onto the user stack.
346	 */
347	if (copy_to_user(&frame->sigreturn_code, __user_rt_sigreturn,
348			 sizeof(frame->sigreturn_code)))
349		return -EFAULT;
350 
351	addr = (unsigned long)&frame->sigreturn_code;
352	/* Make sure the two instructions are pushed to icache. */
353	flush_icache_range(addr, addr + sizeof(frame->sigreturn_code));
354 
355	regs->ra = addr;
356#endif /* CONFIG_MMU */
357 
358	/*
359	 * Set up registers for signal handler.
360	 * Registers that we don't modify keep the value they had from
361	 * user-space at the time we took the signal.
362	 * We always pass siginfo and mcontext, regardless of SA_SIGINFO,
363	 * since some things rely on this (e.g. glibc's debug/segfault.c).
364	 */
365	regs->epc = (unsigned long)ksig->ka.sa.sa_handler;
366	regs->sp = (unsigned long)frame;
367	regs->a0 = ksig->sig;                     /* a0: signal number */
368	regs->a1 = (unsigned long)(&frame->info); /* a1: siginfo pointer */
369	regs->a2 = (unsigned long)(&frame->uc);   /* a2: ucontext pointer */
370 
371#if DEBUG_SIG
372	pr_info("SIG deliver (%s:%d): sig=%d pc=%p ra=%p sp=%p\n",
373		current->comm, task_pid_nr(current), ksig->sig,
374		(void *)regs->epc, (void *)regs->ra, frame);
375#endif
376 
377	return 0;
378}
379 
380static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
381{

주요 구문 해설

원본 75줄은 그대로 표시하며, 검토한 43개 구문에 설명을 붙였습니다.

L307 sp = sigsp(sp, ksig) - framesize;

SA_ONSTACK 등에 따라 일반 또는 대체 스택의 기준 SP를 선택한 뒤 프레임 크기를 뺍니다. 낮은 주소 방향으로 자라는 스택에서 프레임 공간을 확보합니다.

L310 sp &= ~0xfUL;

SP의 하위 4비트를 지워 주소를 16바이트 경계로 내립니다. 처리기 진입이 RISC-V ABI의 스택 정렬을 지키게 합니다.

L312 return (void __user *)sp;

계산한 스택 위치를 사용자 프레임 포인터로 반환합니다. 여기서는 위치만 정하며 실제 자료 쓰기는 호출자에서 수행합니다.

L315static int setup_rt_frame(struct ksignal *ksig, sigset_t *set,

신호 설정과 복귀용 마스크를 받아 RISC-V 사용자 스택에 rt 신호 프레임을 구성합니다.

L316 struct pt_regs *regs)

중단된 레지스터 상태를 받습니다. 프레임에 기존 값을 저장한 뒤 이 regs의 PC·SP·인자를 처리기 진입 상태로 바꿉니다.

L318 struct rt_sigframe __user *frame;

사용자 신호 프레임의 위치를 담습니다. get_sigframe으로 주소를 구한 뒤 접근 가능 범위를 검사합니다.

L319 long err = 0;

프레임 구성 중 생기는 복사 오류를 누적합니다. 모든 쓰기가 성공했을 때만 0이 유지됩니다.

L320 unsigned long __maybe_unused addr;

MMU가 없는 구성에서 스택에 만든 sigreturn 명령의 주소를 보관합니다. MMU 구성에서는 쓰지 않아도 되므로 __maybe_unused를 붙입니다.

L321 size_t frame_size = get_rt_frame_size(false);

현재 태스크에 필요한 신호 프레임 크기를 구합니다. false를 넘겨 가능한 모든 확장 상태의 최대 크기 대신 현재 활성화된 벡터 상태 등을 반영하고, ABI가 요구하는 16바이트 정렬에 맞춥니다.

L323 frame = get_sigframe(ksig, regs, frame_size);

기존 사용자 SP와 SA_ONSTACK 설정에 따라 프레임을 놓을 사용자 스택 위치를 정합니다. 힙 메모리를 할당하는 함수가 아니며, 대체 스택 경계를 넘는 등의 문제는 뒤의 접근 검사에서 실패하도록 반환합니다.

L324 if (!access_ok(frame, frame_size))

계산한 프레임 전체가 허용된 사용자 주소 범위인지 검사합니다. 실제 페이지가 쓰기 가능한지는 이어지는 복사에서도 확인됩니다.

L325 return -EFAULT;

프레임 주소 범위가 잘못되었으므로 -EFAULT를 반환하고 사용자 메모리에 기록하지 않습니다.

L327 err |= copy_siginfo_to_user(&frame->info, &ksig->info);

신호의 번호와 원인 등의 siginfo를 사용자 프레임에 복사합니다. 실패를 err에 누적하여 필요한 정보가 빠진 채 신호 처리기를 실행하지 않게 합니다.

L330 err |= __put_user(0, &frame->uc.uc_flags);

신호 프레임의 uc_flags를 0으로 초기화합니다. __put_user의 오류 반환값을 err에 합쳐 이후 프레임 작성 실패 여부를 판단합니다.

L331 err |= __put_user(NULL, &frame->uc.uc_link);

uc_link를 NULL로 설정해 연결된 다음 문맥이 없음을 나타냅니다. 사용자 페이지 쓰기가 실패하면 그 오류도 err에 남습니다.

L332 err |= __save_altstack(&frame->uc.uc_stack, regs->sp);

중단 당시 SP를 기준으로 대체 신호 스택의 설정과 현재 사용 상태를 uc_stack에 보관합니다. 신호 처리 후 원래 스택 상태로 복귀할 때 사용하는 정보입니다.

L333 err |= setup_sigcontext(frame, regs);

중단된 일반 레지스터와 지원·활성 상태에 따른 FP·벡터 문맥을 신호 프레임에 저장합니다. 신호 처리기가 끝난 뒤 sigreturn이 이 자료로 사용자 실행 상태를 복원합니다.

L334 err |= __copy_to_user(&frame->uc.uc_sigmask, set, sizeof(*set));

복귀 때 되살릴 신호 차단 집합 set을 프레임의 uc_sigmask로 복사합니다. __copy_to_user는 복사하지 못한 바이트 수를 반환하므로, 0이 아니면 err가 실패를 나타냅니다.

L335 if (err)

siginfo·ucontext·레지스터·신호 마스크 저장 중 하나라도 실패했는지 검사합니다. 실패한 프레임으로 복귀 레지스터를 준비하지 않습니다.

L336 return -EFAULT;

프레임 저장을 완료하지 못했으므로 사용자 메모리 오류 -EFAULT를 반환합니다.

L339#ifdef CONFIG_MMU

MMU가 있는 커널은 시그널 handler의 복귀 주소를 사용자 vDSO의 rt_sigreturn으로 정합니다. MMU가 없으면 vDSO가 없으므로 아래 분기에서 복귀 시스템 호출 명령 두 개를 사용자 스택에 직접 넣습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L340 regs->ra = (unsigned long)VDSO_SYMBOL(

신호 처리기의 복귀 주소 RA를 vDSO의 rt_sigreturn 진입점으로 설정합니다. 처리기가 반환하면 이 사용자 코드가 커널에 문맥 복원을 요청합니다.

L341 current->mm->context.vdso, rt_sigreturn);

현재 mm에 매핑된 vDSO 기준주소와 rt_sigreturn 심볼 오프셋을 합쳐 실제 사용자 복귀 주소를 구합니다.

L342#else

MMU가 없는 구성에는 vDSO가 없어 사용자 스택의 시그널 프레임에 rt_sigreturn 요청 명령 두 개를 복사합니다. handler의 복귀 주소는 그 코드가 되며, 커널은 복귀 시스템 호출로 원래 문맥을 복원합니다.

L347 if (copy_to_user(&frame->sigreturn_code, __user_rt_sigreturn,

vDSO가 없는 MMU 미사용 구성에서는 rt_sigreturn을 요청하는 짧은 명령열을 사용자 스택의 sigreturn_code에 복사합니다.

L348 sizeof(frame->sigreturn_code)))

복사 길이를 프레임에 마련된 sigreturn_code 크기로 지정합니다. 복사되지 않은 바이트가 남으면 조건이 참이 되어 오류를 반환합니다.

L349 return -EFAULT;

스택의 sigreturn 코드를 완전히 쓰지 못했으므로 프레임 준비를 -EFAULT로 실패 처리합니다.

L351 addr = (unsigned long)&frame->sigreturn_code;

사용자 스택에 쓴 sigreturn 코드의 시작 주소를 addr에 보관합니다. 명령 캐시 동기화와 처리기의 RA 설정에 같은 주소를 사용합니다.

L353 flush_icache_range(addr, addr + sizeof(frame->sigreturn_code));

MMU가 없는 구성에서 스택에 방금 써 넣은 sigreturn 코드 구간의 명령 캐시를 동기화합니다. 처리기의 복귀 주소가 이 코드이므로 CPU가 이전 명령 바이트가 아닌 새 두 명령을 실행할 수 있어야 합니다.

L355 regs->ra = addr;

처리기의 복귀 주소를 스택에 준비한 sigreturn 코드로 바꿉니다. MMU 미사용 환경에서 vDSO를 대신하는 반환 경로입니다.

L356#endif /* CONFIG_MMU */

339행에서 시작한 선택 구간을 마칩니다. MMU가 있는 커널은 시그널 handler의 복귀 주소를 사용자 vDSO의 rt_sigreturn으로 정합니다. MMU가 없으면 vDSO가 없으므로 아래 분기에서 복귀 시스템 호출 명령 두 개를 사용자 스택에 직접 넣습니다.

L365 regs->epc = (unsigned long)ksig->ka.sa.sa_handler;

sret 뒤 signal handler에서 시작하도록 saved EPC를 교체한다.

L366 regs->sp = (unsigned long)frame;

저장된 SP를 새 신호 프레임 위치로 바꿉니다. 처리기는 원래 문맥을 보관한 프레임 아래쪽 스택을 사용하여 실행됩니다.

L367 regs->a0 = ksig->sig; /* a0: signal number */

RISC-V 함수 호출 규약의 첫 인자 a0에 신호 번호를 넣습니다.

L368 regs->a1 = (unsigned long)(&frame->info); /* a1: siginfo pointer */

두 번째 인자 a1에 사용자 프레임의 siginfo 주소를 넣습니다. 이 구현은 SA_SIGINFO 여부와 무관하게 해당 포인터를 준비합니다.

L369 regs->a2 = (unsigned long)(&frame->uc); /* a2: ucontext pointer */

세 번째 인자 a2에 ucontext 주소를 넣습니다. 신호 처리기는 이 자료로 중단된 실행 문맥을 살펴볼 수 있습니다.

L371#if DEBUG_SIG

DEBUG_SIG는 시그널 전달 상황을 출력할 때 쓰는 이 파일의 디버깅 조건입니다. 켜면 task·시그널 번호·PC·복귀 주소·스택을 기록하고, 꺼지면 로그만 빠지며 시그널 프레임 구성은 그대로 수행합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L372 pr_info("SIG deliver (%s:%d): sig=%d pc=%p ra=%p sp=%p\n",

DEBUG_SIG가 켜진 빌드에서 신호 전달 상태를 로그로 남깁니다. 태스크와 신호 번호, 처리기 PC·복귀 주소·스택을 함께 출력합니다.

L373 current->comm, task_pid_nr(current), ksig->sig,

진단 로그에 현재 태스크 이름·PID와 전달 신호 번호를 넘깁니다. 같은 신호가 어떤 태스크로 갔는지 구분할 자료입니다.

L374 (void *)regs->epc, (void *)regs->ra, frame);

진단 로그에 설정을 마친 PC·RA와 프레임 주소를 넘깁니다. 포인터 출력용 형변환이며 레지스터를 변경하지 않습니다.

L375#endif

371행에서 시작한 선택 구간을 마칩니다. DEBUG_SIG는 시그널 전달 상황을 출력할 때 쓰는 이 파일의 디버깅 조건입니다. 켜면 task·시그널 번호·PC·복귀 주소·스택을 기록하고, 꺼지면 로그만 빠지며 시그널 프레임 구성은 그대로 수행합니다.

L377 return 0;

신호 프레임과 처리기 진입 레지스터를 성공적으로 준비했음을 0으로 반환합니다.

L380static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)

선택된 신호의 rseq 상태를 처리한 뒤 사용자 ABI에 맞는 신호 프레임을 만듭니다. 끝에서는 준비 결과와 단일 단계 추적 상태를 공통 신호 코드에 알립니다.

05 · WORKED EXAMPLE

숫자로 검산하기

01

가변 vector state가 signal frame에 미치는 영향

기본 frame 1KiB, vector state 4KiB, altstack 8KiB에서 signal 두 개가 중첩된다고 가정한다.

  1. first정렬과 header를 제외해도 첫 frame은 약 5KiB를 사용한다.
  2. nested두 번째 frame도 같은 state를 저장하면 누적 약 10KiB로 8KiB altstack을 넘는다.
  3. failureget_sigframe 또는 user write가 실패하면 kernel은 SIGSEGV 처리 경로로 가며 손상된 일부 frame을 복원에 사용하지 않는다.
  4. mitigation실제 최대 VL/XSAVE feature를 기준으로 sigaltstack 크기를 산정하고 guard page를 둔다.

결론MINSIGSTKSZ 같은 고정 상수만으로 scalable state의 worst case를 보장할 수 없는 이유를 frame byte 수로 확인할 수 있다.

06 · DEEP DIVE

주요 동작 상세 분석

01

공통 커널 코드와 아키텍처별 함수의 역할 구분

exit-to-user path가 deliverable signal을 고르고 architecture setup_rt_frame()이 user stack 또는 altstack에 ABI frame을 만든다. handler return trampoline은 rt_sigreturn syscall로 들어와 frame을 copy/validate한 뒤 pt_regs와 signal mask를 복원한다.

kernel-owned pt_regs에서 user-writable signal frame으로 상태를 복사하는 순간 신뢰 경계가 바뀐다. sigreturn은 user가 수정한 frame을 읽으므로 privilege bit, canonical address, reserved field를 반드시 검증한다.

02

arm64: rt_sigframe과 extensible context record chain

sigcontext 뒤 FPSIMD, ESR, SVE, ZA, ZT 같은 _aarch64_ctx record를 magic/size로 연결한다. setup_rt_frame()은 VL에 따라 frame 크기를 계산하고 handler x0-x2와 PC/LR/SP를 설정한다.

user frame write를 모두 성공시킨 뒤 pt_regs를 handler context로 바꿔야 하며 MTE tag와 altstack alignment도 지켜야 한다. 디버깅할 때는 SP 16-byte alignment, frame record magic/size, FPSIMD/SVE VL, PC/LR, PSTATE와 sigmask를 확인한다.

03

x86-64: ucontext, sigcontext와 XSAVE fpstate pointer

x64 rt frame은 pretcode, ucontext, siginfo와 fpstate area를 user stack에 둔다. x64_setup_rt_frame()은 pt_regs를 sigcontext에 복사하고 handler RDI/RSI/RDX와 RIP/RSP를 설정한다.

red-zone 회피, 16-byte stack alignment, CET shadow stack와 XSAVE frame validation을 함께 적용한다. 디버깅할 때는 frame rsp, uc_mcontext, fpstate magic/size/xfeatures, CS/SS/RFLAGS, CET SSP와 restorer 주소를 본다.

04

RISC-V: rt_sigframe의 sigcontext와 vector state datap pointer

integer pt_regs와 FP state를 frame에 저장하고 vector 사용 task는 vstate header와 datap 영역을 정렬해 추가한다. handler a0-a2와 RA trampoline, EPC/SP를 새 값으로 바꾼다.

vector state size 계산과 user pointer write가 완료된 뒤 pt_regs를 publish하고 sigreturn에서 SSTATUS의 허용 bit만 복원한다. 디버깅할 때는 EPC/SP/RA, a0-a2, sigmask, fp fcsr, vector vlenb/vl/vtype/vstart와 SSTATUS를 확인한다.

05

객체를 사용하고 해제하는 주체와 시점을 확인한다

signal frame은 user stack memory에 있으며 nested signal마다 아래쪽으로 추가된다. altstack overflow, stack mapping 변경과 handler가 frame을 덮는 경우를 모두 오류로 처리해야 한다.

주소나 레지스터 값만 확인하면 갱신되지 않은 상태를 놓칠 수 있다. 상태를 만드는 코드, 다른 코드에 전달하는 시점, 사용하는 코드, 해제하는 시점을 함께 기록한다.

06

최대 지연 시간은 명령 하나의 실행 시간만으로 결정되지 않는다

fixed integer context 외에 SVE/SME, XSAVE/AMX, RISC-V vector처럼 가변 크기 state를 frame에 materialize하면 signal delivery와 sigreturn 시간이 state byte 수에 비례해 증가한다.

평균값뿐 아니라 인터럽트를 비활성화한 시간, 다른 CPU의 응답 시간, 펌웨어 호출 시간, 재시도 횟수를 따로 확인해야 최대 지연의 원인을 찾을 수 있다.

07 · FAILURE

실패를 어떤 증거로 나눌 것인가

분류관찰되는 결과첫 확인값
arm64record size overflow, duplicate magic, 잘못된 VL 또는 PSTATE privilege bit를 허용하면 out-of-bounds read와 privilege corruption 위험이 생긴다.SP 16-byte alignment, frame record magic/size, FPSIMD/SVE VL, PC/LR, PSTATE와 sigmask를 확인한다.
x86-64noncanonical RIP/RSP, 잘못된 CS/SS, forged xfeatures 또는 shadow-stack token 불일치는 sigreturn fault로 막아야 한다.frame rsp, uc_mcontext, fpstate magic/size/xfeatures, CS/SS/RFLAGS, CET SSP와 restorer 주소를 본다.
RISC-VVLEN 기반 size overflow, vector datap pointer가 frame 밖을 가리키는 경우 또는 SSTATUS.SPP 조작을 검증하지 않으면 memory corruption과 privilege 문제가 생긴다.EPC/SP/RA, a0-a2, sigmask, fp fcsr, vector vlenb/vl/vtype/vstart와 SSTATUS를 확인한다.

08 · LAB

재현과 계측 절차

  1. handler에서 ucontext를 출력하고 GDB pt_regs와 비교해 signal delivery가 바꾼 PC/SP/argument를 확인한다.
  2. vector state를 dirty하게 만든 뒤 altstack 크기를 줄여 frame allocation 실패 경로를 재현한다.
  3. 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
  4. 소스만 읽고 끝내지 않고 최종 vmlinux의 objdump -dr, readelf -SW 결과로 선택된 alternative와 section 배치를 확인한다.

09 · REFERENCES

원문 위치

Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.