기본 콘텐츠로 건너뛰기

3월, 2026의 게시물 표시

Fixing a Crash in SNI Callbacks

CPython: SNI 콜백에서 ‘죽은 SSL 객체’ 참조로 크래시 나던 케이스가 고쳐졌다(gh-146080) meta_description: CPython에서 SNI(server_name) 콜백이 호출되는 타이밍에 SSLSocket/SSLObject가 이미 GC로 사라진 경우, 내부 C 콜백이 NULL을 DECREF 하며 크래시할 수 있던 버그가 수정됐다(gh-146080). 이 글은 어떤 상황에서 발생하는지, 파이썬 코드 레벨에서 무엇을 조심해야 하는지, 그리고 라이브러리 작성자가 테스트로 재현/회귀 방지하는 방법을 정리한다. meta_keywords: python,cpython,ssl,sni,servername callback,wrap_bio,MemoryBIO,garbage collection,crash,segfault,SSLSocket,SSLObject,OpenSSL,AWS-LC,핸드셰이크,약한참조,콜백,테스트,회귀 meta_robots: index,follow SNI(Server Name Indication) 콜백은 “TLS 핸드셰이크 도중, 클라이언트가 보낸 server name(hostname)에 따라 인증서/컨텍스트를 바꾸고 싶다” 같은 고급 케이스에서 사용된다. 문제는 이 콜백이 엄청 이른 시점(핸드셰이크 중간) 에 실행되고, 구현 실수로 SSLSocket / SSLObject 의 생명주기를 잘못 다루면 “파이썬 예외”가 아니라 프로세스 크래시 로 튈 수 있다는 점이다. 지난 72시간 내 merged 된 CPython 변경(gh-146080)은 딱 그 모서리를 하나 메운다. SNI 콜백이 호출되려는 순간 콜백의 소유자(SSLSocket/SSLObject)가 이미 GC로 사라져 내부 포인터가 NULL이 될 수 있는 상황에서 NULL을 Py_DECREF() 하며 크래시할 수 있던 부분을 Py_XDECREF() 로 바꿔서 “정상적인 SSL 에러”로 처리하게 했다. 1) 무엇이 고쳐졌나: 한 줄이...

Propagate -X to child processes

`multiprocessing` 자식 프로세스에서만 -X 옵션이 사라진다면: PR #146005가 정리한 “인터프리터 플래그 전달” meta_description: CPython PR #146005는 subprocess/multiprocessing이 자식 파이썬 프로세스를 띄울 때 -X 옵션을 누락 없이 전달하도록 수정한다. dev/tracemalloc/importtime 등 런타임 디버그 옵션이 부모에서는 켜졌는데 자식에서만 꺼지는 문제를 줄인다. 운영/디버깅 관점에서 어떤 증상이 사라지는지, 어떤 -X 옵션이 특히 중요하며, 호환성/주의점은 무엇인지 정리한다. meta_keywords: python, multiprocessing, subprocess, -X, xoptions, dev mode, tracemalloc, importtime, faulthandler, debugging, child process, CI, production, practical meta_robots: index,follow 운영 디버깅에서 제일 짜증나는 버그 유형이 있다. 부모 프로세스에서는 디버그 옵션이 켜져 있는데 자식 프로세스(특히 multiprocessing 으로 띄운)에서는 꺼져 있다 그래서 로그가 갈라지고, 재현이 흐려진다. -X tracemalloc 은 켰는데, 자식에서 스냅샷이 없다 -X importtime 켰는데, 자식 프로세스의 import 시간은 안 찍힌다 -X dev 켰는데, 자식에서만 경고가 안 뜬다 PR #146005(gh-146004)는 이 ‘갈라짐’을 줄이는 방향으로 subprocess 쪽 플래그 전달을 정리한다. 핵심은 간단하다. 부모 인터프리터에 설정된 -X 옵션(xoptions)을, 자식 파이썬 프로세스 실행 인자에 일관되게 전달한다. 1) PR #146005가 바꾼 것: “일부만 전달” → “전부 전달(정렬해서)” diff의 핵심은 Lib/subprocess.py ...

OrderedDict popitem leak fix

