PEP 768 – CPython용 안전한 외부 디버거 인터페이스
- Author:
- Pablo Galindo Salgado <pablogsal at python.org>, Matt Wozniski <godlygeek at gmail.com>, Ivona Stojanovic <stojanovic.i at hotmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Type:
- Standards Track
- Created:
- 25-Nov-2024
- Python-Version:
- 3.14
- Post-History:
- 11-Dec-2024
- Resolution:
- 17-Mar-2025
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 디버거와 프로파일러가 실행 중인 Python 프로세스에 안전하게 연결할 수 있도록 CPython에 오버헤드가 없는 디버깅 인터페이스를 추가할 것을 제안합니다. 이 인터페이스는 인터프리터의 일반 실행 경로를 수정하거나 런타임 오버헤드를 추가하지 않고 디버거 코드를 연결할 수 있는 안전한 실행 지점을 제공합니다.
이 인터페이스의 주요 활용 사례는 프로세스 ID를 사용하여 gdb -p와 유사하게 pdb가 실행 중인 프로세스에 연결할 수 있도록 하는 것이며, 이를 통해 개발자는 Python 애플리케이션을 중지하거나 다시 시작하지 않고 실시간으로 대화식 검사와 디버깅을 수행할 수 있습니다.
동기
프로덕션 및 실시간 환경에서 Python 프로세스를 디버깅하는 일은 고유한 어려움을 제시합니다. 개발자는 서비스를 중지하거나 다시 시작하지 않고 애플리케이션 동작을 분석해야 하는 경우가 많으며, 이는 고가용성 시스템에서 특히 중요합니다. 일반적인 상황으로는 교착 상태 진단, 메모리 사용량 검사 또는 실시간으로 예기치 않은 동작 조사 등이 있습니다.
실행 중인 프로세스에 연결할 수 있는 Python 도구는 매우 적으며, 주된 이유는 이를 수행하려면 운영 체제 디버깅 인터페이스와 CPython 내부 구조 모두에 대한 깊은 전문 지식이 필요하기 때문입니다. GDB 및 LLDB와 같은 C/C++ 디버거는 잘 알려진 기법을 사용하여 프로세스에 연결할 수 있지만, Python 도구는 이러한 저수준 메커니즘을 모두 구현하는 동시에 추가적인 복잡성도 처리해야 합니다. 예를 들어 GDB가 대상 프로세스에서 코드를 실행해야 할 때는 다음을 수행합니다.
- ptrace를 사용하여 실행 가능한 메모리의 작은 영역을 할당합니다(말처럼 쉽지는 않습니다).
- 작은 기계어 시퀀스를 작성합니다 - 일반적으로 함수 프롤로그, 원하는 명령어 및 레지스터를 복원하는 코드로 구성됩니다.
- 대상 스레드의 모든 레지스터를 저장합니다.
- 명령어 포인터를 주입된 코드로 변경합니다.
- 주입된 코드의 끝에 있는 중단점에 도달할 때까지 프로세스를 실행합니다.
- 원래 레지스터를 복원하고 실행을 계속합니다.
Python 도구도 이와 동일한 코드 주입 문제에 직면하지만, 여기에 추가적인 복잡성 계층이 더해집니다. 이러한 도구는 위 메커니즘을 구현해야 할 뿐만 아니라 인터프리터 루프, 가비지 컬렉터, 스레드 상태 및 참조 카운팅 시스템을 비롯한 CPython의 런타임 상태를 이해하고 안전하게 상호작용해야 합니다. 이러한 저수준 시스템 조작과 깊은 도메인별 인터프리터 지식의 결합으로 인해 Python 디버깅 도구를 구현하는 일은 매우 어려워집니다.
이를 시도하는 소수의 도구(예를 들어 DebugPy 및 Memray)는 GDB 및 LLDB와 같은 시스템 디버거를 사용하여 코드를 강제로 주입하는 차선책이자 안전하지 않은 방법에 의존합니다. 주입된 코드가 인터프리터의 실행 주기 중 어느 시점에서든 실행될 수 있기 때문에 이 접근 방식은 근본적으로 안전하지 않습니다 - 메모리 할당, 가비지 컬렉션 또는 스레드 상태 관리와 같은 중요한 작업 중에도 실행될 수 있습니다. 이 경우 결과는 치명적입니다. 이미 malloc()안에 있는 동안 메모리를 할당하려고 하면 충돌이 발생하고, 가비지 컬렉션 중에 객체를 수정하면 인터프리터 상태가 손상되며, 잘못된 시점에 스레드 상태에 접근하면 교착 상태가 발생합니다.
다양한 도구는 주입된 코드를 위한 별도 스레드를 생성하거나, 작업 타이밍을 신중하게 조정하거나, 프로세스를 중지할 적절한 지점을 선택하려고 하는 등 복잡한 우회 방법을 통해 이러한 위험을 최소화하려고 합니다. 그러나 이러한 완화책으로는 근본적인 문제를 완전히 해결할 수 없습니다. 인터프리터의 협력이 없다면 어떤 순간에든 코드를 실행해도 안전한지 알 방법이 없기 때문입니다. 신중하게 구현된 도구조차 인터프리터와 협력하는 것이 아니라 근본적으로 인터프리터에 맞서 작동하기 때문에 인터프리터를 충돌시킬 수 있습니다.
근거
안전하지 않은 코드 주입으로 도구가 인터프리터의 한계를 우회하도록 강요하는 대신, 안전한 실행을 보장하는 적절한 디버깅 인터페이스를 CPython에 확장하여 추가할 수 있습니다. 몇 가지 스레드 상태 필드를 추가하고 인터프리터의 기존 평가 루프와 통합하면, 디버깅 작업이 명확하게 정의된 안전 지점에서만 발생하도록 보장할 수 있습니다. 이를 통해 정상 실행 중 오버헤드가 없는 상태를 유지하면서 충돌과 손상의 가능성을 제거할 수 있습니다.
핵심은 임의의 지점에 코드를 주입할 필요가 없다는 것입니다. 다음 안전한 기회에 코드를 실행하기를 원한다는 신호를 인터프리터에 보내기만 하면 됩니다. 이 접근 방식은 인터프리터의 자연스러운 실행 흐름에 맞춰 작동하며, 그 흐름에 맞서지 않습니다.
이 아이디어를 PyPy 개발 팀에 설명한 후, 이 제안은 이미 PyPy에서 구현되었으며, 이는 그 실현 가능성과 효과를 모두 입증합니다. 이들의 구현은 정상 실행 중 런타임 오버헤드를 전혀 발생시키지 않으면서 안전한 디버깅 기능을 제공할 수 있음을 보여 줍니다. 제안된 메커니즘은 현재 디버깅 접근 방식과 관련된 위험을 줄일 뿐만 아니라 향후 개선을 위한 기반도 마련합니다. 예를 들어, 이 프레임워크를 널리 사용되는 옵저버빌리티 도구와 통합하여 인터프리터 성능이나 메모리 사용량에 대한 실시간 정보를 제공할 수 있습니다. 이 인터페이스의 유력한 사용 사례 중 하나는 pdb가 실행 중인 Python 프로세스에 연결되도록 하는 것입니다. 이는 gdb가 사용자로 하여금 프로세스 ID로 프로그램에 연결할 수 있게 하는 방식(gdb -p <pid>)과 유사합니다. 이 기능을 사용하면 개발자는 실행 중인 애플리케이션의 상태를 검사하고, 식을 평가하며, 코드를 동적으로 단계별로 실행할 수 있습니다. 이 접근 방식은 Python의 디버깅 기능을 이러한 모드를 지원하는 다른 주요 프로그래밍 언어 및 디버깅 도구의 기능과 일치시킬 것입니다.
사양
이 제안은 외부 프로세스가 잘 정의된 안전 지점에서 Python 인터프리터의 코드 실행을 트리거할 수 있도록 하는 안전한 디버깅 메커니즘을 도입합니다. 핵심은 시스템 디버거를 통해 직접 코드를 주입하는 대신, 인터프리터의 기존 평가 루프와 스레드 상태를 활용하여 디버깅 작업을 조정할 수 있다는 것입니다.
이 메커니즘은 디버거가 대상 프로세스의 특정 메모리 위치에 기록하고, 인터프리터가 정상 실행 주기 중 해당 위치를 확인하는 방식으로 작동합니다. 인터프리터가 디버거가 연결을 요청했음을 감지하면, 안전하게 수행할 수 있을 때에만 요청된 작업을 실행합니다. 즉, 내부 잠금이 유지되지 않고 모든 데이터 구조가 일관된 상태일 때 실행합니다.
런타임 상태 확장
원격 디버깅을 지원하기 위해 PyThreadState에 새로운 구조체가 추가됩니다.
typedef struct {
int debugger_pending_call;
char debugger_script_path[...];
} _PyRemoteDebuggerSupport;
이 구조체는 PyThreadState에 추가되며, 정상 실행 중에는 절대 액세스되지 않는필드 몇 개만 추가합니다. debugger_pending_call 필드는 디버거가 실행을 요청했는지를 나타내며, debugger_script_path는 인터프리터가 안전 지점에 도달했을 때 실행될 Python 소스 파일(.py)의 파일 시스템 경로를 제공합니다. 경로는 컴파일된 Python 코드(.pyc)나 다른 형식이 아닌 Python 소스 파일을 가리켜야 합니다.
debugger_script_path의 크기는 바이너리 크기와 디버깅 스크립트 경로가 가질 수 있는 최대 길이 사이의 절충이 됩니다. 스레드별 메모리 오버헤드를 제한하기 위해 이 값을 512바이트로 제한합니다. 이 크기는 디버거 지원 구조체의 일부로도 제공되므로 디버거가 얼마만큼 기록할 수 있는지 알 수 있습니다. 향후 필요할 경우 이 값을 확장할 수 있습니다.
디버그 오프셋 테이블
Python 3.12에서는 PyRuntime 구조체의 시작 부분에 배치되는 디버그 오프셋 테이블이 도입되었습니다. 이 섹션에는 외부 도구가 ASLR이나 Python의 컴파일 방식과 관계없이 중요한 런타임 구조체를 안정적으로 찾을 수 있도록 하는 _Py_DebugOffsets 구조체가 포함되어 있습니다.
이 제안은 디버거 지원을 위한 새 필드를 기존 디버그 오프셋 테이블에 추가합니다.
struct _debugger_support {
uint64_t eval_breaker; // Location of the eval breaker flag
uint64_t remote_debugger_support; // Offset to our support structure
uint64_t debugger_pending_call; // Where to write the pending flag
uint64_t debugger_script_path; // Where to write the script path
uint64_t debugger_script_path_size; // Size of the script path buffer
} debugger_support;
이러한 오프셋을 사용하면 디버거가 대상 프로세스의 메모리 공간에서 중요한 디버깅 제어 구조체를 찾을 수 있습니다. eval_breaker와 remote_debugger_support 오프셋은 각각의 PyThreadState를 기준으로 하며, debugger_pending_call과 debugger_script_path 오프셋은 각각의 _PyRemoteDebuggerSupport 구조체를 기준으로 합니다. 따라서 새 구조체와 그 필드를 메모리상의 위치와 관계없이 찾을 수 있습니다. debugger_script_path_size는 연결하는 도구에 버퍼의 크기를 알려 줍니다.
연결 프로토콜
디버거가 Python 프로세스에 연결하려는 경우 다음 단계를 따릅니다.
- 프로세스에서
PyRuntime구조체를 찾습니다:- 프로세스 메모리에서 Python 바이너리(실행 파일 또는 libpython)를 찾습니다(OS에 따라 달라지는 프로세스).
- 바이너리 형식(ELF/Mach-O/PE)에서
.PyRuntime섹션 오프셋을 추출합니다. - 오프셋을 바이너리의 로드 주소로 재배치하여 실행 중인 프로세스에서 실제
PyRuntime주소를 계산합니다.
PyRuntime구조체 시작 부분의_Py_DebugOffsets를 읽어 디버그 오프셋 정보에 액세스합니다.- 오프셋을 사용하여 원하는 스레드 상태를 찾습니다.
- 오프셋을 사용하여 해당 스레드 상태 내의 디버거 인터페이스 필드를 찾습니다.
- 제어 정보를 기록합니다.
- 대부분의 디버거는 프로세스 메모리에 쓰기 전에 프로세스를 일시 중지합니다. 이는 SIGSTOP 또는 ptrace를 사용하여 프로세스를 일시 중지하는 GDB와 같은 도구의 표준 방식입니다. 이 접근 방식은 프로세스 메모리에 쓸 때 발생하는 경쟁 상태를 방지합니다. 프로세스를 중지하지 않으려는 프로파일러 및 기타 도구도 이 인터페이스를 사용할 수 있지만, 발생 가능한 경쟁 상태를 처리해야 합니다. 이는 프로파일러에서 일반적으로 고려해야 하는 사항입니다.
_PyRemoteDebuggerSupport의debugger_script_path필드에 Python 소스 파일(.py)의 파일 경로를 기록합니다._PyRemoteDebuggerSupport의debugger_pending_call플래그를 1로 설정합니다.eval_breaker필드의_PY_EVAL_PLEASE_STOP_BIT을 설정합니다.
인터프리터가 다음 안전 지점에 도달하면 디버거가 지정한 파일에 포함된 Python 코드를 실행합니다.
인터프리터 통합
인터프리터의 일반 평가 루프에는 이미 신호, 주기적 작업 및 기타 인터럽트를 처리하기 위한 eval_breaker플래그 검사가 포함되어 있습니다. 일반 실행 중 오버헤드가 전혀 발생하지 않도록 eval_breaker가 설정된 경우에만 디버거 보류 호출을 검사하여 이 기존 메커니즘을 활용합니다. 이 검사에는 오버헤드가 없습니다. 실제로 Linux perf로 프로파일링하면 이 분기가 매우 예측 가능하다는 것을 확인할 수 있습니다. 정상 실행 중에는 debugger_pending_call검사가 절대 수행되지 않으므로 최신 CPU가 이 분기를 효과적으로 지나 추측 실행할 수 있습니다.
디버거가 eval_breaker플래그와 debugger_pending_call을 모두 설정하면, 인터프리터는 다음 안전 지점에서 제공된 디버깅 코드를 실행합니다. 평가 브레이커가 검사될 때마다 인터프리터가 일관된 상태에 있음이 보장되므로, 이 모든 과정은 완전히 안전한 컨텍스트에서 수행됩니다.
처음에는 debugger_pending_call에 허용되는 값이 0과 1뿐이며, 다른 값은 향후 사용을 위해 예약됩니다.
코드가 실행되기 전에 감사 이벤트가 발생하므로, 시스템 관리자가 원하는 경우 이 메커니즘을 감사하거나 비활성화할 수 있습니다.
// In ceval.c
if (tstate->eval_breaker) {
if (tstate->remote_debugger_support.debugger_pending_call) {
tstate->remote_debugger_support.debugger_pending_call = 0;
const char *path = tstate->remote_debugger_support.debugger_script_path;
if (*path) {
if (0 != PySys_Audit("debugger_script", "%s", path)) {
PyErr_Clear();
} else {
FILE* f = fopen(path, "r");
if (!f) {
PyErr_SetFromErrno(OSError);
} else {
PyRun_AnyFile(f, path);
fclose(f);
}
if (PyErr_Occurred()) {
PyErr_WriteUnraisable(...);
}
}
}
}
}
실행 중인 코드에서 Python 예외가 발생하면 해당 예외는 코드가 실행된 스레드에서 unraisable exception으로 처리됩니다.
Python API
모든 도구에서 이러한 단계를 다시 구현하지 않고 원격 프로세스에서 Python 코드를 안전하게 실행할 수 있도록, 이 제안에서는 sys 모듈을 새 함수로 확장합니다. 이 함수는 디버거 또는 외부 도구가 지정된 Python 프로세스의 컨텍스트 내에서 임의의 Python 코드를 실행할 수 있도록 합니다.
def remote_exec(pid: int, script: str|bytes|PathLike) -> None:
"""
Executes a file containing Python code in a given remote Python process.
This function returns immediately, and the code will be executed by the
target process's main thread at the next available opportunity, similarly
to how signals are handled. There is no interface to determine when the
code has been executed. The caller is responsible for making sure that
the file still exists whenever the remote process tries to read it and that
it hasn't been overwritten.
Args:
pid (int): The process ID of the target Python process.
script (str|bytes|PathLike): The path to a file containing
the Python code to be executed.
"""
API의 사용 예는 다음과 같습니다.
import sys
import uuid
# Execute a print statement in a remote Python process with PID 12345
script = f"/tmp/{uuid.uuid4()}.py"
with open(script, "w") as f:
f.write("print('Hello from remote execution!')")
try:
sys.remote_exec(12345, script)
except Exception as e:
print(f"Failed to execute code: {e}")
구성 API
재배포자, 시스템 관리자 또는 사용자가 이 메커니즘을 비활성화할 수 있도록, 인터프리터의 동작을 제어하는 여러 방법이 제공됩니다.
런타임 동작을 제어하기 위한 새 PYTHON_DISABLE_REMOTE_DEBUG 환경 변수가 제공됩니다. 빈 문자열을 포함하여 어떤 값으로든 설정되면, 인터프리터는 이 메커니즘을 사용한 디버거 연결 시도를 모두 무시합니다.
사용자가 런타임에 이 기능을 비활성화할 수 있도록 이 환경 변수와 함께 새로운 -X disable-remote-debug 플래그가 Python 인터프리터에 추가됩니다.
또한 재배포자가 원하는 경우 원격 디버깅 지원 없이 Python을 빌드할 수 있도록 새로운 --without-remote-debug 플래그가 configure 스크립트에 추가됩니다.
원격 디버깅 상태를 나타내는 새 플래그를 디버그 오프셋을 통해 사용할 수 있게 하여, 도구가 원격 프로세스에서 해당 기능이 비활성화되었는지 조회할 수 있게 합니다. 이렇게 하면 도구는 연결되었다고 잘못 판단한 뒤 스크립트가 실행되지 않는 대신, 작동하지 않는 이유를 설명하는 유용한 오류 메시지를 제공할 수 있습니다.
멀티스레딩 고려 사항
전반적인 실행 방식은 Python이 시그널을 내부적으로 처리하는 방식과 유사합니다. 인터프리터는 주입된 코드가 안전 지점에서만 실행되고 인터프리터 내부의 원자적 연산을 절대 중단하지 않도록 보장합니다. 이 방식은 대부분의 실제 상황에서 적시에 실행되도록 하면서 디버깅 작업이 인터프리터 상태를 손상시키지 않게 합니다.
그러나 이 인터페이스를 통해 주입된 디버깅 코드는 어느 스레드에서든 실행될 수 있습니다. 시그널 처리기는 메인 스레드에서만 실행될 수 있으므로, 이 동작은 Python이 시그널을 처리하는 방식과 다릅니다. 디버거가 실행 중인 모든 스레드에 코드를 주입하려면 모든 PyThreadState에 코드를 주입해야 합니다. 디버거가 처음 사용 가능한 스레드에서 코드를 실행하려면 모든 PyThreadState에 코드를 주입해야 하며, 주입된 코드는 다른 스레드가 이미 실행했는지 확인해야 합니다(아마도 어떤 모듈의 전역 네임스페이스에 플래그를 설정하는 방식일 것입니다).
주입된 코드가 실행될 때에도 전역 인터프리터 잠금(Global Interpreter Lock, GIL)이 평소와 같이 실행을 계속 제어한다는 점에 유의하십시오. 즉, 대상 스레드가 현재 GIL을 계속 보유하는 C 확장 기능을 실행 중이라면 해당 작업이 완료되어 GIL을 사용할 수 있게 될 때까지 주입된 코드를 실행할 수 없습니다. 그러나 이 인터페이스는 주입된 코드 자체에 필요한 경우를 넘어 추가적인 GIL 경합을 일으키지 않습니다. 중요한 점은 이 인터페이스가 Python의 자유 스레드 모드와 완전히 호환된다는 것입니다.
디버거가 일부 코드를 주입하여 실행되도록 한 뒤 프로세스에 사전 등록된 시그널을 보내는 것이 유용할 수 있습니다. 이 시그널은 외부 리소스를 기다리며 차단된 I/O 또는 절전 상태를 중단하고 주입된 코드를 실행할 안전한 기회를 제공할 수 있습니다.
하위 호환성
이 변경은 기존 Python 코드나 인터프리터 성능에 영향을 주지 않습니다. 추가된 필드는 디버거 연결 중에만 액세스되며, 확인 메커니즘은 기존 인터프리터 안전 지점을 활용합니다.
보안 영향
이 인터페이스는 새로운 보안 문제를 일으키지 않습니다. 주어진 프로세스 내부의 임의 메모리에 이미 쓰고 시스템에서 임의 코드를 실행할 수 있는 프로세스만 사용할 수 있기 때문입니다(실행할 Python 코드가 포함된 파일을 만들기 위해 필요합니다).
또한 코드 실행은 인터프리터의 감사 훅에 의해 제어되며, 감사 훅을 사용하여 민감한 환경에서 코드 실행을 모니터링하거나 방지할 수 있습니다.
기존 운영 체제 보안 메커니즘은 공격자가 임의의 메모리 쓰기 권한을 획득하지 못하도록 방어하는 데 효과적입니다. PEP에서는 대상 프로세스에 메모리를 쓰는 방법을 명시하지 않지만, 실제로는 다른 디버거와 도구가 이미 사용하고 있는 표준 시스템 호출을 사용하게 됩니다. 몇 가지 예는 다음과 같습니다.
- Linux에서는 process_vm_readv() 및 process_vm_writev() 시스템 호출을 사용하여 다른 프로세스의 메모리를 읽고 씁니다. 이러한 작업은 ptrace 접근 모드 검사에 의해 제어되며, 이는 디버거 연결을 제어하는 검사와 동일합니다. 프로세스는 적절한 권한이 있는 경우에만 다른 프로세스의 메모리를 읽거나 쓸 수 있습니다(일반적으로 root 또는 CAP_SYS_PTRACE 기능이 필요하지만, 보안에 덜 엄격한 배포판에서는 동일한 uid로 실행되는 모든 프로세스가 연결하도록 허용할 수 있습니다).
- macOS에서는 Mach 태스크 시스템을 통해 mach_vm_read_overwrite() 및 mach_vm_write()를 활용합니다. 이러한 작업에는 운영 체제가 엄격하게 제어하는
task_for_pid()액세스가 필요합니다. 기본적으로 액세스는 root로 실행 중인 프로세스 또는 Apple의 보안 프레임워크가 부여한 특정 권한을 가진 프로세스로 제한됩니다. - Windows에서는 ReadProcessMemory() 및 WriteProcessMemory() 함수가 유사한 기능을 제공합니다. 액세스는 Windows 보안 모델을 통해 제어되며, 프로세스에는 일반적으로 동일한 사용자 컨텍스트 또는 적절한 권한이 필요한 PROCESS_VM_READ 및 PROCESS_VM_WRITE 권한이 필요합니다. 이는 디버거에 필요한 것과 동일한 권한이므로 플랫폼 전반에서 일관된 보안 의미 체계를 보장합니다.
모든 메커니즘은 다음을 보장합니다.
- 권한이 부여된 프로세스만 메모리를 읽거나 쓸 수 있습니다.
- 기존 디버거 연결을 관장하는 것과 동일한 보안 모델이 적용됩니다.
- 운영 체제가 디버깅을 위해 이미 제공하는 범위를 넘어서는 추가 공격 표면이 노출되지 않습니다.
- 공격자가 임의의 메모리에 쓸 수 있더라도, 이미 파일 시스템 액세스 권한을 보유하고 있지 않은 한 이를 임의 코드 실행으로 확장할 수 없습니다.
메모리 작업 자체는 충분히 검증되었으며 GDB, LLDB 및 다양한 시스템 프로파일러와 같은 도구에서 수십 년 동안 안전하게 사용되어 왔습니다.
이 메커니즘을 통해 Python 프로세스에 연결하려는 모든 시도는 시스템 수준의 모니터링 도구와 Python 감사 훅으로 탐지할 수 있다는 점에 유의해야 합니다. 이러한 투명성은 추가적인 책임성 계층을 제공하여 관리자가 민감한 환경에서 디버깅 작업을 감사할 수 있도록 합니다.
또한 OS 수준의 보안 제어에 엄격하게 의존하므로 기존 시스템 정책이 계속 유효하게 적용됩니다. 엔터프라이즈 환경에서는 추가 구성 없이 표준 도구와 정책을 사용하여 관리자가 디버깅 제한을 계속 적용할 수 있음을 의미합니다. 예를 들어 Linux의 ptrace_scope 또는 macOS의 taskgated를 활용하여 디버거 액세스를 제한하면 제안된 인터페이스에도 동일하게 적용됩니다.
기존 보안 프레임워크와의 호환성을 유지함으로써 이 설계는 새 인터페이스를 도입할 때 기존에 확립된 사항을 변경할 필요가 없도록 합니다.
보안 시나리오
- 외부 공격자에게 프로세스의 임의 메모리에 쓸 수 있는 능력은 이미 심각한 보안 문제입니다. 공격자는 이미 프로세스에서 임의 코드를 실행할 수 있는 능력을 보유하고 있으므로 이 인터페이스는 새로운 공격 표면을 도입하지 않습니다. 이 인터페이스는 기존 디버거와 정확히 동일한 방식으로 동작하며 추가적인 보안 위험을 도입하지 않습니다.
- 임의의 메모리 쓰기 액세스는 얻었지만 임의 코드 실행 권한은 얻지 못한 공격자의 경우, 이 인터페이스로 권한을 확장할 수 없습니다. 특정 메모리 위치를 계산하고 해당 위치에 쓰는 능력이 필요하지만, Python 프로세스 외부에 있는 다른 시스템 리소스를 침해하지 않고서는 이를 사용할 수 없습니다.
또한 실행될 코드가 인터프리터의 감사 훅에 의해 통제된다는 사실은 시스템 관리자가 코드 실행을 모니터링하고 제어할 수 있음을 의미합니다. 이는 공격자가 애플리케이션 및 파일 시스템을 침해했더라도, 이 인터페이스를 악의적인 목적으로 활용하는 것은 공격자에게 매우 위험한 선택임을 의미합니다. 공격자는 자신의 행위가 시스템 관리자에게 노출될 위험을 감수해야 하며, 관리자는 공격을 탐지할 뿐만 아니라 이를 방지하기 위한 조치도 취할 수 있습니다.
마지막으로, 공격자가 프로세스에 대한 임의 메모리 쓰기 액세스 권한을 보유하고 파일 시스템을 침해했다면, 이미 다른 기존 메커니즘을 사용하여 임의 코드 실행으로 권한을 확장할 수 있으므로 이 시나리오에서 이 인터페이스는 새로운 위험을 도입하지 않는다는 점에 유의해야 합니다.
이를 가르치는 방법
도구 작성자에게 이 인터페이스는 안전하지 않은 시스템 디버거 접근 방식을 대체하여 디버거 연결을 구현하는 표준 방법이 됩니다. Python 개발자 가이드의 한 섹션에서는 debugger_support 오프셋과 시스템 API를 사용하여 이들과 상호 작용하는 방법을 포함하여 메커니즘의 내부 작동 방식을 설명할 수 있습니다.
최종 사용자는 인터페이스를 알 필요가 없으며, 향상된 디버깅 도구의 안정성과 신뢰성만 누리면 됩니다.
참조 구현
pdb에 원격 지원을 추가하는 프로토타입을 포함한 참조 구현은 여기에서 찾을 수 있습니다.
거부된 아이디어
버퍼에 Python 코드 작성하기
디버거가 원격 프로세스의 버퍼에 Python 코드가 포함된 파일의 경로를 기록하도록 하기로 했습니다. 이는 원격 프로세스의 버퍼에 실행할 Python 코드 자체를 기록하는 것보다 더 안전한 것으로 판단되었습니다. 프로세스에서 임의 쓰기 권한을 얻었지만 임의 코드 실행이나 파일 시스템 조작 권한은 얻지 못한 공격자가 이 인터페이스를 통해 임의 코드 실행으로 권한을 상승할 수 없기 때문입니다.
그러나 이를 위해서는 연결하는 디버거가 실행할 코드가 포함된 파일을 생성할 때 파일 시스템 권한에 각별히 주의해야 합니다. 공격자가 해당 파일을 덮어쓰거나, 파일 경로의 심볼릭 링크를 공격자가 제어하는 위치를 가리키도록 바꿀 수 있다면, 디버거가 실행하려는 코드 대신 공격자의 악성 코드가 실행되도록 강제할 수 있습니다.
단일 런타임 버퍼 사용하기
이 PEP를 검토하는 동안 모든 디버거 통신에 런타임 수준의 단일 공유 버퍼를 사용하자는 제안이 있었습니다. 이 방식은 더 단순하고 메모리도 덜 필요해 보였지만, 여러 디버거가 서로 다른 스레드에서 작업을 조정해야 하는 경우나 단일 디버거가 복잡한 디버깅 작업을 지휘해야 하는 경우를 실제로 방지한다는 사실을 발견했습니다. 단일 공유 버퍼를 사용하면 모든 디버깅 작업이 직렬화되므로 디버거가 서로 다른 스레드에서 독립적으로 작업할 수 없게 됩니다.
스레드별 버퍼 방식은 스레드가 많은 애플리케이션에서 메모리 오버헤드가 발생함에도 불구하고, 각 디버거가 대상 스레드와 독립적으로 통신할 수 있도록 하여 이러한 중요한 디버깅 시나리오를 가능하게 합니다.
감사의 말
이 제안을 논의하는 동안 통찰력 있는 의견과 제안을 해 주신 CF Bolz-Tereick 님께 감사드립니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.