기본 콘텐츠로 건너뛰기

“스택에 잠깐 빌린 버퍼”가 발목 잡는 순간: traceback.c의 alloca 크기 계산 버그 수정(PR #145814)

thumbnail

스택에 잠깐 빌린 버퍼가 발목 잡는 순간: 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나 경고 옵션을 켠 빌드에서 스택 경계가 찌그러지는 신호로 먼저 나온다. 그리고 그 순간부터는 원인을 보여주는 도구(트레이스백)가 원인을 가리는 도구가 될 수 있다.

laptop coding /></p>

<hr />

<h2>왜 traceback 경로에서 이런 문제가 중요해지나: 실패를 보여주는 코드가 또 실패하면 곤란하다</h2>

<p>traceback 경로는 보통 메인 기능이 아니다. 하지만 크래시/예외 상황에서 실행된다. 그 말은 곧, 이미 상황이 나쁘다는 뜻이다.</p>

<p>이때 디버깅 도구(faulthandler 포함)가 추가로 무너지면, 실무에선 원인 모름으로 남는다. 특히 CI에서만 재현되는 크래시나, 특정 컴파일러/플랫폼에서만 생기는 문제는 그 순간부터 추적 비용이 폭발한다.</p>

<p>그래서 traceback.c 같은 파일에서의 메모리/스택 관련 버그 수정은 체감상 가치가 크다. 잘못되면 단지 한 기능이 깨지는 게 아니라, 고장 난 시스템의 유일한 관찰 창이 흐려지기 때문이다.</p>

<hr />

<h2>PR이 건드린 지점(모양): VLA를 alloca로 바꾸면, 크기 계산이 곧 계약이 된다</h2>

<p>VLA는 컴파일러/빌드 옵션에 따라 허용되지 않거나 경고가 커질 수 있다. 그래서 VLA를 alloca로 바꾸는 건 흔한 선택지다.</p>

<p>문제는 alloca가 스택에 잠깐 빌린 버퍼라는 점이다. 힙처럼 실패를 처리할 여지가 제한적이고, 크기 계산이 틀리면 바로 다른 스택 프레임을 건드릴 수 있다.</p>

<p>이번 PR은 그 크기 계산이 잘못될 수 있는 모양을 수정한다.</p>

<p>여기선 실제 diff를 그대로 재현하지 않고, 구조만 보이는 의사코드로 적는다. (실제 변경은 PR diff/커밋을 참고.)</p>

<pre><code class=/* (모양) 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

이미지 크레딧/라이선스

댓글

이 블로그의 인기 게시물

Django에서 트랜잭션 관리하기

Django에서 트랜잭션 관리하기 안녕하세요! 오늘은 Django에서 데이터베이스 트랜잭션을 효과적으로 관리하는 방법에 대해 알아보겠습니다. 1. 트랜잭션의 중요성 트랜잭션은 데이터베이스의 일관성과 무결성을 보장하는 중요한 개념입니다. Django에서는 여러 가지 방법으로 트랜잭션을 관리할 수 있습니다. 1.1 기본 개념 원자성(Atomicity) : 트랜잭션은 모두 실행되거나 모두 실행되지 않아야 합니다. 일관성(Consistency) : 트랜잭션 전후로 데이터베이스의 일관성이 유지되어야 합니다. 격리성(Isolation) : 동시에 실행되는 트랜잭션들이 서로 영향을 주지 않아야 합니다. 지속성(Durability) : 완료된 트랜잭션의 결과는 영구적으로 저장되어야 합니다. 2. Django의 트랜잭션 관리 2.1 기본 설정 # settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'mydatabase', 'USER': 'myuser', 'PASSWORD': 'mypassword', 'HOST': 'localhost', 'PORT': '5432', 'ATOMIC_REQUESTS': True, # 모든 뷰를 트랜잭션으로 래핑 } } 2.2 데코레이터 사용 from django.db import transaction @transaction.atomic def create_order(user, items): order = Order.objects.create(user=...

AWS S3 + CloudFront로 정적 파일 서빙 완전 가이드

AWS S3 + CloudFront로 정적 파일 서빙 완전 가이드 안녕하세요! 오늘은 AWS S3와 CloudFront를 사용하여 정적 파일을 효율적으로 서빙하는 방법에 대해 알아보겠습니다. 왜 S3와 CloudFront를 사용할까요? 높은 가용성 : AWS의 글로벌 인프라를 활용 빠른 전송 속도 : CloudFront의 CDN 기능으로 전 세계 사용자에게 빠른 전송 비용 효율성 : 사용한 만큼만 지불 보안 : AWS의 보안 기능 활용 확장성 : 트래픽 증가에 자동 대응 1. S3 버킷 설정 1.1 버킷 생성 및 설정 import boto3 def create_s3_bucket(): s3 = boto3.client('s3') # 버킷 생성 bucket_name = 'your-static-files-bucket' s3.create_bucket( Bucket=bucket_name, CreateBucketConfiguration={ 'LocationConstraint': 'ap-northeast-2' } ) # 버킷 정책 설정 bucket_policy = { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObje...

RDS에서 Django 앱 성능을 높이는 데이터베이스 설정 팁

RDS에서 Django 앱 성능을 높이는 데이터베이스 설정 팁 안녕하세요! 오늘은 AWS RDS를 사용하는 Django 애플리케이션의 성능을 최적화하는 방법에 대해 알아보겠습니다. 1. RDS 인스턴스 최적화 1.1 인스턴스 타입 선택 # RDS 인스턴스 크기 조정 import boto3 def resize_rds_instance(): rds = boto3.client('rds') response = rds.modify_db_instance( DBInstanceIdentifier='your-db', DBInstanceClass='db.t3.large', # 워크로드에 맞는 인스턴스 타입 선택 ApplyImmediately=True ) return response['DBInstance'] 1.2 파라미터 그룹 설정 def create_parameter_group(): rds = boto3.client('rds') # PostgreSQL 파라미터 그룹 생성 response = rds.create_db_parameter_group( DBParameterGroupName='django-optimized', DBParameterGroupFamily='postgres13', Description='Optimized parameters for Django applications' ) # 성능 관련 파라미터 설정 parameters = [ { 'ParameterName': 'shared_buffers', 'ParameterValue': '2GB...