FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Use-after-free in `s_pack_internal` via re-entrant `__bool__` · Issue #143379 · python/cpython · GitHub

Repository navigation

Use-after-free in s_pack_internal via re-entrant __bool__ #143379

Description

What happened?

Struct.pack() iterates over soself->s_codes. For '?', np_bool calls PyObject_IsTrue(arg), which can run user __bool__. A re-entrant __bool__ that calls Struct.__init__() rebuilds the format and frees the old s_codes array while s_pack_internal is still iterating. The loop then dereferences the stale formatcode pointer (code->...), triggering a heap-use-after-free in s_pack_internal.

Proof of Concept:

import struct

S = struct.Struct('?I')

class Evil():
    def __bool__(self):
        # Reinitialize the struct while Struct.pack() is iterating over s_codes.
        # The new format frees the previous codes array, leaving the active
        # iterator with dangling pointers.
        S.__init__('I')
        return True

S.pack(Evil(), 0)

Affected Versions:

Details
Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:9c4638d, Oct 17 2025, 11:19:30) ASAN 1
Python 3.10.19+ (heads/3.10:0142619, Oct 17 2025, 11:20:05) [GCC 13.3.0] ASAN 1
Python 3.11.14+ (heads/3.11:88f3f5b, Oct 17 2025, 11:20:44) [GCC 13.3.0] ASAN 1
Python 3.12.12+ (heads/3.12:8cb2092, Oct 17 2025, 11:21:35) [GCC 13.3.0] ASAN 1
Python 3.13.9+ (heads/3.13:0760a57, Oct 17 2025, 11:22:25) [GCC 13.3.0] ASAN 1
Python 3.14.0+ (heads/3.14:889e918, Oct 17 2025, 11:23:02) [GCC 13.3.0] ASAN 1
Python 3.15.0a1+ (heads/main:fbf0843, Oct 17 2025, 11:23:37) [GCC 13.3.0] ASAN 1

Vulnerable Code Snippet:

Click to expand
/* Buggy Re-entrant Path */
/*
 * Guts of the pack function.
 *
 * Takes a struct object, a tuple of arguments, and offset in that tuple of
 * argument for where to start processing the arguments for packing, and a
 * character buffer for writing the packed string.  The caller must insure
 * that the buffer may contain the required length for packing the arguments.
 * 0 is returned on success, 1 is returned if there is an error.
 *
 */
static int
s_pack_internal(PyStructObject *soself, PyObject *const *args, int offset,
                char* buf, _structmodulestate *state)
{
    formatcode *code;
    /* XXX(nnorwitz): why does i need to be a local?  can we use
       the offset parameter or do we need the wider width? */
    Py_ssize_t i;

    memset(buf, '\0', soself->s_size);
    i = offset;
    for (code = soself->s_codes; code->fmtdef != NULL; code++) {  /* crashing pointer derived */
        const formatdef *e = code->fmtdef;  /* Crash site */
        char *res = buf + code->offset;
        Py_ssize_t j = code->repeat;
        while (j--) {
            PyObject *v = args[i++];
            if (e->format == 's') {
                Py_ssize_t n;
                int isstring;
                const void *p;
                isstring = PyBytes_Check(v);
                if (!isstring && !PyByteArray_Check(v)) {
                    PyErr_SetString(state->StructError,
                                    "argument for 's' must be a bytes object");
                    return -1;
                }
                if (isstring) {
                    n = PyBytes_GET_SIZE(v);
                    p = PyBytes_AS_STRING(v);
                }
                else {
                    n = PyByteArray_GET_SIZE(v);
                    p = PyByteArray_AS_STRING(v);
                }
                if (n > code->size)
                    n = code->size;
                if (n > 0)
                    memcpy(res, p, n);
            } else if (e->format == 'p') {
                Py_ssize_t n;
                int isstring;
                const void *p;
                isstring = PyBytes_Check(v);
                if (!isstring && !PyByteArray_Check(v)) {
                    PyErr_SetString(state->StructError,
                                    "argument for 'p' must be a bytes object");
                    return -1;
                }
                if (isstring) {
                    n = PyBytes_GET_SIZE(v);
                    p = PyBytes_AS_STRING(v);
                }
                else {
                    n = PyByteArray_GET_SIZE(v);
                    p = PyByteArray_AS_STRING(v);
                }
                if (code->size == 0) {
                    n = 0;
                }
                else if (n > (code->size - 1)) {
                    n = code->size - 1;
                }
                if (n > 0)
                    memcpy(res + 1, p, n);
                if (n > 255)
                    n = 255;
                *res = Py_SAFE_DOWNCAST(n, Py_ssize_t, unsigned char);
            } else {
                if (e->pack(state, res, v, e) < 0) {
                    if (PyLong_Check(v) && PyErr_ExceptionMatches(PyExc_OverflowError))
                        PyErr_SetString(state->StructError,
                                        "int too large to convert");
                    return -1;
                }
            }
            res += code->size;
        }
    }

    /* Success */
    return 0;
}

