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:
- Create or retrieve the parent-owned Job Object.
- Call
CreateProcess(..., creationflags | CREATE_SUSPENDED, ...). - Call
AssignProcessToJobObject(job, process_handle). - Call
ResumeThread(primary_thread_handle)exactly once. - Publish the native handles to CPython's normal
subprocess.Popenlogic.
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()andPopen.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
finallyblocks, Pythonatexithandlers, 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_SUSPENDEDstate is consumed by the package. A successfulPopenreturns a running, job-managed process rather than a suspended one. asyncio.create_subprocess_execandasyncio.create_subprocess_shellare 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
subprocesssubclass/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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a8f1a196e4e3f63d00eedec3c7fc32d474823b997d12e1d175e92704d39d3e2
|
|
| MD5 |
796d59de52f3fc1db8cbca382d884094
|
|
| BLAKE2b-256 |
f0c1d81df193e63291c6d7be29ea5e589ec08b1b3320d48fe909f9e025f2ab36
|
Provenance
The following attestation bundles were made for win_safesubprocess-0.1.0.tar.gz:
Publisher:
publish.yml on mokusatsu/win_safesubprocess
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
win_safesubprocess-0.1.0.tar.gz -
Subject digest:
5a8f1a196e4e3f63d00eedec3c7fc32d474823b997d12e1d175e92704d39d3e2 - Sigstore transparency entry: 2163475901
- Sigstore integration time:
-
Permalink:
mokusatsu/win_safesubprocess@24be26f05f78e3869fdec986ad4b478e1a7573ec -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mokusatsu
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@24be26f05f78e3869fdec986ad4b478e1a7573ec -
Trigger Event:
release
-
Statement type:
File details
Details for the file win_safesubprocess-0.1.0-py3-none-any.whl.
File metadata
- Download URL: win_safesubprocess-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d82b7964c147e79c511db479323b8a262564b43d13cc336612e5df13676b398
|
|
| MD5 |
665f5161f673828a1d8f0d546a147f00
|
|
| BLAKE2b-256 |
78c7cb957829e2103853cbf0fcc3008513b81f1cd73761388202daf2f86c03ec
|
Provenance
The following attestation bundles were made for win_safesubprocess-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on mokusatsu/win_safesubprocess
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
win_safesubprocess-0.1.0-py3-none-any.whl -
Subject digest:
2d82b7964c147e79c511db479323b8a262564b43d13cc336612e5df13676b398 - Sigstore transparency entry: 2163475938
- Sigstore integration time:
-
Permalink:
mokusatsu/win_safesubprocess@24be26f05f78e3869fdec986ad4b478e1a7573ec -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mokusatsu
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@24be26f05f78e3869fdec986ad4b478e1a7573ec -
Trigger Event:
release
-
Statement type: