Bug report
Bug description:
Segfault: NULL deref in os__path_normpath_impl (posixmodule.c:6149)
os__path_normpath_impl doesn't NULL-check the PyUnicode_From* result; on a bytes path under OOM it feeds NULL to PyUnicode_EncodeFSDefault, which dereferences it in unicode_encode_utf8 (unicodeobject.c:5681).
AI Disclaimer: this gist was drafted by Claude Code, which also generated the reduced reproducer.
Crash report
os__path_normpath_impl builds the normalized path with PyUnicode_FromOrdinal('.')
or PyUnicode_FromWideChar(...), either of which can return NULL under memory
pressure. When the input path was bytes, it then re-encodes with
Py_SETREF(result, PyUnicode_EncodeFSDefault(result)) without checking result for
NULL. PyUnicode_EncodeFSDefault(NULL) reaches unicode_encode_utf8(NULL, …), whose
PyUnicode_Check(unicode) dereferences the NULL ob_type → SIGSEGV. Reproduces on all
build configurations (it is a plain NULL dereference, not a debug-only assert).
Reproducer
Minimal, stdlib-only:
import posix
from _testcapi import set_nomemory, remove_mem_hooks
for start in range(1, 2000):
set_nomemory(start, 0)
try:
try:
posix._path_normpath(b"foo//bar/../baz") # bytes path -> EncodeFSDefault branch
finally:
remove_mem_hooks()
except BaseException:
pass
(posix._path_normpath is the C accelerator behind ntpath.normpath; on POSIX it is
reachable as posix._path_normpath. A bytes argument is required to take the
PyBytes_Check re-encode branch.)
Backtrace
Program received signal SIGSEGV, Segmentation fault.
#0 unicode_encode_utf8 (unicode=0x0, ...) Objects/unicodeobject.c:5681 # PyUnicode_Check(NULL)->ob_type
#1 os__path_normpath_impl (module, path) Modules/posixmodule.c:6149 # Py_SETREF(result, EncodeFSDefault(result)), result==NULL
#2 os__path_normpath Modules/clinic/posixmodule.c.h:2701
#3 _Py_BuiltinCallFastWithKeywords_StackRef Python/ceval.c:841
#4 _PyEval_EvalFrameDefault Python/generated_cases.c.h:2603
unicode == 0x0 confirmed in gdb: the result produced just above is NULL and flows
straight into the encoder.
Root cause
Modules/posixmodule.c, os__path_normpath_impl (L6134-6152):
PyObject *result;
...
if (!norm_len) {
result = PyUnicode_FromOrdinal('.'); /* may return NULL under OOM */
}
else {
result = PyUnicode_FromWideChar(norm_path, norm_len); /* may return NULL under OOM */
}
if (PyBytes_Check(path->object)) {
Py_SETREF(result, PyUnicode_EncodeFSDefault(result)); /* L6149: result unchecked */
}
return result;
The two PyUnicode_From* constructors allocate and can fail; result is never
NULL-checked before the bytes-path re-encode. PyUnicode_EncodeFSDefault(NULL)
forwards NULL to unicode_encode_utf8, which assumes a valid object.
Suggested fix
Guard the re-encode (and the function already returns result==NULL correctly to
propagate the error):
if (result != NULL && PyBytes_Check(path->object)) {
Py_SETREF(result, PyUnicode_EncodeFSDefault(result));
}
return result;
(Equivalently, if (result == NULL) return NULL; before the PyBytes_Check.)
Hardening PyUnicode_EncodeFSDefault / unicode_encode_utf8 to reject NULL would
convert the crash into a clean error elsewhere, but the missing check belongs here.
Notes
Found via OOM-injection fuzzing (_testcapi.set_nomemory); surfaced by the catalog
ingest as a new site (unicode_encode_utf8) distinct from OOM-0009 (replace,
unicodeobject.c:10783). Clean NULL dereference — reproduces on every build
(ft_debug_asan, ft_release, jit, upstream), unlike the debug-only assert/ASan-only
findings. Original vehicle python-7/profiling_sampling_cli-segmentation_fault reached
it via an os.path operation on a bytes path; reduced to the 3-line trigger above.
Versions
- main (3.16.0a0), commit 15d7406. Reproduces (SIGSEGV) on all four local builds
(free-threaded debug+ASan, free-threaded release, JIT, upstream release).
Part of python/cpython#151763 — an umbrella tracking 35 OOM-related crash findings.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Windows
Bug report
Bug description:
Segfault: NULL deref in
os__path_normpath_impl(posixmodule.c:6149)os__path_normpath_impldoesn't NULL-check thePyUnicode_From*result; on abytespath under OOM it feeds NULL toPyUnicode_EncodeFSDefault, which dereferences it inunicode_encode_utf8(unicodeobject.c:5681).AI Disclaimer: this gist was drafted by Claude Code, which also generated the reduced reproducer.
Crash report
os__path_normpath_implbuilds the normalized path withPyUnicode_FromOrdinal('.')or
PyUnicode_FromWideChar(...), either of which can returnNULLunder memorypressure. When the input path was
bytes, it then re-encodes withPy_SETREF(result, PyUnicode_EncodeFSDefault(result))without checkingresultforNULL.
PyUnicode_EncodeFSDefault(NULL)reachesunicode_encode_utf8(NULL, …), whosePyUnicode_Check(unicode)dereferences the NULLob_type→ SIGSEGV. Reproduces on allbuild configurations (it is a plain NULL dereference, not a debug-only assert).
Reproducer
Minimal, stdlib-only:
(
posix._path_normpathis the C accelerator behindntpath.normpath; on POSIX it isreachable as
posix._path_normpath. Abytesargument is required to take thePyBytes_Checkre-encode branch.)Backtrace
unicode == 0x0confirmed in gdb: theresultproduced just above is NULL and flowsstraight into the encoder.
Root cause
Modules/posixmodule.c,os__path_normpath_impl(L6134-6152):The two
PyUnicode_From*constructors allocate and can fail;resultis neverNULL-checked before the
bytes-path re-encode.PyUnicode_EncodeFSDefault(NULL)forwards NULL to
unicode_encode_utf8, which assumes a valid object.Suggested fix
Guard the re-encode (and the function already returns
result==NULLcorrectly topropagate the error):
(Equivalently,
if (result == NULL) return NULL;before thePyBytes_Check.)Hardening
PyUnicode_EncodeFSDefault/unicode_encode_utf8to reject NULL wouldconvert the crash into a clean error elsewhere, but the missing check belongs here.
Notes
Found via OOM-injection fuzzing (
_testcapi.set_nomemory); surfaced by the catalogingest as a new site (
unicode_encode_utf8) distinct from OOM-0009 (replace,unicodeobject.c:10783). Clean NULL dereference — reproduces on every build
(ft_debug_asan, ft_release, jit, upstream), unlike the debug-only assert/ASan-only
findings. Original vehicle
python-7/profiling_sampling_cli-segmentation_faultreachedit via an
os.pathoperation on a bytes path; reduced to the 3-line trigger above.Versions
(free-threaded debug+ASan, free-threaded release, JIT, upstream release).
Part of python/cpython#151763 — an umbrella tracking 35 OOM-related crash findings.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Windows