リモートデバッグのアタッチプロトコル
************************************

このプロトコルを使うと、外部ツールから実行中の CPython プロセスにアタ
ッチし、Python コードをリモートで実行できます。

別の Python プロセスへのアタッチには、プラットフォームによって追加の権
限や設定が必要な場合があります。


リモートデバッグの無効化
========================

リモートデバッグのサポートを無効にするには、次のいずれかを使います。

* インタープリタを起動する前に、環境変数 "PYTHON_DISABLE_REMOTE_DEBUG"
  を "1" に設定します。

* コマンドラインオプション "-X disable-remote-debug" を使います。

* ビルドフラグ "--without-remote-debug" を指定して Python をコンパイル
  します。


必要な権限
**********

リモートデバッグのために実行中の Python プロセスにアタッチするには、ほ
とんどのプラットフォームで特別な設定が必要です。 具体的な要件と問題の
解決手順は、オペレーティングシステムによって異なります。

-[ Linux ]-

通常は自分のプロセスをデバッグできますが、よく使われる設定の中にはそれ
を無効にするものがあります。 一部の Linux ディストリビューションでは、
システムのセキュリティを強化するために **ptrace の制限** (別名「Yama」
) を有効にしています。 最近の "setpriv" コマンド (2025 年 6 月リリース
の util-linux 2.41) を使うと、プロセスごとに ptrace の制限を緩められま
す。

   "setpriv --ptracer any python3"

(この設定は *デバッグ対象の* プロセスに対して行います。) 次のコマンド
で、再起動するまで全プロセスの ptrace の制限を無効にすることもできます
。

   "echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope"

通常は "/etc/sysctl.d" で、この設定を永続化することもできます。

注釈:

  "ptrace_scope" を無効にするとシステムのセキュリティが弱まるため、安
  全性の要求が低い環境でのみ行ってください。

セキュリティフィルターによって "ptrace" システムコールが無効になってい
る場合もあります。 特に、一部のコンテナーソフトウェアの古いバージョン
ではよくあることでした。 Docker 19.03 以降 (2019 年リリース) と
containerd 1.6.7 以降 (2022 年リリース) は、Linux カーネル 4.8 以降で
実行すると、コンテナー内での "ptrace" システムコールの使用を自動的に許
可します。 これらのバージョンに更新できない場合は、"--security-opt
seccomp=unconfined" のようなオプションを指定してコンテナーを作成し、そ
のコンテナーのシステムコール用セキュリティフィルターを無効にできます。
ただし、コンテナーの隔離が弱まるため、安全性の要求が低い環境でのみ行っ
てください。

自分が所有して *いない* プロセスをトレースするには、スーパーユーザーま
たは同等の権限が必要です。 これは、set-user-ID や set-group-ID のプロ
セスなど、セキュリティ資格情報を変更したプロセスにも当てはまります
(Python ではまれです)。 "sudo -E" を付けてデバッグコマンドを実行してみ
てください。

注釈:

  "CAP_SYS_PTRACE" ケーパビリティは、自分のプロセスだけでなく *あらゆ
  る* プロセスをデバッグできる点で、スーパーユーザー権限と同等です。
  インターネット上では、ptrace の制限やシステムコールフィルターを回避
  するためにこのケーパビリティを使うよう勧める情報を見かけるかもしれま
  せん。 "sudo" と同様に実際に機能する場合もありますが、デバッグする側
  のプロセスに必要以上のアクセス権を与えるため、安全性の要求が低い環境
  でのみ行ってください。

最後に、一つのプロセスを同時にトレースできるのは一つのトレーサーだけで
す。 "strace" や "gdb" などですでに Python プロセスにアタッチしている
場合、同時にリモートデバッグは使えません。 (スーパーユーザー権限でもこ
の制限は回避できません。)

-[ macOS ]-

デフォルトでは、macOS は他のプロセスをデバッグする機能を無効にしていま
す。

Python のバイナリに、デバッグを許可する **エンタイトルメント** を含む
**アドホックコード署名** を付けることで、そのバイナリをデバッグ可能に
できます。 (アドホック「署名」は単なる設定です。 実際の暗号署名は含ま
ず、証明書や Apple Developer Program への加入なども必要ありません。)

