본문으로 건너뛰기
피드

C 코드는 거의 어디에나 정의되지 않은 동작이 숨어 있다는 얘기

security 약 10분
vote
0
댓글
북마크

30년 가까이 C와 C++를 써온 개발자가 “비자명한 C/C++ 코드에는 사실상 정의되지 않은 동작이 있다”고 주장한 글이다. 흔한 메모리 버그뿐 아니라 정렬 안 된 포인터, char를 넘긴 isxdigit, float에서 int 캐스팅, NULL 포인터, printf 포맷 불일치 같은 일상적인 코드까지 위험하다는 게 핵심이다. 결론은 C/C++를 버리자는 게 아니라, 2026년에는 대규모 언어 모델(LLM)을 동원해 정의되지 않은 동작을 찾고 사람이 검토하는 프로세스가 필요하다는 쪽에 가깝다.

  • 1

    정의되지 않은 동작은 최적화 옵션 문제가 아니라, 컴파일러와 하드웨어가 애초에 그런 상황을 고려하지 않아도 되는 계약 문제다.

  • 2

    C23 표준에는 undefined라는 단어가 283번 나오고, 명시되지 않아 사실상 정의되지 않은 영역까지 합치면 위험면은 더 넓다.

  • 3

    정렬 안 된 포인터, signed char와 isxdigit, float-to-int 캐스팅, NULL 표현, printf 가변 인자 타입 불일치 같은 코드가 모두 실제 버그 표면이 될 수 있다.

  • 4

    저자는 OpenBSD의 find 코드에서도 LLM으로 정의되지 않은 동작과 out-of-bounds write를 찾아 패치를 보냈다고 말한다.

  • 5

    C/C++ 코드베이스를 당장 버릴 수는 없지만, LLM을 보조 리뷰어로 써서 UB를 대규모로 줄이는 방식이 필요하다는 주장이다.

“C를 잘 쓰면 된다”는 말이 점점 방어가 안 된다는 주장

  • 글쓴이는 C/C++를 30년 가까이 거의 매일 써온 사람인데도, 결론이 꽤 세다. “비자명한 C/C++ 코드에는 정의되지 않은 동작(Undefined Behavior, UB)이 있다”는 쪽임.

    • double-free, use-after-free, 배열 범위 밖 접근, 초기화 안 된 메모리 읽기 같은 건 다들 위험하다고 알고 있음.
    • 그런데 글의 핵심은 그런 뻔한 메모리 버그가 아니라, 평소에 별생각 없이 쓰는 코드에도 표준상 UB가 숨어 있다는 것임.
  • “최적화 끄면 괜찮지 않나?”도 틀린 접근이라고 봄.

    • UB는 컴파일러가 “어? 네 실수 잡았다” 하고 공격적으로 최적화하는 문제가 아니라, 컴파일러가 애초에 “그런 코드는 유효하지 않으니 고려하지 않아도 된다”고 가정하는 계약 문제임.
    • 그래서 최적화 옵션을 끄더라도, 컴파일러 내부 단계나 하드웨어로 의도가 전달되는 과정에서 이미 보장 자체가 사라질 수 있음.

중요

> 글쓴이가 인용한 수치가 꽤 묵직함. C23 표준에는 “undefined”라는 단어가 283번 나오고, 명시되지 않아서 undefined가 되는 영역까지 합치면 실제 위험면은 더 넓다는 주장임.

생각보다 가까운 곳에 있는 UB 사례들

  • 정렬(alignment)이 맞지 않는 포인터를 만드는 것부터 이미 위험하다고 설명함.

    • 예를 들어 int 포인터가 sizeof(int)의 배수 주소를 가리키지 않는다면, 어떤 플랫폼에서는 그냥 동작하고 어떤 플랫폼에서는 SIGBUS로 터질 수 있음.
    • Linux Alpha에서는 커널이 일부 load를 소프트웨어로 보정해주기도 했지만, 다른 경우에는 크래시가 났고 SPARC도 SIGBUS가 날 수 있음.
    • x86/amd64에서는 대체로 관대하게 돌아가지만, 그건 표준이 보장해서가 아니라 그 플랫폼이 봐주는 것에 가깝다는 얘기임.
  • 더 골때리는 포인트는 “역참조하기 전”에도 이미 UB일 수 있다는 점임.

    • 정렬 안 된 주소를 int*로 캐스팅하는 순간부터 문제가 될 수 있다고 봄.
    • 미래 아키텍처가 int 포인터의 하위 비트를 태깅이나 보안 용도로 쓴다고 해도, 표준상 불가능한 포인터를 만든 코드가 항의할 근거가 약함.
  • isxdigit()에 char를 그대로 넘기는 코드도 함정이라고 지적함.

    • isxdigit()은 int를 받지만, 허용되는 값은 unsigned char로 표현 가능한 값 또는 EOF임.
    • char가 signed인 아키텍처에서 128 이상 값이 음수로 승격되면, 테이블 인덱싱 같은 구현에서 엉뚱한 메모리를 읽을 수 있음.
    • 데스크톱 앱에서는 그냥 크래시나 이상한 값으로 끝날 수 있지만, 임베디드나 유저스페이스 네트워크 드라이버 같은 환경에서는 I/O mapped memory를 건드리는 식의 더 이상한 일이 생길 수도 있다고 봄.
  • float를 int로 캐스팅하는 단순한 코드도 만만치 않음.

    • float 값이 int로 표현 가능한 범위를 벗어나면 UB이고, non-finite 값이어도 UB라고 설명함.
    • 그러면 INT_MAX와 비교하려고 float를 int로 바꾸면 그 자체가 위험하고, INT_MAX를 float로 바꾸면 정확히 표현되는지 또 따져야 함.
    • “초를 밀리초 정수로 바꾸려고 곱하고 캐스팅하는 코드” 같은 흔한 패턴도 입력 범위에 따라 위험해질 수 있다는 얘기임.

NULL, printf, divide by zero도 그냥 교과서 문제가 아님

  • 주소 0에 객체를 두는 것도 C 표준 관점에서는 실전에서 거의 답이 없다고 봄.

    • C의 NULL은 “기계 주소 0”을 의미하는 게 아니라 C 추상 머신의 null pointer constant임.
    • 역사적으로 NULL 포인터가 실제 주소 0이 아닌 머신도 있었고, memset으로 포인터 필드를 0으로 채운다고 반드시 NULL 포인터가 된다고 보장할 수도 없음.
    • OS 커널이나 임베디드처럼 주소 0을 실제로 다루고 싶은 코드에서는 이 간극이 꽤 불편해짐.
  • printf 계열의 가변 인자도 타입이 조금만 어긋나면 UB가 됨.

    • %ld가 필요한 곳에 long long을 넘기거나, %lld가 필요한 곳에 long을 넘기는 식의 실수는 표준상 안전하지 않음.
    • NULL을 %p에 넘길 때도 NULL 매크로가 정수 0처럼 해석될 수 있어서, 명시적으로 void*로 맞추는 식의 주의가 필요하다고 봄.
    • uid_t를 출력하려고 해도 signed인지 unsigned인지, 어떤 포맷 매크로를 써야 하는지 계속 타입 계약을 따라가야 함.
  • divide by zero는 다들 알지만, 보안 관점에서는 더 민감하다고 짚음.

    • 분모가 신뢰할 수 없는 입력에서 온다면 단순 크래시가 아니라 서비스 거부나 더 큰 취약점 표면이 될 수 있음.
    • C/C++에서는 “아마 이 값은 0이 아니겠지”가 코드 계약으로 남아 있으면, 언젠가 공격 입력이나 엣지 케이스가 그 부분을 찌를 수 있음.

⚠️주의

> 이 글의 불편한 결론은 “숙련자가 조심하면 된다”가 아니라 “숙련자도 놓친다”에 가까움. 특히 플랫폼을 바꾸거나 컴파일러가 바뀌거나 최적화 조건이 달라지면, 운 좋게 돌아가던 코드가 갑자기 터질 수 있음.

그래서 LLM을 코드 리뷰에 넣자는 결론

  • 글쓴이는 대규모 언어 모델(LLM)이 C 코드에서 UB를 찾는 데 사람보다 꽤 잘한다고 말함.

    • 아무 C 코드나 던지고 UB를 찾아보라고 하면 요즘 모델은 꽤 높은 확률로 맞는 지적을 한다는 경험담을 공유함.
    • 본인 코드에서 문제를 찾은 뒤, 더 성숙하고 엄격하게 작성됐을 OpenBSD의 find 코드에도 시도했고, out-of-bounds write와 로직 버그를 찾아 패치를 보냈다고 함.
  • 다만 “AI가 고쳐준 코드 그냥 머지하자”는 얘기는 아님.

    • 글쓴이도 LLM 결과를 보고 사람이 다시 확인해야 한다고 선을 그음.
    • 문제는 이 검토가 주니어에게 맡기기엔 너무 미묘하고, 시니어에게 맡기기엔 너무 많은 반복 노동이라는 점임.
    • 그래서 “AI slop을 커밋하지 않으면서도, 사람 리뷰어를 압도하지 않는 규모의 UB 정리 방식”이 필요하다고 정리함.
  • C/C++ 코드베이스를 당장 버릴 수 없다는 현실도 인정함.

    • 레거시 시스템, 커널, 임베디드, 고성능 네트워크, 런타임, 데이터베이스 같은 영역에는 C/C++가 여전히 깊게 박혀 있음.
    • 하지만 “그냥 원래 이렇게 해왔다”로 계속 두기에는 2026년의 컴파일러, 하드웨어, 보안 환경이 너무 달라졌다는 게 글의 큰 메시지임.