`OrderedDict.popitem()`을 돌렸는데 메모리가 내려오지 않는다: PR #146537이 손본 ‘가능한 누수’의 실전 증상 meta_description: CPython PR #146537은 OrderedDict의 popitem() 경로에서 발생할 수 있는 메모리 누수 가능성을 수정한다. 반복 popitem을 쓰는 캐시/큐/스케줄러에서 “요소는 지웠는데 RSS가 안 내려가는” 현상이 왜 나올 수 있는지, 어떤 버전에서 조심해야 하는지, 그리고 운영 코드에서 진단(tracemalloc/heap snapshot)과 완화(업그레이드, 패턴 변경)를 실무적으로 정리한다. meta_keywords: python, OrderedDict, popitem, memory leak, RSS, tracemalloc, CPython, cache, queue, eviction, dict, reference, practical, 운영, 진단 meta_robots: index,follow 운영에서 메모리 이슈는 늘 불쾌하다. 특히 더 불쾌한 건 이런 형태다. 캐시에서 항목을 “계속 지운다” 모니터링 그래프에서 객체 개수도 줄어든다 그런데 RSS가 안 내려온다(혹은 아주 천천히만) 이때 팀은 서로 다른 결론으로 갈라진다. “파이썬은 원래 메모리 반환을 안 해” “어딘가 참조가 남았어(진짜 leak)” “컨테이너가 찢어져서 조각난 거야(fragmentation)” 정답은 케이스 바이 케이스인데, 이런 논쟁의 바닥엔 늘 같은 질문이 있다. “내 코드가 지운 게 진짜로 지워졌나?” CPython PR #146537은 이 질문에 직접 연결되는 작은 수정이다. [3.14] Fix possible memory leak in OrderedDict popitem 이 글은 PR을 곧이곧대로 요약하기보다, “운영에서 popitem이 등장하는 상황”에 맞춰서 설명한다. 1) 왜 popitem() 이 실무에서 중요하...

Empty WHEEL_PKG_DIR: ensurepip CWD trap

`WHEEL_PKG_DIR=''`가 CWD를 훑는 순간: PR #146357이 막아준 ensurepip의 ‘빈 문자열 함정’ meta_description: CPython PR #146357은 ensurepip에서 WHEEL_PKG_DIR 이 빈 문자열일 때 Path('') 가 현재 작업 디렉터리(CWD)로 해석되어, 의도치 않게 CWD에서 wheel 파일을 찾는 문제를 수정한다. 운영/빌드 환경에서 설정 값이 빈 문자열로 들어가는 흔한 케이스와, 이를 방어하는 패턴(환경변수/빌드 플래그 검증, 경로 truthiness 체크)을 실무 관점으로 정리한다. meta_keywords: python, ensurepip, WHEEL_PKG_DIR, sysconfig, wheel, pip, virtualenv, build, CWD, Path(’’), environment variable, packaging, security, CI, practical, 운영 meta_robots: index,follow ensurepip 은 평소엔 존재감이 없다가, 한 번 문제를 만나면 굉장히 현실적인 방식으로 사람을 괴롭힌다. “왜 이 환경에선 pip 부트스트랩이 이상하지?” “왜 현재 폴더에서 wheel을 찾지?” “왜 CI에서만 재현되지?” PR #146357(gh-146310)은 그 중에서도 아주 작은 조건 하나 가 얼마나 큰 행동 변화를 만드는지를 보여준다. 핵심은 이거다. WHEEL_PKG_DIR 가 빈 문자열( '' )이면, Path('') 가 CWD로 해석되어 ensurepip이 현재 디렉터리 에서 wheel 파일을 찾게 될 수 있다. 이건 “버그 같다”로 끝나는 문제가 아니다. 빌드/패키징 파이프라인이 예기치 않게 CWD에 의존하게 되고 특정 폴더에서만 동작이 달라지고 보안/무결성 관점에서도 찝찝한 동작이 된다 1) PR #146357이 ...

JSON Array Hook: Control Lists