次のコマンドは、必要なエンタイトルメントを含む "get-task-allow.plist"
ファイルを作成し、Python のバイナリに追加します。

   echo '{"com.apple.security.get-task-allow": true}' | plutil -convert xml1 -o get-task-allow.plist -
   codesign --sign - --entitlements get-task-allow.plist path/to/bin/python3

where "path/to/bin/python3" is the path to your Python binary, which
you can find by e.g. running "which python3" or evaluating
"sys.base_executable" at the Python REPL. (These instructions are for
a non-framework build of Python. Framework builds may need to be
configured differently.)

その後は、そのバイナリで起動した自分の Python プロセスをデバッグできる
はずです。

また、Linux と同様に、"sudo" などのスーパーユーザー権限を持つプロセス
はこの検査の対象にならず、システム上のどのユーザーのプロセスもデバッグ
できます。 ただし、OS が提供するコマンドなどの特定のバイナリには、シス
テム整合性保護による追加の検査があります。

-[ Windows ]-

別のプロセスにアタッチするには、通常、管理者権限でデバッグツールを実行
する必要があります。 コマンドプロンプトまたはターミナルを管理者として
起動してください。

管理者権限があっても、"SeDebugPrivilege" 特権を有効にしないとアクセス
できないプロセスもあります。

ファイルやフォルダーへのアクセスの問題を解決するには、セキュリティのア
クセス許可を調整します。

   1. ファイルまたはフォルダーを右クリックし、**プロパティ** を選択し
      ます。

   2. **セキュリティ** タブで、アクセスできるユーザーとグループを確認
      します。

   3. **編集** をクリックしてアクセス許可を変更します。

   4. 自分のユーザーアカウントを選択します。

   5. **アクセス許可** で、必要に応じて **読み取り** または **フル コ
      ントロール** にチェックを入れます。

   6. **適用**、**OK** の順にクリックして確定します。

注釈:

  先に進む前に、必要な権限 をすべて満たしていることを確認してください
  。

この節では、外部ツールから実行中の CPython プロセスに Python スクリプ
トを注入し、実行するための低水準のプロトコルを説明します。

この仕組みは、リモートの Python プロセスに ".py" ファイルの実行を指示
する "sys.remote_exec()" 関数の基盤です。 ただし、この節はその関数の使
い方を説明するものではありません。 対象の Python プロセスの "pid" と実
行する Python ソースファイルのパスを入力として受け取る、基盤となるプロ
トコルを詳しく説明します。 ここで説明する情報を使えば、プログラミング
言語にかかわらず、このプロトコルを独自に実装できます。

警告:

  注入したスクリプトは、インタープリタが安全に評価できる地点に到達して
  から実行されます。 そのため、対象プロセスの実行状態によっては、実行
  が遅れることがあります。

スクリプトを注入すると、対象プロセスのインタープリタが次に安全に評価で
きる地点に到達したときに、そのスクリプトを実行します。 これにより、実
行中の Python アプリケーションの動作や構造を変更せずに、リモート実行機
能を提供できます。

以降の節では、メモリ内でインタープリタの構造体を見つける方法、内部フィ
ールドに安全にアクセスする方法、コードの実行を開始させる方法など、プロ
トコルの手順を説明します。 プラットフォームによる違いは該当箇所に記し
、各操作を明確にするための実装例も示します。


PyRuntime 構造体の位置の特定
****************************

外部ツールが実行時に見つけやすいように、CPython は "PyRuntime" 構造体
をバイナリ内の専用セクションに配置します。 このセクションの名前と形式
はプラットフォームによって異なります。 例えば、ELF システムでは
".PyRuntime"、macOS では "__DATA,__PyRuntime" を使います。 ツールはデ
ィスク上のバイナリを調べることで、この構造体のオフセットを見つけられま
す。

"PyRuntime" 構造体には CPython のグローバルなインタープリタの状態が格
納されており、インタープリタのリスト、スレッド状態、デバッガーを支援す
るフィールドなどの内部データにアクセスできます。

To work with a remote Python process, a debugger must first find the
memory address of the "PyRuntime" structure in the target process.
This address can’t be hardcoded or calculated from a symbol name,
because it depends on where the operating system loaded the binary.

