Skip to main content

Race-free Windows subprocess trees that terminate when their Python parent exits

Project description

win-safesubprocess

win-safesubprocess is a dependency-free subprocess wrapper for CPython on Windows. It creates every child with CREATE_SUSPENDED, assigns the process to an unnamed kill-on-close Job Object, and only then resumes the primary thread. When the Python parent process terminates, Windows terminates the associated child and descendant processes.

日本語要約:Windowsで親Pythonプロセスが終了した際に、起動した子・孫プロセスを Job Objectで終了させるためのパッケージです。子プロセスはCREATE_SUSPENDEDで 作成し、Job Objectへの割当完了後にだけ再開します。そのため、割当前に子が孫を 起動する競合を排除します。

Installation

python -m pip install win-safesubprocess

Runtime dependencies are not required. CPython 3.10 through 3.14 is covered by the supplied CI matrix.

Usage

The common synchronous subprocess APIs are exposed under familiar names:

import sys
import win_safesubprocess as subprocess

result = subprocess.run(
    [sys.executable, "-c", "print('protected')"],
    capture_output=True,
    text=True,
    check=True,
)
print(result.stdout)

For a long-running process:

from win_safesubprocess import PIPE, Popen

process = Popen(
    ["worker.exe", "--serve"],
    stdout=PIPE,
    stderr=PIPE,
    text=True,
)

Supported helpers are run, call, check_call, check_output, getoutput, and getstatusoutput. Common constants, exceptions, CompletedProcess, and list2cmdline are also re-exported.

Guaranteed Windows start order

A successful Popen(...) follows this sequence:

  1. Create or retrieve the parent-owned Job Object.
  2. Call CreateProcess(..., creationflags | CREATE_SUSPENDED, ...).
  3. Call AssignProcessToJobObject(job, process_handle).
  4. Call ResumeThread(primary_thread_handle) exactly once.
  5. Publish the native handles to CPython's normal subprocess.Popen logic.

The child cannot execute application code or create descendants between steps 2 and 3. If assignment or resumption fails, the unpublished child is terminated, its termination is confirmed, both native handles are closed, and the original error is raised. The package never silently falls back to a runnable unmanaged child.

How CPython compatibility is preserved

CPython's Windows Popen._execute_child contains version-specific handling for pipes, inherited handles, STARTUPINFO, shell behavior, audit hooks, and command line conversion. This package does not vendor that private implementation. Instead, it clones the function supplied by the active interpreter with a private globals dictionary and replaces only its _winapi object with a forwarding proxy that intercepts CreateProcess.

Import fails closed if a future CPython version no longer has the expected shape. This is safer than silently bypassing the suspended-start invariant.

Job lifetime and tree behavior

One unnamed Job Object is shared by all safe subprocesses created by a Python parent. Its handle is explicitly non-inheritable and remains owned by the parent process. Windows closes the handle during process teardown on normal exit, os._exit(), crashes, and external TerminateProcess termination. The job's JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE policy then terminates all remaining members, including inherited descendants.

The shared design intentionally means:

  • every process started through this package has the same parent lifetime;
  • a root child may exit while its descendants continue, but those descendants remain in the job and are killed when the Python parent terminates;
  • Popen.kill() and Popen.terminate() retain standard behavior and target the individual root process, not every package-managed process;
  • no per-launch Job Object handle is leaked in long-running parent processes.

Important limitations

  • Job termination is forceful. Child finally blocks, Python atexit handlers, service stop handlers, and buffered writes are not guaranteed to complete.
  • This is lifecycle containment, not a security sandbox. It does not restrict file, network, token, memory, or process-handle access.
  • A host may already place the Python parent in a Job Object. Modern Windows can support nested jobs, but host policies may still reject assignment. The package terminates the suspended child and raises instead of weakening the guarantee.
  • Breakaway limits are not enabled on the package-owned job, so ordinary descendants cannot opt out with CREATE_BREAKAWAY_FROM_JOB.
  • The internal CREATE_SUSPENDED state is consumed by the package. A successful Popen returns a running, job-managed process rather than a suspended one.
  • asyncio.create_subprocess_exec and asyncio.create_subprocess_shell are not patched. Use these synchronous APIs directly or call them from a worker thread in an async application.
  • Outside Windows, the API is a transparent subprocess subclass/wrapper. It does not add parent-death behavior on POSIX systems.

Development

python -m pip install -e ".[dev]"
python -m pytest
ruff check .
ruff format --check .
mypy src
mypy --platform win32 src
python -m build
python -m twine check --strict dist/*

The Windows integration suite verifies actual Job Object membership, non-inheritance of the job handle, suspended-start ordering, fail-closed setup cleanup, concurrent launches, and child-plus-grandchild termination after normal exit, os._exit(), and external TerminateProcess.

See design notes, release instructions, contribution guidelines, and security policy.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

win_safesubprocess-0.1.0.tar.gz (24.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

win_safesubprocess-0.1.0-py3-none-any.whl (13.9 kB view details)

Uploaded Python 3

File details

Details for the file win_safesubprocess-0.1.0.tar.gz.

File metadata

  • Download URL: win_safesubprocess-0.1.0.tar.gz
  • Upload date:
  • Size: 24.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for win_safesubprocess-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5a8f1a196e4e3f63d00eedec3c7fc32d474823b997d12e1d175e92704d39d3e2
MD5 796d59de52f3fc1db8cbca382d884094
BLAKE2b-256 f0c1d81df193e63291c6d7be29ea5e589ec08b1b3320d48fe909f9e025f2ab36

See more details on using hashes here.

Provenance

The following attestation bundles were made for win_safesubprocess-0.1.0.tar.gz:

Publisher: publish.yml on mokusatsu/win_safesubprocess

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file win_safesubprocess-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for win_safesubprocess-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2d82b7964c147e79c511db479323b8a262564b43d13cc336612e5df13676b398
MD5 665f5161f673828a1d8f0d546a147f00
BLAKE2b-256 78c7cb957829e2103853cbf0fcc3008513b81f1cd73761388202daf2f86c03ec

See more details on using hashes here.

Provenance

The following attestation bundles were made for win_safesubprocess-0.1.0-py3-none-any.whl:

Publisher: publish.yml on mokusatsu/win_safesubprocess

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page