기본 콘텐츠로 건너뛰기

Stack Pointer and Recursion Safety

thumbnail

CPython: 재귀(RecursionError) 보호가 더 ‘실제 스택 포인터’ 기반으로 정교해졌다 — 왜 중요하냐면

meta_description: CPython이 _Py_get_machine_stack_pointer()를 “진짜 머신 스택 포인터(sp/rsp)”로 반환하도록 바꾸면서, 재귀 한계/스택 오버플로 보호 로직이 더 일관되게 동작하도록 개선됐다(GH-126910). 이 글은 어떤 문제가 있었는지(프레임 주소 vs 스택 포인터, 스택 전환 코너 케이스), 무엇이 바뀌었는지(soft/hard limit 체크 정리), 그리고 임베딩/샌드박스/깊은 파서 입력처럼 재귀가 자주 터지는 실무 환경에서 어떤 이득이 있는지 설명한다. meta_keywords: python,cpython,recursion,RecursionError,stack pointer,_Py_get_machine_stack_pointer,stack overflow,soft limit,hard limit,Py_FatalError,sanitizers,UBSan,embedding,parser,xml,security,robustness,debugging,limits meta_robots: index,follow

재귀 제한은 파이썬에서 흔히 “그냥 RecursionError 뜨는 거”로만 보이지만, CPython 입장에서는 꽤 민감한 영역이다.

  • 너무 늦게 막으면: C 스택을 진짜로 넘어서 프로세스 크래시로 간다
  • 너무 일찍 막으면: 정상적인 입력/코드도 쓸데없이 RecursionError가 난다
  • 예외를 던지는 순간에도 스택이 충분히 남아 있어야 한다(안 그러면 예외 처리 중에 또 터진다)

이번 CPython PR(GH-126910)은 이 방어선을 한 단계 더 “하드웨어에 가깝게” 정리한다.

핵심만 말하면:

  • _Py_get_machine_stack_pointer()가 이제 “프레임 주소 비슷한 값”이 아니라
  • 실제 스택 포인터(sp/rsp)를 읽어서 반환하도록 바뀌었다

겉보기엔 내부 구현 변경인데, 이런 변화는 임베딩/샌드박스/파서 입력(예: XML)처럼 재귀가 자주 문제 되는 환경에서 안정성에 영향을 준다.


1) 왜 스택 포인터를 ‘진짜’로 읽는 게 중요할까

기존 코드에서는 플랫폼에 따라 스택 위치를 추정할 때,

  • __builtin_frame_address(0) 같은 “프레임 주소” 계열을 쓰거나
  • fallback으로 스택에 있는 지역 변수 주소(&here)를 쓰는 식의 방법이 섞여 있었다.

이런 값들은 대부분 “대충 스택 근처”는 맞지만, 상황에 따라 정확히 원하는 값(현재 SP)과 차이가 날 수 있다.

그리고 재귀/스택 보호는 경계값에서 동작하는 로직이라,

  • 경계 근처에서 흔들리면
  • “어떤 환경에서는 RecursionError로 끝나는데, 어떤 환경에서는 FatalError/크래시”

같은 이상한 차이가 생길 수 있다.

이번 PR은 아예 아키텍처별로 스택 포인터를 읽는 경로를 명시한다.

  • aarch64: inline asm로 sp
  • x86_64: inline asm로 rsp
  • MSVC x64: _AddressOfReturnAddress()
  • 그 외: 지역 변수 주소 fallback

즉, 가능한 곳에서는 “진짜 SP”를 쓰도록 한다.


2) soft limit / hard limit 체크가 어떻게 정리됐나

PR diff를 보면, 재귀 체크 흐름이 더 명확해진다.

2-1) ‘재귀 한계에 도달했는가?’는 soft limit 기준으로 간단히

기존에는 soft limit 주변의 마진(2 * STACK_MARGIN)을 같이 끼워서

  • “지금이 정말 위험 구간인가?”를 섞어 판단하는 코드가 있었다.

이번 변경은 이 단계를 단순화한다.

  • 먼저 “soft limit을 넘어섰나?”만 본다
  • 그 다음에 _Py_CheckRecursiveCall()이 코너 케이스를 처리한다

이건 실무적으로 좋은 방향이다.

  • 빠른 체크는 단순하게
  • 복잡한 예외/오류 보고는 한 곳에서

2-2) hard limit은 “정말 위험할 때”만 Fatal로 간다

_Py_CheckRecursiveCall() 안에서 hard limit을 넘었을 때는

  • 너무 멀리 벗어나면(stack switching 같은 상황을 의심) 조용히 0을 반환(= 여기서는 막지 않음)
  • hard limit 근처에서 넘었으면 “예외를 던질 스택도 없다”고 보고 FatalError

즉, “진짜 스택 오버플로 상황”과 “스택 전환/특수 스레딩 환경”을 구분하려는 의도가 들어가 있다.

이 부분이 왜 중요하냐면, 임베딩이나 사용자 공간 스레드(그린 스레드) 같은 환경에서는 스택 경계 추정이 틀릴 수 있기 때문이다.