"PyRuntime" の位置を特定する方法はプラットフォームによって異なりますが
、大まかな手順は共通です。

1. 対象プロセスで Python のバイナリまたは共有ライブラリがロードされた
   ベースアドレスを調べます。

2. ディスク上のバイナリを使って ".PyRuntime" セクションのオフセットを
   調べます。

3. ベースアドレスにセクションのオフセットを加え、メモリ内のアドレスを
   求めます。

以下の節では、サポートされる各プラットフォームでの方法とコード例を示し
ます。

-[ Linux (ELF) ]-

Linux で "PyRuntime" 構造体の位置を特定するには、次の手順を実行します
。

1. プロセスのメモリマップ (例えば "/proc/<pid>/maps") を読み、Python
   の実行ファイルまたは "libpython" がロードされたアドレスを調べます。

2. バイナリの ELF セクションヘッダーを解析し、".PyRuntime" セクション
   のオフセットを取得します。

3. 手順 1 のベースアドレスにそのオフセットを加え、"PyRuntime" のメモリ
   アドレスを求めます。

実装例を示します。

   def find_py_runtime_linux(pid: int) -> int:
       # Step 1: Try to find the Python executable in memory
       binary_path, base_address = find_mapped_binary(
           pid, name_contains="python"
       )

       # Step 2: Fallback to shared library if executable is not found
       if binary_path is None:
           binary_path, base_address = find_mapped_binary(
               pid, name_contains="libpython"
           )

       # Step 3: Parse ELF headers to get .PyRuntime section offset
       section_offset = parse_elf_section_offset(
           binary_path, ".PyRuntime"
       )

       # Step 4: Compute PyRuntime address in memory
       return base_address + section_offset

Linux では、別のプロセスのメモリを読む主な方法が二つあります。 一つは
"/proc" ファイルシステムを使う方法で、具体的にはプロセスのメモリに直接
アクセスできる "/proc/[pid]/mem" を読みます。 これには、対象プロセスと
同じユーザーであるか root 権限を持つなど、適切な権限が必要です。 もう
一つは、プロセス間でより効率的にメモリをコピーできる
"process_vm_readv()" システムコールを使う方法です。 ptrace の
"PTRACE_PEEKTEXT" 操作でもメモリを読めますが、一度に 1 ワードしか読め
ず、トレースする側とされる側のプロセス間で複数回のコンテキスト切り替え
が必要なため、はるかに低速です。

For parsing ELF sections, the process involves reading and
interpreting the ELF file format structures from the binary file on
disk. The ELF header contains a pointer to the section header table.
Each section header contains metadata about a section including its
name (stored in a separate string table), offset, and size. To find a
specific section like .PyRuntime, you need to walk through these
headers and match the section name. The section header then provides
the offset where that section exists in the file, which can be used to
calculate its runtime address when the binary is loaded into memory.

ELF ファイル形式の詳細は ELF の仕様 を参照してください。

-[ macOS (Mach-O) ]-

macOS で "PyRuntime" 構造体の位置を特定するには、次の手順を実行します
。

1. "task_for_pid()" を呼び出し、対象プロセスの "mach_port_t" タスクポ
   ートを取得します。 このハンドルは、"mach_vm_read_overwrite" や
   "mach_vm_region" などの API でメモリを読むために必要です。

2. メモリ領域を調べ、Python の実行ファイルまたは "libpython" を含む領
   域を探します。

3. Load the binary file from disk and parse the Mach-O headers to find
   the section named "PyRuntime" in the "__DATA" segment.  On macOS,
   symbol names are automatically prefixed with an underscore, so the
   "PyRuntime" symbol appears as "_PyRuntime" in the symbol table, but
   the section name is not affected.

