🔥 Pixel 10 모뎀 펌웨어에 Rust가 들어갔다

#Rust#보안#Android#Google#임베디드
1132자
15분

content image

"모뎀 펌웨어에 Rust를 넣었다고?"

Google 보안 블로그에 올라온 포스트 제목을 보고 멈칫했다. 운영체제도 아니고, 앱도 아니고, 베이스밴드 모뎀이다. 스마트폰에서 가장 깊은 곳에 있는, 그래서 가장 건드리기 어려운 펌웨어 영역이다. Google이 Pixel 10 시리즈의 모뎀 펌웨어에 메모리 안전 언어를 도입한 건 이번이 처음이다. 구체적으로는 C로 작성되어 있던 DNS 파서를 Rust 기반 hickory-proto 라이브러리로 교체했다.

DNS 파서 하나 바꾼 게 뭐 그리 대단한가 싶을 수 있다. 나도 처음엔 그랬다. 그런데 맥락을 알고 나면 이야기가 달라진다.

전화번호만 알면 뚫리는 곳

베이스밴드 모뎀은 스마트폰에서 셀룰러 통신을 담당하는 프로세서다. 통화, 문자, 모바일 데이터 — 무선 네트워크와 관련된 모든 것이 이 칩을 통과한다. 문제는 이 모뎀의 펌웨어가 수십 메가바이트에 달하는 거대한 C/C++ 코드베이스라는 점이다. 그리고 이 코드가 외부 네트워크에서 오는 데이터를 직접 파싱한다.

2023년 3월, Google의 Project Zero 팀이 Samsung Exynos 모뎀에서 18개의 제로데이 취약점을 발견했다. 그중 4개(CVE-2023-24033, CVE-2023-26496, CVE-2023-26497, CVE-2023-26498)는 인터넷을 통한 베이스밴드 원격 코드 실행(RCE)이 가능한 치명적인 것이었다. 공격자가 필요한 건 딱 하나, 피해자의 전화번호뿐이었다. 사용자 상호작용도 필요 없었다. Pixel 6, Pixel 7 시리즈가 영향을 받았고, 같은 Exynos 칩셋을 사용하는 Samsung과 Vivo 기기도 마찬가지였다.

보안 컨퍼런스에서 베이스밴드 해킹은 이미 하나의 분야가 됐다. OffensiveCon 2025에서는 베이스밴드 펌웨어 에뮬레이션과 익스플로잇 기법을 가르치는 트레이닝 세션이 열렸다. 스파이웨어 업체들도 이 영역을 노린다. 제로데이 익스플로잇으로 Predator 같은 악성코드를 배포한 사례가 보고되고 있다.

Google은 Pixel 9에서 메모리 안전 취약점에 대한 다양한 완화 조치를 도입했다. 하지만 완화(mitigation)는 익스플로잇을 어렵게 만들 뿐, 취약점 자체를 없애지는 못한다. Pixel 10에서 Google이 택한 전략은 다르다. 아예 메모리 안전한 언어로 위험 코드를 다시 작성하는 것이다.

왜 하필 DNS 파서인가

전화를 걸 때 DNS가 동작한다는 사실을 아는 사람은 많지 않다. 현대 셀룰러 통신은 디지털 데이터 네트워크로 전환됐고, 착신 전환 같은 기본적인 동작조차 DNS 서비스에 의존한다. DNS는 복잡한 프로토콜이고, 외부에서 들어오는 신뢰할 수 없는 데이터를 파싱해야 한다.

이게 C로 구현되면 위험하다. 실제로 2024년에 공개된 CVE-2024-27227은 Android 모뎀의 DNS 클라이언트(Lassen)에서 발견된 취약점이다. 악의적인 DNS 응답으로 OOB(Out-of-Bounds) 읽기/쓰기를 비롯한 여러 메모리 문제를 트리거할 수 있었다. 심각도가 높은(High) 취약점으로 분류됐다.

DNS 파서는 공격 표면이 명확하고, 외부 입력을 직접 처리하며, 메모리 안전 취약점의 역사가 있는 코드다. Rust로 교체하기에 이상적인 후보였다.

