Corner Cases and Failure Modes
This is the catalogue of what happens when things go wrong: bounds are reached, peers disappear, frames arrive out of order, configurations disagree. Each entry states the condition, the mechanism that handles it, and the observable outcome — what an engineer with a bus analyser and a debugger would actually see.
Entries marked latent are correct today but rest on an assumption worth knowing. Entries marked design limit are deliberate simplifications with a stated cost.
1. Frame level
| # | Condition | Mechanism | Observable outcome |
|---|---|---|---|
| 1.1 | Standard-identifier frame received | Identifier-width gate, first in both pipelines | Ignored entirely: no answer, no counter, no notification. This is what lets can-lite share a bus with another protocol |
| 1.2 | Empty payload on a validated category | Emptiness checked before the sequence check | Answered “invalid payload”; the sequence counter is untouched |
| 1.3 | Empty payload on a non-validated category | Reaches the handler, which under-runs while reading | The handler’s validity check produces “invalid payload”; a handler that omits the check acts on zeros |
| 1.4 | Full eight-byte payload on a validated category | The sequence number occupies the first byte | Only seven bytes reach the category — a payload designed for eight silently loses its last field |
| 1.5 | Payload longer than eight bytes | Impossible: the frame type is bounded | — |
| 1.6 | Driver delivers frames out of order | None — ordering is assumed | Validated commands draw spurious sequence errors; the client resynchronises each time and progress stalls. Latent: ordering is a driver requirement (Chapter 3, §4) |
| 1.7 | Driver completes a send from inside the send call | None | Queue drain recurses once per queued frame — at most eight deep. Latent: the shipped host adapters defer completions for exactly this reason |
2. The outbound queue
| # | Condition | Mechanism | Observable outcome |
|---|---|---|---|
| 2.1 | A ninth simultaneous send | Queue-full refusal | The frame is dropped and never retried. What the caller sees depends on who it was: a category’s send reports failure; a client command reports failure without consuming a sequence number; an acknowledgement is simply lost |
| 2.2 | An acknowledgement lost to a full queue | Its completion discards the outcome | The client’s acknowledgement timeout fires instead. Deliberate: retrying would deepen a queue that is already saturated |
| 2.3 | A send completion that itself sends | The queue advances before the completion runs | Safe: the new frame goes out immediately if the queue emptied, otherwise it is appended behind the one just started |
| 2.4 | Two owners claim the send notification | Checked at run time, in release builds too | Immediate assertion. Only the protocol object may claim it |
| 2.5 | The node’s address is changed with frames already queued | None | Queued frames keep the identifier they were built with; only later frames carry the new address |
| 2.6 | The driver reports a transmission failure | The outcome propagates to the caller | Categories ignore it; segmentation treats it as an abort. Design limit: there is no bus-off recovery machine in the library |
3. The server’s receive pipeline
| # | Condition | Mechanism | Observable outcome |
|---|---|---|---|
| 3.1 | Frame addressed to another node | Address filter | Silent — answering would produce noise proportional to the number of servers |
| 3.2 | Broadcast frame | The filter admits it | Accepted and dispatched; this is how the client’s heartbeat reaches every server |
| 3.3 | Rate limit reached | Rate gate | Silent, including any acknowledgement that would have followed. The specification defines a “rate limited” status, but it is deliberately never sent |
| 3.4 | Burst straddling a window reset | The window is fixed and resets on a timer | Up to twice the configured number can be accepted in quick succession. Design limit: a sliding window would need a timestamp per frame (§7.2) |
| 3.5 | A response arrives at a server | Command/response gate | Silent; prevents a response being dispatched as a command |
| 3.6 | One server sees another’s acknowledgement or discovery answer | Those message types are numerically in the command range, so the address filter, not the type gate, rejects them | Silent, because such frames carry the sending server’s own address. Latent: the protection here is addressing, not typing |
| 3.7 | Frame for an unregistered category | Category lookup fails | Silent — the frame may be legitimate traffic for a different server |
| 3.8 | Known category, unknown message type | Dispatch reports “not handled” | Answered “unknown command” |
| 3.9 | One category too many registered | Slot limit | Registration refused; the system category holds one slot from construction |
| 3.10 | Two categories claiming the same identity | Identity scan | Registration refused — including re-registering the same object |
| 3.11 | Registration failure ignored by the application | None | The category is invisible: its commands are dropped silently (3.7) and its client waits for answers that never come |
| 3.12 | A category destroyed while still registered | None | The registration list holds a dangling entry, and the next frame for it dispatches into freed memory. Latent: lifetime is the application’s responsibility |
| 3.13 | A category acknowledges before being registered | Checked at run time | Assertion. Only reachable when a test drives a category directly |
4. Sequence validation
| # | Condition | Mechanism | Observable outcome |
|---|---|---|---|
| 4.1 | First validated command after the server starts | The baseline is adopted | Accepted whatever its value: a client that restarts from zero needs no help from the server |
| 4.2 | A gap in the sequence | Rejection | Answered “sequence error” carrying the expected number; the counter does not advance |
| 4.3 | Repeated gaps | Rejection is idempotent | Every rejection names the same expected number — no drift, no lockout |
| 4.4 | The counter wraps | Modular arithmetic | Transparent |
| 4.5 | The client process restarts mid-session | Resynchronisation from the expected number | One command lost, one round trip wasted, traffic continues (§7.1) |
| 4.6 | Two clients commanding one server | One shared counter | Each breaks the other’s ordering; both see near-continuous sequence errors. Design limit: one client per server (REQ-CAN-006.1) |
| 4.7 | One client using two validated categories on one server | One counter per server on each side | Correct: one interleaved stream, one ordering |
| 4.8 | A client sending without a sequence number to a validating server | None | The payload’s first byte is read as a sequence number: sporadic rejection plus a mis-parsed payload |
| 4.9 | A client sending with a sequence number to a non-validating server | None | The sequence byte is delivered as payload, and every field is shifted by one |
| 4.10 | A send refused by a full queue | The number is taken and committed separately | The number is not consumed; the next attempt reuses it and the server sees no gap |
Entries 4.8 and 4.9 are the two configuration errors with no diagnostic at all (Chapter 5, §5).
5. Client state
| # | Condition | Mechanism | Observable outcome |
|---|---|---|---|
| 5.1 | A ninth server commanded | Round-robin eviction of a sequence slot | The evicted server’s counter restarts; its next command draws one sequence error and resynchronises (§7.3) |
| 5.2 | A ninth server heard from | Round-robin eviction of a liveness slot | The evicted server is reported offline and the new one online. On a bus with more active servers than slots the application sees continuous churn — the honest signal that the client is under-provisioned |
| 5.3 | A second command to a server before the first is answered | The tracked command is replaced | The first answer no longer matches and does not cancel the timer, which now belongs to the second command |
| 5.4 | Two consecutive commands with the same identity | Matching is by category and message type only | The first command’s acknowledgement cancels the second command’s timer, so a lost answer to the second goes unreported. Design limit: one outstanding command per server |
| 5.5 | A malformed acknowledgement, or one claiming an impossible source | Guarded before it is acted on | Ignored: no timer cancelled, no counter changed; the command’s timeout fires normally |
| 5.6 | An acknowledgement from a server other than the one commanded | State is kept per server | Only that server’s state is touched; the commanded server’s timer keeps running |
| 5.7 | A stale sequence-error acknowledgement arrives late | Applied unconditionally | The counter jumps to a value the server may have moved past, costing one further round trip. Self-correcting |
| 5.8 | Committing a number for a server never asked about | Checked at run time | Assertion. Only reachable by bypassing the category’s send path |
| 5.9 | A second discovery before the first answer | A single pending completion | The first completion is overwritten and never called |
| 5.10 | A discovery answer from a different server | The completion carries no identity | The pending completion is satisfied by the wrong server’s answer |
| 5.11 | Discovery never answered | No timeout exists | The completion stays pending forever; an application needing a deadline arms its own timer |
| 5.12 | No observer attached | Notification checks first | Safe: events are discarded silently |
6. Liveness and heartbeat
| # | Condition | Mechanism | Observable outcome |
|---|---|---|---|
| 6.1 | The node is busy sending | The heartbeat timer restarts on every outgoing frame | No heartbeat at all; liveness is carried by the traffic itself |
| 6.2 | A client commands continuously and never sends a heartbeat | The server restarts its timer on any correctly addressed frame | The client is not declared offline for being busy |
| 6.3 | The bus goes silent | Liveness timers expire | Both sides report their peer offline |
| 6.4 | A prolonged outage | The offline state is remembered | Reported once, not once per timer expiry |
| 6.5 | A server heartbeat reaches the client | Liveness is marked before dispatch | Reported online; the frame then finds no handler in the client’s system category, which is harmless |
| 6.6 | A hardware acceptance filter that excludes the broadcast address | None | The server never sees its client’s heartbeat and reports it offline once per timeout on an idle bus. Latent (Chapter 3, §4) |
| 6.7 | Peers with different protocol versions | The version is carried but not checked | They communicate regardless. Design limit: version negotiation exists on the wire only |
7. Four walkthroughs
7.1 A client restart mid-session
The lost command is the application’s problem, deliberately: replaying a command with side effects is not the protocol layer’s decision to make. The application learns of it from the missing response, or from the acknowledgement timeout.
7.2 The rate-limit window boundary
The limit is a tumbling window, not a sliding one. Sizing a server therefore means budgeting for twice the number in the worst case, or configuring half of what the hardware can absorb.
7.3 The ninth server
The cost of eviction is bounded and self-healing: one wasted round trip per re-admission. Refusing to talk to a ninth server would fail harder for no benefit.
7.4 A transfer abandoned mid-flight
The last line catches applications out. The completion means “the transfer succeeded”; failure arrives on the abort path, and an application that handles only the completion waits forever.
8. Segmentation
| # | Condition | Mechanism | Observable outcome |
|---|---|---|---|
| 8.1 | Payload beyond the protocol’s length field, or beyond the configured buffer | Refused when the transfer is requested | Nothing is transmitted |
| 8.2 | Empty payload | Refused | Segmentation has no zero-length encoding |
| 8.3 | A second transfer while the channel’s sender is busy | Refused | One transfer at a time per channel; more concurrency means more channels |
| 8.4 | No free channel | Refused | Both registration and transmission report it |
| 8.5 | Registering an identifier already used by another channel | Lookup matches either identifier of a pair | Refused; overlapping pairs would make routing ambiguous |
| 8.6 | A channel never released after an abort | Application responsibility | The slot stays occupied. The protocol layer releases it automatically, so this bites only a direct user of the transport |
| 8.7 | No flow control within N_Bs | Sender timer | Abort |
| 8.8 | No continuation frame within N_Cr | Receiver timer | Abort; the partial payload is discarded |
| 8.9 | The peer asks to wait too many times in a row | Wait limit | Abort |
| 8.10 | The peer reports overflow | Flow-status check | Abort |
| 8.11 | A continuation frame out of sequence | Sequence check | Abort; the partial payload is discarded |
| 8.12 | The frame sequence number wraps | Modular on both sides | Transparent; long payloads are ordinary |
| 8.13 | A first frame declaring a length that would fit in one frame | Length check | Abort — such a payload must be sent as a single frame |
| 8.14 | A first frame shorter than a full frame | Size check | Abort |
| 8.15 | A first frame declaring more than the buffer holds | Capacity check | Overflow is signalled to the peer, then abort |
| 8.16 | A single frame longer than the buffer | Capacity check | Abort, without signalling — the peer has already finished |
| 8.17 | A single frame arriving mid-reassembly | The partial transfer is discarded | The single frame is delivered as a complete payload |
| 8.18 | An unsolicited continuation frame | State check | Ignored silently — not an error |
| 8.19 | An extra continuation frame after completion | The receiver is idle again | Ignored |
| 8.20 | A frame carrying more bytes than remain | Bounded by the declared length | Surplus dropped, so padding from a padding-using peer is harmless on the last frame |
| 8.21 | A send fails mid-transfer | The failure is reported as an abort | The reason is imprecise, the outcome correct |
| 8.22 | A pacing request the standard reserves | Mapped to the slowest legal value | Conservative by design |
| 8.23 | A peer that requires padded frames | can-lite never pads | No interoperation. Design limit (Chapter 8, §6) |
9. Category level
| # | Condition | Mechanism | Observable outcome |
|---|---|---|---|
| 9.1 | A validated handler that does not skip the sequence number | None | Every field is shifted by one byte; values are plausible and wrong. Not detectable at compile time |
| 9.2 | A handler that does not check payload validity | Reads return zeros | The command executes with zeroed parameters |
| 9.3 | A completion never called | None | No response, no acknowledgement; the client’s timeout fires |
| 9.4 | A completion called twice | None | Two answers to one command |
| 9.5 | Overlapping commands in a stateful category | The category does not serialise | A second command is dispatched while the first is pending; a category that cannot cope must refuse it itself |
| 9.6 | A client crashing mid-upgrade | The session timer | The application is told to release its staging state. No frame is sent — there is nobody to tell |
| 9.7 | A supervisor polling progress while the client is dead | Polling does not extend the session | The session still expires, as intended |
| 9.8 | A firmware block lost or replayed | The application compares block indices | Reported as a category error; the checksum at the end is the backstop |
| 9.9 | An observer that allocates or blocks | None | Heap use on a no-heap target, or a stalled receive path. The rule is stated, not enforced |
10. Configuration and topology
| # | Condition | Mechanism | Observable outcome |
|---|---|---|---|
| 10.1 | A server configured with the broadcast address | Checked at construction | Assertion |
| 10.2 | Two servers sharing an address | None | Both accept the same commands and both answer; the client attributes every answer to one node and their counters diverge. Addresses must be unique by construction |
| 10.3 | Two protocol objects on one bus interface | The interface holds a single receive callback | The second construction replaces the first’s callback, and the first stops receiving entirely |
| 10.4 | A liveness timeout no longer than the peer’s heartbeat interval | None | On an idle bus the peer times out before the heartbeat arrives, producing an online/offline flap (Chapter 11, §2) |
| 10.5 | An acknowledgement timeout shorter than the server’s worst-case handler latency | None | Spurious timeouts for commands that are merely slow |
| 10.6 | A rate limit below the client’s steady-state command rate | Rate gate | Commands are dropped silently, and the client sees timeouts with no explanation on the bus |
11. What is deliberately not handled
| Not handled | Consequence | Mitigation |
|---|---|---|
| Authentication and integrity | Any node on the bus can command any server, including a firmware upgrade | Application-level signature checks; physical bus security |
| Confidentiality | Payloads are visible to every node | Application-level encryption |
| Replay by an attacker | Sequence numbers deter accidents, not adversaries | Application-level nonces or signatures |
| Bus-off recovery | Send failures are reported, but there is no recovery machine | Driver-level recovery, plus the liveness notifications to detect the outage |
| Duplicate address detection | Silent misbehaviour (10.2) | Assign addresses by construction or provisioning |
| Priority inside the outbound queue | The queue is first-in-first-out, so an emergency frame queued behind telemetry waits for it | Keep the queue shallow; prioritisation happens on the bus, not in the queue |
| More than eight servers, categories or queued frames | Eviction, refusal or a dropped frame as catalogued above | Compile-time limits; raising them is a library change |