Skip to content

Fix RX behavioral defects: reassembly asserts, filter config, callback re-entrancy#260

Merged
pavel-kirienko merged 2 commits into
masterfrom
fix/rx-behavioral-correctness
Jul 2, 2026
Merged

Fix RX behavioral defects: reassembly asserts, filter config, callback re-entrancy#260
pavel-kirienko merged 2 commits into
masterfrom
fix/rx-behavioral-correctness

Conversation

@pavel-kirienko

@pavel-kirienko pavel-kirienko commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

v5 TX/RX hardening in two parts. Every fix ships with a regression test that fails on the pre-fix code and passes after.

RX behavioral fixes (canard.c)

  • rx_subscribe: accumulate filters_dirty (||) — a duplicate subscribe no longer cancels a pending acceptance-filter reconfiguration (was silent RX loss on filter-capable nodes).
  • rx_session_accept / rx_session_complete_slot: drop the false slot->iface_index == ses->iface_index assert that aborted on valid redundant-interface traffic (per-slot affinity is still enforced at admission).
  • rx_parse (v0 anonymous): route on the 2 low data-type-ID bits per the DroneCAN spec (the upper bits are a random discriminator), with a weak HW acceptance filter for v0 DTID ≤ 3 — still one filter entry per subscription.
  • rx_session_update: dispatch on_message last, so a callback that unsubscribes itself can't cause a use-after-free.
  • canard_new: set filters_dirty = filter_count > 0 so forced Heartbeat/NodeStatus occupancy filters program even before the first subscription.
  • Forced-filter dedup: replace the alias-prone single-frame probe with a filter-coverage (subset) test rx_filter_covered.

Available-interfaces bitmap (intentional canard_new API break)

canard_new() gains an iface_bitmap of the interfaces available on the node, ANDed into every enqueue at the single TX chokepoint (tx_push). Requests/responses and all-publishes no longer queue frames for interfaces that can't be drained. A zero bitmap declares a listen-only node (TX is a clean no-op). Bits beyond CANARD_IFACE_COUNT are rejected. Call sites, the demo, and the README example are updated.

Verification

Full suite green in the Debug build (assertions on); clang-tidy + cppcheck + clang-format clean. Each fix was confirmed to be a real guard by reverting its hunk and watching the test fail — including a refcount/cursor divergence caught under the leak-detecting allocator. The RX and interface changes were each independently reviewed by Codex (gpt-5.5 xhigh) and Claude Fable 5 (xhigh) with no major findings.

🤖 Generated with Claude Code

pavel-kirienko and others added 2 commits July 2, 2026 19:39
…k re-entrancy

Adversarial review of the v5 RX pipeline surfaced several behavioral defects,
each fixed here with a regression test that fails on the pre-fix code:

- rx_subscribe: accumulate filters_dirty instead of overwriting it, so a duplicate
  subscribe (returning the incumbent) can no longer cancel a hardware acceptance
  filter reconfiguration requested by a prior subscribe/unsubscribe/set_node_id.

- rx_session_accept / rx_session_complete_slot: drop the false
  (slot->iface_index == ses->iface_index) assertion. Session interface affinity
  legitimately migrates on a stale&&fresh admission while an in-flight slot at a
  different priority keeps its own affinity, so the assertion aborted on valid
  redundant-interface traffic. Per-slot affinity is still enforced at admission.

- rx_parse: mask the anonymous UAVCAN v0 message data-type-ID to its 2 low bits
  ((can_id>>8)&0x3) per the DroneCAN spec; the upper CAN-ID bits carry a random
  discriminator, not the data-type-ID. rx_filter_for_subscription emits a weak
  filter for v0 messages with data-type-ID <= 3 so anonymous frames are admitted
  by hardware, keeping one filter entry per subscription.

- rx_session_update: do not dereference the session after dispatching on_message
  (it may unsubscribe and free the session -> use-after-free). Documented that a
  callback may unsubscribe itself but must not otherwise reenter the library.

- canard_new: mark filters dirty when filter_count > 0 so the forced occupancy
  filters are programmed on the first poll even before any subscription.

- Forced Heartbeat/NodeStatus occupancy dedup now uses a filter-coverage (subset)
  test (rx_filter_covered) instead of a single representative-frame probe, which
  could be aliased by a subscription filter and silently suppress the occupancy
  filter for some priorities/sources.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
canard_new() now takes an iface_bitmap describing the interfaces available on
the node. It is ANDed into the requested interface bitmap at the single TX
chokepoint (tx_push), so transfers are never queued for interfaces that cannot
be drained -- previously requests/responses (which always target all
interfaces) and all-publishes wasted heap on undrainable queues. A zero bitmap
declares a listen-only node: every enqueue becomes a clean no-op (no spool, no
error counter). canard_new rejects a bitmap with bits beyond CANARD_IFACE_COUNT.

All call sites, the demo, and the README example are updated. Regression tests
cover masking (request/publish/v0), the listen-only no-op, and -- under the
leak-detecting intrusive allocator -- refcount/cursor consistency and correct
transfer-freeing on the empty-intersection path.

Reviewed by Codex (gpt-5.5 xhigh) and Claude Fable 5 (xhigh): no major findings.
Also includes minor documentation tweaks (AGENTS.md, README).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pavel-kirienko pavel-kirienko merged commit 1206003 into master Jul 2, 2026
14 checks passed
@pavel-kirienko pavel-kirienko deleted the fix/rx-behavioral-correctness branch July 2, 2026 21:13
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