`json.loads()`가 리스트를 그냥 리스트로만 만들던 시대가 끝난다: PR #146441의 `array_hook` 실전 용도 meta_description: CPython PR #146441은 JSON 디코더에 array_hook 파라미터를 추가해, JSON 배열을 파싱할 때 리스트 대신 원하는 타입으로 변환할 수 있게 했다. tuple/커스텀 리스트/검증 래퍼 등으로 일괄 변환하는 방법과, object_hook/object_pairs_hook과의 차이, 운영 코드에서 실수하기 쉬운 포인트(성능/메모리/예외 처리)를 정리한다. meta_keywords: python, json, loads, JSONDecoder, array_hook, object_hook, object_pairs_hook, decoder, parsing, tuple, list, validation, performance, CPython, practical, 운영, 변환 meta_robots: index,follow JSON을 오래 만져본 사람은 다 안다. object_hook 는 있다(딕셔너리 후처리) object_pairs_hook 도 있다(순서 보존/중복 키 처리) 근데 이상하게도 “배열”에는 훅이 없었다. 그래서 우리는 늘 한 번 더 손을 댔다. loads() 로 일단 파싱하고 리스트를 전부 걷어내며(재귀) tuple로 바꾸거나, 커스텀 컨테이너로 감싸거나, 값 검증을 한다 PR #146441(gh-146440)은 그 빈 칸을 메운다. JSON 디코더에 array_hook 가 들어간다. 이제 “배열이 파싱되는 순간” 리스트를 다른 타입으로 바꾸는 길이 열린다. 1) 뭐가 추가됐나: load/loads 와 JSONDecoder 에 array_hook diff를 보면 핵심은 단순하다. json.load() / json.loads() 에 array_hook= 인자가 추가된다 json.JSONDeco...

Audit Hooks Can Leak: Socket Fix

감사(audit) 훅이 소켓을 느리게 죽인다: PR #146248이 보여준 관측 코드의 메모리 누수 패턴 meta_description: CPython PR #146248은 socket 모듈에서 audit hook을 통해 참조(reference)와 버퍼(buffer)가 새는 문제를 수정했다. 보안/관측을 위해 sys.addaudithook를 붙였는데, 의도치 않게 객체 수명을 늘려 메모리 누수처럼 보일 수 있다. 운영 코드에서 audit hook를 안전하게 쓰는 패턴(최소 정보만 복사, 객체 저장 금지, 버퍼 처리, 샘플링/비동기화)을 정리한다. meta_keywords: python, socket, audit hook, sys.addaudithook, CPython, memory leak, reference leak, buffer leak, observability, security, GC, tracemalloc, weakref, logging, sampling, asyncio, 운영, 메모리 meta_robots: index,follow 운영에서 메모리가 서서히 오르는 서비스를 잡다 보면, 진짜 원인이 비즈니스 로직이 아닌 경우가 꽤 많다. 디버그 로그 트레이싱/계측(telemetry) 보안/감사(audit) 즉, 관측 코드 가 시스템을 갉아먹는 케이스. CPython PR #146248(gh-146245)은 이걸 아주 교과서적으로 보여준다. socket 모듈이 audit hook과 엮이는 과정에서 reference/buffer leak 이 생길 수 있었고, 그걸 고쳤다. 이 글은 PR을 번역하려는 글이 아니다. 실무에서 바로 쓰는 관점으로 정리한다. audit hook가 왜 메모리 누수처럼 보일 수 있는지 sys.addaudithook를 붙일 때 뭘 저장하면 위험한지 로그를 남기면서도 객체 수명을 늘리지 않는 방법 1) PR #146248 한 줄 요약: 감사 훅이 데이터를 잡고 있었다 ...

Shell Returncodes: -N vs 128+N

`shell=True`인데 returncode가 음수가 아니라서 당황했다면: PR #146255가 정리한 “셸의 책임” meta_description: POSIX에서 subprocess의 returncode는 보통 신호 종료면 -N이라고 배웠지만, shell=True 나 asyncio.create_subprocess_shell() 에서는 그 규칙이 깨질 수 있다. CPython PR #146255는 “returncode는 셸의 종료 상태를 반영하며, 신호를 128+N 같은 코드로 매핑할 수 있다”는 점을 문서로 명확히 했다. 운영 코드에서 재현/로깅/알람을 어떻게 설계해야 덜 흔들리는지 정리한다. meta_keywords: subprocess, asyncio, returncode, shell=True, create_subprocess_shell, create_subprocess_exec, POSIX, signal, 128+N, Bash, exit status, SIGTERM, SIGKILL, 프로세스, 종료코드, 운영, 재현, 로깅, 알람, 파이썬 meta_robots: index,follow 운영하다 보면 이 상황을 한 번은 만난다. 프로세스가 SIGTERM으로 죽었으니 returncode == -15 일 거라고 생각했는데 실제 로그에는 143 이 찍혀 있다(= 128 + 15) 어떤 경우는 또 -15 로 찍힌다 그래서 대시보드가 갈라지고, “이번 장애는 신호 종료냐? 정상 종료냐?” 분류가 흔들린다. CPython PR #146255는 이 혼란의 원인을 문서로 깔끔하게 정리한다. 핵심은 한 줄이다. shell=True 면 returncode는 ‘자식 프로세스’가 아니라 ‘셸(/bin/sh)의 종료 상태’를 반영한다. 셸은 신호를 그대로 “음수”로 내보내지 않을 수 있고, 대신 128+N 같은 규칙으로 매핑할 수 있다(참고자료). 1) PR #146255가 실제로 추가한 문장(요약) diff를...

