Python 3.15 (the final release is currently scheduled for October 9, 2026) introduces a new interpreter-startup mechanism worth adding to the DFIR checklist: .start files.
A .start file placed in a Python site-packages directory contains one or more references in the form package.module:callable. During normal interpreter initialization, Python resolves those references, imports the corresponding modules, and invokes the callables before control reaches the first line of user-supplied Python code. The application itself does not need to import the module and does not need to know that the .start file exists.
From a security perspective, that makes .start files an interesting execution primitive. An attacker who can write to an applicable site-packages directory can arrange for code to execute whenever an affected Python interpreter starts. Depending on where the file is placed, this may affect a single virtual environment, a user’s Python installations, or a system-wide interpreter.
This is not an accidental Python feature. .start files were introduced deliberately by PEP 829 as a more structured replacement for executable import lines in .pth files (see my article about .pth files here). The new design substantially improves auditability: the configuration file itself can no longer contain arbitrary Python statements. It does not, however, remove pre-start arbitrary code execution. The referenced module is imported and its callable runs with the privileges and environment of the Python process. See the official Python documentation for details.
Traditionally, Python’s site module processes .pth files from site-packages directories during interpreter initialization. Their original purpose is extending sys.path, but there is a second capability, import something. Lines beginning with import are executed. A package could effectively arrange for arbitrary Python statements to execute whenever an affected interpreter started.
Python 3.15 consequently begins deprecating executable import lines inside .pth files and introduces .start files as their structured replacement. A .start file does not contain arbitrary statements. Instead, it contains references such as foo.submod:initialize. Python resolves foo.submod, obtains the initialize object and calls it without arguments.
The important point from a security perspective is that this is still code execution during interpreter startup. PEP 829 deliberately makes this execution path easier to identify and audit, which is a substantial improvement over arbitrary one-line Python inside .pth. But if an attacker can modify an applicable site-packages directory, a .start file can still arrange execution every time the affected Python environment starts. The PEP itself explicitly notes that the pre-start execution attack surface is not eliminated; entry points remain capable of arbitrary execution once their module is imported and their callable invoked.
There is also an important migration detail: if a .start file and a .pth file share the same base name, executable import lines in the corresponding .pth file are ignored. From a forensic perspective, this matters when reconstructing startup behavior because the presence of both artifacts does not necessarily mean that both execution mechanisms were active.
.start files live in the same site-packages directories Python already inspects for .pth files. On a typical Linux installation, these might include paths resembling:
/usr/lib/python3.15/site-packages/
/usr/local/lib/python3.15/site-packages/
~/.local/lib/python3.15/site-packages/
Or use:
However, there is a slight forensic problem with doing that. If the interpreter is compromised through a .start file, simply starting it may already execute the code we are trying to investigate. For examination, using -S is therefore much safer:
python3.15 -S -c 'import site; print(site.getsitepackages())'
The -S option suppresses automatic initialization through site. Merely importing the site module afterwards does not retroactively perform the normal site modifications unless site.main() is explicitly called. A .start entry point executes before the first line of the program the user actually intended to run. Python’s documentation explicitly states that the startup entry points are executed regardless of whether the associated module would otherwise have been used by the program. -S, which disables site processing entirely, prevents them from executing.
The following test intentionally uses a harmless payload. It writes information about the Python process into /tmp/python315-start-lab.log and sets an environment variable that we can inspect from the Python program afterward. First, create an isolated virtual environment:
python3.15 -m venv /tmp/py315-start-lab
PY=/tmp/py315-start-lab/bin/python
Verify the interpreter:
At the time of writing, this should return something similar to:
Next, determine the virtual environment’s site-packages directory:
SITE="$("$PY" -c 'import site; print(site.getsitepackages()[0])')"
echo "$SITE"
For this lab, it should resemble:
/tmp/py315-start-lab/lib/python3.15/site-packages
Now create the module which will contain our startup callable. Python processes .pth and .start files in alphabetical filename order within each site-packages directory. The 00- prefix used here is therefore intentional and causes this entry point to be encountered early among startup files in the same directory.
cat > "$SITE/startup_probe.py" <<'PY'
from datetime import datetime, timezone
from pathlib import Path
import os
import sys
def initialize():
os.environ["PYTHON_START_LAB"] = "executed"
event = (
f"time={datetime.now(timezone.utc).isoformat()} "
f"pid={os.getpid()} "
f"ppid={os.getppid()} "
f"uid={os.getuid()} "
f"executable={sys.executable!r} "
f"argv={sys.argv!r}\n"
)
with Path("/tmp/python315-start-lab.log").open("a") as f:
f.write(event)
PY
So far nothing special has happened. startup_probe.py is simply another Python module.
Now create the startup entry point:
cat > "$SITE/00-startup-probe.start" <<'EOF'
startup_probe:initialize
EOF
The contents of our site-packages directory now include:
startup_probe.py
00-startup-probe.start
Remove any previous marker file:
rm -f /tmp/python315-start-lab.log
Then launch Python with an otherwise completely unrelated command:
"$PY" -c 'import os; print("main:", os.getenv("PYTHON_START_LAB"))'
The output should be:
Our -c expression never imported startup_probe. Nevertheless, by the time our expression began executing, startup_probe.initialize() had already run. Examining the marker file from the shell:
cat /tmp/python315-start-lab.log
should produce something resembling:
time=2026-10-03T08:31:12.123456+00:00 pid=18442 ppid=17101 uid=1000 executable='/tmp/py315-start-lab/bin/python' argv=['-c']
Run something completely different:
and examine the file again:
cat /tmp/python315-start-lab.log
There should now be another execution record. The interesting part is that the Python command itself contains no reference to startup_probe. From the outside, this is simply a normal interpreter invocation. The startup code executes inside the same Python process before the intended expression begins.
Either inspect the filesystem externally or use Python with -S. For example:
python3.15 -S <<'PY'
from pathlib import Path
import site
directories = set(site.getsitepackages())
user_site = site.getusersitepackages()
if user_site:
directories.add(user_site)
for directory in sorted(directories):
root = Path(directory)
if not root.is_dir():
continue
print(f"\n[{root}]")
for path in sorted(root.glob("*.start")):
print(f" {path}")
try:
for line in path.read_text(encoding="utf-8-sig").splitlines():
if line.strip() and not line.lstrip().startswith("#"):
print(f" -> {line}")
except Exception as exc:
print(f" [error: {exc}]")
PY
For virtual environments, enumerate the environments independently as well. An application-specific venv may not appear in the site directories of the system interpreter you happen to be using. For a forensic sweep, it can therefore be useful to search for .start files directly rather than restricting the search to directories named site-packages. Distribution-specific layouts, custom Python installations, and application-bundled environments may use different paths.
find /usr /usr/local /opt /home \
-type f \
-name '*.start' \
-print 2>/dev/null
Finding the .start file is only the first step. The file itself merely points to a Python object, so the referenced module should be examined as well. During an investigation, I would correlate the .start file’s creation and modification times with the referenced module, determine whether both belong to an expected Python package, and check application-specific virtual environments separately. A legitimate .start file is not inherently suspicious; unexpected provenance or modification is the more useful signal.
PEP 829 makes Python startup behavior more explicit by replacing executable .pth lines with structured .start entry points. That is a security improvement, but it does not remove code execution during interpreter startup.
For DFIR practitioners, the practical takeaway is simple: with Python 3.15, .start files become another artifact worth checking when investigating unexpected Python execution or persistence. I have not yet found evidence of the mechanism being abused in the wild, which is unsurprising given how new Python 3.15 is. For environments adopting 3.15, however, .start files should now be part of the Python startup artifacts considered during triage.
What I learned today: Short blog posts about novel information for me.