実装例を示します。

   def find_py_runtime_macos(pid: int) -> int:
       # Step 1: Get access to the process's memory
       handle = get_memory_access_handle(pid)

       # Step 2: Try to find the Python executable in memory
       binary_path, base_address = find_mapped_binary(
           handle, name_contains="python"
       )

       # Step 3: Fallback to libpython if the executable is not found
       if binary_path is None:
           binary_path, base_address = find_mapped_binary(
               handle, name_contains="libpython"
           )

       # Step 4: Parse Mach-O headers to get __DATA,__PyRuntime section offset
       section_offset = parse_macho_section_offset(
           binary_path, "__DATA", "__PyRuntime"
       )

       # Step 5: Compute the PyRuntime address in memory
       return base_address + section_offset

macOS で別のプロセスのメモリにアクセスするには、Mach-O 固有の API とフ
ァイル形式を使います。 まず、"task_for_pid()" で "task_port" ハンドル
を取得し、対象プロセスのメモリ空間にアクセスできるようにします。 この
ハンドルを使うと、"mach_vm_read_overwrite()" などの API でメモリを操作
できます。

"mach_vm_region()" で仮想メモリ空間を走査すると、プロセスのメモリを調
べられます。 "proc_regionfilename()" は、各メモリ領域にロードされたバ
イナリファイルの特定に役立ちます。 Python のバイナリまたはライブラリを
見つけたら、その Mach-O ヘッダーを解析して "PyRuntime" 構造体の位置を
特定します。

Mach-O 形式では、コードとデータをセグメントとセクションに分けて格納し
ます。 "PyRuntime" 構造体は、"__DATA" セグメント内の "__PyRuntime" と
いうセクションにあります。 実行時のアドレスを求めるには、バイナリのベ
ースアドレスとなる "__TEXT" セグメントを見つけ、続いて目的のセクション
を含む "__DATA" セグメントを探します。 ベースアドレスと Mach-O ヘッダ
ーの適切なセクションオフセットを組み合わせて、最終的なアドレスを求めま
す。

macOS で別のプロセスのメモリにアクセスするには、通常、root 権限やデバ
ッグする側のプロセスに付与された特別なセキュリティエンタイトルメントな
ど、昇格された権限が必要です。

-[ Windows (PE) ]-

Windows で "PyRuntime" 構造体の位置を特定するには、次の手順を実行しま
す。

1. ToolHelp API を使い、対象プロセスにロードされたすべてのモジュールを
   列挙します。 これには CreateToolhelp32Snapshot、Module32First、
   Module32Next などの関数を使います。

2. "python.exe" または "python*XY*.dll" に対応するモジュールを特定し、
   ベースアドレスを記録します。 "X" と "Y" は Python のメジャーバージ
   ョン番号とマイナーバージョン番号です。

3. "PyRuntim" セクションを探します。 PE 形式のセクション名には 8 文字
   の制限 ("IMAGE_SIZEOF_SHORT_NAME" として定義) があるため、元の名前
   "PyRuntime" は切り詰められます。 このセクションに "PyRuntime" 構造
   体があります。

4. セクションの相対仮想アドレス (RVA) を取得し、モジュールのベースアド
   レスに加えます。

実装例を示します。

   def find_py_runtime_windows(pid: int) -> int:
       # Step 1: Try to find the Python executable in memory
       binary_path, base_address = find_loaded_module(
           pid, name_contains="python"
       )

       # Step 2: Fallback to shared pythonXY.dll if the executable is not
       # found
       if binary_path is None:
           binary_path, base_address = find_loaded_module(
               pid, name_contains="python3"
           )

       # Step 3: Parse PE section headers to get the RVA of the PyRuntime
       # section. The section name appears as "PyRuntim" due to the
       # 8-character limit defined by the PE format (IMAGE_SIZEOF_SHORT_NAME).
       section_rva = parse_pe_section_offset(binary_path, "PyRuntim")

       # Step 4: Compute PyRuntime address in memory
       return base_address + section_rva

Windows で別のプロセスのメモリにアクセスするには、
"CreateToolhelp32Snapshot()" や "Module32First()/Module32Next()" など
の Windows API 関数で、ロードされたモジュールを列挙します。
"OpenProcess()" 関数は対象プロセスのメモリ空間にアクセスするためのハン
ドルを返し、"ReadProcessMemory()" でメモリを操作できるようにします。

ロードされたモジュールを列挙して Python のバイナリまたは DLL を探すこ
とで、プロセスのメモリを調べられます。 見つかったら、その PE ヘッダー
を解析して "PyRuntime" 構造体の位置を特定します。

