From b3b0101ef6cdc2fe55db3cd2747b1c9eae09c632 Mon Sep 17 00:00:00 2001 From: David Rebbe Date: Tue, 15 Sep 2026 23:06:32 -0400 Subject: [PATCH] fix: handle SpyMessage attribute deletion --- src/object_spy_message.cpp | 7 ++++++- tests/test_spy_message.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/object_spy_message.cpp b/src/object_spy_message.cpp index 0c1c4a7a..4367623a 100644 --- a/src/object_spy_message.cpp +++ b/src/object_spy_message.cpp @@ -138,6 +138,11 @@ static PyObject* spy_message_object_getattr(PyObject* o, PyObject* attr_name) static int spy_message_object_setattr(PyObject* o, PyObject* name, PyObject* value) { + // tp_setattro receives NULL for deletion. Let descriptors reject deletion + // before any custom setter inspects the value or changes message storage. + if (value == NULL) + return PyObject_GenericSetAttr(o, name, value); + spy_message_object* obj = (spy_message_object*)o; if (PyUnicode_CompareWithASCIIString(name, "Data") == 0) { Py_ssize_t length = _copy_byte_tuple(value, name, obj->msg.Data, sizeof(obj->msg.Data)); @@ -484,4 +489,4 @@ bool setup_spy_message_object(PyObject* module) Py_INCREF(&spy_message_j1850_object_type); PyModule_AddObject(module, SPY_MESSAGE_J1850_OBJECT_NAME, (PyObject*)&spy_message_j1850_object_type); return true; -} \ No newline at end of file +} diff --git a/tests/test_spy_message.py b/tests/test_spy_message.py index f25371da..dbe9bf75 100644 --- a/tests/test_spy_message.py +++ b/tests/test_spy_message.py @@ -1,7 +1,48 @@ +import os +import subprocess +import sys +import textwrap + import pytest import ics +@pytest.mark.parametrize("message_type", ["SpyMessage", "SpyMessageJ1850"]) +@pytest.mark.parametrize("attribute", ["Data", "AckBytes", "Header", "ExtraDataPtr", "Protocol", "ExtraDataPtrEnabled"]) +@pytest.mark.parametrize("initialized", [False, True]) +def test_attribute_deletion_is_safe(message_type, attribute, initialized): + # Isolate native crashes so every affected attribute/type is reported. + code = textwrap.dedent(f""" + import ics + + msg = ics.{message_type}() + if {initialized!r}: + msg.Protocol = ics.SPY_PROTOCOL_CANFD + msg.Data = (1, 2, 3) + msg.AckBytes = (4, 5) + msg.Header = (6, 7) + msg.ExtraDataPtr = (8, 9, 10) + fields = ("Data", "AckBytes", "Header", "ExtraDataPtr", "Protocol", + "ExtraDataPtrEnabled", "NumberBytesData", "NumberBytesHeader") + before = tuple(getattr(msg, field) for field in fields) + try: + delattr(msg, {attribute!r}) + except (AttributeError, TypeError): + pass + else: + raise AssertionError("deletion should be rejected") + assert tuple(getattr(msg, field) for field in fields) == before + # The message remains usable, including replacing and freeing its buffer. + msg.ExtraDataPtr = (11, 12) + assert msg.ExtraDataPtr == (11, 12) + del msg + """) + env = os.environ.copy() + env["PYTHONPATH"] = os.pathsep.join(str(path) for path in sys.path) + result = subprocess.run([sys.executable, "-c", code], env=env, capture_output=True, text=True, timeout=30) + assert result.returncode == 0, f"exit {result.returncode}\n{result.stdout}\n{result.stderr}" + + def test_data_roundtrip(): msg = ics.SpyMessage() msg.Data = (1, 2, 3, 4, 5, 6, 7, 8)