Rust 커널 프로그래밍 가이드 (1) — 커널이 Rust를 받아들인 이유와 최소 모듈 작성

리눅스 커널 CVE 목록을 오래 들여다본 사람이라면 use-after-free, 버퍼 오버플로우, 널 포인터 역참조 같은 메모리 안전성 버그가 얼마나 자주 반복되는지 알 것이다. 리눅스 안정 커널 메인테이너 그렉 크로아-하트만은 지난 25년간의 커널 CVE 중 약 80%가 메모리 안전 언어였다면 애초에 존재하지 않았을 버그라고 평가한 바 있다. Rust for Linux는 커널 전체를 다시 쓰자는 게 아니라, 새로 작성하는 드라이버 코드부터 이런 종류의 버그를 언어 차원에서 막아보자는 프로젝트다. 2022년 12월 커널 6.1에 실험적 지원으로 처음 들어온 이후 4년 가까이 지난 지금, 2026년 7월 15일 크로아-하트만은 뭄바이 Open Source Summit India에서 “커널 프로젝트 안에서 Rust는 더 이상 실험이 아니다. 영구적으로 자리잡았다”고 공식 선언했다. 이 글에서는 Rust for Linux가 어떤 방식으로 동작하는지, 지금까지 어디까지 왔는지, 그리고 실제 최소 Rust 커널 모듈을 어떻게 빌드하는지 정리한다.

Rust for Linux란

핵심 아이디어는 커널의 C API와 직접 맞닿는 부분을 전부 unsafe로 감싸 신중하게 검증된 추상화 계층(kernel crate) 안에 가둬버리는 것이다. 이 추상화가 제대로 되어 있다면, 그 위에서 실제 드라이버를 작성하는 개발자는 unsafe 키워드를 한 번도 쓰지 않고도 코드를 완성할 수 있다. C 헤더에서 Rust가 호출할 수 있는 raw 바인딩은 bindgen이 자동으로 생성해주고, 인라인 함수나 복잡한 매크로처럼 bindgen이 다루지 못하는 부분은 rust/helpers/에 작은 wrapper 함수를 추가해 노출한다. 기존 C 코드를 Rust로 재작성할 계획은 없고, 새 코드 — 주로 드라이버 — 를 Rust로 작성할 수 있는 선택지를 여는 데 초점이 맞춰져 있다.

지금까지의 진행 상황

  • 2022년 12월, 커널 6.1: Rust 실험적 지원 최초 병합.
  • 2024년 초, 커널 6.8: 첫 Rust 드라이버인 ASIX AX88772A 네트워크 PHY 드라이버와 PHY 추상화 계층이 메인라인에 들어감.
  • 커널 6.12 기반 Android 16: 구글의 ashmem(익명 공유 메모리) 서브시스템이 Rust로 포함됨.
  • 2025년 10월: 안드로이드 프로세스 간 통신(IPC)의 핵심인 Binder 드라이버의 Rust 구현이 메인라인에 병합됨. 기존 C 구현을 완전히 대체하는 게 목표다.
  • GPIO 서브시스템: 핀 컨트롤 조작을 위한 Rust 추상화가 마련됨.
  • 그래픽스 서브시스템: 신규 드라이버는 이제 Rust로만 받는다는 방침이 정해짐.
  • 2026년 7월 15일: 커널 7.0을 앞두고 “실험적” 딱지가 공식적으로 떨어짐.

개발 환경 준비

커널 소스 트리에서 빌드 요구사항이 충족됐는지부터 확인한다. Kconfig가 Rust 지원 여부를 판단할 때 쓰는 것과 같은 로직으로 검사해준다.

make LLVM=1 rustavailable

rustup은 우분투 apt 저장소의 rustc/cargo 패키지와는 다른 도구다. apt로 받을 수 있는 rustc는 우분투 배포 주기에 맞춰 고정된 버전 하나만 설치되는 반면, rustup은 여러 toolchain(stable/beta/nightly, 버전별)을 내려받아 프로젝트마다 필요한 버전으로 전환할 수 있게 해주는 버전 관리자다. 커널 트리가 특정 rustc 버전을 요구하는 경우가 많아서, 커널 Rust 개발에는 apt의 rustc 대신 rustup 쪽을 쓰는 걸 권장한다. 우분투에서 rustup을 설치하는 방법은 두 가지다.

하나는 우분투가 제공하는 rustup snap 패키지를 쓰는 방법이다.

sudo snap install rustup --classic

별도 Ubuntu 24.04 LTS 테스트 머신에서 실행해보면 이렇게 끝난다.

rustup 1.29.0 from Canonical✓ installed

다만 snap으로 설치한 rustup에는 한 가지 제약이 있다. rustup install stable까지는 curl 스크립트로 설치했을 때와 동일하게 동작하지만, 완료 메시지 끝에 다음 안내가 붙는다.

