1P by GN⁺ | ★ favorite | 댓글 1개
  • GCC 14의 -fanalyzer는 C 코드의 여러 실행 경로를 상징 실행으로 따라가며, 런타임 전에 잡기 어려운 결함을 컴파일 시점에 찾는 데 초점을 둠
  • 이번 개선은 무한 루프 감지, 버퍼 오버플로 시각화, C 문자열 추적, 오염 분석 기본 활성화로 나뉘며 진단 결과를 더 이해하기 쉽게 만드는 데 집중됨
  • -Wanalyzer-out-of-bounds는 단순 경고를 넘어 텍스트 다이어그램으로 버퍼와 쓰기 위치의 관계를 보여주고, UTF-8 문자열에서 어느 문자 중간에 넘치는지도 표시함
  • null_terminated_string_arg(PARAM_IDX) 속성은 널 종료 문자열을 기대하는 API 조건을 분석기와 코드 독자에게 알려, 널 종료되지 않은 버퍼 전달 경로를 더 잘 드러냄
  • 오염 분석은 -fanalyzer 선택 시 기본으로 켜지며, 공격자 제어 값이 상한 검사 없이 크기·인덱스·오프셋 등에 쓰이는 흐름을 더 쉽게 확인할 수 있음

-fanalyzer가 다루는 문제 범위

  • -fanalyzer는 GCC의 정적 분석 패스로, C 소스 코드의 여러 실행 경로를 상징 실행해 컴파일 시점에 결함을 찾으려 함
  • GCC 14의 개선 사항은 2024년 4월 공식 릴리스를 앞두고 정리됐으며, 글 작성 시점에는 GCC 14.1 릴리스가 2024년 4월 중 기대되는 상태였음
  • GCC 14.0 프리릴리스는 Fedora 40 Beta에서 이미 사용 중임
  • 예제들은 Compiler Explorer에서 새 컴파일러 옵션과 함께 시험해볼 수 있음

단순 무한 루프 감지

  • GCC 14에는 새 경고 -Wanalyzer-infinite-loop가 추가됨
  • 예제 코드는 중첩 for 문에서 두 번째 루프 조건은 j < n인데 증감식은 i++로 남아 있는 형태임
    • 첫 번째 for 문을 복사한 뒤 증감식의 ij로 바꾸지 않은 실수에 해당함
    • 분석기는 이 경로에서 j < n의 true 분기를 계속 따라가며 루프 반복을 경고함
  • 현재 진단 출력은 이벤트 번호 (1)부터 (5)까지 순서대로 읽어야 흐름을 이해하기 쉬움
  • GCC 15에서는 제어 흐름 경로를 강조하는 ASCII 아트 같은 방식으로 가독성을 높이는 방안이 희망 사항으로 남아 있음
  • 해당 코드는 Compiler Explorer 예제에서 실행해볼 수 있음

버퍼 오버플로를 텍스트로 시각화

  • GCC 13에서 분석기는 -Wanalyzer-out-of-bounds를 통해 경계 검사를 지원하기 시작함
  • GCC 14에서는 예측된 버퍼 오버플로의 공간 관계를 텍스트 기반 다이어그램으로 출력할 수 있음
  • char buf[10]strcpy(buf, "hello")를 수행한 뒤 strcat(buf, " world!")를 호출하는 예제에서 스택 기반 버퍼 오버플로가 감지됨
    • 기존 메시지는 buf 용량이 10바이트이고 byte 10부터 byte 12까지 범위를 벗어난 쓰기가 발생한다고 표시함
    • GCC 14의 다이어그램은 strcpy가 채운 목적지 버퍼와 strcat 시작점이 되는 기존 종료 NUL 바이트를 함께 보여줌
  • 비ASCII 문자열 예제에서도 UTF-8 표현을 따라가며 오버플로 위치를 표시함
    • char buf[11]"サツキ"를 복사한 뒤 "メイ"를 이어붙이는 코드가 예시로 사용됨
    • 다이어그램은 오버플로가 문자, 즉 U+30E1의 중간에서 발생함을 보여줌
  • 관련 코드는 ASCII 문자열 예제비ASCII 문자열 예제에서 확인할 수 있음

C 문자열 연산 추적 강화

  • GCC 14 분석기는 C 문자열 연산 추적을 개선해, 널 종료 바이트를 찾으며 버퍼를 스캔하는 API를 시뮬레이션함
  • 포인터가 널 종료되지 않은 버퍼를 가리키는데 해당 API에 전달되는 경로가 있으면 경고함
  • 새 함수 속성 null_terminated_string_arg(PARAM_IDX)는 특정 매개변수가 널 종료 문자열이어야 한다는 조건을 분석기와 코드 독자에게 알려줌
  • 예제에서는 example_fn(const char *p)null_terminated_string_arg(1)nonnull 속성이 붙어 있음
    • char str[3] = "abc";는 널 종료 바이트를 담을 공간이 없음
    • example_fn(str) 호출 시 분석기는 str 끝 이후 1바이트를 읽는 스택 기반 버퍼 과다 읽기를 경고함
    • 진단에는 example_fn의 1번 인자가 널 종료 문자열 포인터여야 한다는 note도 함께 출력됨
  • 이 예제는 Compiler Explorer에서 실행해볼 수 있음