3) UBSan(Undefined Behavior Sanitizer) 환경도 고려된다

PR에는 _Py_UNDEFINED_BEHAVIOR_SANITIZER 감지 매크로가 추가되고,

  • 스택 마진(log2)이 조정되는 조건에 UBSan이 포함된다.

이건 일반 사용자 입장에선 티가 안 나지만,

  • CI
  • 디버그 빌드
  • 산티나이저 기반 테스트

에서 “재귀/스택 관련 테스트가 더 일관되게 통과/실패한다”는 쪽의 의미가 있다.


4) 실무에서 어디가 체감될까

대부분의 서비스 코드는 재귀를 직접 쓰지 않아도, 아래 영역에서 재귀가 등장한다.

(A) 파서 입력(예: XML, 수식, 중첩 구조)

PR에서 테스트 수정이 들어간 것도 test_pyexpat의 “깊게 중첩된 컨텐츠 모델” 케이스다.

핵심은:

  • RecursionError로 안전하게 끝나야 한다
  • 크래시하면 안 된다

이건 단순한 안정성 이슈를 넘어, 입력 기반 공격 표면에서도 중요하다.

(B) 임베딩/플러그인/샌드박스

한 프로세스 안에서 파이썬을 “부분적으로” 돌리는 환경은

  • 스레드/스택 모델이 표준적인 것과 다를 수 있고
  • 이런 환경에서 스택 경계 판단이 애매하면
  • 재귀 보호 로직이 뜻밖의 방식으로 동작할 수 있다.

“가능하면 진짜 SP를 쓰자”는 방향은 이런 환경에서 특히 낫다.

(C) 관측/장애 대응

현장에서 가장 짜증나는 건 이거다.

  • 어떤 입력은 RecursionError인데
  • 어떤 입력은 갑자기 프로세스가 죽는다

원인이 스택 경계값/플랫폼 차이일 수 있으면, 추적이 어렵다.

이번 변경은 그런 ‘환경별 흔들림’을 줄이는 쪽에 가깝다.


5) 내가 할 일: 일반 사용자 vs 라이브러리/플랫폼 개발자

  • 일반 사용자: 할 일 거의 없음. 다만 “깊게 중첩된 입력”을 다룬다면, 파이썬 업그레이드 우선순위에서 이런 안정성 패치를 무시하지 않는 편이 좋다.

  • 라이브러리/플랫폼 개발자:

    • 재귀가 깊어질 수 있는 입력을 받는 API라면, RecursionError를 정상 경로로 취급(= 잡아서 안전하게 실패 응답)
    • 테스트에 “매우 깊은 중첩” 케이스를 넣어 크래시가 아닌 예외로 끝나는지 고정

추가로 현실적인 팁 하나.

  • “RecursionError는 버그다”라고 몰아붙이기보다
  • “깊은 중첩 입력은 원래 실패해야 한다(그리고 실패는 예외로 끝나야 한다)”

라고 제품 요구사항 수준에서 정해두는 게 좋다.

예:

  • XML/JSON 같은 입력은 최대 깊이를 두고
  • 그 깊이를 넘으면 400/422 같은 에러로 안전하게 종료
  • 로그엔 입력 전체를 찍지 말고(폭발 위험) 깊이/길이 같은 메타만 남기기

이런 운영 규칙이 있으면, 파이썬 런타임의 재귀 보호 로직이 조금 바뀌어도 서비스가 덜 흔들린다.


6) 결론: ‘재귀 예외’는 기능이 아니라 안전장치다

RecursionError는 불편한 예외가 아니라, 런타임이 “여기서 더 가면 죽는다”라고 말해주는 안전장치다.

이번 GH-126910 같은 변경은

  • 그 안전장치가 더 정확한 센서(진짜 스택 포인터)를 쓰게 만들고
  • 특수 환경(스택 전환/임베딩/산티나이저)에서도 더 일관되게 동작하도록

정리하는 방향이다.

표준 사용자에게는 조용한 패치지만, 운영/플랫폼 쪽에서는 이런 조용한 패치가 결국 가장 큰 차이를 만든다.

한 줄 요약:

  • “재귀 제한은 성능 옵션이 아니라, 프로세스 생존을 위한 안전장치다.”

이 PR이 당장 API를 바꾸진 않지만, “RecursionError가 뜨는지/크래시로 죽는지” 같은 운영 결과에는 영향을 줄 수 있는 종류의 변경이다.

그래서 깊은 중첩 입력을 다루는 서비스라면, 이런 내부 변경도 릴리스 노트에서 한 번쯤 눈여겨볼 가치가 있다.


Keywords

python,cpython,recursion,RecursionError,stack pointer,stack overflow,soft limit,hard limit,Py_FatalError,UBSan,embedding,parser,xml,security,robustness,debugging


이미지 크레딧/라이선스

  • Gaming PC (Unsplash).jpg — Maxime Rossignol / CC0
    • https://commons.wikimedia.org/wiki/File:Gaming_PC_(Unsplash).jpg
    • http://creativecommons.org/publicdomain/zero/1.0/deed.en

댓글

이 블로그의 인기 게시물

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...