커널 하드닝 옵션 randstruct와 Rust는 지금까지 한 커널에 함께 들어갈 수 없었습니다. Kconfig에 depends on !RANDSTRUCT가 박혀 있어 둘 중 하나를 켜면 다른 하나가 조용히 꺼집니다. 문제는 이 규칙이 커널의 표준 커버리지 빌드인 allmodconfig에서 randstruct 쪽 손을 들어주고 있었다는 점입니다. Rust 툴체인이 멀쩡히 설치된 머신에서도 allmodconfig가 Rust 코드를 한 줄도 빌드하지 않고 지나갔습니다.
마크 브라운(Mark Brown)이 9월 1일 올린 패치가 이 순서를 뒤집었습니다. Rust 툴체인이 쓸 수 있는 상태면 randstruct의 기본값을 끄도록 바꾼 한 줄짜리 Kconfig 수정으로, 키스 쿡(Kees Cook)이 같은 날 hardening 브랜치에 반영했고 9월 2일 메인라인에 들어가 7.3-rc2에 포함됐습니다. Phoronix는 이를 “기본 커널 설정이 randstruct를 잃는다”고 보도했는데, 실제로 영향을 받는 범위는 그보다 좁습니다. 로컬에서 커널 6.18.38 트리로 패치 전후를 직접 빌드해 어디까지가 사실인지 확인했습니다.
바뀐 것은 Kconfig 한 줄
패치 본문은 security/Kconfig.hardening의 default 조건 한 줄입니다.
choice
prompt "Randomize layout of sensitive kernel structures"
- default RANDSTRUCT_FULL if COMPILE_TEST && (GCC_PLUGINS || CC_HAS_RANDSTRUCT)
+ default RANDSTRUCT_FULL if !(RUST_IS_AVAILABLE && HAVE_RUST) && COMPILE_TEST && (GCC_PLUGINS || CC_HAS_RANDSTRUCT)
default RANDSTRUCT_NONE여기서 눈여겨볼 것은 원래 조건에도 이미 COMPILE_TEST가 붙어 있었다는 사실입니다. randstruct가 기본으로 켜지던 것은 처음부터 COMPILE_TEST=y인 빌드, 즉 allmodconfig와 allyesconfig 같은 커버리지 빌드에 한정된 이야기였습니다. 마크 브라운은 커밋 메시지에서 !RUST를 직접 참조하면 순환 의존이 생기기 때문에 툴체인 가용성을 나타내는 RUST_IS_AVAILABLE과 아키텍처 지원을 나타내는 HAVE_RUST로 우회했다고 밝혔습니다. 그 결과 Rust와 randstruct가 둘 다 꺼진 설정이 나올 수도 있지만, 그쪽이 기대에 가까운 결과라는 판단입니다.
allmodconfig에서 Rust가 통째로 빠지는 과정
패치 전 상태를 그대로 가진 6.18.38 트리로 재현했습니다. GCC 플러그인 헤더(gcc-13-plugin-dev)와 Rust 툴체인이 모두 설치된 환경이 조건입니다.
$ make kernelversion
6.18.38
$ grep -n -A2 'prompt "Randomize layout' security/Kconfig.hardening
287: prompt "Randomize layout of sensitive kernel structures"
288- default RANDSTRUCT_FULL if COMPILE_TEST && (GCC_PLUGINS || CC_HAS_RANDSTRUCT)
289- default RANDSTRUCT_NONE
$ grep -n 'RANDSTRUCT' init/Kconfig
2093: depends on !GCC_PLUGIN_RANDSTRUCT
2094: depends on !RANDSTRUCT
2093~2094번 줄이 config RUST 블록 안에 있는 의존성입니다. 이제 allmodconfig를 돌려봅니다.
$ make O=/tmp/before allmodconfig
#
# configuration written to .config
#
$ grep -E 'RANDSTRUCT|^CONFIG_RUST=' /tmp/before/.config
# CONFIG_RANDSTRUCT_NONE is not set
CONFIG_RANDSTRUCT_FULL=y
# CONFIG_RANDSTRUCT_PERFORMANCE is not set
CONFIG_RANDSTRUCT=y
CONFIG_GCC_PLUGIN_RANDSTRUCT=y
CONFIG_RANDSTRUCT_KUNIT_TEST=m
$ grep -E '^CONFIG_(RUST_IS_AVAILABLE|HAVE_RUST)' /tmp/before/.config
CONFIG_RUST_IS_AVAILABLE=y
CONFIG_HAVE_RUST=y
CONFIG_RUST=y가 아예 출력되지 않습니다. Rust 툴체인이 쓸 수 있다고 판정됐는데도(RUST_IS_AVAILABLE=y) 의존성이 깨져 CONFIG_RUST 심볼 자체가 .config에 기록되지 않은 상태입니다. “off”라고 표시조차 되지 않으니 빌드 로그만 봐서는 Rust가 빠졌다는 사실을 알아채기 어렵습니다.
같은 트리에 위 한 줄 패치를 적용하고 다시 돌리면 결과가 뒤집힙니다.
$ grep -E 'RANDSTRUCT|^CONFIG_RUST=' /tmp/after/.config
CONFIG_RUST=y
CONFIG_RANDSTRUCT_NONE=y
# CONFIG_RANDSTRUCT_FULL is not set
# CONFIG_RANDSTRUCT_PERFORMANCE is not set
CONFIG_RANDSTRUCT_KUNIT_TEST=m
$ diff <(grep -E '=(y|m)$' /tmp/before/.config | sort) \
<(grep -E '=(y|m)$' /tmp/after/.config | sort) \
| grep '^>' | grep -i rust | grep -vi trust | wc -l
22
패치 하나로 커버리지 빌드에 Rust 심볼 22개가 새로 들어왔고, 그중 14개는 실제로 컴파일되는 모듈입니다. rnull 블록 드라이버(BLK_DEV_RUST_NULL), Rust로 작성된 cpufreq DT 드라이버(CPUFREQ_DT_RUST), PCI·플랫폼·misc device 샘플 드라이버가 모두 여기 포함됩니다. 그동안 allmodconfig 기반 CI가 이 코드들을 한 번도 컴파일하지 않고 통과시켜 왔다는 뜻입니다.
| 빌드 타깃 | 패치 전 | 패치 후 |
|---|---|---|
allmodconfig | RANDSTRUCT_FULL=y, Rust 없음 | RANDSTRUCT_NONE=y, CONFIG_RUST=y |
defconfig | RANDSTRUCT_NONE=y | RANDSTRUCT_NONE=y (변화 없음) |
| 배포판 커널 설정 | 직접 지정한 값이 그대로 유지됨 (default 조건과 무관) | |
defconfig 사용자는 잃을 것이 없습니다
“기본 설정에서 하드닝이 빠진다”는 표현 때문에 일반 빌드가 약해진다고 읽히기 쉽지만, 같은 트리에서 defconfig를 뽑아보면 패치 전에도 randstruct는 꺼져 있습니다.
$ make O=/tmp/def defconfig
$ grep -E 'RANDSTRUCT|COMPILE_TEST' /tmp/def/.config
# CONFIG_COMPILE_TEST is not set
CONFIG_RANDSTRUCT_NONE=y
# CONFIG_RANDSTRUCT_FULL is not set
# CONFIG_RANDSTRUCT_PERFORMANCE is not set
COMPILE_TEST가 꺼진 설정에서는 애초에 default 조건이 성립하지 않아 RANDSTRUCT_NONE으로 떨어집니다. 배포판 커널도 마찬가지로 자기 설정 파일에 값을 명시하므로 이번 변경의 영향을 받지 않습니다. 정확히 말하면 이번 패치는 “하드닝을 끈” 것이 아니라 “커버리지 빌드에서 randstruct와 Rust 중 무엇을 우선할지”를 바꾼 것입니다.
다만 KSPP(Kernel Self-Protection Project)가 CONFIG_RANDSTRUCT_FULL=y를 권장 설정으로 올려두고 있다는 점은 짚어둘 만합니다. KSPP 권고를 그대로 따르는 하드닝 커널을 빌드한다면 Rust 드라이버는 여전히 하나도 쓸 수 없습니다. 이번 패치는 그 배타 관계 자체를 푼 것이 아닙니다.
randstruct와 Rust가 함께 못 켜지는 이유
randstruct는 컴파일 시점에 구조체 필드 순서를 섞습니다. 대상은 __randomize_layout으로 표시된 구조체와, RANDSTRUCT_FULL에서 추가로 잡히는 “전부 함수 포인터로만 이뤄진 구조체”입니다. 6.18 트리 기준으로 __randomize_layout 표시는 77곳에 있고, struct file_operations처럼 함수 포인터만 모인 구조체가 자동으로 섞이는 쪽에 해당합니다.
문제는 Rust 쪽 바인딩을 만드는 bindgen이 C 코드를 libclang으로 파싱한다는 점입니다. GCC 플러그인이 적용한 필드 순서 랜덤화는 libclang 입장에서 보이지 않으므로, C 쪽에서 섞인 오프셋과 Rust 쪽에서 계산한 오프셋이 어긋납니다. 커널 트리의 rust/Makefile에 이 한계가 주석으로 남아 있습니다.
# bindgen relies on libclang to parse C. Ideally, bindgen would support a GCC
# plugin backend and/or the Clang driver would be perfectly compatible with GCC.
#
# For the moment, here we are tweaking the flags on the fly. This is a hack,
# and some kernel configurations may not work (e.g. `GCC_PLUGIN_RANDSTRUCT`
# if we end up using one of those structs).Rust 쪽에서 이 구조체들을 실제로 건드립니다. 예를 들어 rust/kernel/miscdevice.rs는 bindings::file_operations 상수를 직접 만들어 vtable로 넘깁니다. 오프셋이 어긋나면 잘못된 필드에 함수 포인터를 꽂는 셈이라 컴파일 에러도 없이 런타임에 무너집니다. Kconfig에서 아예 depends on !RANDSTRUCT로 막아둔 이유가 여기 있습니다.
Clang 네이티브 randstruct라는 출구
randstruct 구현은 두 가지입니다. GCC 플러그인 방식(GCC_PLUGIN_RANDSTRUCT)과 Clang 16 이상이 지원하는 -frandomize-layout-seed-file 방식(CC_HAS_RANDSTRUCT)입니다. 후자라면 bindgen이 쓰는 libclang이 같은 랜덤화 정보를 그대로 물려받으므로 원리상 오프셋이 어긋날 이유가 없습니다.
미겔 오헤다(Miguel Ojeda)가 3월에 이 점을 파고든 패치를 올렸습니다. 의존성을 depends on !RANDSTRUCT || CC_IS_CLANG으로 완화해 Clang 빌드에 한해 둘을 함께 켜자는 제안입니다. CONFIG_RANDSTRUCT가 켜지면 task_struct의 flags·mm·group_leader 같은 필드가 익명 구조체(__bindgen_anon_1) 안으로 들어가기 때문에 Rust 쪽 접근 코드를 #[cfg]로 분기시키는 작업이 함께 들어갔고, 안드레아스 힌드보르그(Andreas Hindborg)가 rnull 드라이버로 120개 워크로드를 측정해 통계적으로 유의미한 성능 변화가 없다고 보고했습니다.
다만 이 패치는 아직 메인라인에 들어가지 않았습니다. 9월 10일 기준 torvalds 트리의 init/Kconfig는 여전히 다음과 같습니다.
config RUST
bool "Rust support"
depends on HAVE_RUST
depends on RUST_IS_AVAILABLE
select EXTENDED_MODVERSIONS if MODVERSIONS
depends on !MODVERSIONS || GENDWARFKSYMS
depends on !GCC_PLUGIN_RANDSTRUCT
depends on !RANDSTRUCT
...
즉 이번 7.3 변경은 근본 해법이 도착할 때까지 커버리지 빌드만 임시로 구해내는 조치입니다. 마크 브라운도 커밋 메시지에서 “work around”라고 표현했습니다.
정리
헤드라인만 보면 커널이 하드닝 기능을 기본에서 내린 것처럼 읽히지만, 실제로 바뀐 것은 COMPILE_TEST 빌드의 우선순위입니다. defconfig로 커널을 빌드하는 개인 사용자나 자체 설정을 쓰는 배포판은 이번 변경으로 잃는 것이 없고, 반대로 Rust 드라이버 22개 심볼이 커버리지 빌드에 편입되면서 그동안 컴파일조차 되지 않던 코드가 CI 그물에 걸리게 됐습니다.
정작 남은 문제는 그대로입니다. randstruct와 Rust는 여전히 한 커널에 공존할 수 없고, KSPP 권장 설정을 따르는 하드닝 빌드는 Rust 드라이버를 포기해야 합니다. 이 선택을 없애려면 Clang 네이티브 randstruct를 허용하는 쪽 작업이 메인라인에 들어가야 합니다. 하드닝 옵션을 켜서 커널을 직접 빌드하고 있다면, 지금 자기 .config에서 CONFIG_RANDSTRUCT와 CONFIG_RUST가 어떤 상태인지 한 번 확인해 볼 만합니다.
참고
- Phoronix — Linux 7.3 Now Disabling RandStruct Security Feature By Default If Rust Support Present
- LKML — [PATCH v4] hardening: Default randstruct off with rust for better allmodconfig support
- LKML — [PATCH v2] rust: allow Clang-native RANDSTRUCT configs
- Kernel Self-Protection Project — Recommended Settings