hickory-proto를 선택한 이유

Google은 오픈소스 Rust DNS 크레이트를 여러 개 평가한 뒤 hickory-proto를 골랐다. 선택 기준은 실용적이었다. 유지보수가 활발하고, 테스트 커버리지가 75% 이상이며, Rust 커뮤니티에서 널리 사용되고 있다는 점이 결정적이었다. 장기 지원 가능성도 중요했다.

다만 hickory-proto에는 큰 문제가 하나 있었다. no_std 지원이 없었다. 베어메탈 환경인 모뎀 펌웨어에서 Rust 표준 라이브러리의 OS 의존 기능은 쓸 수 없다. Google은 hickory-proto와 그 의존성들에 no_std 지원을 직접 추가해서 업스트림에 기여했다.

이 PR들의 부산물로 no_std URL 파서도 생겼다. 모뎀 프로젝트에서 시작된 작업이 다른 임베디드 프로젝트에도 혜택을 주는 셈이다.

빌드 시스템 통합: 예상보다 어려운 여정

코드를 작성하는 것과 기존 펌웨어에 통합하는 것은 전혀 다른 문제다. Pixel 모뎀 펌웨어는 Google의 임베디드 라이브러리 프레임워크인 Pigweed를 빌드 시스템으로 사용한다. Pigweed는 GN을 통해 Rust 타겟을 지원하고, rustc를 직접 호출한다.

Google은 두 가지 접근법을 검토했다. 첫 번째는 cargo로 staticlib을 먼저 빌드한 뒤 링킹 단계에 넣는 방법, 두 번째는 rustc를 직접 호출해서 기존 빌드 시스템에 Rust 컴파일 단계를 통합하는 방법이었다. 첫 번째 방식은 Rust 컴포넌트가 늘어나면 중복 심볼 에러가 발생할 수 있어 확장성이 떨어진다. Google은 두 번째 방식을 선택했다.

모든 Rust 크레이트(hickory-proto, 의존성들, core, compiler_builtins, alloc)를 rlib으로 컴파일하고, 최종적으로 하나의 staticlib 타겟으로 묶은 뒤, llvm-ar -x로 오브젝트 파일을 추출해서 기존 C/C++ 링커에 전달하는 구조다.

여기서 예상치 못한 문제가 터졌다. compiler_builtins 크레이트에 정의된 memsetmemcpy의 weak 심볼이 모뎀 펌웨어의 최적화된 구현을 덮어쓴 것이다. 양쪽 다 weak 심볼이라 링커가 어느 쪽을 우선해야 할지 판단하지 못했다. 결과는 전력 소비와 성능의 회귀(regression)였다. 해결 방법은 의외로 단순했다.

bash
llvm-ar -t <rust staticlib> | grep compiler_builtins | xargs llvm-ar -d <rust staticlib>
bash
llvm-ar -t <rust staticlib> | grep compiler_builtins | xargs llvm-ar -d <rust staticlib>

링킹 전에 compiler_builtins의 오브젝트를 아카이브에서 제거하는 한 줄짜리 스크립트였다. 이런 종류의 삽질은 임베디드 Rust 통합에서 흔히 마주치는 문제다. 문서화되지 않은 함정이 빌드 시스템 깊은 곳에 숨어 있다.

서드파티 크레이트 빌드에는 Fuchsiacargo-gnaw를 활용했다. cargo metadata로 의존성을 해석한 뒤 GN 빌드 규칙을 자동 생성하는 도구다. hickory-proto는 기능 플래그를 다 꺼도 30개 이상의 의존 크레이트를 가져오기 때문에, 수동으로 빌드 규칙을 작성하면 유지보수가 불가능에 가깝다.

371KB의 의미

최종 코드 크기를 보면 흥미롭다.

구성 요소크기
Rust 구현 shim (DNS 응답 수신 시 hickory-proto 호출)4KB
core, alloc, compiler_builtins (재사용 가능, 일회성 비용)17KB
hickory-proto 라이브러리와 의존성350KB
합계371KB

