‘atomic_inc’ from incompatible pointer type [-Werror=incompatible-pointer-types]

벤더 커널을 빌드하다가 아래와 같은 에러를 만났다.

/home/worker/.../kernel-source/include/linux/cred.h:258:28: error: passing argument 1 of 'atomic_inc' from incompatible pointer type [-Werror=incompatible-pointer-types]
  258 |                 atomic_inc(&new->usage);
      |                            ^~~~~~~~~~~
      |                            |
      |                            atomic_long_t * {aka atomic64_t *}

커널을 건드린 적도 없는데 cred.h, 그것도 struct cred의 참조 카운트인 usage 필드에서 타입 불일치가 난 것이다. 확인해보니 이 벤더 트리는 usage의 타입을 원래의 atomic_t에서 atomic_long_t(64비트 아키텍처에서는 atomic64_t와 동일)로 폭을 넓혀두었는데, 정작 그 필드를 증가시키는 atomic_inc() 호출부 하나는 예전 타입 그대로 남아 있었다. 이 글에서는 커널의 atomic_t/atomic64_t/atomic_long_t API가 어떻게 다른지, 이런 타입 불일치가 왜 생기는지, 그리고 최소 재현 코드로 직접 에러를 내고 고치는 과정을 정리한다.

핵심 개념: 커널 atomic API 패밀리

세 타입 모두 같은 이름의 연산(_read/_set/_inc/_dec/_add/_cmpxchg 등)을 제공하지만, 각각 별도의 구조체와 접두사를 쓰는 독립된 API라 서로의 포인터를 섞어 쓸 수 없다.

타입기반 타입대표 함수비고
atomic_tint (32비트)atomic_inc(), atomic_read()가장 흔히 쓰는 참조 카운트 타입, 32비트 오버플로에 취약
atomic64_ts64 (64비트)atomic64_inc(), atomic64_read()아키텍처와 무관하게 항상 64비트
atomic_long_tlongatomic_long_inc(), atomic_long_read()64비트 아키텍처에서는 atomic64_t로, 32비트에서는 atomic_t로 대체됨

GCC는 함수 프로토타입 기준으로 인자 타입을 엄격히 검사하므로, atomic_t *를 받는 atomic_inc()atomic_long_t *를 넘기면 리턴 타입(둘 다 void)과 무관하게 바로 incompatible-pointer-types 에러가 난다.

atomic_t 참조 카운트는 32비트라 이론상 20억 번 넘게 증가시키면 오버플로로 use-after-free까지 이어질 수 있다는 지적이 꾸준히 있었고, mainline에서도 2023년 8월 Kees Cook이 cred.usage를 아예 오버플로 시 경고를 발생시키는 refcount_t로 바꾸는 패치를 올렸다. 지금 만난 벤더 트리는 그 정식 전환 전에 자체적으로 필드 폭만 64비트로 넓혀 오버플로를 현실적으로 어렵게 만드는 하드닝을 먼저 적용해둔 것으로 보이고, 그 과정에서 호출부 하나를 놓친 게 이 에러의 원인이다.

실전: 최소 재현 및 수정

실제 커널 헤더 없이도 같은 구조의 코드로 동일한 에러를 재현할 수 있다.

typedef struct { int counter; } atomic_t;
typedef struct { long counter; } atomic_long_t;

static inline void atomic_inc(atomic_t *v) { v->counter++; }
static inline void atomic_long_inc(atomic_long_t *v) { v->counter++; }

struct cred {
    atomic_long_t usage;   /* 원래는 atomic_t였다가 64비트로 확장됨 */
};

void get_new_cred(struct cred *new)
{
    atomic_inc(&new->usage);   /* 호출부는 여전히 옛 타입 기준 */
}

int main(void) { struct cred c = {0}; get_new_cred(&c); return 0; }
$ gcc -Werror=incompatible-pointer-types -c cred_demo.c -o cred_demo.o
cred_demo.c: In function 'get_new_cred':
cred_demo.c:11:16: error: passing argument 1 of 'atomic_inc' from incompatible pointer type [-Werror=incompatible-pointer-types]
   11 |     atomic_inc(&new->usage);
      |                ^~~~~~~~~~~
      |                |
      |                atomic_long_t *
cred_demo.c:4:41: note: expected 'atomic_t *' but argument is of type 'atomic_long_t *'
    4 | static inline void atomic_inc(atomic_t *v) { v->counter++; }
      |                               ~~~~~~~~~~^
cc1: some warnings being treated as errors

원래 GCC 에러 메시지의 구조(argument 1, expected/actual 타입, note 위치)가 그대로 재현된다. 수정은 필드 타입에 맞는 API로 호출부만 바꾸면 된다.

void get_new_cred(struct cred *new)
{
    atomic_long_inc(&new->usage);   /* usage 타입에 맞춰 atomic_long_* API로 교체 */
}
$ gcc -Werror=incompatible-pointer-types -c cred_demo.c -o cred_demo.o
$ echo $?
0

주의사항

  • 필드 타입을 바꿀 때는 그 필드를 참조하는 모든 파일을 찾아 함께 바꿔야 한다. grep -rn '\->usage' include/linux/cred.h kernel/cred.c처럼 정의부뿐 아니라 실제 사용처 전체를 확인할 것.
  • atomic_long_t는 아키텍처의 word size를 따르므로, 32비트 타겟에서는 여전히 32비트다. 아키텍처와 무관하게 항상 64비트를 보장하려면 atomic64_t를 명시적으로 써야 한다.
  • 단순 폭 확장은 오버플로를 “어렵게” 만들 뿐 막지는 못한다. refcount_t는 0에서 증가하거나 언더플로가 나면 즉시 WARN을 띄우는 안전장치가 있어 오버플로 방지가 목적이라면 이쪽이 더 근본적인 해법이다.
  • 이런 에러가 -Werror로 빌드 타임에 걸린 것 자체는 정상 동작이다. -Wno-error=incompatible-pointer-types로 우회하면 컴파일은 되지만 32비트 값을 64비트 필드로 잘못 해석하는 등 런타임 버그로 이어질 수 있다.

마무리

이 에러는 결국 헤더에서 필드 타입만 바꾸고 그 필드를 쓰는 호출부 하나를 놓친, 흔한 리팩터링 누락이다. 다만 대상이 struct cred처럼 보안 경계에 쓰이는 참조 카운트라는 점에서, GCC가 이걸 경고가 아니라 에러로 잡아준 게 오히려 다행이다. 비슷한 타입 불일치를 만나면 먼저 어떤 atomic API 패밀리로 통일할지 정하고, 정의부와 호출부를 함께 grep해서 빠짐없이 바꾸는 순서로 접근하면 된다.

참고

답글 남기기