CTR_EL0 가로채기가 ARM64 폰을 멈춘 원인, 실행 코드 정렬과 메모리 속성 추적
- postmarketOS용 ARM64 베어메탈 하이퍼바이저가 CTR_EL0 읽기 가로채기를 켜면 휴대폰이 무작위로 멈추고, 몇 초 뒤 watchdog이 시스템을 재시작했음
- 가로챈 MRS 명령의 레지스터 보존과 예외 처리 트램펄린은 QEMU 단일 단계 실행에서 정상으로 확인됐지만, 실제 기기에서는 핸들러가 의도한 출력 레지스터조차 바꾸지 않는 것처럼 보였음
- 원인은 실행 가능한 msr_accessors 배열을 런타임에 정렬한 코드였음. ARM은 I-cache와 D-cache가 자동으로 일관성을 유지하지 않으므로, 데이터 변경 뒤 그 버퍼의 기계어를 실행하면 이전 명령을 가져오거나 변경분이 RAM에 반영되지 않을 수 있음
- 작성자는 배열 정렬을 빌드 단계로 옮겨 핸들러 동작을 고쳤지만 부팅 실패는 계속됐음. 모든 예외 벡터에 로그를 넣어도 예외가 잡히지 않았고,
/proc/last_kmsg에는 e2fsck의/dev/urandom읽기 직후 로그만 남았음 - Hacker News 토론에서는 ARM 명세가 Device 메모리의 추측적 명령어 페치를, 모든 예외 수준에서 execute-never로 표시하지 않은 경우 막지 않는다는 ARM DDI 0487K.a 문구를 인용했음
Hacker News opinions
NX 비트는 이해하는데, 이 글에서 NX와 추측 실행 접근이 어떻게 이어지는지 모르겠음. 흥미로운 주제가 여럿 섞였는데 따라갈 배경지식이 부족하네.
CPU는 성능 때문에 코드를 추측 실행했다가 예측이 틀리면 결과를 되돌림. 이 ARM 구현처럼 그 과정이 잘못되면 실행하라고 하지 않은 주소를 실행하려다 멈출 수 있고, Spectre나 Meltdown도 비슷한 부류의 문제임.
내 이해로는 NX가 추측적 명령어 페치를 막아서 Spectre 계열 취약점에 관여한 CPU 동작도 제한함. 원래 NX의 목적과는 좀 다른 효과라 놀라움.
내가 보기엔 NX와 W^X는 별로임. JIT는 필요한 때 코드를 쓰는 방식이 자연스럽고, 캐시 라인 플러시는 너무 불편한 우회책이라 자기 수정 코드를 일급으로 다뤄야 함.
그건 진짜 핫테이크네.
자기 수정 코드는 RAM이 너무 느려서 현대 CPU에선 성능이 안 나옴. 큰 코드 작업집합을 돌리는 브라우저나 UI는 I-cache에서 빠르게 가져오고 디코드하는 게 중요하고, 모든 명령어 페치가 저장과 충돌하는지 검사하면 비용이 너무 큼. Apple도 명시적 무효화로 JIT를 충분히 굴리고 있고.
use-after-free가 함수 포인터를 덮으면 NX는 그 포인터 호출 시점에 바로 트랩을 내서 실패 지점 근처에서 죽게 해줌. 오래된 바이트를 코드로 실행하면 크래시와 스택 트레이스가 훨씬 엉망일 수 있음.
ARM 설계가 이상하게 느껴짐. Device 메모리에서 데이터 프리페치는 막으면서 명령어 프리페치는 허용할 이유가 있나, 둘 다 막아야 하는 것 아닌가.
내 추측으로는 프런트엔드 하드웨어에 어떤 메모리 속성을 전달할지 줄이려는 설계 같음. Device 속성을 데이터 프리페처 쪽에만 보내고, 프런트엔드에는 NX만 보내면 배선과 파이프라인 부담이 줄어들 테니까.
여기서 명령어 프리페치는 사실상 프리페치가 아니라 추측 실행임. 분기 예측이 틀릴지 fetcher는 디코드 전까지 모르니 현대 코어는 거의 항상 추측적으로 가져오고, Cortex-A53처럼 겉보기엔 인오더인 코어도 분기 예측기를 씀.
더 이상한 건 Device 영역에서 의도적으로 코드를 실행하는 행위는 UNPREDICTABLE이라는 점임. 안정적으로 실행하면 안 되는 곳에서 추측적 명령어 페치는 할 수 있다는 얘기라 이상함.
ARM 공급사가 마음대로 구현을 바꿀 수 있다는 표현은 과장임. 자체 코어를 설계할 수 있는지는 라이선스마다 다르고 Meta 같은 큰 회사도 모두 가능한 게 아님. NVIDIA가 합법적인 동작에 새 트랩을 넣으면 Apple용 바이너리가 Grace에서 깨질 수 있으니 ARM도 사용자 공간 호환성을 강하게 요구함.
글이 인용한 Linux 커널에는 Apple CPU가 하이퍼바이저 모드에서 AArch64 명세와 달라지는 경우를 위한 우회 코드가 있음. 글에서 말한 공급사는 명세를 조금 덜 신경 써도 되는 일부 회사를 뜻한 것으로 봄.
Device 메모리로의 추측적 명령어 페치가 기기를 죽이는 버그는 내가 본 버그 중에서도 손꼽힘. Cortex-M4에서 MPU 없이 겪었고, 메모리 속성을 못 바꿔서 컴파일된 코드 배치를 억지로 바꿔 페치를 막는 수밖에 없었음. ARM DDI 0487K.a B2.15.2도 모든 예외 수준에서 execute-never로 표시하지 않으면 Device 속성만으로는 추측적 명령어 페치를 막지 않는다고 적고 있음.
요즘 주소 0에 뭘 매핑하는 건 나쁜 관례로 봄. 1:1 identity mapping을 쓰는 경우도 물리 주소 0에 아무것도 없게 만들어서 굳이 매핑할 필요를 없애더라.