Affects only main / 3.15-dev — not a PSRT case
Per python.org/dev/security/, the PSRT does not accept reports that only affect pre-release versions. The _remote_debugging.BinaryWriter API is new in 3.15-dev and does not exist on any supported branch, so this is a regular bug report rather than a private security advisory.
| Branch | Modules/_remote_debugging/binary_io_writer.c exists? |
Affected? |
|---|---|---|
| main (3.15-dev) | yes | yes |
| 3.14, 3.13, 3.12, 3.11, 3.10 | no | no |
(The whole _remote_debugging.BinaryWriter API was added in 3.15-dev as part of the new profiling.sampling binary-replay workflow.)
What's wrong
Modules/_remote_debugging/binary_io_writer.c::binary_writer_create calls fopen(filename, "wb") to open the output file, but does not fire PySys_Audit("open", "sCi", filename, 'w', 0) first. The canonical pattern in cpython is to fire the audit hook before fopen:
Modules/readline.c:347— firesPySys_Audit("open", "sCi", filename, 'w', 0)beforefopen.Modules/_io/fileio.c::_io_FileIO___init___impl— firesPySys_Audit("open", "OOi", ...)before opening the FD.
binary_writer_create follows neither precedent. An operator's open audit-hook does not fire when _remote_debugging.BinaryWriter(target) writes to target, even though the operator's same hook DOES fire on builtins.open(target, "w") and _io.FileIO(target, "w").
Reproducer
On a fresh main checkout (a debug build is fine, e.g. ./configure --with-pydebug && make -j8):
import os, sys, _remote_debugging events = [] def hook(event, args): if event == "open": events.append({"path": str(args[0]) if args else None, "mode": str(args[1]) if len(args) > 1 else None}) TARGET = "/tmp/poc-binary-writer.prof" CONTROL = "/tmp/poc-builtins-open.txt" for p in (TARGET, CONTROL): if os.path.exists(p): os.unlink(p) sys.addaudithook(hook) # Negative control — should fire the audit hook (built-in `open`): with open(CONTROL, "w") as f: f.write("control\n") # Target — should fire the audit hook per PEP 578, currently does NOT: COMP_NONE = getattr(_remote_debugging, "COMPRESSION_NONE", 0) w = _remote_debugging.BinaryWriter(TARGET, sample_interval_us=1000, compression=COMP_NONE, start_time_us=0) w.close() control_fired = any(e["path"] == CONTROL for e in events) target_fired = any(e["path"] == TARGET for e in events) print(f"control_fired={control_fired} target_fired={target_fired}") # Expected (post-fix): control_fired=True target_fired=True # Actual on baseline: control_fired=True target_fired=False for p in (TARGET, CONTROL): if os.path.exists(p): os.unlink(p)
Suggested fix
Insert the canonical PySys_Audit call before the fopen. Pattern matches Modules/readline.c:347 verbatim.
diff --git a/Modules/_remote_debugging/binary_io_writer.c b/Modules/_remote_debugging/binary_io_writer.c @@ -802,6 +802,10 @@ binary_writer_create(const char *filename, uint64_t sample_interval_us, int comp } } + if (PySys_Audit("open", "sCi", filename, 'w', 0) < 0) { + goto error; + } + writer->fp = fopen(filename, "wb"); if (!writer->fp) { PyErr_SetFromErrnoWithFilename(PyExc_IOError, filename);
Empirical verification
Patched against fork SHA 836fbdaa (close to upstream main):
| Run | control_fired (builtins.open(CONTROL)) |
target_fired (BinaryWriter(TARGET)) |
|---|---|---|
| Baseline | true | false (vuln) |
| Patched | true | true |
Negative control fires correctly on both trees; mutation-confirmed sound (reverting just the patched line returns target_fired=false).
PoC soundness verdict: sound.
Severity
PEP 578 contract violation (audit-hook visibility gap on a documented profiling tool's file-write path), not RCE. An attacker who can invoke _remote_debugging.BinaryWriter writes to arbitrary paths without operator hook visibility. Recommend topic-security / topic-audit-hooks triage labels.
Notes for triage
- 4-line idiomatic insertion that matches the canonical PEP 578 implementation pattern in cpython (
Modules/readline.c:347). - No public-API surface change:
_remote_debugging.BinaryWriter()constructor signature, return type, and exception types are unchanged. - Operators who reject the event via hook (raise an exception) will now see
BinaryWriter()raise — aligns with all other audited file-open call sites.
This issue is part of a small set of PEP 578 audit-hook bypass findings observed on main; a sibling issue covers posix._clearenv() / os.environ.clear() (file Modules/posixmodule.c).