오염 분석 기본 활성화

CVE-2011-2210로 본 커널 분석 사례

  • Linux 커널의 CVE-2011-2210 발췌 예제가 오염 분석 사례로 사용됨
  • __SYSCALL_DEFINEx 매크로에 __attribute__((tainted_args))를 추가해 osf_getsysinfo 인자가 신뢰 경계를 넘어온 값이며 오염된 값으로 취급되어야 함을 분석기에 알림
  • GCC 14 분석기는 copy_to_user(buffer, hwrpb, nbytes)에서 공격자 제어 값 nbytes를 상한 검사 없이 크기로 사용한다고 경고함
    • 진단은 nbytesif (nbytes < sizeof(*hwrpb))에서 하한 검사만 받았음을 표시함
    • copy_to_user의 세 번째 매개변수는 access(write_only, 1, 3) 속성으로 크기 매개변수임을 나타냄
  • 문제는 정제 조건이 if (nbytes < sizeof(*hwrpb))로 작성된 점임
  • 커널에서 분석기를 실행해 취약점을 찾고 분석기 오탐을 수정하는 작업은 계속 진행 중임

댓글과 토론

Hacker News 의견들
  • 내게 fanalyzer는 Clang 대비 GCC의 킬러 기능 중 하나임. 오류를 설명해줘서 C 프로그래밍이 훨씬 쉬워지고, 오류 메시지도 개발자 친화적이라는 면에서 Rust와 비슷하게 느껴지기 시작했음

    • Rust가 특히 HN에서 메모리 안전성과 좋은 추상화로 크게 주목받는 건 알지만, Rust의 인기 중 얼마나 많은 부분이 오류 메시지 덕분인지 궁금함
      기술 학습을 그만두는 가장 큰 이유는 답답하거나 불명확한 오류인 경우가 많음. 주제를 조금 벗어났지만, C를 좋아하고 Rust 수준의 오류 메시지가 있으면 더 좋아질 것 같다는 뜻이었음
    • Clang에도 비슷한 도구인 Clang Static Analyzer가 있음: https://clang-analyzer.llvm.org/
    • 정반대의 경험을 했음. Clang이 GCC보다 훨씬 나은 오류 메시지를 자주 보여주고, 일부 경고나 오류 구현은 더 많은 경우를 잡아내며, clang-tidy는 훨씬 나은 정적 분석을 해줌
    • C++를 그렇게 싫어했던 이유 중 하나가 떠오름. error: missing semicolon 대신 템플릿 인스턴스화 관련 오류 메시지가 1000줄 넘게 쏟아지곤 했음
    • 이 얘기는 꽤 놀랍게 들림. GCC 분석기가 Clang 정적 분석기가 이미 보고하지 않는 무엇을 더 잡아주는지 궁금함
      GCC 분석기를 몇 번 써봤지만 출력을 읽기 좋게 만들어주는 괜찮은 프런트엔드를 찾지 못했음. Clang은 꽤 괜찮은 HTML 출력, CodeChecker, Xcode 통합 등 여러 선택지가 있는데, GCC 쪽 출력은 어떻게 읽는지 궁금함. 게다가 GCC가 Clang보다 오탐을 훨씬 많이 내는 것 같음
  • 다른 스레드에 댓글이 36개 더 있음: https://news.ycombinator.com/item?id=39918278
    “GCC 14 Boasts Nice ASCII Art for Visualizing Buffer Overflows (phoronix.com)”, 2시간 전 글임

  • 몇 달 전에 작은 Linux 유틸리티를 만들었음. 임의의 실행 파일을 대체하는 드롭인 shim으로, 호출되면 원래 프로그램인 척하면서 원본을 fork하고 stdout/stderr에 연결했음
    오류 출력은 그 프로그램의 맥락을 아는 맞춤 GPT 어시스턴트로 보내고, 어시스턴트가 원래 오류를 사람이 읽기 쉬운 형태로 바꾼 뒤 shim의 stderr로 내보냈음. GCC/Clang의 concept/template 관련 중첩된 컴파일러 덤프를 보기가 지겨워서 쓰고 있었지만, 원하면 어떤 프로그램에도 쓸 수 있었음. 잘 동작했지만 심하게 아파서 더 작업하지 못했고, 누군가 다시 제대로 만들고 일반화해주면 좋은 프로젝트가 될 것 같음

  • 분석 결과에 더 나은 출력 형식이 있으면 좋겠음. 지금 방식은 스크린 리더에는 지옥임

    • 참고로 GCC 13에 SARIF 출력을 구현했고, 예를 들어 VS Code에서 플러그인으로 볼 수 있음. 다만 ASCII 아트는 포함되지 않음
      출력 예시는 여기서 볼 수 있음: https://godbolt.org/z/aan6Kfxds
      글의 첫 번째 예제에 명령줄 옵션 -fdiagnostics-format=sarif-stderr를 추가한 것임. 다이어그램을 SVG로 출력하는 실험도 했지만 GCC 14에 넣을 만큼 충분히 다듬지는 못했음
  • 원래 코드는 nbytes < sizeof(*hwrpb)이면 -1을 반환하고, copy_to_user(buffer, hwrpb, nbytes)가 실패하면 -2를 반환하는 형태였음. 적용된 수정은 nbytes > sizeof(*hwrpb) 검사였지만, 올바른 수정은 copy_to_user(buffer, hwrpb, sizeof(*hwrpb))라고 봄
    hwrpb 포인터에서 sizeof(*hwrpb)가 아닌 크기를 복사해 나가는 건 말이 되지 않음

    • 호출자가 nbytes = 4를 넘기고 sizeof(hwrpb)가 16바이트라면, 호출자 버퍼에서 12바이트를 과하게 복사해서 소유하지 않은 메모리를 읽을 수 있음. 피해야 한다고 봄
      더 나은 해법은 호출자와 피호출자가 모두 지원하는 최소 바이트만 복사하는 것임. 예를 들어 nbytes = MIN(nbytes, sizeof(hwrpb));처럼 하면 됨. hwrpb->size의 버전 정보가 지켜진다는 전제에서는, 구조체 일부가 초기화되지 않았더라도 하위/상위 호환성을 보장할 수 있음
    • 맞음. 하지만 버퍼 크기가 주어진 이상 호출자 버퍼 끝을 덮어쓰는 것도 말이 안 되므로, nbytes보다 긴 값을 넘길 수도 없음
  • 정말 훌륭함. 들어간 작업량이 엄청나 보임. 난이도는 표준 라이브러리와 C 표준에 fat pointer/배열 view를 도입하는 것과 비슷한 수준으로 보임

  • -Wstringop-overflow는 오탐이 너무 많아서 가장 먼저 끄는 경고임. 분석기 변형이라고 더 나을지는 의심스러움

    • 계속 삐삐 울려서 머리가 아프고 졸리다는 이유로 일산화탄소 감지기 배터리를 빼는 것과 비슷하지 않나 싶음
  • 아주 멋짐. 요즘 C 개발을 많이 하지는 않아서 strcpystrcat이 얼마나 자주 쓰이는지 궁금함. 마지막으로 확인했을 때는 goto만큼이나 금기시되던 것들에 가까웠음
    물론 커널 개발에서는 goto가 자주 선호된다는 건 알고 있음. C 문자열 분석이 실제로 얼마나 도움이 되는지 궁금함

    • 어떤 맥락에서는 goto 사용이 명백하게 올바르고 우아함. 무조건 피하려고 하면 유지보수하기 어려운 못생기고 꼬인 코드가 될 수 있음. 흔하진 않아도 유효한 쓰임새가 있음
      strcpy 같은 함수는 덜 권장되지만, 더 강한 불변식, 예컨대 언어 수준 불변식이 깨지지 않는 한 맞다는 게 보장되는 상황도 있음. 그런 경우가 깨졌다면 이미 훨씬 큰 문제가 있는 것임. 드문 경우지만, 명목상 더 안전한 대안이 이득 없이 약간 덜 효율적일 수 있다는 주장도 가능함
    • C에 없는 구조적 프로그래밍 구문과 논리적으로 동등하게 쓰는 한, 일부 goto 사용은 여전히 C에서 관용적임. 조심은 필요하지만, 어쨌든 C니까 그럴 수 있음
      다만 longjmp는 전혀 좋아하지 않음
    • 단순한 goto 사용에는 문제가 없음. 반면 strxcpy 계열은 완전히 엉망이고 어떤 이유로도 쓰면 안 됨. 커널에서 쓰인다는 사실이 끔찍함
      그런 함수들과 그것들을 “고치려던” 모든 실패한 시도는 궤도에서 날려버렸어야 함
    • goto는 신중하게 쓰면 괜찮음. strcpystrcat도 코드가 맞다는 걸 알고 있고 틀리면 큰 문제가 생긴다는 의미에서는 “괜찮음”. 안타깝게도 이 설명은 C 대부분에 해당함
    • gotostrcat, strcpy만큼 금기라고 보지는 않음. goto는 괜찮고, 같은 스코프에서 정확한 크기의 malloc 없이 쓰이는 strcatstrcpy코드 냄새에 가까움
  • 아주 좋음. 무엇이 잘못됐는지 설명하는 상세 보고서가 모두 붙어 있어서 반가움