info: self-update is disabled for this build of rustup
info: any updates to rustup will need to be fetched with your system package manager

즉 snap 버전은 rustup self update가 막혀 있어서, rustup 자체를 최신 버전으로 올리려면 sudo snap refresh rustup을 따로 실행해야 한다. curl 스크립트로 설치한 쪽은 rustup self update가 그대로 동작하므로, rustup 자체의 업데이트 주기를 스스로 관리하고 싶다면 curl 스크립트 쪽이 조금 더 예측 가능하다.

다른 하나는 배포판에 상관없이 동일하게 동작하는, rust-lang.org가 직접 제공하는 설치 스크립트를 쓰는 방법이다.

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

실제로 Ubuntu 24.04 LTS에서 curl 스크립트로 설치해보면 다음과 같이 진행된다.

info: downloading installer
info: profile set to default
info: default host triple is x86_64-unknown-linux-gnu
info: syncing channel updates for stable-x86_64-unknown-linux-gnu
info: downloading 6 components

  stable-x86_64-unknown-linux-gnu installed - rustc 1.97.1 (8bab26f4f 2026-07-14)

Rust is installed now. Great!

둘 중 어느 쪽으로 설치하든 이후 사용법은 동일하다. 설치가 끝나면 새 셸을 열거나 source "$HOME/.cargo/env"를 실행해 PATH를 반영한 뒤, 필요한 컴포넌트를 rustup으로 준비한다.

rustup override set stable
rustup component add rust-src

여기까지는 문제없이 끝나지만, bindgen 자체를 설치하기 전에 libclang부터 챙겨야 한다. libclang은 bindgen이 C 헤더를 해석하는 데 쓰는 라이브러리인데, apt로 rustc/cargo만 준비된 상태에서 곧바로 cargo install --locked bindgen-cli를 실행하면 설치 자체는 성공한다. 문제는 그 다음이다. libclang 없이 실제로 bindgen을 실행하면 이렇게 실패한다.

$ bindgen test.h -o test_bindings.rs
thread 'main' panicked at ...:
Unable to find libclang: "couldn't find any valid shared libraries matching:
['libclang.so', 'libclang-*.so', 'libclang.so.*', 'libclang-*.so.*'],
set the `LIBCLANG_PATH` environment variable to a path where
one of these files can be found (invalid: [])"

이 에러가 재현되는지는 환경마다 다르다는 걸 두 대의 테스트 머신에서 직접 확인했다. libclang이 전혀 없는 최소 설치 환경에서는 위 에러가 그대로 났지만, 다른 개발 도구를 이것저것 설치해둔 머신에서는 libclang-dev 없이도 libclang1-18(런타임 공유 라이브러리 패키지)만으로 bindgen이 바로 동작했다. clang-sys가 찾는 패턴(libclang-*.so.* 등)이 버전이 붙은 libclang-18.so.1 같은 파일도 매치하기 때문이다. 즉 핵심은 “libclang-dev를 반드시 설치해야 한다”가 아니라 “libclang 공유 라이브러리가 어떤 경로로든 시스템에 있어야 한다”는 것이다. 어느 쪽인지 매번 확인하기 번거로우니, 확실하게 하려면 Ubuntu 24.04(noble) 기준 libclang-dev 메타패키지를 설치해두는 게 가장 안전하다. 내부적으로 libclang-18-dev에 의존한다.

sudo apt install libclang-dev

libclang이 준비된 뒤에 bindgen-cli를 설치한다.

cargo install --locked bindgen-cli

커널을 LLVM=1로 빌드할 때 쓰는 clang/llvm 툴체인도 결국 같은 libclang을 공유하므로, 커널 소스 쪽 요구사항까지 맞추려면 clang 패키지도 함께 설치해두는 게 안전하다. 조건이 갖춰졌다면 커널 설정에서 CONFIG_RUST를 켜고 다음으로 빌드한다.

make LLVM=1

최소 Rust 커널 모듈 — rust_minimal.rs

커널 소스 트리의 samples/rust/rust_minimal.rs는 가장 짧은 형태의 Rust 커널 모듈 예제다.

// SPDX-License-Identifier: GPL-2.0

//! Rust minimal sample.

use kernel::prelude::*;

module! {
    type: RustMinimal,
    name: "rust_minimal",
    authors: ["Rust for Linux Contributors"],
    description: "Rust minimal sample",
    license: "GPL",
    params: {
        test_parameter: i64 {
            default: 1,
            description: "This parameter has a default of 1",
        },
    },
}

struct RustMinimal {
    numbers: KVec<i32>,
}

