![]()
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
댓글
댓글 쓰기