Skip to content

Null pointer dereference in TextIOWrapper.close() via re-entrant closed property that detaches buffer #142594

@jackfromeast

Description

@jackfromeast

What happened?

TextIOWrapper.close() queries the stream’s closed property. A user-defined RawIOBase.closed can re-enter and call wrapper.detach(), clearing self->buffer. The function then continues and calls self->buffer.close() on a NULL pointer, leading to a null-pointer dereference in _PyObject_GetMethod/_Py_TYPE.

Proof of Concept:

import io

class EvilBuffer(io.RawIOBase):
    def __init__(self):
        super().__init__()
        self.wrapper = None
        self.detached = False

    @property
    def closed(self):
        if self.wrapper is not None and not self.detached:
            self.detached = True
            self.wrapper.detach()
        return False

raw = EvilBuffer()
wrapper = io.TextIOWrapper(raw)
raw.wrapper = wrapper
wrapper.close()

Affected Versions:

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

Related Code Snippet

/*[clinic input]
@critical_section
_io.TextIOWrapper.close
[clinic start generated code]*/

static PyObject *
_io_TextIOWrapper_close_impl(textio *self)
/*[clinic end generated code: output=056ccf8b4876e4f4 input=8e12d7079d5ac5c1]*/
{
    PyObject *res;
    int r;
    CHECK_ATTACHED(self);
		
    // Bug: Re-enter the closed property method which detached the textio buffer 
    res = _io_TextIOWrapper_closed_get_impl(self);
    if (res == NULL)
        return NULL;
    r = PyObject_IsTrue(res);
    Py_DECREF(res);
    if (r < 0)
        return NULL;

    if (r > 0) {
        Py_RETURN_NONE; /* stream already closed */
    }
    else {
        PyObject *exc = NULL;
        if (self->finalizing) {
            res = PyObject_CallMethodOneArg(self->buffer, &_Py_ID(_dealloc_warn),
                                            (PyObject *)self);
            if (res) {
                Py_DECREF(res);
            }
            else {
                PyErr_Clear();
            }
        }
        if (_PyFile_Flush((PyObject *)self) < 0) {
            exc = PyErr_GetRaisedException();
        }
				
		// Crash: self->buffer=0x0 after detached.
        res = PyObject_CallMethodNoArgs(self->buffer, &_Py_ID(close));
        if (exc != NULL) {
            _PyErr_ChainExceptions1(exc);
            Py_CLEAR(res);
        }
        return res;
    }
}

Sanitizer Output:

=================================================================
==1558952==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000008 (pc 0x64c4d0fea39a bp 0x7ffe22fcb5f0 sp 0x7ffe22fcb530 T0)
==1558952==The signal is caused by a READ memory access.
==1558952==Hint: address points to the zero page.
    #0 0x64c4d0fea39a in _Py_TYPE Include/object.h:277
    #1 0x64c4d0fea39a in _PyObject_GetMethodStackRef Objects/object.c:1698
    #2 0x64c4d0f2a796 in PyObject_VectorcallMethod Objects/call.c:840
    #3 0x64c4d13e717d in PyObject_CallMethodNoArgs Include/cpython/abstract.h:65
    #4 0x64c4d13e717d in _io_TextIOWrapper_close_impl Modules/_io/textio.c:3169
    #5 0x64c4d13e7277 in _io_TextIOWrapper_close Modules/_io/clinic/textio.c.h:1160
    #6 0x64c4d0f48571 in method_vectorcall_NOARGS Objects/descrobject.c:448
    #7 0x64c4d0f28e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #8 0x64c4d0f28f72 in PyObject_Vectorcall Objects/call.c:327
    #9 0x64c4d11a7056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #10 0x64c4d11eae54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #11 0x64c4d11eb148 in _PyEval_Vector Python/ceval.c:2001
    #12 0x64c4d11eb3f8 in PyEval_EvalCode Python/ceval.c:884
    #13 0x64c4d12e2507 in run_eval_code_obj Python/pythonrun.c:1365
    #14 0x64c4d12e2723 in run_mod Python/pythonrun.c:1459
    #15 0x64c4d12e357a in pyrun_file Python/pythonrun.c:1293
    #16 0x64c4d12e6220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #17 0x64c4d12e64f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #18 0x64c4d133774d in pymain_run_file_obj Modules/main.c:410
    #19 0x64c4d13379b4 in pymain_run_file Modules/main.c:429
    #20 0x64c4d13391b2 in pymain_run_python Modules/main.c:691
    #21 0x64c4d1339842 in Py_RunMain Modules/main.c:772
    #22 0x64c4d1339a2e in pymain_main Modules/main.c:802
    #23 0x64c4d1339db3 in Py_BytesMain Modules/main.c:826
    #24 0x64c4d0dbd645 in main Programs/python.c:15
    #25 0x7cb5cbe2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #26 0x7cb5cbe2a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #27 0x64c4d0dbd574 in _start (/home/jackfromeast/Desktop/entropy/tasks/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: ff3dc40ea460bd4beb2c3a72283cca525b319bf0)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV Include/object.h:277 in _Py_TYPE
==1558952==ABORTING

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirtopic-IOtype-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