impl kernel::Module for RustMinimal {
    fn init(_module: &'static ThisModule) -> Result<Self> {
        pr_info!("Rust minimal sample (init)\n");
        pr_info!("Am I built-in? {}\n", !cfg!(MODULE));
        pr_info!(
            "test_parameter: {}\n",
            *module_parameters::test_parameter.value()
        );

        let mut numbers = KVec::new();
        numbers.push(72, GFP_KERNEL)?;
        numbers.push(108, GFP_KERNEL)?;
        numbers.push(200, GFP_KERNEL)?;

        Ok(RustMinimal { numbers })
    }
}

impl Drop for RustMinimal {
    fn drop(&mut self) {
        pr_info!("My numbers are {:?}\n", self.numbers);
        pr_info!("Rust minimal sample (exit)\n");
    }
}

module! 매크로가 C의 module_init()/module_exit(), MODULE_LICENSE() 같은 매크로들을 한 번에 대체한다. 모듈이 로드될 때는 kernel::Module 트레이트의 init()이 호출되는데, 여기서 KVec(커널 전용 할당자를 쓰는 벡터 타입)에 값을 채워 넣고 RustMinimal 인스턴스를 반환한다. numbers.push(72, GFP_KERNEL)?처럼 커널 메모리 할당 함수마다 실패 가능성을 Result로 명시적으로 다루게 강제되는 점이 C와 가장 다른 부분이다. 모듈이 언로드될 때는 Drop 트레이트의 drop()이 자동으로 호출되어 정리 작업을 하는데, C처럼 별도의 exit 함수에서 해제를 깜빡하는 실수를 구조적으로 줄여준다.

Rust도 완벽한 방패는 아니다 — CVE-2025-68260

2025년 12월 16일, Rust로 작성된 커널 코드에 처음으로 CVE가 할당됐다. 대상은 앞서 언급한 Rust Binder 드라이버였다. drivers/android/binder/node.rs의 death_list(프로세스가 죽었을 때 알림을 등록해두는 연결 리스트) 처리 로직에서, 동기화 없이 리스트 포인터를 조작하는 unsafe 코드가 매우 드문 동시성 상황에서 리스트를 손상시켜 커널 패닉을 유발할 수 있는 레이스 컨디션이었다. 이 unsafe 블록은 “이 포인터는 이 락을 쥐고 있는 동안에만 유효하다”, “이 객체는 리스트에 있는 동안 해제될 수 없다” 같은 불변조건(invariant)을 사람이 직접 보증해야 하는 코드였는데, 그 보증이 특정 동시성 인터리빙에서 깨졌다. 커널 6.18.1과 6.19-rc1에서 패치됐다.

이 사례는 Rust가 메모리 안전성 버그를 “구조적으로 줄여준다”는 것과 “완전히 없애준다”는 것이 다른 얘기라는 걸 보여준다. 추상화 계층 바깥으로 나가 unsafe를 직접 쓰는 순간부터는, 그 블록 안의 불변조건을 지키는 책임은 결국 사람에게 돌아온다.

주의사항

  • 커널 전체가 Rust로 옮겨가는 게 아니다. 기존 C 코드를 재작성할 계획은 없고, 새로 작성하는 드라이버 위주로 선택지가 열려 있는 상태다. 여전히 커널 대부분은 C다.
  • 그래픽스 서브시스템처럼 신규 드라이버에 Rust를 사실상 강제하는 곳이 있는 반면, 대부분의 서브시스템은 아직 C가 기본이라 프로젝트마다 정책이 다르다는 점을 확인하고 시작해야 한다.
  • 안전한 추상화 계층으로 감싸지 못해 unsafe를 직접 써야 하는 지점은 여전히 존재하며, CVE-2025-68260처럼 그 안의 불변조건이 깨지면 C와 똑같이 메모리 손상이나 패닉으로 이어질 수 있다.
  • 커널 트리가 요구하는 rustc/rust-src 버전과 로컬에 설치된 rustup 툴체인 버전이 어긋나면 make LLVM=1 rustavailable부터 실패하므로, 커널 버전에 맞는 툴체인 버전을 문서에서 먼저 확인하는 게 좋다.

마무리

Rust for Linux는 “커널을 Rust로 다시 쓰자”가 아니라 “새로 짜는 코드부터는 언어가 흔한 버그 클래스를 미리 막아주게 하자”는 실용적인 접근에 가깝다. 4년 가까운 실험 기간을 거쳐 그래픽스·Binder 같은 실제 서브시스템에 자리잡았고, 이제는 실험 딱지까지 뗀 만큼 앞으로 더 많은 서브시스템에서 선택지로 등장할 가능성이 크다. 다만 CVE-2025-68260이 보여주듯 unsafe 경계 안쪽은 여전히 사람의 몫이라, “Rust라서 안전하다”보다는 “안전한 추상화가 어디까지 감싸주는지”를 따져보는 태도가 더 정확하다.

참고

답글 남기기