static int
np_bool(_structmodulestate *state, char *p, PyObject *v, const formatdef *f)
{
    int y;
    _Bool x;
    y = PyObject_IsTrue(v);  /* Reentrant call site */
    if (y < 0)
        return -1;
    x = y;
    memcpy(p, &x, sizeof x);
    return 0;
}

/* Clobbering Path */
static int
Struct___init___impl(PyStructObject *self, PyObject *format)
{
    /* ... */
    Py_SETREF(self->s_format, format);
    return prepare_s(self);
}

static int
prepare_s(PyStructObject *self)
{
    /* ... */
    if (self->s_codes != NULL)
        PyMem_Free(self->s_codes);  /* state mutate site */
    self->s_codes = codes;
    /* ... */
    return 0;
}

Sanitizer Output:

Click to expand
=================================================================
==1556376==ERROR: AddressSanitizer: heap-use-after-free on address 0x50c00001dbe0 at pc 0x7cab79d66817 bp 0x7ffeb5e74870 sp 0x7ffeb5e74860
READ of size 8 at 0x50c00001dbe0 thread T0
    #0 0x7cab79d66816 in s_pack_internal Modules/_struct.c:2173
    #1 0x7cab79d6705a in s_pack Modules/_struct.c:2214
    #2 0x56312cfa56ed in method_vectorcall_FASTCALL Objects/descrobject.c:402
    #3 0x56312cf85e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #4 0x56312cf85f72 in PyObject_Vectorcall Objects/call.c:327
    #5 0x56312d204056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #6 0x56312d247e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #7 0x56312d248148 in _PyEval_Vector Python/ceval.c:2001
    #8 0x56312d2483f8 in PyEval_EvalCode Python/ceval.c:884
    #9 0x56312d33f507 in run_eval_code_obj Python/pythonrun.c:1365
    #10 0x56312d33f723 in run_mod Python/pythonrun.c:1459
    #11 0x56312d34057a in pyrun_file Python/pythonrun.c:1293
    #12 0x56312d343220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #13 0x56312d3434f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #14 0x56312d39474d in pymain_run_file_obj Modules/main.c:410
    #15 0x56312d3949b4 in pymain_run_file Modules/main.c:429
    #16 0x56312d3961b2 in pymain_run_python Modules/main.c:691
    #17 0x56312d396842 in Py_RunMain Modules/main.c:772
    #18 0x56312d396a2e in pymain_main Modules/main.c:802
    #19 0x56312d396db3 in Py_BytesMain Modules/main.c:826
    #20 0x56312ce1a645 in main Programs/python.c:15
    #21 0x7cab7a62a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #22 0x7cab7a62a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #23 0x56312ce1a574 in _start (/home/jackfromeast/Desktop/entropy/tasks/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: ff3dc40ea460bd4beb2c3a72283cca525b319bf0)

0x50c00001dbe0 is located 32 bytes inside of 120-byte region [0x50c00001dbc0,0x50c00001dc38)
freed by thread T0 here:
    #0 0x7cab7aafc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x56312d04c96d in _PyMem_RawFree Objects/obmalloc.c:91
    #2 0x56312d04ecd9 in _PyMem_DebugRawFree Objects/obmalloc.c:2955
    #3 0x56312d04ed1a in _PyMem_DebugFree Objects/obmalloc.c:3100
    #4 0x56312d076348 in PyMem_Free Objects/obmalloc.c:1070
    #5 0x7cab79d652ce in prepare_s Modules/_struct.c:1662
    #6 0x7cab79d657ec in Struct___init___impl Modules/_struct.c:1771
    #7 0x7cab79d659cf in Struct___init__ Modules/clinic/_struct.c.h:68
    #8 0x56312d0916b3 in wrap_init Objects/typeobject.c:10073
    #9 0x56312cfa4651 in wrapperdescr_raw_call Objects/descrobject.c:523
    #10 0x56312cfa4f48 in wrapperdescr_call Objects/descrobject.c:570
    #11 0x56312cf85c71 in _PyObject_MakeTpCall Objects/call.c:242
    #12 0x56312cf85f19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #13 0x56312cf85f72 in PyObject_Vectorcall Objects/call.c:327
    #14 0x56312d204056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #15 0x56312d247e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #16 0x56312d248148 in _PyEval_Vector Python/ceval.c:2001
    #17 0x56312cf859b8 in _PyFunction_Vectorcall Objects/call.c:413
    #18 0x56312cf85e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #19 0x56312cf8603f in PyObject_CallOneArg Objects/call.c:395
    #20 0x56312d098648 in call_unbound_noarg Objects/typeobject.c:3040
    #21 0x56312d0b3fa0 in maybe_call_special_no_args Objects/typeobject.c:3153
    #22 0x56312d0b42c2 in slot_nb_bool Objects/typeobject.c:10464
    #23 0x56312d0425b0 in PyObject_IsTrue Objects/object.c:2060
    #24 0x7cab79d622b6 in np_bool Modules/_struct.c:743
    #25 0x7cab79d66689 in s_pack_internal Modules/_struct.c:2166
    #26 0x7cab79d6705a in s_pack Modules/_struct.c:2214
    #27 0x56312cfa56ed in method_vectorcall_FASTCALL Objects/descrobject.c:402
    #28 0x56312cf85e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #29 0x56312cf85f72 in PyObject_Vectorcall Objects/call.c:327

