리눅스 커널 7.3-rc2, kmalloc() 호출을 kmalloc_obj()로 추가 전환 — Coccinelle이 놓친 scoped_guard 함수들

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 / kvcallockvmalloc_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_obj290kzalloc(243
kzalloc_objs139kcalloc(131
kvzalloc_objs35kvcalloc(34
kmalloc_obj32kmalloc_array(26
kmalloc_objs30kmalloc(24
kvmalloc_objs15kvmalloc_array(14
kzalloc_flex / kmalloc_flex14 / 9kvzalloc(4
kvzalloc_obj / kvzalloc_flex3 / 1이미 _obj 형태, GFP_KERNEL만 삭제92

대부분은 kzallockcalloc 변환입니다. 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.hscoped_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로 백포트할 때는 할당 줄이 충돌할 수 있습니다.

참고

답글 남기기