diff --git a/Lib/test/test_code.py b/Lib/test/test_code.py index 631e5fd2499b13e..de5e5edeff7d389 100644 --- a/Lib/test/test_code.py +++ b/Lib/test/test_code.py @@ -493,6 +493,34 @@ def foo(): with self.assertRaisesRegex(SystemError, msg): foo() + @cpython_only + def test_co_code_with_invalid_monitoring_data(self): + for opcode_name in ("INSTRUMENTED_LINE", "INSTRUMENTED_INSTRUCTION"): + with self.subTest(opcode_name=opcode_name): + script = textwrap.dedent(f""" + import dis + import marshal + + def func(): + pass + + bytecode = bytearray(func.__code__.co_code) + bytecode[0] = dis._all_opmap[{opcode_name!r}] + code = func.__code__.replace(co_code=bytes(bytecode)) + operations = ( + lambda: code.co_code, + lambda: marshal.dumps(code), + ) + for operation in operations: + try: + operation() + except SystemError as exc: + assert str(exc) == "cannot de-instrument code object with invalid monitoring data" + else: + raise AssertionError("expected malformed code object to be rejected") + """) + assert_python_ok("-c", script) + @requires_debug_ranges() def test_co_positions_artificial_instructions(self): import dis diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-06-19-00-00.gh-issue-155295.Km7QpL.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-06-19-00-00.gh-issue-155295.Km7QpL.rst new file mode 100644 index 000000000000000..ddbcfa706352ff2 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-06-19-00-00.gh-issue-155295.Km7QpL.rst @@ -0,0 +1,2 @@ +Prevent a crash when accessing ``code.co_code`` on a malformed code object +that contains instrumented opcodes without the corresponding monitoring data. diff --git a/Objects/codeobject.c b/Objects/codeobject.c index d7955cc7390a7ab..ab8e63066b51a8c 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -2199,11 +2199,23 @@ _PyCode_Clear_Executors(PyCodeObject *code) #endif -static void +static int deopt_code(PyCodeObject *code, _Py_CODEUNIT *instructions) { Py_ssize_t len = Py_SIZE(code); for (int i = 0; i < len; i++) { + int opcode = _PyCode_CODE(code)[i].op.code; + _PyCoMonitoringData *monitoring = code->_co_monitoring; + if ((opcode == INSTRUMENTED_LINE && + (monitoring == NULL || monitoring->lines == NULL)) || + (opcode == INSTRUMENTED_INSTRUCTION && + (monitoring == NULL || monitoring->per_instruction_opcodes == NULL))) { + PyErr_SetString( + PyExc_SystemError, + "cannot de-instrument code object with invalid monitoring data"); + return -1; + } + _Py_CODEUNIT inst = _Py_GetBaseCodeUnit(code, i); assert(inst.op.code < MIN_SPECIALIZED_OPCODE); int caches = _PyOpcode_Caches[inst.op.code]; @@ -2213,6 +2225,7 @@ deopt_code(PyCodeObject *code, _Py_CODEUNIT *instructions) } i += caches; } + return 0; } PyObject * @@ -2234,9 +2247,13 @@ _PyCode_GetCode(PyCodeObject *co) code = PyBytes_FromStringAndSize((const char *)_PyCode_CODE(co), _PyCode_NBYTES(co)); if (code != NULL) { - deopt_code(co, (_Py_CODEUNIT *)PyBytes_AS_STRING(code)); - assert(cached->_co_code == NULL); - FT_ATOMIC_STORE_PTR(cached->_co_code, code); + if (deopt_code(co, (_Py_CODEUNIT *)PyBytes_AS_STRING(code)) < 0) { + Py_CLEAR(code); + } + else { + assert(cached->_co_code == NULL); + FT_ATOMIC_STORE_PTR(cached->_co_code, code); + } } } Py_END_CRITICAL_SECTION(); diff --git a/Python/marshal.c b/Python/marshal.c index 25353f6e6896249..8baa328d5225356 100644 --- a/Python/marshal.c +++ b/Python/marshal.c @@ -106,6 +106,7 @@ module marshal #define WFERR_NESTEDTOODEEP 2 #define WFERR_NOMEMORY 3 #define WFERR_CODE_NOT_ALLOWED 4 +#define WFERR_EXCEPTION 5 // An exception is already set. typedef struct { FILE *fp; @@ -694,7 +695,8 @@ w_complex_object(PyObject *v, char flag, WFILE *p) PyCodeObject *co = (PyCodeObject *)v; PyObject *co_code = _PyCode_GetCode(co); if (co_code == NULL) { - p->error = WFERR_NOMEMORY; + assert(PyErr_Occurred()); + p->error = WFERR_EXCEPTION; return; } W_TYPE(TYPE_CODE, p); @@ -1941,6 +1943,9 @@ _PyMarshal_WriteObjectToString(PyObject *x, int version, int allow_code) PyErr_SetString(PyExc_ValueError, "marshalling code objects is disallowed"); break; + case WFERR_EXCEPTION: + assert(PyErr_Occurred()); + break; default: case WFERR_UNMARSHALLABLE: PyErr_SetString(PyExc_ValueError,