From 689605d5bdbe2e482209f726e2e0ba716dd1fae8 Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Sun, 23 Aug 2026 12:12:13 +0200 Subject: [PATCH] fix: leak in os.sendfile --- Lib/test/test_os/test_os.py | 11 +++++++ ...-08-23-11-56-00.gh-issue-156287.jDxO8d.rst | 2 ++ Modules/posixmodule.c | 33 +++++++++++++------ 3 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-23-11-56-00.gh-issue-156287.jDxO8d.rst diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 81b3043eb7e75bc..5a013d7535e801a 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -3878,6 +3878,17 @@ async def test_trailers(self): await self.server.wait_closed() self.assertEqual(self.server_buffer, b"abcde123456789") + @requires_headers_trailers + async def test_headers_released_on_invalid_trailers(self): + # Validation errors after iov_setup of headers must still release + # the exported buffers, otherwise the bytearray cannot be resized. + header = bytearray(b"header") + with self.assertRaisesRegex(TypeError, + r"sendfile\(\) trailers must be a sequence"): + os.sendfile(self.sockno, self.fileno, 0, 0, + headers=[header], trailers=object()) + header.append(0) + @requires_headers_trailers @requires_32b async def test_headers_overflow_32bits(self): diff --git a/Misc/NEWS.d/next/Library/2026-08-23-11-56-00.gh-issue-156287.jDxO8d.rst b/Misc/NEWS.d/next/Library/2026-08-23-11-56-00.gh-issue-156287.jDxO8d.rst new file mode 100644 index 000000000000000..c412a167bbd3b47 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-23-11-56-00.gh-issue-156287.jDxO8d.rst @@ -0,0 +1,2 @@ +Fix a leak of header buffer exports in :func:`os.sendfile` on macOS and +FreeBSD. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index bec305a4042c49d..51fcf0e3a2feb39 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -12523,11 +12523,15 @@ os_sendfile_impl(PyObject *module, int out_fd, int in_fd, PyObject *offobj, #ifndef __APPLE__ off_t sbytes; #endif - Py_buffer *hbuf, *tbuf; + Py_buffer *hbuf = NULL, *tbuf = NULL; struct sf_hdtr sf; + int failed = 1; + int saved_errno = 0; sf.headers = NULL; sf.trailers = NULL; + sf.hdr_cnt = 0; + sf.trl_cnt = 0; if (headers != NULL) { if (!PySequence_Check(headers)) { @@ -12546,8 +12550,10 @@ os_sendfile_impl(PyObject *module, int out_fd, int in_fd, PyObject *offobj, if (i > 0) { sf.hdr_cnt = (int)i; if (iov_setup(&(sf.headers), &hbuf, - headers, sf.hdr_cnt, PyBUF_SIMPLE) < 0) - return NULL; + headers, sf.hdr_cnt, PyBUF_SIMPLE) < 0) { + sf.headers = NULL; + goto cleanup; + } #ifdef __APPLE__ for (i = 0; i < sf.hdr_cnt; i++) { Py_ssize_t blen = sf.headers[i].iov_len; @@ -12555,7 +12561,7 @@ os_sendfile_impl(PyObject *module, int out_fd, int in_fd, PyObject *offobj, if (sbytes >= OFF_T_MAX - blen) { PyErr_SetString(PyExc_OverflowError, "sendfile() header is too large"); - return NULL; + goto cleanup; } sbytes += blen; } @@ -12567,25 +12573,28 @@ os_sendfile_impl(PyObject *module, int out_fd, int in_fd, PyObject *offobj, if (!PySequence_Check(trailers)) { PyErr_SetString(PyExc_TypeError, "sendfile() trailers must be a sequence"); - return NULL; + goto cleanup; } else { Py_ssize_t i = PySequence_Size(trailers); if (i < 0) - return NULL; + goto cleanup; if (i > INT_MAX) { PyErr_SetString(PyExc_OverflowError, "sendfile() trailer is too large"); - return NULL; + goto cleanup; } if (i > 0) { sf.trl_cnt = (int)i; if (iov_setup(&(sf.trailers), &tbuf, - trailers, sf.trl_cnt, PyBUF_SIMPLE) < 0) - return NULL; + trailers, sf.trl_cnt, PyBUF_SIMPLE) < 0) { + sf.trailers = NULL; + goto cleanup; + } } } } + failed = 0; _Py_BEGIN_SUPPRESS_IPH do { Py_BEGIN_ALLOW_THREADS @@ -12598,11 +12607,15 @@ os_sendfile_impl(PyObject *module, int out_fd, int in_fd, PyObject *offobj, } while (ret < 0 && errno == EINTR && !(async_err = PyErr_CheckSignals())); _Py_END_SUPPRESS_IPH - int saved_errno = errno; + saved_errno = errno; + +cleanup: if (sf.headers != NULL) iov_cleanup(sf.headers, hbuf, sf.hdr_cnt); if (sf.trailers != NULL) iov_cleanup(sf.trailers, tbuf, sf.trl_cnt); + if (failed) + return NULL; if (ret < 0) { if ((saved_errno == EAGAIN) || (saved_errno == EBUSY)) {