9월 6일 공개된 리눅스 커널 7.3-rc2에는 기능 추가 없이 코드 모양만 바꾸는 대형 패치가 하나 들어갔습니다. 키스 쿡(Kees Cook)이 보낸 kmalloc_obj() 전환 풀 리퀘스트입니다. 304개 파일에서 637줄이 추가되고 726줄이 삭제됐습니다. 커널 곳곳의 kmalloc(sizeof(...), GFP_KERNEL) 호출을 타입을 인자로 받는 kmalloc_obj() 계열 매크로로 바꾼 것입니다.
이런 트리 전체 변경(treewide change)은 보통 머지 윈도우에 들어갑니다. 쿡도 "사이클 중간이라 늦었다면 7.4 머지 윈도우 둘째 주에 다시 보내겠다"고 적었습니다. 리누스 토발즈는 "이런 작업은 -rc1 직후 사이클 초반이 가장 좋다. 이번 주 초였으면 더 좋았겠지만 '이번 주'가 '다음 머지 윈도우'보다는 낫다"며 받아들였습니다. 이번 전환이 무엇을 바꾸는지, 왜 7.0에서 한 번 끝낸 작업을 다시 돌려야 했는지 패치를 직접 받아 확인했습니다.
kmalloc_obj()는 무엇인가
kmalloc_obj() 계열은 커널 7.0에 들어온 할당 매크로입니다. 크기 대신 할당할 타입(또는 변수)을 받고, 반환값도 void *가 아니라 그 타입의 포인터입니다. 이번 패치에서 실제로 바뀐 줄 몇 개를 옮기면 이렇습니다.
/* arch/loongarch/kvm/intc/dmsintc.c */
- s = kzalloc(sizeof(struct loongarch_dmsintc), GFP_KERNEL);
+ s = kzalloc_obj(struct loongarch_dmsintc);
/* drivers/base/property.c — kcalloc은 kzalloc_objs로 */
- const char **values __free(kfree) = kcalloc(nval, sizeof(*values), GFP_KERNEL);
+ const char **values __free(kfree) = kzalloc_objs(*values, nval);
/* GFP_KERNEL이 아닌 플래그는 그대로 남습니다 */
- src = kmalloc_array(src_nents, sizeof(*src), GFP_ATOMIC);
+ src = kmalloc_objs(*src, src_nents, GFP_ATOMIC);
/* fs/afs/dir.c — 가변 길이 배열(flexible array) 구조체 */
- symlink = kmalloc_flex(struct afs_symlink, content, clen + 1, GFP_KERNEL);
+ symlink = kmalloc_flex(struct afs_symlink, content, clen + 1);기존 함수와 새 매크로의 대응은 Coccinelle 스크립트 scripts/coccinelle/api/kmalloc_objs.cocci에 정의돼 있습니다.
| 기존 호출 | 바뀐 형태 |
|---|---|
kmalloc(sizeof(*p), gfp) | kmalloc_obj(*p, gfp) |
kzalloc(sizeof(struct foo), gfp) | kzalloc_obj(struct foo, gfp) |
kmalloc_array(n, sizeof(*p), gfp) | kmalloc_objs(*p, n, gfp) |
kcalloc(n, sizeof(*p), gfp) | kzalloc_objs(*p, n, gfp) |
kvmalloc_array / kvcalloc | kvmalloc_objs / kvzalloc_objs |
kmalloc(struct_size(p, fam, n), gfp) | kmalloc_flex(*p, fam, n, gfp) |
위 결과의 , GFP_KERNEL 인자 | 삭제 (생략 시 기본값이 GFP_KERNEL) |
sizeof, char 배열이나 정수 타입 버퍼, sizeof(void *) 할당은 스크립트가 일부러 건드리지 않습니다. 바이트 버퍼는 타입을 붙일 의미가 없고, void * 크기는 사람이 한 번 더 봐야 하기 때문입니다.
7.3-rc2 전환의 구성
패치(3a2c4d55e32a)를 내려받아 새 매크로가 등장한 줄과 사라진 기존 호출을 직접 세어봤습니다.
| 바뀐 형태 | 줄 수 | 원래 호출 | 줄 수 |
|---|---|---|---|
kzalloc_obj | 290 | kzalloc( | 243 |
kzalloc_objs | 139 | kcalloc( | 131 |
kvzalloc_objs | 35 | kvcalloc( | 34 |
kmalloc_obj | 32 | kmalloc_array( | 26 |
kmalloc_objs | 30 | kmalloc( | 24 |
kvmalloc_objs | 15 | kvmalloc_array( | 14 |
kzalloc_flex / kmalloc_flex | 14 / 9 | kvzalloc( | 4 |
kvzalloc_obj / kvzalloc_flex | 3 / 1 | 이미 _obj 형태, GFP_KERNEL만 삭제 | 92 |
대부분은 kzalloc과 kcalloc 변환입니다. 92줄은 이미 kmalloc_obj 계열을 쓰고 있었는데 GFP_KERNEL을 명시해 둔 코드였습니다. 7.0 이후 새로 작성된 코드에서도 옛 습관이 남아 있었다는 뜻입니다.
7.0에서 끝낸 작업을 다시 돌린 이유
원래 전환은 7.0-rc1에서 한 번 크게 이뤄졌습니다. 당시 풀 리퀘스트의 diffstat은 8,018개 파일, 20,062줄 추가, 20,914줄 삭제였습니다. 이번 7.3-rc2 패치는 그때 빠진 두 부류를 채웁니다.
- 7.0 이후 새로 들어온
kmalloc()계열 호출 cleanup.h의scoped_guard()계열 매크로를 쓰는 함수 안의 호출
두 번째가 핵심입니다. Coccinelle은 C를 파싱해 패턴을 찾는데, scoped_guard(lock, &x) { ... }처럼 함수 호출 뒤에 블록이 붙는 매크로를 문법으로 이해하지 못했습니다. 그래서 이런 매크로를 쓰는 함수 안의 할당은 첫 번째 전환에서 빠졌습니다. 이번 패치의 사운드 드라이버 부분이 그런 사례입니다.
/* sound/core/oss/mixer_oss.c — snd_mixer_oss_build_test() */
struct snd_ctl_elem_info *info __free(kfree) =
- kmalloc(sizeof(*info), GFP_KERNEL);
+ kmalloc_obj(*info);
if (!info)
return -ENOMEM;
scoped_guard(rwsem_read, &card->controls_rwsem) {쿡은 spatch의 --macro-file 옵션으로 scoped_...() 매크로들을 Coccinelle의 YACFE_ITERATOR로 선언해 우회했습니다. list_for_each_entry()처럼 뒤에 블록이 오는 반복 매크로를 알려주는 표시인데, 가장 가까운 제어 흐름 표시자라서 골랐다고 설명했습니다. 빌드 테스트는 x86, arm64, arm, loongarch, mips, powerpc, riscv, s390의 allmodconfig에서 새 경고 없이 통과했습니다.
매크로가 실제로 막아주는 것
include/linux/slab.h의 정의를 보면 핵심은 두 줄입니다. 크기를 size_mul()로 계산하고, 결과를 (TYPE *)로 캐스팅합니다.
#define __alloc_objs(KMALLOC, GFP, TYPE, COUNT) \
({ \
const size_t __obj_size = size_mul(sizeof(TYPE), COUNT); \
(TYPE *)KMALLOC(__obj_size, GFP); \
})
#define kmalloc_obj(VAR_OR_TYPE, ...) \
__alloc_objs(kmalloc, default_gfp(__VA_ARGS__), typeof(VAR_OR_TYPE), 1)
/* include/linux/gfp.h */
#define __default_gfp(a,b,...) b
#define default_gfp(...) __default_gfp(,##__VA_ARGS__,GFP_KERNEL)효과를 확인하려고 이 매크로들을 유저 공간으로 옮겨 kmalloc()을 크기를 출력하는 malloc() 래퍼로 바꾼 실험 코드를 만들었습니다. 빌드는 GCC 13.3에서 했습니다.
/* include/linux/slab.h, gfp.h, overflow.h의 매크로를 유저 공간으로 옮긴 실험 코드 */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
typedef unsigned int gfp_t;
#define GFP_KERNEL 0x0cc0U
#define GFP_ATOMIC 0x0800U
static void *kmalloc(size_t size, gfp_t gfp)
{
printf(" kmalloc(size=%zu, gfp=0x%x)\n", size, gfp);
return size > (1UL << 30) ? NULL : malloc(size);
}
static inline size_t size_mul(size_t a, size_t b)
{
size_t bytes;
if (__builtin_mul_overflow(a, b, &bytes))
return SIZE_MAX;
return bytes;
}
#define __default_gfp(a, b, ...) b
#define default_gfp(...) __default_gfp(, ##__VA_ARGS__, GFP_KERNEL)
#define __alloc_objs(KMALLOC, GFP, TYPE, COUNT) \
({ \
const size_t __obj_size = size_mul(sizeof(TYPE), COUNT); \
(TYPE *)KMALLOC(__obj_size, GFP); \
})
#define kmalloc_obj(VAR_OR_TYPE, ...) \
__alloc_objs(kmalloc, default_gfp(__VA_ARGS__), typeof(VAR_OR_TYPE), 1)
#define kmalloc_objs(VAR_OR_TYPE, COUNT, ...) \
__alloc_objs(kmalloc, default_gfp(__VA_ARGS__), typeof(VAR_OR_TYPE), COUNT)
struct hdr { uint32_t id; };
struct packet { uint32_t id; uint8_t payload[60]; };
int main(void)
{
puts("[1] kmalloc_obj(*pkt) / GFP 생략 / GFP_ATOMIC");
struct packet *pkt = kmalloc_obj(*pkt);
struct packet *pkt2 = kmalloc_obj(*pkt2, GFP_ATOMIC);
puts("[2] 개수 곱셈 오버플로");
size_t n = SIZE_MAX / 8 + 2;
uint64_t *a = kmalloc(n * sizeof(uint64_t), GFP_KERNEL);
uint64_t *b = kmalloc_objs(*b, n);
printf(" open-coded: %s, kmalloc_objs: %s\n", a ? "non-NULL" : "NULL", b ? "non-NULL" : "NULL");
#ifdef WRONG_TYPE
struct packet *p1 = kmalloc(sizeof(struct hdr), GFP_KERNEL);
struct packet *p2 = kmalloc_obj(struct hdr);
#endif
free(pkt); free(pkt2); free(a); free(b);
return 0;
}$ gcc -Wall -o kobj_demo kobj_demo.c && ./kobj_demo
[1] kmalloc_obj(*pkt) / GFP 생략 / GFP_ATOMIC
kmalloc(size=64, gfp=0xcc0)
kmalloc(size=64, gfp=0x800)
[2] 개수 곱셈 오버플로
kmalloc(size=8, gfp=0xcc0)
kmalloc(size=18446744073709551615, gfp=0xcc0)
open-coded: non-NULL, kmalloc_objs: NULL
GFP 플래그를 생략하면 GFP_KERNEL(0xcc0)이 들어가고, 개수 곱셈이 넘치면 size_mul()이 SIZE_MAX로 포화시켜 할당이 실패합니다. 반면 직접 곱한 n * sizeof(uint64_t)는 8바이트로 줄어든 채 할당에 성공했습니다. 원래의 kcalloc()/kmalloc_array()도 오버플로를 검사하므로, 이 차이는 곱셈을 손으로 쓰던 코드에서만 생깁니다.
타입 검사는 커널 빌드 옵션인 -Werror=incompatible-pointer-types를 켜고 잘못된 타입을 넘겨 확인했습니다.
struct packet *p1 = kmalloc(sizeof(struct hdr), GFP_KERNEL); /* 4바이트만 할당 */
struct packet *p2 = kmalloc_obj(struct hdr);$ gcc -Wall -Werror=incompatible-pointer-types -DWRONG_TYPE -o /dev/null kobj_demo.c
kobj_demo.c:28:1: error: initialization of ‘struct packet *’ from incompatible pointer type ‘struct hdr *’ [-Werror=incompatible-pointer-types]
kobj_demo.c:34:5: note: in expansion of macro ‘__alloc_objs’
kobj_demo.c:55:25: note: in expansion of macro ‘kmalloc_obj’
cc1: some warnings being treated as errors
kmalloc()은 void *를 돌려주므로 64바이트짜리 구조체에 4바이트를 할당해도 컴파일러가 아무 말도 하지 않습니다. kmalloc_obj()는 struct hdr *를 돌려주므로 같은 실수가 빌드 에러가 됩니다.
커널 코드를 작성하는 입장에서 달라지는 것
| 상황 | 영향 |
|---|---|
| 메인라인에 새 코드를 보낼 때 | kzalloc(sizeof(*p), GFP_KERNEL) 대신 kzalloc_obj(*p)를 쓰는 것이 트리 전체의 새 기준입니다. |
GFP_KERNEL 명시 | 생략하는 것이 관례가 됐습니다. GFP_ATOMIC, GFP_NOWAIT 같은 다른 플래그는 계속 적어야 합니다. |
| 7.3 이후 패치를 LTS(6.12, 6.18 등)로 백포트할 때 | 7.0 이전 커널에는 kmalloc_obj()가 없어 그대로 적용되지 않습니다. 할당 줄을 되돌리거나 호환 매크로가 필요합니다. |
| out-of-tree 모듈 | 여러 커널 버전을 지원한다면 LINUX_VERSION_CODE로 분기하거나 기존 kmalloc()을 유지하는 편이 안전합니다. |
scoped_guard()를 쓰는 코드에 Coccinelle을 돌릴 때 | 기본 설정으로는 함수가 파싱되지 않아 조용히 빠질 수 있습니다. --macro-file 설정이 필요합니다. |
kmalloc_obj()는 할당하는 타입 정보를 할당 지점에 남기므로, 이후 추가 하드닝이나 정렬(alignment)을 고려한 할당에 활용할 수 있다는 점도 도입 이유로 꼽힙니다. 이번 정리는 그 전제인 "커널의 구조체 할당은 모두 _obj 계열을 거친다"는 상태에 더 가까워지는 작업입니다.
정리
- 7.3-rc2에
kmalloc_obj()추가 전환이 머지됐습니다. 304개 파일, +637/-726줄 규모입니다. - 대상은 7.0 이후 새로 들어온 호출과,
scoped_guard()때문에 Coccinelle이 파싱하지 못해 빠졌던 호출입니다. _obj계열은 타입이 다른 포인터에 대입하면 빌드 에러를 내고, 개수 곱셈 오버플로를 포화시키며,GFP_KERNEL을 기본값으로 둡니다.- 리누스 토발즈는 사이클 중간이지만 다음 머지 윈도우까지 미루는 것보다 낫다며 받아들였습니다.
- 7.3 이후 커밋을 7.0 이전 LTS로 백포트할 때는 할당 줄이 충돌할 수 있습니다.
참고
- [[GIT PULL] kmalloc_obj() conversions for v7.3-rc2 — 키스 쿡의 풀 리퀘스트와 리누스 토발즈의 답장](https://ratatoskr.run/lkml/2026/09/17517909/t)
- treewide: refresh kmalloc_obj() conversions — 커밋 3a2c4d55e32a
- [[GIT PULL] kmalloc_obj treewide refactor for v7.0-rc1](https://ratatoskr.run/lkml/2026/02/3421066/t)
- scripts/coccinelle/api/kmalloc_objs.cocci
- Phoronix — Linux 7.3-rc2 Transitions More kmalloc Calls To The Superior kmalloc_obj
- LWN — slab: Introduce kmalloc_obj() and family