Repository navigation
Windows: debug allocator failure during audit-hook cleanup #161074
Description
Crash report
Summary
On one Windows 11 x64 machine, a standalone standard-library program exits with 0xC0000005 during interpreter finalization when started with -X dev. All its worker threads have joined and its atexit callback has written a completion marker before the native failure. The actual process exit, not that marker, was checked independently.
Earlier local dumps with matching official symbols show an audit-hook cleanup / debug allocator failure followed by the Windows fatal-error reporting null dereference described in #116502. This report concerns the preceding allocator failure and its environment dependence, not a second report of that null-dereference bug. The origin/registration time of the native audit entry is unknown; a CPython root cause is not established.
The same verified Python binary and portable target program exited successfully with -X dev on a GitHub-hosted Windows Server 2022 runner. A subsequent local control without a debugger still failed. There is no application import, database, network access or scheduled faulthandler.dump_traceback_later() timer in these control runs.
Versions and environments
Main comparison: official PSF CPython 3.15.2 Windows x64 embeddable distribution, GIL build. The local reference and hosted reference were compared against the same archive and extracted executable/DLL hashes.
Recorded sys.version from the target process (not a separate python -VV invocation):
3.15.2 (tags/v3.15.2:5e58eaa, Oct 25 2026, 17:27:03) [MSC v.1951 64 bit (AMD64)]
- Failing host: Windows 11 Pro
10.0.26300.0, x64; final no-debugger comparison used PowerShell 7.6.5. - Successful host: Windows Server 2022
10.0.20348.0, GitHub imagewin22/20261026.347.1, PowerShell 7.6.9. - Archive SHA256:
d012f3aa29cf07436dae8aea1fec0fd195c2612a0b059b1f915ae0afb9ad1f62. - Python DLL SHA256:
9862572fd449f32a62c1eaec2c6750a9c9fa512f4c9c2ea0e66776cf9212b7c2. - Official 3.14.10 and an earlier self-built 3.12.16 also had a local development-mode finalization failure; this does not establish that all failures have the same origin.
Relevant environmental caveat: Norton modules were present in earlier local dump module lists. Their presence does not establish causation or identify who registered the native audit hook. Protection/audit configuration was not disabled or altered. No general claim of a CPython-only defect or a defect in a security product is made.
Observed comparison
These are bounded individual observations, not a statistical failure-rate claim.
| Python 3.15.2 target | External debugger/collector | Mode | Independently observed process exit |
|---|---|---|---|
| Local Windows 11 | DbgEng | Normal | 0 |
| Local Windows 11 | DbgEng | -X dev |
0xC0000005; earlier symbolized audit/debug-free/fatal-output stack |
| GitHub Windows Server 2022 | None | Normal | 0, 8.178 s |
| Same hosted job | None | -X dev |
0, 8.136 s |
| Local Windows 11, later control | None | -X dev |
0xC0000005, 11.797 s; no new native stack captured |
The final local run had isolated=1, ignore_environment=1, no_site=1, dev_mode=true, timer_started=false; 40 thread batches joined, eight seconds of work, no time/memory limit violation and no forced termination. Its bounded stderr inspection detected Fatal Python error and bad ID (1,128 characters, not truncated); the full stderr text was not retained. These indicators do not prove the same audit entry or native stack as the earlier dumps.
The previous collector is therefore not necessary for the local symptom. OS/build and process-environment differences remain. -X dev enables several facilities, so this comparison does not isolate only the memory allocator.
Expected result
The standard-library workload and interpreter should finish normally. If an allocator contract violation is detected, an actionable diagnostic should be available; the known secondary fatal-output crash currently complicates that investigation.
Target program and execution conditions
Below is the portable diagnostic target used for the comparison. It is not claimed to be the smallest reproducer or to fail on an arbitrary machine. It retains the original metadata check for application imports (copper_modules_loaded); this is just a boolean check of names in sys.modules, not an application import. The only imports are standard-library modules. Use control, not timer, for this report.
Steps corresponding to the recorded target protocol:
- Save the code as
minimal.py. Create a fresh output directory<BASE>\minimal-case-devunder an absolute temporary<BASE>directory. - Launch the chosen
pythonw.exeas a child process using:For the normal control omitpythonw.exe -I -S -B -X dev <absolute-path-to-minimal.py> <BASE>\minimal-case-dev control 4 8 <BASE>-X dev. The4argument is not used to start a timer incontrolmode. - The measured PowerShell/.NET
ProcessStartInfocoordinator usedUseShellExecute=false, redirected/drained stdout and stderr, and cleared the child environment exceptSYSTEMROOT,WINDIR,TEMP,TMP. It used a working directory outside the application checkout. Afterminimal-ready.jsonappears, create the empty file<BASE>\minimal-case-dev\gowithin the target's 15-second gate timeout. - Wait for the actual child process handle to signal exit and record its exit code. The recorded coordinator had a 45-second process limit and a sampled 256 MiB working/private memory limit; neither triggered.
minimal-closed.jsonalone is not proof of clean termination. There is no DbgEng/other collector in the final local control or the hosted comparisons.
The source below is the tested target, not a newly executed reduction. The PowerShell coordinators differed in their local path checks and debugger-presence check; no perfect single-factor comparison of every OS/process condition is claimed.
minimal.py — standard-library-only diagnostic target
"""Standalone CPython frame/timer candidate. Standard library only; no Copper."""
import atexit,faulthandler,json,os,sys,threading,time,types
from pathlib import Path
def main():
root=Path(sys.argv[1]).resolve();mode=sys.argv[2];interval=float(sys.argv[3]);seconds=float(sys.argv[4])
if mode not in ('control','timer') or not 0<interval<=4 or not 0<seconds<=8:raise ValueError('BOUNDED_VARIANT_REQUIRED')
if not root.name.startswith('minimal-case-') or not root.is_relative_to(Path(sys.argv[5])):raise ValueError('OWN_ROOT_REQUIRED')
state={'pid':os.getpid(),'variant':mode,'interval':interval,'seconds':seconds,'python':sys.version,'executable':sys.executable,'isolated':sys.flags.isolated,'ignore_environment':sys.flags.ignore_environment,'no_site':sys.flags.no_site,'dev_mode':sys.flags.dev_mode,'xoptions':sys._xoptions,'copper_modules_loaded':any(x.startswith('copper_') for x in sys.modules),'phase':'ready','timer_started':False,'threads_joined':False}
def save(name):
with (root/name).open('w',encoding='utf-8') as f:json.dump(state,f,indent=2)
save('minimal-ready.json')
deadline=time.monotonic()+15
while not (root/'go').exists():
if time.monotonic()>=deadline:raise TimeoutError('OWN_CAPTURE_GATE_TIMEOUT')
time.sleep(.005)
output=open(os.devnull,'w');stop=threading.Event();workers=[];batches=0;calls=0
def template(value):return value+1
def churn(end):
nonlocal calls
while not stop.is_set() and time.monotonic()<end:
code=template.__code__.replace();fn=types.FunctionType(code,{})
assert fn(1)==2
calls+=1
del fn,code
def cleanup():
faulthandler.cancel_dump_traceback_later()
output.close()
state.update(phase='atexit_complete',timer_cancelled=True,threads_joined=all(not t.is_alive() for t in workers),copper_modules_loaded=any(x.startswith('copper_') for x in sys.modules))
save('minimal-closed.json')
atexit.register(cleanup)
if mode=='timer':
faulthandler.dump_traceback_later(interval,repeat=True,file=output)
state['timer_started']=True
state['phase']='frame_thread_turnover';save('minimal-running.json')
started=time.monotonic();end=started+seconds
try:
while time.monotonic()<end:
thread=threading.Thread(target=churn,args=(min(end,time.monotonic()+.2),),name='own-transient-frame')
workers[:]=[thread];thread.start();thread.join(1)
if thread.is_alive():raise RuntimeError('OWN_THREAD_JOIN_PENDING')
batches+=1
finally:
stop.set()
for thread in workers:thread.join(1)
state.update(phase='main_return_before_atexit',batches=batches,function_calls=calls,elapsed=time.monotonic()-started,threads_joined=all(not t.is_alive() for t in workers))
save('minimal-return.json')
# Timer stays scheduled until atexit, as in the historical test-only prefix.
if __name__=='__main__':main()Earlier native context — distinct from the latest no-debugger run
Earlier local development-mode runs of the standalone workload on the official reference builds were analyzed with PDB GUID/age matched to the loaded Python DLLs. The recorded path is:
_Py_Finalize / finalize_interp_clear
-> _PySys_ClearAuditHooks
-> PyMem_RawFree / _PyMem_DebugRawFree
-> _PyMem_DebugCheckAddress
-> _Py_FatalErrorFormat
-> fatal_error
-> fatal_output_debug
-> NULL read (Python DLL / CRT, depending on build)
The primary debug-allocator check happens before the secondary null read. The originating audit-entry allocation and lifetime were not reconstructed. A full dump from the earlier 3.12 investigation also did not recover the optimized-away original entry pointer at the final error site. Those older observations must not be silently assigned to the most recent 3.15.2 no-debugger run, for which only exit/marker/stderr indicators are available.
Raw memory dumps, private paths and application files are not attached. This report does not assert an exploitable vulnerability or propose disabling audit/security checks.
Specific triage question and related reports
Is there a known initialization/allocator-contract issue that could explain a native audit entry being allocated before a debug-allocator transition and later released through the debug Raw allocator? That is only a hypothesis; prior memory corruption remains another possibility. We have not identified which component registered the entry.
I found #116502 for the secondary Windows fatal-output null dereference. The earlier-auditing discussion #81686 concerns event delivery, not this observed allocator/free mismatch; I did not find an exact primary-failure duplicate in the searches performed. Please redirect/mark duplicate if a suitable existing report is available.
What focused observation would best distinguish registration-before-allocator-setup from later corruption, without disabling machine protection?
CPython versions tested on:
3.12, 3.14, 3.15. Main branch not tested. The independently compared official distribution in the main table is 3.15.2.
Operating systems tested on:
Windows (local Windows 11 Pro and GitHub-hosted Windows Server 2022).
Output from running 'python -VV' on the command line:
No separate version-only process was run; the exact sys.version captured inside the test target is quoted above.
Disclosure: AI tools assisted with the diagnostic program, recorded test analysis and preparation of this report. The reported measurements are from actual retained runs; hypotheses, missing stack data and the absence of a verified fix are explicitly separated. No new target run was performed merely to submit this report.
Activity