When Exceptions Change: struct.Struct Boundaries

struct.Struct가 던지는 예외가 바뀌면, 내 장애 대응도 바뀐다: PR #145851 읽는 법 meta_description: CPython PR #145851은 struct.Struct의 내부 구현을 손보면서, 비ASCII 포맷 처리와 초기화되지 않은 객체의 속성 접근에서 던지는 예외 타입을 더 일관되게 정리했다. “예외 타입 하나쯤”이 테스트/에러 핸들링/호환성에서 어떤 차이를 만들 수 있는지, diff에서 어디를 보고 내 코드에 무엇을 점검할지 정리한다. meta_keywords: struct, Struct, format, ValueError, UnicodeEncodeError, UnicodeDecodeError, AttributeError, RuntimeError, CPython, PR145851, non-ASCII, surrogateescape, bytes, str, exception, 테스트, 호환성, 에러처리, 파이썬 meta_robots: index,follow 운영에서 제일 성가신 버그는 “같은 입력인데 어떤 환경에서만 터지는 것”이다. 그중에서도 예외 타입이 바뀌는 종류는 더 귀찮다. 코드는 except UnicodeEncodeError: 로 잡고 있었는데, 어느 날부터 ValueError 가 날아오고 테스트는 “정확한 예외 클래스”를 기대하고 있어서 갑자기 줄줄이 깨지고 로그/알람은 예외 이름을 키로 집계해서, 그래프가 한순간에 찢어진다 CPython PR #145851은 딱 그 종류의 변화다. 겉으로는 “구현 디테일 변경”인데, 실제로는 예외의 계약(contract) 을 조금 더 선명하게 만든다. PR이 말하는 변화 3가지(그리고 왜 실무에선 체감이 큰지) PR 페이지 요약과 diff를 보면 변화는 세 묶음으로 정리된다. 1) non-ASCII 문자열 포맷 을 넣을 때: UnicodeEncodeError 대신 ValueError 2) non-ASCII bytes 포맷 을...

One Line, Two Writes: When pprint Breaks

`stream.write` 한 줄이 만든 차이: pprint가 커스텀 스트림에서 깨지는 이유를 고친 PR #145894 meta_description: pprint.PrettyPrinter가 frozendict를 출력할 때 write = stream.write 를 캐시해놓고도 다시 stream.write(...) 를 호출하던 실수를 PR #145894가 고쳤다. 출력은 단순하다는 가정이 깨지는 순간(래퍼/프록시/테스트 스트림)에서 어떤 증상이 생기는지, 내 코드에서 어떻게 빨리 재현·방어할지 정리한다. meta_keywords: pprint, PrettyPrinter, stream, write, frozendict, CPython, PR145894, GH145887, logging, proxy, wrapper, io, StringIO, TextIOBase, monkeypatch, regression, 테스트, 출력, 버그, 파이썬 meta_robots: index,follow 로그나 리포트 출력은 보통 그냥 문자열 찍는 것으로 취급된다. 그래서 더 골치 아프다. 출력이 깨지면 대개 기능이 멈추는 게 아니라, 디버깅이 멈춘다. 이번 CPython PR #145894는 그 전형적인 케이스다. 바뀐 건 한 줄인데, 가끔만 깨지는 출력 버그를 없앤다. PR이 바꾼 건 한 줄이다: write = stream.write 를 해놓고 왜 다시 stream.write 를 불렀나 PR #145894가 건드린 곳은 pprint.PrettyPrinter._pprint_frozendict() 다. diff를 보면 함수 시작에서 이렇게 한다. write = stream.write 이건 흔한 패턴이다. stream.write 를 로컬 변수로 잡아두면(lookup/cache) 그 아래에서 호출이 조금 싸지고 커스텀 스트림이 들어와도 write라는 기능만 있으면 된다는 의도를 코드에 남긴다 그런데 버그는 이런 식으로 생긴다....

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

