![]()
스택에 잠깐 빌린 버퍼가 발목 잡는 순간: traceback.c의 alloca 크기 계산 버그 수정(PR #145814)
meta_description: CPython의 traceback 경로에서 VLA를 alloca로 대체하는 과정에서, 크기 계산이 잘못되면 스택에 할당되는 버퍼 크기가 의도와 달라질 수 있다. PR #145814는 traceback.c의 alloca 크기 계산 버그를 수정해 위험한 경계를 줄였다. 어떤 한 줄이 왜 위험했는지, 그리고 빌드 옵션/포팅에서 이런 버그가 왜 튀어나오는지 실무 관점으로 정리한다. meta_keywords: CPython, traceback, faulthandler, alloca, VLA, stack allocation, size calculation, integer overflow, compiler, build option, portability, traceback.c, crash, debug build, sanitizers, C bug, memory safety, regression, diff meta_robots: index,follow
C 확장 버그는 흔히 힙에서 뭔가가 잘못됐다로 끝나는데, 가끔은 힙보다 얄팍한 곳에서 일이 난다. 스택.
traceback 출력은 더더욱 안전해 보이는 영역이다. 예외가 터졌을 때 문자열을 만들고, 프레임을 찍고, 디버깅을 돕는다. 그런데 바로 그 지점이 빌드 옵션이나 컴파일러 선택에 따라 민감해질 수 있다.
PR #145814는 그런 종류의 패치다. 요지는 VLA(variable length array)를 쓰던 코드를 alloca 기반으로 바꾸는 과정에서, alloca에 넘기는 크기 계산이 틀릴 수 있는 모양을 고쳤다는 것.
이 글의 목적은 이 버전은 안전/위험 같은 판단을 내리는 게 아니다. diff에서 바뀐 한 줄이 왜 위험할 수 있었는지, 그리고 비슷한 류의 버그가 어떤 환경에서 튀어나오는지 감각을 남기는 쪽이다.
내가 이걸 실무 이슈라고 느끼는 이유는 단순하다. 이런 버그는 보통 기능 테스트로는 잘 안 잡힌다. traceback 출력이 살짝 이상해진다고 해서, CI가 바로 터지진 않는다. 대신 sanitizers나 경고 옵션을 켠 빌드에서 스택 경계가 찌그러지는 신호로 먼저 나온다. 그리고 그 순간부터는 원인을 보여주는 도구(트레이스백)가 원인을 가리는 도구가 될 수 있다.
/* (모양) before: 크기 계산이 요소 개수/바이트 단위를 헷갈릴 수 있는 형태 */
size_t n = count; /* 요소 개수 */
char *buf = alloca(n * sizeof(buf)); /* sizeof(buf)는 포인터 크기 */
/* ... buf에 데이터를 채움 ... */
위처럼 포인터의 sizeof를 곱하는 형태가 들어가면, 개발자 의도가 sizeof(*buf)나 요소 타입의 크기였을 가능성이 있다. 포인터 크기를 곱하면 바이트 수가 기대와 달라진다. 더 위험한 경우는, 타입이 바뀌거나 포팅 과정에서 이 한 줄이 더 이상 눈에 띄지 않게 되는 것이다.
PR #145814는 이런 종류의 실수를 정리한다. 즉, alloca에 넘기는 바이트 수를 실제 요소 타입/길이 기준으로 맞추고, 계산이 눈에 보이게 만든다.
이런 수정은 겉으로는 사소해 보이지만, 디버깅 경로에선 레버리지가 크다. 스택 버퍼 크기가 잘못 잡히면 결과는 두 갈래다. 운이 좋으면 바로 크래시로 끝나고, 운이 나쁘면 한동안은 그럴듯한 출력이 나오다가 어느 순간만 깨진다. 후자가 훨씬 골치 아프다. 운영에서 제일 비싼 건 재현이 안 되는 문제이기 때문이다.
/* (모양) after: 요소 타입 기준으로 크기 계산을 고정 */
size_t n = count;
char *buf = alloca(n * sizeof(*buf));
이 변경이 의미하는 건, alloca를 쓰는 순간부터 크기 계산이 안전 계약이라는 사실을 코드에 새겨 넣는다는 점이다.
실무에서 이 버그가 튀어나오는 장면: 빌드 옵션/컴파일러가 다른 팀의 미래
이런 버그는 로컬에서 잘 안 보일 수 있다.
한 컴파일러는 VLA를 허용해서 원래 경로로 가고, 다른 컴파일러/옵션은 VLA를 막아서 alloca 경로가 활성화된다. 그리고 그 경로의 크기 계산 버그가 그때서야 드러나는 식이다.
실무에선 이런 일이 포팅이나 하드닝에서 자주 일어난다. 특정 플랫폼에서만 C 표준 옵션이 다르고, 특정 경고를 에러로 올렸고, 그 결과로 코드를 손보다가 생긴다.
그런데 traceback/fault handler 같은 코드는 평소엔 거의 안 타는 길이라서 테스트가 얇다. 그래서 한 줄이 틀려도 오래 살아남을 수 있다.
이번 PR이 주는 운영 힌트는 간단하다.
- 디버깅/관찰 경로는 기능 코드만큼이나 빌드 매트릭스에 넣어야 한다
- VLA→alloca 같은 변환은 성능 개선이 아니라 포팅 계약 변경이다
코드 리뷰 때의 체크 포인트: alloca는 작게, 분명하게, 계산을 눈에 띄게
alloca는 편할 때가 있다. 임시 버퍼가 필요하고, 실패 처리가 귀찮고, 수명이 짧다.
하지만 리뷰에서는 이런 순서로 보게 된다.
첫째, 요소 개수와 바이트 크기가 섞여 있지 않은가. 둘째, sizeof가 포인터가 아니라 요소 타입을 가리키는가. 셋째, count가 외부 입력/계산 결과일 때 과도하게 커지면 어떻게 되는가.
이번 PR의 성격은 결국 계산을 눈에 띄게 고쳐서, 그 다음 검토가 가능하게 만든다는 데 있다.
그리고 이런 변경은 faulthandler/traceback 같은 경로에서 더 가치가 있다. 실패를 보여주는 코드가 조용히 실패하면, 운영자는 어디서부터 손대야 할지 잃어버린다.
개인적으로는 이런 패치가 기능 추가보다 신뢰에 더 직접적으로 기여한다고 본다. 에러가 났을 때 최소한의 정보가 남는다는 건, 결국 개발 속도에 대한 투자라서.
Labels: faulthandler,traceback,alloca
References
- CPython PR #145814 — Fix alloca size calculation in traceback.c
- PR diff
- Merge commit 59d97683c19923b06e2b2110efadb90fe37f53f3
이미지 크레딧/라이선스
- Laptop coding programs (Unsplash).jpg — Tirza van Dijk / CC0
댓글
댓글 쓰기