371KB. hickory-proto는 임베디드용으로 설계된 라이브러리가 아니라서 크기가 최적화되어 있지 않다. Pixel 모뎀은 메모리가 넉넉한 편이라 문제없지만, 다른 임베디드 시스템에서는 이 크기가 장벽이 될 수 있다. Google은 이후 기능 플래그를 추가해서 필요한 기능만 조건부 컴파일하는 작업이 필요할 거라고 인정했다.

하지만 core, alloc, compiler_builtins의 17KB는 한 번만 지불하면 되는 비용이다. 두 번째, 세 번째 Rust 컴포넌트를 추가할 때는 이 오버헤드가 사라진다. 진입 장벽은 높지만, 일단 들어가면 확장 비용은 낮아진다.

더 큰 그림: Android에서 Rust가 만들어낸 성과

Pixel 10의 모뎀 Rust 도입은 고립된 실험이 아니다. Google은 Android 전체에서 Rust를 공격적으로 채택해왔고, 그 결과가 숫자로 나타나고 있다.

2019년 Android 취약점의 76%가 메모리 안전 문제였다. 2024년에는 이 비율이 24%로 떨어졌다. 2025년에는 처음으로 메모리 안전 취약점 비율이 전체의 20% 아래로 내려갔다. 같은 해, Android에 추가된 Rust 코드의 양이 C++ 코드를 처음으로 넘어섰다. Rust 코드의 메모리 안전 취약점 밀도는 C/C++ 대비 1000분의 1 수준이라고 Google은 밝혔다.

그런데 중요한 건 이 숫자가 앱 레벨에서만 나온 결과가 아니라는 점이다. 커널, 펌웨어, 저수준 시스템 컴포넌트까지 Rust가 침투하고 있다. Pixel 10의 모뎀은 그 최전선에 있다.

이걸 보고 든 생각

솔직히 이 포스트를 읽으면서 가장 인상 깊었던 건 DNS 파서를 Rust로 바꿨다는 사실 자체가 아니다. 베이스밴드 모뎀이라는, 하드웨어에 밀착된 레거시 C 코드베이스에 Rust를 실제로 통합해서 프로덕션에 올렸다는 과정이다.

no_std 지원을 위해 오픈소스에 직접 기여하고, weak 심볼 충돌이라는 예상 밖의 문제를 디버깅하고, 30개 넘는 의존 크레이트의 빌드 규칙을 자동화하는 — 이런 작업들이 하나하나 쌓여야 겨우 DNS 파서 하나를 교체할 수 있었다. 임베디드 Rust의 현실은 "Rust로 다시 쓰면 안전하다"라는 한 문장으로 요약되지 않는다.

그래도 방향은 분명하다. Google이 2024년 9월에 발표한 "Deploying Rust in Existing Firmware Codebases"는 방법론이었다. 이번 포스트는 그 방법론의 첫 번째 실전 적용 결과다. 그리고 한 번 corealloc이 모뎀에 들어간 이상, 다음 Rust 컴포넌트의 진입 비용은 훨씬 낮다.

다음에 Google이 모뎀에서 Rust로 교체할 대상이 무엇일지가 궁금하다. RTP 파서? SIP 스택? 아니면 NAS(Non-Access Stratum) 프로토콜 처리? 어떤 것이든, 371KB짜리 DNS 파서가 그 시작점이 된 셈이다.

참고 자료

YouTube 영상

채널 보기
스칼라 곱셈과 내적의 기하학적 의미 | 선형대수학
내적의 기하학적 의미와 코사인 유사도 원리 | 선형대수학
Trie 자료구조 파이썬 구현: Search와 Starts With 연산 | Trie 자료구조 이야기
7편, 파이썬으로 구현하는 B-Tree
13편, 인덱스가 많으면 왜 느려질까? 쓰기 증폭과 인덱스 튜닝의 이해
마지막편, 트라이 노드를 50% 이상 줄이는 방법? 압축 트라이 성능 분석 | Trie 자료구조 이야기
마지막편, 10억 개 데이터 검색이 0.3ms면 끝나는 이유와 LSM-Tree의 등장
숫자 하나가 AI 모델의 운명을 바꾼다? | 선형대수학