스택에 잠깐 빌린 버퍼가 발목 잡는 순간: 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에서 바뀐 한 줄이 왜 위험할 수 있었는지, 그리고 비슷한 류의 버그가 어떤 환경에서 튀어나오는지 감각을 남기는 쪽이다. 내가 이걸 실무 이슈...

When repr Locks: Why functools Wraps PyDict_Next in a Critical Section

“repr 한 줄”이 락이 되는 순간: free-threaded에서 functools가 PyDict_Next를 critical section으로 감싼 이유 meta_description: free-threaded 환경에서 dict 순회는 ‘그냥 읽기’가 아니라 동시성 계약의 일부가 된다. gh-145446(PR #145487)는 functools 내부에서 PyDict_Next로 kwargs 등을 순회하는 루프를 critical section으로 감싸고, 에러 처리/정리 순서를 바꿔 경계면을 명확히 했다. 어떤 루프가 왜 바뀌었는지, 실무에서 확장 모듈/FFI의 dict 순회를 어떻게 짜야 덜 흔들리는지 정리한다. meta_keywords: functools, free-threaded, critical section, CPython, PyDict_Next, kwargs, partial, repr, dict iteration, error handling, cleanup, lock, GIL-less, 동시성, data race, refcount, C extension, FFI, 안정성 meta_robots: index,follow free-threaded 쪽 변경을 보다 보면, 가끔 “이게 왜 여기서?” 싶은 지점이 나온다. 이번 gh-145446 / PR #145487이 그렇다. 사람들이 보통 위험하다고 생각하는 건 네이티브 확장이나 I/O 같은 데다. 그런데 패치가 들어간 곳은 functools 다. 더 정확히는 kwargs 같은 dict를 순회하는 루프. 이게 재미있는 이유는, 이 경로가 대부분의 서비스에서 ‘겉보기 무해한 곳’으로 쓰이기 때문이다. 로그에서 객체를 찍는 repr 캐시 키를 만들기 위해 kwargs를 정렬/나열하는 코드 에러 메시지에 함수/인자를 조금 더 친절하게 담는 코드 이런 건 기능이 아니라 운영이다. 운영 코드가 동시성과 만나는 순간, “그냥 출력”이 “잠깐의 락”이 된다. 이번 PR은 “큰...

When Observability Races: Fixing list.__sizeof__ Data Races in Free-Threaded CPython

“용량은 그냥 숫자”라고 믿었던 대가: free-threaded에서 list.__sizeof__ 레이스가 생기는 지점 meta_description: free-threaded 빌드에서 list.__sizeof__가 리스트의 capacity 정보를 읽는 순간, 다른 스레드의 리사이즈와 맞물려 데이터 레이스가 날 수 있다. gh-145036(PR #145365)이 어떤 값을 어떻게 읽도록 바꿨는지, 그리고 실무에서 관찰/측정 코드를 어떻게 다뤄야 하는지 정리한다. meta_keywords: free-threaded, CPython, list, __sizeof__, capacity, allocated, data race, atomic, TSAN, thread sanitizer, list_resize, ob_item, memory model, 관찰코드, 측정, 디버깅, 동시성, GIL-less meta_robots: index,follow 리스트의 __sizeof__() 는 대개 “큰 의미 없는 관찰”로 취급된다. 로그에 한 번 찍고, 메모리 사용량 대충 추정하고, 병목을 찾아보는 정도. 나도 그렇게 써왔다. 그런데 free-threaded(실험적 GIL-less 방향)로 가면, 이런 관찰 코드도 동시성의 일부가 된다. “안전하게 실패하는 관찰”이 아니라 “관찰 자체가 경쟁에 끼어드는 순간”이 생긴다. 이번 gh-145036 / PR #145365는 그 지점을 건드린다. 요지는 과장하면 간단하다. 다른 스레드가 리스트의 내부 버퍼를 키우거나 줄이는 동안(capacity가 바뀌는 동안) 한 스레드가 list.__sizeof__() 에서 capacity 관련 값을 읽으면 그 읽기가 레이스로 잡힐 수 있다 여기서 중요한 건 “이 버전은 안전/위험” 같은 결론이 아니다. PR이 고친 건 특정 경로에서의 읽기 방식이고, 그게 왜 문제였는지를 이해하면 실무에서 얻을 힌트가 생긴다. 특히 TSAN으로 free-threaded를 돌리며 경...