Skip to content

Fix 32/64-bit type mismatches, integer overflows, and SpyMessage memory corruption#233

Open
drebbe-intrepid wants to merge 4 commits into
masterfrom
fix/64bit-overflow-audit
Open

Fix 32/64-bit type mismatches, integer overflows, and SpyMessage memory corruption#233
drebbe-intrepid wants to merge 4 commits into
masterfrom
fix/64bit-overflow-audit

Conversation

@drebbe-intrepid

Copy link
Copy Markdown
Collaborator

Follows up the audit that motivated #232/#231. Includes the find_devices
new unsigned int[N] fix from #232 and hardens the same loop
(PyLong_AsUnsignedLongMask + error check so NEODEVICE_ALL / 0x80000000 masks
don't raise OverflowError on Windows).

SpyMessage attribute handlers (object_spy_message.cpp)

  • J1850 Header setter wrote up to 8 bytes into the 4-byte Header array,
    spilling into Data[0..3]; lengths now validated per field (ValueError)
  • ExtraDataPtr length truncated through uint16_t / NumberBytesData; now
    range-checked per protocol
  • Broken !data && !PyLong_Check(data) checks could deref NULL and leaked
    pending TypeErrors into later calls
  • getattr("ExtraDataPtr") returned Py_None without incref (refcount
    underflow on Python < 3.12)
  • setter could delete[] DLL-owned buffers; ExtraDataPtrEnabled path left a
    dangling pointer (double free in dealloc)
  • removed T_OBJECT_EX member entries exposing descriptors over raw struct
    memory; fixed cross-type offsetof in the J1850 table

32/64-bit and overflow fixes (methods.cpp)

  • find_devices scalar-new heap overflow (== Fixing broken (undefined behavior) ics.find_devices device_type filter #232) + mask conversion
  • get/set_device_settings 'k' wrote 8 bytes into a 4-byte enum on LP64
  • get_accessory_firmware_version 'i' into 1-byte char; uart_write 'p' into
    1-byte bool
  • get_serial_number returned negative for serials >= 0x80000000
  • script_get_script_status sizeof(&arr[0]) reported 127 of 255 on Win64
  • read_jupiter_firmware size_t parsed/returned through 'i'
  • coremini/read_bin ftell() validated before malloc
  • get_rtc offset, reflash progress callback, bus voltage / vnet getters
    widened to correct format chars

Cross-platform constant normalization (generators)

  • ics.DeviceSettingsNone was -1 on Windows vs 4294967295 on Linux; enum
    members > 0x7FFFFFFF now emitted as positive, struct field made c_uint32
  • descIdType corrected to signed int16
  • serial_number property masks the int32 field for high serials

Adds tests: test_spy_message.py, test_neo_device_ex.py, test_structures.py;
hardens test_defines.py. All 26 pass locally on win_amd64 / CPython 3.14.

drebbe-intrepid and others added 4 commits June 12, 2026 11:22
…dlers

- Header setter wrote up to 8 bytes into the 4-byte J1850 Header array,
  spilling into Data[0..3]; tuple lengths are now validated against the
  real field sizes (ValueError instead of silent corruption)
- ExtraDataPtr setter truncated the tuple length through uint16_t and
  through NumberBytesData for non length-packing protocols; lengths are
  now range checked per protocol
- Broken null checks (!data && !PyLong_Check(data)) could dereference
  NULL and leaked pending TypeErrors into later calls (surfacing as
  SystemError); element conversion errors now propagate immediately
- getattr("ExtraDataPtr") returned Py_None without incref, underflowing
  None's refcount on Python < 3.12
- ExtraDataPtr setter could delete[] DLL-owned buffers on received
  messages (noExtraDataPtrCleanup) and the ExtraDataPtrEnabled path left
  a dangling pointer behind after freeing (double free in dealloc)
- Removed T_OBJECT_EX member entries that exposed descriptors
  reinterpreting raw struct memory as PyObject* (reachable via
  type(msg).__dict__) and fixed cross-type offsetof in the J1850 table

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- find_devices: scalar new unsigned int(N) allocated one element instead
  of an array (heap overflow with more than one device type, delete[] on
  scalar new); device type masks like NEODEVICE_ALL now convert through
  PyLong_AsUnsignedLongMask with error checks instead of overflowing
  PyLong_AsLong on Windows
- get/set_device_settings: 'k' wrote a full unsigned long (8 bytes on
  Linux/macOS x64) into the 4-byte EPlasmaIonVnetChannel_t enum,
  corrupting the stack; parse with 'I' and cast
- get_accessory_firmware_version: 'i' wrote 4 bytes into a 1-byte char
  index (now int with a 0..255 range check)
- uart_write: 'p' wrote a full int into a 1-byte bool (now int); return
  bytesActuallySent via 'n' instead of truncating size_t through 'i'
- get_serial_number: serials >= 0x80000000 ("ZIK0ZK".."ZZZZZZ") came
  back negative through Py_BuildValue("i"); use 'I'
- script_get_script_status: sizeof(parameters)/sizeof(&parameters[0])
  divided by pointer size, reporting 127 instead of 255 entries on
  Win64; elements returned via PyLong_FromUnsignedLong instead of a
  signed truncating 'i' (also fixes a per-element reference leak)
- read_jupiter_firmware: 'i' wrote 4 of size_t's 8 bytes and rejected
  sizes >= 2GB; parse/return with 'n'
- coremini load / read_bin: validate ftell() results (32-bit long on
  Windows) before malloc
- get_rtc: return the __int64 offset via 'L' instead of casting through
  unsigned long and 'i'
- open_device: check PyLong_AsUnsignedLong errors for integer and
  base36 serial arguments (previously left pending exceptions and an
  0xFFFFFFFF serial), free the int() conversion result
- reflash progress callback: pass unsigned long progress with 'k'
- FlashDevice2 (internal builds): avoid sign-extending the -1 network
  sentinel to 64 bits on LP64
- byte-sized 'b' parse targets declared as int/unsigned int now use
  unsigned char (single byte writes into 4-byte stack slots)
- get_bus_voltage / vnet channel getters: return unsigned long values
  via 'k' instead of 'i'

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ics.DeviceSettingsNone was -1 on Windows (MSVC wraps the 0xFFFFFFFF
enumerator to a signed int) but 4294967295 on Linux/macOS and in the
generated Python enum, so settings.DeviceSettingType comparisons gave
different answers per platform.

- extract_icsneo40_defines.py: enum members whose header value is a hex
  literal above 0x7FFFFFFF are now emitted through
  PyLong_FromUnsignedLongLong(static_cast<unsigned int>(...)) so every
  platform exposes the positive value; intentionally negative
  enumerators (= -1) are unaffected
- generate_icsneo40_structs.py: struct fields of enum types with an
  enumerator above 0x7FFFFFFF are emitted as ctypes.c_uint32 instead of
  c_int32 so the value round-trips (s_device_settings.DeviceSettingType)
- generate_icsneo40_structs.py: descIdType is int16_t in icsnVC40.h, not
  uint16_t; DescriptionID fields are now signed and consistent with
  ics_spy_message_vsb
- regenerate src/setup_module_auto_defines.cpp
- tests: pin DeviceSettingsNone, DeviceSettingType round-trip and
  DescriptionID signedness; drop the hasattr() guards in test_defines so
  missing constants fail loudly, and pin the remaining high-bit defines
  and the serial range constants

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NeoDevice.SerialNumber is an int32_t, so serials "ZIK0ZK".."ZZZZZZ"
read back negative and the property returned a negative decimal string.
Mask to 32 bits before the range checks, and use < for the
MIN_BASE36_SERIAL boundary so "A0000" (the first base36 serial) renders
as base36 instead of decimal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@drebbe-intrepid

Copy link
Copy Markdown
Collaborator Author

@pierreluctg I had this in progress for a little bit. I had AI audit the code base and it caught what you fixed in #232 before you submitted it. I just need to do a deep dive review of the code before I merge it.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant