기본 콘텐츠로 건너뛰기

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를 돌리며 경...