기술 맥락

  • 이 글에서 중요한 선택은 C/C++를 버리자는 게 아니라, UB를 찾는 리뷰 경로에 LLM을 넣자는 거예요. 왜냐하면 UB는 문법 에러처럼 바로 보이는 문제가 아니라 표준, 컴파일러, 아키텍처 지식이 겹쳐야 보이는 경우가 많거든요.

  • 기존 정적 분석 도구만으로 충분하지 않다는 뉘앙스도 있어요. 왜냐하면 도구는 정해진 규칙을 안정적으로 잡는 데 강하지만, “이 캐스팅이 특정 플랫폼에서는 왜 위험한가” 같은 설명과 수정 방향까지 이어가는 데는 사람이 계속 맥락을 보태야 하거든요.

  • LLM을 쓰는 이유는 후보를 빠르게 많이 뽑아내기 위해서예요. 다만 글쓴이는 모델이 낸 패치를 그대로 믿는 방식은 경계해요. 왜냐하면 UB 수정은 겉보기 동작을 바꿀 수 있고, 리뷰어가 표준상 문제와 실제 영향 범위를 같이 확인해야 하기 때문이에요.

  • 이 접근이 특히 잘 맞는 곳은 오래된 C/C++ 코드베이스예요. 왜냐하면 수십 년간 돌아간 코드라도 x86에서는 우연히 안전해 보였을 수 있고, ARM이나 RISC-V, 새 컴파일러, 다른 최적화 조건에서는 전혀 다른 결과가 나올 수 있거든요.

이 글이 세게 들리는 이유는 “C 개발자가 조심하면 된다”는 오래된 믿음을 정면으로 건드리기 때문임. 특히 레거시 C/C++를 운영하는 팀이라면 보안 점검을 정적 분석 도구에만 맡길 게 아니라, LLM 기반 리뷰를 사람 검토와 묶어 반복 가능한 프로세스로 만드는 쪽을 진지하게 볼 만함.

댓글

댓글

댓글을 불러오는 중...

security

한양대 에리카와 네이버클라우드, 클라우드·보안·AI 인재 키우는 산학협력 체결

한양대 에리카가 네이버클라우드와 첨단 분야 지역인재 양성과 글로벌 산학협력을 위한 업무협약을 맺었다. 협력 범위는 클라우드, 사이버보안, 블록체인, 개인정보보호, 인공지능(AI), 디지털 전환(DX) 교육·연구 기반 구축까지 포함된다.

security

악성 npm 패키지가 AI 개발도구의 지침 파일과 MCP까지 노리기 시작함

이스트시큐리티가 웹과 탈중앙화금융 개발자를 겨냥한 악성 npm 패키지 캠페인을 포착했어. 공격자는 유명 웹3 도구를 사칭하는 데서 그치지 않고, AI 에이전트가 읽는 프로젝트 지침 파일과 MCP 기반 외부 도구 호출까지 공격 경로로 삼으려 했어.

security

금융권, 앤트로픽 미토스가 찾은 오픈소스 취약점에 긴급 점검 들어감

앤트로픽의 AI 모델 클로드 미토스가 1000개 넘는 오픈소스에서 대량의 취약점 후보를 찾아냈고, 그중 일부가 실제 취약점으로 검증돼 공개됐어. 금융당국은 nginx, wolfSSL, FreeRDP, Ghost 같은 널리 쓰이는 구성요소를 중심으로 금융권에 긴급 자산 점검과 패치 적용을 권고했어.

security

애플이 양자 내성 암호화 검증 코드를 공개했다, 핵심은 수학적 증명

애플이 corecrypto 라이브러리의 포스트 양자 암호화 구현과 검증 코드를 GitHub에 공개했다. ML-KEM, ML-DSA 구현과 형식 검증 접근을 공개해 보안 연구자들이 직접 검토할 수 있게 했고, 이 기술은 25억 대 이상 활성 기기에서 쓰이는 암호화 기반과 연결된다.

security

라라벨 번역 패키지 태그가 통째로 바뀌었다, 개발자 비밀값 털리는 공급망 공격

전 세계 라라벨 개발자가 쓰는 Laravel-Lang 패키지가 공격을 받아 Git 태그가 악성 버전을 가리키도록 바뀌었다. 5월 22일 약 90분 동안 4개 저장소의 태그가 교체됐고, 감염된 패키지는 AWS 키, GitHub 토큰, Stripe 시크릿, 암호화폐 지갑 복구 구문, SSH 개인키 등을 노렸다.