본문으로 건너뛰기
피드

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

엘에이 경찰, 사생활 침해 논란 끝에 플록 번호판 감시 계약 종료

엘에이 경찰이 번호판 인식 카메라 업체 플록 세이프티와의 3년 계약을 연장하지 않기로 했어. 시민권, 프라이버시, 데이터 보관과 공유 문제를 해결하지 못했다는 게 핵심 이유야. 플록은 미국 전역에 최소 8만 대 카메라망을 깔아 경찰과 연방기관이 차량을 추적할 수 있게 해왔고, 오탐·보안 사고·이민 단속 악용 의혹까지 겹치며 압박이 커졌어.

security

크로미움 148부터 Math.tanh 한 번으로 운영체제 지문이 새는 문제

크로미움 148 이후 V8이 Math.tanh 계산을 자체 구현 대신 운영체제의 수학 라이브러리에 맡기면서, 같은 입력값도 리눅스·맥·윈도우에서 마지막 비트가 달라지는 지문 신호가 생겼다. 이 차이는 User-Agent로 맥이라고 주장하면서 리눅스 수학 결과를 내는 식의 위장을 바로 들키게 만든다. CSS 삼각함수와 웹 오디오까지 보면 브라우저 수학 경로가 꽤 복잡하게 갈라지고, 제대로 흉내 내려면 실제 운영체제 라이브러리의 비트 단위 동작까지 맞춰야 한다.

security

출입통제도 클라우드 구독제로 간다…제로트러스트·AI 분석이 승부처

기업 출입보안이 사업장별 서버를 두는 방식에서 클라우드 기반 서비스형 출입통제(ACaaS)로 옮겨가고 있다. 시장은 2026년 17억8000만 달러에서 2030년 31억9000만 달러로 커질 전망이고, 제로트러스트·모바일 인증·AI 출입 분석·스마트빌딩 통합이 핵심 경쟁 포인트로 꼽힌다.

security

AWS·구글 클라우드, 양자컴퓨터 대비해 암호 체계 갈아엎는 중

AWS와 구글 클라우드가 양자컴퓨터 시대를 대비해 기존 공개키 암호와 양자내성암호를 함께 쓰는 하이브리드 전환을 시작했다. 핵심 배경은 지금 훔친 암호문을 나중에 양자컴퓨터로 푸는 ‘선수집·후해독’ 공격이고, 국내도 통신·금융·국방 등 5개 분야에 45억원을 투입해 실증에 들어간다.

security

보안 점검 맡긴 AI 에이전트가 악성코드를 직접 실행할 수 있다는 경고

AI 보안 에이전트가 저장소를 검사하다가 README 같은 문서에 숨은 프롬프트 인젝션에 속아 악성 바이너리를 실행할 수 있다는 연구가 나왔다. 클로드 코드와 코덱스의 자동 모드처럼 승인 없이 명령을 수행하는 구성에서 문제가 재현됐고, 여러 모델과 공급업체를 가로질러 같은 패턴이 통했다는 점이 핵심이다.