previously allocated by thread T0 here:
    #0 0x7cab7aafd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x56312d04d284 in _PyMem_RawMalloc Objects/obmalloc.c:63
    #2 0x56312d04c655 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2887
    #3 0x56312d04c6bd in _PyMem_DebugRawMalloc Objects/obmalloc.c:2920
    #4 0x56312d04df3b in _PyMem_DebugMalloc Objects/obmalloc.c:3085
    #5 0x56312d076204 in PyMem_Malloc Objects/obmalloc.c:1041
    #6 0x7cab79d6529c in prepare_s Modules/_struct.c:1655
    #7 0x7cab79d657ec in Struct___init___impl Modules/_struct.c:1771
    #8 0x7cab79d659cf in Struct___init__ Modules/clinic/_struct.c.h:68
    #9 0x56312d0ac3c0 in type_call Objects/typeobject.c:2460
    #10 0x56312cf85c71 in _PyObject_MakeTpCall Objects/call.c:242
    #11 0x56312cf85f19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #12 0x56312cf85f72 in PyObject_Vectorcall Objects/call.c:327
    #13 0x56312d204056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #14 0x56312d247e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #15 0x56312d248148 in _PyEval_Vector Python/ceval.c:2001
    #16 0x56312d2483f8 in PyEval_EvalCode Python/ceval.c:884
    #17 0x56312d33f507 in run_eval_code_obj Python/pythonrun.c:1365
    #18 0x56312d33f723 in run_mod Python/pythonrun.c:1459
    #19 0x56312d34057a in pyrun_file Python/pythonrun.c:1293
    #20 0x56312d343220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #21 0x56312d3434f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #22 0x56312d39474d in pymain_run_file_obj Modules/main.c:410
    #23 0x56312d3949b4 in pymain_run_file Modules/main.c:429
    #24 0x56312d3961b2 in pymain_run_python Modules/main.c:691
    #25 0x56312d396842 in Py_RunMain Modules/main.c:772
    #26 0x56312d396a2e in pymain_main Modules/main.c:802
    #27 0x56312d396db3 in Py_BytesMain Modules/main.c:826
    #28 0x56312ce1a645 in main Programs/python.c:15
    #29 0x7cab7a62a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: heap-use-after-free Modules/_struct.c:2173 in s_pack_internal
Shadow bytes around the buggy address:
  0x50c00001d900: 00 00 00 00 00 00 00 fa fa fa fa fa fa fa fa fa
  0x50c00001d980: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fa
  0x50c00001da00: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
  0x50c00001da80: fd fd fd fd fd fd fd fa fa fa fa fa fa fa fa fa
  0x50c00001db00: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fa
=>0x50c00001db80: fa fa fa fa fa fa fa fa fd fd fd fd[fd]fd fd fd
  0x50c00001dc00: fd fd fd fd fd fd fd fa fa fa fa fa fa fa fa fa
  0x50c00001dc80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50c00001dd00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50c00001dd80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50c00001de00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
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
==1556376==ABORTING

CPython versions tested on:

3.9, 3.10, 3.11, 3.12, 3.13, 3.14, 3.15

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.10.19+ (heads/3.10:0142619, Oct 17 2025, 11:20:05) [GCC 13.3.0]

Linked PRs

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    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


      Back | FazBrowse Home | New Git URL