PE 形式では、コードとデータをセクションに分けて格納します。
"PyRuntime" 構造体は「PyRuntim」というセクションにあります (PE の名前
の長さ制限が 8 文字なので、「PyRuntime」から切り詰められています)。 実
行時のアドレスを求めるには、モジュールエントリーからモジュールのベース
アドレスを調べ、PE ヘッダーで目的のセクションを探します。 ベースアドレ
スと PE セクションヘッダーのセクション仮想アドレスを組み合わせて、最終
的なアドレスを求めます。

Windows で別のプロセスのメモリにアクセスするには、通常、管理者権限やデ
バッグする側のプロセスに付与された "SeDebugPrivilege" 特権など、適切な
権限が必要です。


_Py_DebugOffsets の読み取り
***************************

"PyRuntime" 構造体のアドレスを特定したら、次は "PyRuntime" ブロックの
先頭にある "_Py_DebugOffsets" 構造体を読み取ります。

この構造体は、インタープリタやスレッド状態のメモリを安全に読み取るため
に必要な、バージョンごとのフィールドオフセットを提供します。 オフセッ
トは CPython のバージョンによって異なるため、使用前に互換性を確認する
必要があります。

デバッグ用オフセットを読み取って検査するには、次の手順を実行します。

1. 対象プロセスの "PyRuntime" アドレスから、"_Py_DebugOffsets" 構造体
   のサイズと同じバイト数のメモリを読み取ります。 この構造体は
   "PyRuntime" メモリブロックの先頭にあります。 そのレイアウトは
   CPython の内部ヘッダーで定義され、同じマイナーバージョン内では変わ
   りませんが、メジャーバージョンが変わると変更される可能性があります
   。

2. 構造体に有効なデータが含まれているか確認します。

   * "cookie" フィールドは、想定するデバッグ用マーカーと一致しなければ
     なりません。

   * "version" フィールドは、デバッガーが使う Python インタープリタの
     バージョンと一致しなければなりません。

   * デバッガーか対象プロセスのいずれかがプレリリース版 (アルファ版、
     ベータ版、リリース候補版など) を使っている場合は、バージョンが完
     全に一致しなければなりません。

   * "free_threaded" フィールドは、デバッガーと対象プロセスの両方で同
     じ値でなければなりません。

3. 構造体が有効なら、格納されたオフセットを使ってメモリ内のフィールド
   の位置を特定できます。 いずれかの検査に失敗した場合は、誤った形式で
   メモリを読まないように、デバッガーは操作を中止するべきです。

"_Py_DebugOffsets" を読み取って検査する実装例を示します。

   def read_debug_offsets(pid: int, py_runtime_addr: int) -> DebugOffsets:
       # 手順 1: 対象プロセスの PyRuntime アドレスからメモリを読む
       data = read_process_memory(
           pid, address=py_runtime_addr, size=DEBUG_OFFSETS_SIZE
       )

       # 手順 2: バイト列を _Py_DebugOffsets 構造体に復元する
       debug_offsets = parse_debug_offsets(data)

       # 手順 3: 構造体の内容を検証する
       if debug_offsets.cookie != EXPECTED_COOKIE:
           raise RuntimeError("Invalid or missing debug cookie")
       if debug_offsets.version != LOCAL_PYTHON_VERSION:
           raise RuntimeError(
               "Mismatch between caller and target Python versions"
           )
       if debug_offsets.free_threaded != LOCAL_FREE_THREADED:
           raise RuntimeError("Mismatch in free-threaded configuration")

       return debug_offsets

警告:

  **プロセスの一時停止を推奨**競合状態を避けてメモリの一貫性を確保する
  ため、インタープリタの内部状態を読み書きする前に、対象プロセスを一時
  停止することを強く推奨します。 Python ランタイムは通常の実行中に、ス
  レッドの作成や破棄など、インタープリタのデータ構造の変更を並行して行
  うことがあります。 その結果、無効なメモリの読み書きが発生する可能性
  があります。デバッガーは、"ptrace" でプロセスにアタッチするか、
  "SIGSTOP" シグナルを送ることで実行を一時停止できます。 デバッガー側
  のメモリ操作が完了してから実行を再開するべきです。

  注釈:

    プロファイラーやサンプリング方式のデバッガーなど、一部のツールはプ
    ロセスを停止せずに動作する場合があります。 その場合、更新途中のメ
    モリや一貫性のないメモリを扱えるように、明示的に設計しなければなり
    ません。 ほとんどのデバッガー実装では、プロセスを一時停止するのが
    最も安全で堅牢な方法です。


