#커널 크래시 디버깅 및 TroubleShooting
- Race로 mmc_wait_data_done() 함수에서 커널 패닉
- "cat /d/shrinker" 입력 시 커널 패닉
- 함수 포인터 미지정으로 xfrm_local_error() 커널 패닉
- preempt 조건으로 ___might_sleep() 함수 크래시
- 스택 카나리: __stack_chk_fail() 함수 크래시
- 스택 카나리: tcp_v4_rcv -> __stack_chk_fail 크래시
- 뮤텍스 데드락(Mutex Deadlock)으로 락업(lockup)
- 디바이스 드라이버 Signature 문제로 커널 크래시
- 메모리 불량 커널 크래시 @find_vma_links()
- 메모리 불량 커널 크래시 @ttwu_do_activate()
- Race로 ipv6_ifa_notify() Stuck - watchdog reset
- tty_wakeup() 함수 Data Abort
- irq_affinity_notify() 함수 Data Abort
- cpuacct_charge() 함수 Data Abort
- 워크큐(workqueue) 락업(1)
- 워크큐(workqueue) 락업(2)
- 워크큐(workqueue) 락업(3)
최근 흥미로운 커널 패닉이 나왔는데요. 디버깅 과정을 공유 좀 하고자 해요.
일단 콜스택부터 볼께요. sock_has_perm() 함수가 돌다가 갑자기 __stack_chk_fail() 함수 호출로 panic()이 일어났거든요. 왜 이런 현상이 발생했을까요?
crash> bt e5752c00
PID: 1787 TASK: e5752c00 CPU: 4 COMMAND: "net_socket"
bt: WARNING: stack address:0xe853fa38, program counter:0xc0ee5b60
#0 [<c0ed8b64>] (panic) from [<c0125038>]
#1 [<c0125038>] (__stack_chk_fail) from [<c032b6cc>]
#2 [<c032b6cc>] (sock_has_perm) from [<c0327d00>]
#3 [<c0327d00>] (security_socket_recvmsg) from [<c0ceb1c8>]
#4 [<c0ceb1c8>] (sock_recvmsg) from [<c0cec474>]
#5 [<c0cec474>] (___sys_recvmsg) from [<c0ced5b4>]
#6 [<c0ced5b4>] (__sys_recvmsg) from [<c0106820>]
Trace32로 콜스택을 잡으면 아래 정보를 확인할 수 있습니다. 커널 패닉이 발생한 흔적이 보이죠.
-000|panic(fmt = 0xC1379761)
-001|__stack_chk_fail()
-002|sock_has_perm(?, sk = 0xE98D3400, perms = 2)
-003|security_socket_recvmsg(?, ?, ?, ?)
-004|__sock_recvmsg(inline)
-004|sock_recvmsg(sock = 0xC3C99C00, msg = 0xE853FF7C, size = 65536, flags = 0)
-005|___sys_recvmsg.part.5(sock = 0xC3C99C00, msg = 0xA08813B0, msg_sys = 0xE853FF7C, flags = 0, nosec = 0)
-006|fput_light(inline)
-006|__sys_recvmsg(?, msg = 0xA08813B0, flags = 0)
-007|ret_fast_syscall(asm)
T32(Trace32)로 UP 버튼으로 security_socket_recvmsg() 함수가 호출될 시점 스택 주소를 확인하면, 0xE853FDD8 임을 알 수 있습니다.
-003|security_socket_recvmsg(?, ?, ?, ?)
-004|__sock_recvmsg(inline)
-004|sock_recvmsg(sock = 0xC3C99C00, msg = 0xE853FF7C, size = 65536, flags = 0)
-005|___sys_recvmsg.part.5(sock = 0xC3C99C00, msg = 0xA08813B0, msg_sys = 0xE853FF7C, flags = 0, nosec =
-006|fput_light(inline)
-006|__sys_recvmsg(?, msg = 0xA08813B0, flags = 0)
-007|ret_fast_syscall(asm)
N _ R0 C1379761 R8 E5752C00
Z _ R1 E853FD54 R9 0
C _ R2 DC8CB01F R10 C3C99C00
V _ R3 0 R11 0
Q _ R4 C3C99C00 R12 0
R5 00010000 R13 E853FDD8
0 _ R6 E853FF7C R14 0
1 _ R7 0 PC C0327D00
2 _ SPSR 10 CPSR 01D3
"d.v %y.l 0xE853FDD8" 명령어로 스택에 푸쉬된 심볼을 확인할 수 있습니다.
NSD:E853FD4C| 38 50 12 C0 0xC0125038 \\vmlinux\panic\__stack_chk_fail+0x10
NSD:E853FD50| 61 97 37 C1 0xC1379761 \\vmlinux\Global\kallsyms_token_index+0x5D41
NSD:E853FD54| CC B6 32 C0 0xC032B6CC \\vmlinux\hooks\sock_has_perm+0x9C
NSD:E853FD58| 1F B0 8C DC 0xDC8CB01F
NSD:E853FD5C| 00 00 00 00 0x0
NSD:E853FD60| 00 00 00 00 0x0
NSD:E853FD64| CC B6 32 C0 0xC032B6CC \\vmlinux\hooks\sock_has_perm+0x9C
NSD:E853FD68| 74 FD 53 E8 0xE853FD74
NSD:E853FD6C| A0 FD 53 E8 0xE853FDA0
NSD:E853FD70| 7C FF 53 E8 0xE853FF7C
NSD:E853FD74| 02 05 00 00 0x502 \\vmlinux\Global\NR_syscalls+0x37E
NSD:E853FD78| 84 FD 53 E8 0xE853FD84
NSD:E853FD7C| C4 04 D4 C0 0xC0D404C4 \\vmlinux\af_netlink\netlink_recvmsg\out+0x38
NSD:E853FD80| 70 08 FD EC 0xECFD0870
NSD:E853FD84| 00 00 00 00 0x0
NSD:E853FD88| 00 34 8D E9 0xE98D3400
NSD:E853FD8C| 00 00 00 00 0x0
NSD:E853FD90| 00 00 00 00 0x0
NSD:E853FD94| 00 00 00 00 0x0
NSD:E853FD98| 00 00 00 00 0x0
NSD:E853FD9C| 00 00 00 00 0x0
NSD:E853FDA0| 00 00 00 00 0x0
NSD:E853FDA4| 00 00 00 00 0x0
NSD:E853FDA8| 00 00 00 00 0x0
NSD:E853FDAC| 00 00 00 00 0x0
NSD:E853FDB0| 00 00 00 00 0x0
NSD:E853FDB4| 1F B0 8C DC 0xDC8CB01F
NSD:E853FDB8| 00 0A 82 ED 0xED820A00 // E853FDBC - 0x54 = 0xE853FD68 "where: sub r13,r13,#0x54 ; r13,r13,#84"
NSD:E853FDBC| 00 9C C9 C3 0xC3C99C00 //<<-- R4
NSD:E853FDC0| 00 00 01 00 0x10000 //<<-- R5
NSD:E853FDC4| 7C FF 53 E8 0xE853FF7C //<<-- R6
NSD:E853FDC8| 00 00 00 00 0x0 //<<-- R7
NSD:E853FDCC| 00 2C 75 E5 0xE5752C00 //<<-- R8
NSD:E853FDD0| 00 00 00 00 0x0 //<<-- R9
NSD:E853FDD4| 00 7D 32 C0 0xC0327D00 \\vmlinux\security\security_socket_recvmsg+0x14 //<<-- R14
NSD:E853FDD8| 00 00 00 00 0x0 //<<-- 스택 주소
NSD:E853FDDC| C8 B1 CE C0 0xC0CEB1C8 \\vmlinux\socket\sock_recvmsg+0x58
security_socket_recvmsg 함수에서 sock_has_perm 함수 호출 시 0x54 바이트 만큼 스택을 잡습니다.
NSR:C032B630|E92D43F0 sock_has_perm: push {r4-r9,r14}
NSR:C032B634|E24DD054 sub r13,r13,#0x54 ; r13,r13,#84
이제 코드 리뷰를 좀 해보겠습니다.
[1]: sock_has_perm() 함수가 처음 security_socket_recvmsg() 함수로부터 호출되었을 때 스택 주소는 0xE853FDD8이네요.
[2]: 스택이 푸쉬된 다음에 스택 주소는 0xE853FDBC로 업데이트 되었네요.
Stack: 0xE853FDBC(0xE853FDD8-0x1C)
[3]: 로컬 변수들은 스택 메모리에서 돌죠. 로컬 변수가 스택을 쓸 수 있도록 0x54 바이트 만큼 방을 비워줍니다.
Stack: 0xE853FD68(0xE853FDBC-0x54)
[4]: R7는 0xC1809948 을 갖고 있는데 여기서 __stack_chk_guard란 함수를 링크하네요.
c032b6d4: c1809948 .word 0xc1809948
crash> p &__stack_chk_guard
$3 = (unsigned long *) 0xc1809948 <__stack_chk_guard>.
[5]: R3는 stack canary 매직값 0xdc8cb01f을 갖고 있는데 이런 계산으로 R3 = 0xdc8cb01f = *0xC1809948 업데이트되네요.
아래 명령어를 치면 0xC1809948 주소에 dc8cb01f란 값이 위치해 있어요.
.crash> rd 0xC1809948
c1809948: dc8cb01f
[6]: Stack canary 매직 덤프값 0xdc8cb01f이 스택으로 푸쉬되고 있네요. R3이 갖고 있는 값을 스택에 푸쉬하는 거죠. 0xE853FDB4 주소로 푸쉬되죠.
0xE853FDB4=0xE853FD68+0x4c = SP+0x4c
[7]: [6]번 인스트럭션에서 stack canary magic dump 값을 스택(0xE853FD68+0x4c = SP+0x4c) 공간에 푸쉬했잖아요.
똑같은 메모리 공간에 접근해서 stack canary magic dump 값을 꺼내와요. 이 값 0xdc8cb01f은 R2에 업데이트되죠.
[8]: 여기서 중요한 점은 [6] 명령어가 실행된 다음에 R7 레지스터는 변경된 적이 없다는 점인데요.
R7 레지스터가 0xC1809948 값을 갖고 있어야 하는데 0xC1800048 값이네요.
그래서 R3이 갑자기 0x0으로 바뀌어 있어요.
원래 R7 레지스터가 0xC1809948를 갖고 있으면 당연히 R3은 0xdc8cb01f일텐데 말이죠.
0xc032b638 <sock_has_perm+8>: ldr r7, [pc, #148]
[9]: R2과 R3값이 다르니 커널 패닉이 발생하네요. kernel panic occurs
#Code
0xc032b630 <sock_has_perm>: push {r4, r5, r6, r7, r8, r9, lr} //<<--[2]
0xc032b634 <sock_has_perm+0x4>: sub sp, sp, #84 ; 0x54 //<<--[3]
0xc032b638 <sock_has_perm+0x8>: ldr r7, [pc, #148] ; 0xc032b6d4 <sock_has_perm+0xa4> //<<--[4]
0xc032b63c <sock_has_perm+0xc>: add r6, sp, #28
0xc032b640 <sock_has_perm+0x10>: mov r9, r0
0xc032b644 <sock_has_perm+0x14>: ldr r4, [r1, #420] ; 0x1a4
0xc032b648 <sock_has_perm+0x18>: mov r5, r1
0xc032b64c <sock_has_perm+0x1c>: mov r8, r2
0xc032b650 <sock_has_perm+0x20>: ldr r3, [r7] //<<--[5]
0xc032b654 <sock_has_perm+36>: mov r1, #0
0xc032b658 <sock_has_perm+40>: mov r2, #48 ; 0x30
0xc032b65c <sock_has_perm+44>: mov r0, r6
0xc032b660 <sock_has_perm+48>: str r3, [sp, #76] ; 0x4c //<<--[6]
// snip
0xc032b6b8 <sock_has_perm+0x88>: ldr r2, [sp, #76] ; 0x4c //<<--[7]
0xc032b6bc <sock_has_perm+0x8c>: ldr r3, [r7] //<<--[8]
0xc032b6c0 <sock_has_perm+0x90>: cmp r2, r3
0xc032b6c4 <sock_has_perm+0x94>: beq 0xc032b6cc <sock_has_perm+156>
0xc032b6c8 <sock_has_perm+0x98>: bl 0xc0125028 <__stack_chk_fail> //<<--[9]
좀 어렵게 설명한 것 같은데요. 쉽게 C코드로 변경하면 아래와 같아요.
void sock_has_perm(void)
{
int stack_check = &__stack_chk_guard;
int loc_stack = 0;
loc_stack = stack_check;
// 여러 서브 함수들이 호출됨. 각 함수가 호출이 일어날 때 마다 당연 스택 푸쉬/팝 동작이 수행됨
A();
B();
C();
D();
loc_stack값과 stack_check값이 다르다는 것은 스택이 오염되었다고 판단하므로, 커널 패닉 유발!
if (loc_stack != stack_check) {
__stack_chk_fail();
}
}
0xdc8cb01f은 R2로 업데이트되는데요. "ldr r2, [sp, #76] ; 0x4c"
만약 스택이 오염되었으면 R2가 이상한 값을 갖고 있어야 하는데, 이번 경우는 R3이 이상한 값을 갖고 있어서 커널 패닉이 발생한 거거든요.
이 이슈의 핵심 포인트는 R7 레지스터가 0xC1809948 값을 갖고 있어야 하는데 0xC1800048 값이라는 거구요.
붉은 색으로 책한 공간에 비트 플립이 일어나 발생한 문제 -> 0xC1800048(0xC1809948)로 확인되었어요.
사실 이 디바이스 상태가 좀 안 좋았거든요. 그래서 메모리를 바꾸어서 테스트 해 보았더니 별 문제 없더라구요.
스택 카너리에 대해 좀 더 설명을 하면요. boot_init_stack_canary() 함수에서 스택 카너리를 초기화합니다. 참고로 이 함수는 start_kernel()에서 호출됩니다. 아래 코드를 잠깐 보면 task descriptor(TCB)의 멤버에 stack_canary에 canary 값을 초기화합니다.
extern unsigned long __stack_chk_guard;
static __always_inline void boot_init_stack_canary(void)
{
unsigned long canary;
/* Try to get a semi random initial value. */
get_random_bytes(&canary, sizeof(canary));
canary ^= LINUX_VERSION_CODE;
current->stack_canary = canary;
__stack_chk_guard = current->stack_canary;
}
658 ifdef CONFIG_CC_STACKPROTECTOR_REGULAR
659 stackp-flag := -fstack-protector
660 ifeq ($(call cc-option, $(stackp-flag)),)
661 $(warning Cannot use CONFIG_CC_STACKPROTECTOR_REGULAR: \
662 -fstack-protector not supported by compiler)
663 endif
이 플래그를 설정하면, 리눅스 커널에서 기본으로 호출되는 여러 함수들 하단에 __stack_chk_fail 스택 오염 진단 루틴을 추가합니다.
0xc0100974 <do_one_initcall+404>: bl 0xc0125028 <__stack_chk_fail>
0xc0100d8c <name_to_dev_t+828>: bl 0xc0125028 <__stack_chk_fail>
0xc0107854 <__show_regs+700>: bl 0xc0125028 <__stack_chk_fail>
0xc01219c0 <sha384_neon_final+108>: bl 0xc0125028 <__stack_chk_fail>
0xc0133c34 <sys_newuname+352>: bl 0xc0125028 <__stack_chk_fail>
0xc0133d68 <sys_sethostname+276>: bl 0xc0125028 <__stack_chk_fail>
# Reference: For more information on 'Linux Kernel';
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2
'Core BSP 분석 > 커널 트러블슈팅' 카테고리의 다른 글
[Kernel][Panic] crash due to "signature and/or required key missing" (0) | 2023.05.07 |
---|---|
[Linux][Kernel]뮤텍스 데드락(Mutex Deadlock) 락업(lockup) - "simpleperf" 디버깅 (0) | 2023.05.07 |
[Linux][Kernel] panic@___might_sleep (0) | 2023.05.07 |
[Linux][Kernel][Stability] Kernel panic @0x0 from xfrm_local_error+0x4c (0) | 2023.05.07 |
[Kernel][Debug] "cat /d/shrinker" kernel panic (0) | 2023.05.07 |