Skip to content

Fix correctness bugs in signal decoding and frame sending#166

Open
sebi2k1 wants to merge 2 commits into
masterfrom
fix/correctness-group-1
Open

Fix correctness bugs in signal decoding and frame sending#166
sebi2k1 wants to merge 2 commits into
masterfrom
fix/correctness-group-1

Conversation

@sebi2k1

@sebi2k1 sebi2k1 commented May 31, 2026

Copy link
Copy Markdown
Owner

Summary

Four small, surgical fixes for silent-data-corruption and UB bugs surfaced by a full code-review pass:

  • socketcan.ts (× 2 sites) — replace x << 32 (a no-op on JS 32-bit shift semantics) with x * TWO_TO_32 when reassembling decoded high words. Previously, any signal wider than 32 bits silently lost its top half.
  • socketcan.ts (× 2 sites) — change Signal.update() min/max guards from truthy (this.maxValue && ...) to != null, so a legitimate bound of 0 is enforced instead of skipped.
  • native/can.cc — in RawChannel::Send, reject buffers larger than CAN_MAX_DLEN before the memcpy into struct can_frame.data (8 bytes). Prevents a stack overwrite on caller error. SendFD already clamps to 64 and is unchanged.
  • native/signals.cc — validate bitLength is in 1..64 in both DecodeSignal and EncodeSignal. Out-of-range values previously produced shifts of full type width or by underflowed uint32_t (undefined behavior).

The PR also includes ESLint auto-fix formatting (npm run lint) across parse_kcd.ts, can.d.ts, can_signals.d.ts, and parts of socketcan.ts — trailing commas, switch-statement reformatting, one letconst. No behaviour changes in those hunks.

Items #3 (id | (1<<31) brittleness) and #4 (CAN-FD bit-extraction window) from the same review are out of scope here — #3 is being demoted from "correctness" and #4 is being tracked as its own larger PR.

Test plan

  • docker build -f Dockerfile.build-test -t node-can-build-test . — native + TS build passes (was validated as npm run build:ts locally; Docker not available on review machine).
  • npm run lint — clean.
  • npm test on Linux with vcan0/vcan1:
    • test-signal_conversion.js — 5 new cases pass (high-word regression, bitLength range × 4).
    • test-signal_bounds.js — 5 cases pass covering min/maxValue: 0 boundaries.
    • test-send_overflow.js — passes against real vcan0; skips cleanly if absent.
  • Existing suite still green.

🤖 Generated with Claude Code

Four small, surgical fixes for silent-data-corruption and UB bugs
identified in the full code review:

- socketcan.ts: replace `x << 32` (which is `x << 0` in JS) with
  `x * TWO_TO_32` when reassembling the high word of decoded signals.
  The old expression silently dropped the high 32 bits for any signal
  wider than 32 bits.
- socketcan.ts: switch `min/maxValue` bounds checks from truthy to
  `!= null` so that a legitimate bound of `0` is honoured.
- can.cc: reject Send() payloads larger than CAN_MAX_DLEN before the
  memcpy into the 8-byte `struct can_frame.data`, preventing a stack
  overwrite on caller error.
- signals.cc: validate that `bitLength` is in the range 1..64 in both
  DecodeSignal and EncodeSignal. Out-of-range values previously caused
  shifts of full type width / by negative amounts (UB).

Tests:
- test-signal_conversion.js: high-word regression case plus bitLength
  range validation for encode and decode.
- test-signal_bounds.js: new file covering the `0`-boundary cases for
  Signal min/maxValue.
- test-send_overflow.js: new file asserting Send() throws on oversized
  buffers (skips when vcan0 is unavailable).

Also includes ESLint auto-fix formatting touched by `npm run lint`
(trailing commas, switch reformatting) across `parse_kcd.ts`,
`can.d.ts`, `can_signals.d.ts`, and `socketcan.ts`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous _getvalue / _setvalue always memcpy'd bytes 0..7 of the
local buffer into a uint64_t and shifted from there. That assumption
predates CAN-FD support: for any signal whose bit offset lies past
byte 7 of a 64-byte CAN-FD payload, the routine returned garbage or
hit shift-by-full-type-width UB (Intel offset >= 64) / size_t
underflow on the Motorola shift formula 64 - offset - length
(offset > 64). Encoding had the symmetric bug.

Rewrite both helpers as a windowed fast path plus a per-bit fallback:

- Fast path (essentially all real signals): memcpy 8 bytes starting at
  data[offset/8], endian-swap, then shift by (offset % 8) for Intel or
  (64 - bit_in_byte - length) for Motorola, then mask. Same shape and
  cost as the previous code, just with the byte_start factored out.

- Fallback: when bit_in_byte + length > 64 — a 58..64 bit signal at a
  non-byte-aligned offset, essentially never seen in real CAN
  databases — walk the signal bit by bit. Both endiannesses handled.

The local scratch buffer grows from 64 to 72 bytes (MAX_PAYLOAD_BYTES
+ WINDOW_SAFETY_PAD = 8) so the 8-byte windowed memcpy is always
in-bounds even for a signal sitting at byte 63. The padding stays
zero and any bits read from it are masked off.

EncodeSignal now also memsets the local buffer before the memcpy-in.
The fast-path RMW may touch bytes past the caller's payload inside
the scratch buffer, and previously those bytes were uninitialised
stack memory — harmless because they're never written back, but
flagged as UB by the model.

Two new bounds checks are added in DecodeSignal and EncodeSignal:
  - offset + effectiveBitLength must fit in MAX_TOTAL_BITS (512).
  - signal must fit within the caller's JS buffer length.
Both throw TypeError with a clear message rather than silently
producing wrong results.

The KAYAK_DATA_CHECK debug blocks are removed: the per-bit walk that
they referenced is now the production fallback path, so the
self-comparison is no longer meaningful.

Tests cover all the worked examples plus the boundaries:
- Intel  16-bit at offset 200 (decode + encode, RMW preserves neighbours)
- Motorola 12-bit at byte-aligned offset 144 (decode + encode)
- Motorola 10-bit at non-byte-aligned offset 147 (round-trip)
- 8-bit at byte 63 (last byte of CAN-FD payload)
- 64-bit Intel at offsets 0 and 64 (length-64 boundary)
- 58-bit Intel at offset 7 (exercises the per-bit fallback)
- All four out-of-bounds throws (frame and buffer).

All 13 new cases plus the 27 pre-existing signal tests pass in the
node:22-bookworm-slim Docker container.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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