インタープリタとスレッド状態の位置の特定
****************************************

リモートの Python プロセスにコードを注入して実行する前に、デバッガーは
実行を予約するスレッドを選ぶ必要があります。 リモートコードの注入を制
御するフィールドが、"PyThreadState" オブジェクトに埋め込まれた
"_PyRemoteDebuggerSupport" 構造体にあるためです。 デバッガーはこれらの
フィールドを変更して、注入したスクリプトの実行を要求します。

"PyThreadState" 構造体は、Python インタープリタ内で実行するスレッドを
表します。 スレッドの評価コンテキストを保持し、デバッガーと協調するた
めのフィールドを含みます。 そのため、有効な "PyThreadState" の位置を特
定することが、リモート実行を開始させるための重要な前提条件です。

通常は役割や ID に基づいてスレッドを選びます。 ほとんどの場合はメイン
スレッドを使いますが、ネイティブスレッド ID で特定のスレッドを指定する
ツールもあります。 対象のスレッドを選んだら、デバッガーはインタープリ
タと対応するスレッド状態の構造体をメモリ内で探さなければなりません。

関連する内部構造体の定義は次のとおりです。

* "PyInterpreterState" は、隔離された Python インタープリタのインスタ
  ンスを表します。 各インタープリタは、インポート済みモジュール、組み
  込みの状態、スレッド状態のリストをそれぞれ保持します。 ほとんどの
  Python アプリケーションは一つのインタープリタを使いますが、CPython
  は同じプロセス内での複数のインタープリタをサポートします。

* "PyThreadState" は、インタープリタ内で実行するスレッドを表します。
  実行状態と、デバッガーが使う制御用フィールドを含みます。

スレッドの位置を特定するには、次の手順を実行します。

1. オフセット "runtime_state.interpreters_head" を使い、"PyRuntime" 構
   造体から最初のインタープリタのアドレスを取得します。 ここが、動作中
   のインタープリタの連結リストの先頭です。

2. オフセット "interpreter_state.threads_main" を使い、選んだインター
   プリタに対応するメインスレッドの状態にアクセスします。 通常、最も確
   実に対象にできるのはこのスレッドです。

3. 必要に応じて、オフセット "interpreter_state.threads_head" を使って
   全スレッド状態の連結リストをたどります。 各 "PyThreadState" 構造体
   には "native_thread_id" フィールドがあり、目的のスレッド ID と比較
   して特定のスレッドを探せます。

4. 有効な "PyThreadState" が見つかったら、そのアドレスを、デバッガーの
   制御用フィールドへの書き込みや実行の予約など、プロトコルの後続の手
   順で使えます。

メインスレッドの状態の位置を特定する実装例を示します。

   def find_main_thread_state(
       pid: int, py_runtime_addr: int, debug_offsets: DebugOffsets,
   ) -> int:
       # 手順 1: PyRuntime から interpreters_head を読む
       interp_head_ptr = (
           py_runtime_addr + debug_offsets.runtime_state.interpreters_head
       )
       interp_addr = read_pointer(pid, interp_head_ptr)
       if interp_addr == 0:
           raise RuntimeError("No interpreter found in the target process")

       # 手順 2: インタープリタから threads_main ポインターを読む
       threads_main_ptr = (
           interp_addr + debug_offsets.interpreter_state.threads_main
       )
       thread_state_addr = read_pointer(pid, threads_main_ptr)
       if thread_state_addr == 0:
           raise RuntimeError("Main thread state is not available")

       return thread_state_addr

次の例は、ネイティブスレッド ID でスレッドを探す方法を示します。

   def find_thread_by_id(
       pid: int,
       interp_addr: int,
       debug_offsets: DebugOffsets,
       target_tid: int,
   ) -> int:
       # threads_head から連結リストをたどる
       thread_ptr = read_pointer(
           pid,
           interp_addr + debug_offsets.interpreter_state.threads_head
       )

       while thread_ptr:
           native_tid_ptr = (
               thread_ptr + debug_offsets.thread_state.native_thread_id
           )
           native_tid = read_int(pid, native_tid_ptr)
           if native_tid == target_tid:
               return thread_ptr
           thread_ptr = read_pointer(
               pid,
               thread_ptr + debug_offsets.thread_state.next
           )

       raise RuntimeError("Thread with the given ID was not found")

有効なスレッド状態の位置を特定したら、デバッガーは次の節で説明するよう
に、制御用フィールドを変更して実行を予約できます。


制御情報の書き込み
******************

有効な "PyThreadState" 構造体を特定したら、デバッガーは内部の制御用フ
ィールドを変更して、指定した Python スクリプトの実行を予約できます。
インタープリタはこれらのフィールドを定期的に検査し、正しく設定されてい
れば、評価ループの安全な地点でリモートコードの実行を開始します。

各 "PyThreadState" には、デバッガーとインタープリタの通信に使う
"_PyRemoteDebuggerSupport" 構造体があります。 フィールドの位置は
"_Py_DebugOffsets" 構造体で定義されており、次のものを含みます。

* "debugger_script_path": Python ソースファイル (".py") の完全なパスを
  保持する固定長バッファー。 実行を開始するとき、対象プロセスがこのフ
  ァイルにアクセスして読み取れる必要があります。

* "debugger_pending_call": 整数のフラグ。 "1" に設定すると、スクリプト
  を実行する準備ができたことをインタープリタに伝えます。

* "eval_breaker": 実行中にインタープリタが検査するフィールド。 このフ
  ィールドのビット 5 ("_PY_EVAL_PLEASE_STOP_BIT"、値は "1U << 5") を立
  てると、インタープリタは処理を中断してデバッガーからの操作を確認しま
  す。

注入を完了するには、デバッガーは次の手順を実行しなければなりません。

1. スクリプトの完全なパスを "debugger_script_path" バッファーに書き込
   みます。

2. "debugger_pending_call" を "1" に設定します。

3. "eval_breaker" の現在の値を読み取り、ビット 5
   ("_PY_EVAL_PLEASE_STOP_BIT") を立て、更新した値を書き戻します。 こ
   れにより、デバッガーからの操作を確認するようインタープリタに通知し
   ます。

実装例を示します。

   def inject_script(
       pid: int,
       thread_state_addr: int,
       debug_offsets: DebugOffsets,
       script_path: str
   ) -> None:
       # _PyRemoteDebuggerSupport の先頭のオフセットを求める
       support_base = (
           thread_state_addr +
           debug_offsets.debugger_support.remote_debugger_support
       )

       # 手順 1: debugger_script_path にスクリプトのパスを書く
       script_path_ptr = (
           support_base +
           debug_offsets.debugger_support.debugger_script_path
       )
       write_string(pid, script_path_ptr, script_path)

       # 手順 2: debugger_pending_call を 1 に設定する
       pending_ptr = (
           support_base +
           debug_offsets.debugger_support.debugger_pending_call
       )
       write_int(pid, pending_ptr, 1)

       # 手順 3: eval_breaker の _PY_EVAL_PLEASE_STOP_BIT
       # (ビット 5、値は 1 << 5) を立てる
       eval_breaker_ptr = (
           thread_state_addr +
           debug_offsets.debugger_support.eval_breaker
       )
       breaker = read_int(pid, eval_breaker_ptr)
       breaker |= (1 << 5)
       write_int(pid, eval_breaker_ptr, breaker)

これらのフィールドを設定したら、デバッガーは (プロセスを一時停止してい
た場合は) 実行を再開できます。 インタープリタは次に安全に評価できる地
点で要求を処理し、ディスクからスクリプトを読み込んで実行します。

実行中、スクリプトファイルが存在し、対象プロセスからアクセスできる状態
を保つ責任はデバッガーにあります。

注釈:

  スクリプトは非同期に実行されます。 注入の直後にスクリプトファイルを
  削除してはいけません。 デバッガーは、注入したスクリプトによる作用を
  観測してからファイルを削除するべきです。 何を観測するかは、スクリプ
  トの目的によります。 例えば、リモートプロセスからソケットへの接続が
  あるまで待ってから、スクリプトを削除する方法があります。 そのような
  作用を観測できたら、ファイルは不要になったと判断できます。


まとめ
******

リモートプロセスに Python スクリプトを注入して実行する手順は次のとおり
です。

1. 対象プロセスのメモリ内で "PyRuntime" 構造体の位置を特定します。

2. "PyRuntime" の先頭にある "_Py_DebugOffsets" 構造体を読み取り、検証
   します。

3. オフセットを使って、有効な "PyThreadState" の位置を特定します。

4. Python スクリプトのパスを "debugger_script_path" に書き込みます。

5. "debugger_pending_call" フラグを "1" に設定します。

6. "eval_breaker" フィールドの "_PY_EVAL_PLEASE_STOP_BIT" を立てます。

7. 一時停止していた場合はプロセスの実行を再開します。 スクリプトは、次
   に安全に評価できる地点で実行されます。


セキュリティと脅威モデル
************************

リモートデバッグのプロトコルは、GDB や LLDB などのネイティブデバッガー
と同じ OS の基本機能を使います。 プロセスへのアタッチには、Linux の
"ptrace" / Yama LSM、macOS の "task_for_pid"、Windows の
"SeDebugPrivilege" など、それらのデバッガーと **同じ権限** が必要です
。 Python が新たな権限昇格の経路を導入するわけではありません。 攻撃者
がプロセスへのアタッチに必要な権限をすでに持つ場合、GDB を使ってメモリ
を読んだりコードを注入したりすることもできます。

この機能で何をセキュリティ上の脆弱性とみなすか、また、みなさないかは、
次の原則で定義されます。

アタッチには OS レベルの権限が必要
   On every supported platform the operating system gates cross-
   process memory access behind privilege checks ("CAP_SYS_PTRACE",
   root, or administrator rights).  A report that demonstrates an
   issue only after these privileges have already been obtained is
   **not** a vulnerability in CPython, since the OS security boundary
   was already crossed.

侵害されたプロセスを読み取る際のクラッシュやメモリエラーは脆弱性ではな
い
   対象プロセスからインタープリタの内部状態を読むツールは、そのメモリ
   の形式が正しいと信頼する必要があります。 対象プロセスが破損している
   か、攻撃者に制御されている場合、デバッガーやプロファイラーがクラッ
   シュしたり、無意味な出力を生成したり、予測できない動作をしたりする
   可能性があります。 これは、"ptrace" に基づくすべてのデバッガーが受
   け入れているのと同じリスクです。 この種類のバグ (破損した状態の読み
   取りによるバッファーオーバーフロー、セグメンテーション違反、未定義
   動作) は、セキュリティの問題とは **みなしません** が、堅牢性を高め
   る修正は歓迎します。

対象プロセスの脆弱性は対象外
   デバッグ対象の Python プロセスがすでに侵害されている場合、攻撃者は
   そのプロセス内の実行をすでに制御しています。 その状態からさらに影響
   を与えられることを示しても、リモートデバッグのプロトコルの脆弱性に
   はなりません。


"PYTHON_DISABLE_REMOTE_DEBUG" を使う場面
========================================

環境変数 "PYTHON_DISABLE_REMOTE_DEBUG" (および同等のフラグ "-X
disable-remote-debug") を使うと、運用者は **多層防御** の対策として、
プロトコルのプロセス内で動く部分を無効にできます。 OS レベルの権限検査
ですでに権限のないアクセスを防いでいても、プロセスのデバッグやプロファ
イリングを想定せず、攻撃対象となる機能を減らすことを優先する環境では役
立つことがあります。 例えば、セキュリティを強化した環境やサンドボック
ス内への配置が該当します。

この変数を設定しても、OS レベルの他のデバッグインターフェース
("ptrace"、"/proc"、"task_for_pid" など) には影響 **しません**。 それ
ぞれの権限モデルに従って引き続き利用できます。
