From ca47e7904ca99c337aa495d3fcefda0b420ee5af Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:00:28 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20v0.4.1=20=E2=80=94=20RELAY=20ecosystem?= =?UTF-8?q?=20audit=20fixes,=20pass=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second round of fixes from the SoundMatt x-Net ecosystem audit (cpp-LIN-N2-01, cpp-LIN-01/02/03/05/06/07/11/12/13/14/15/16/17/18): - Add verify_checksum() to actually validate a received checksum against the locally-computed value (ISO 17987 frame reception); HARA SG-03 downgraded from "Implemented" to "Partial" since no real (non-virtual) bus backend calls it yet. - Replace the generic ErrTimeout returned by send_header on no slave response with a distinct lin::Errc::no_response sentinel that still compares equivalent to a timeout for existing callers. - from_message now rejects an unrecognized lin.checksum_type instead of silently defaulting to Classic. - Bump declared RELAY spec version 1.11 -> 2.0 across headers, CLI JSON, README, HARA, and requirements (RELAY v2.0's breaking change is scoped to RCP and does not alter LIN's canonical types). - Fix testdata/relay-vectors/lin-frame.json to encode checksum_type as "enhanced" instead of the bare numeral "1" (independently verified: the vector's checksum 73 matches the Enhanced algorithm for id=16, payload={0xAA,0xBB}, not Classic). - master::Node::set_schedule now accepts an empty schedule (spec Sec 8.3), matching the virtual bus. - DropOldest back-pressure now counts an eviction as a drop instead of a delivery. - Frame::checksum_type now defaults to Classic (the enum zero value), matching the other language bindings' canonical default. - capabilities no longer lists "virtual" in both features and transports. - E2E Receiver::unwrap no longer advances its sequence counter on a rejected frame, so a persistent shifted stream keeps being flagged instead of silently resyncing after one error. - Define the previously-dangling REQ-CLI-001..006 / REQ-SEC-013 requirements referenced by existing fusa:req tags; correct three requirement rationales that cited the wrong Safety Goal; fix HARA's error-handling citation (Sec 15 -> Sec 5) and stale spec version. - Add CHANGELOG.md. Reproduced locally: cmake configure + build clean, ctest 171/171 pass. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- .fusa-reqs.json | 62 +++++++++++++++++++++++++-- CHANGELOG.md | 62 +++++++++++++++++++++++++++ CMakeLists.txt | 2 +- HARA.md | 4 +- README.md | 8 ++-- ROADMAP.md | 22 +++++++++- cli/json.hpp | 8 ++-- include/lin/channel.hpp | 14 +++--- include/lin/lin.hpp | 18 ++++++-- include/lin/relay.hpp | 4 +- requirements/requirements.json | 2 +- src/lin.cpp | 22 +++++++++- src/master/node.cpp | 5 +-- src/safety/e2e.cpp | 4 +- src/virtual/bus.cpp | 16 ++++--- testdata/relay-vectors/lin-frame.json | 4 +- tests/test_master.cpp | 4 +- 17 files changed, 219 insertions(+), 42 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 533cf5a..335b6fb 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -6,7 +6,7 @@ "title": "ValidateFrame rejects ID > 0x3F", "description": "ValidateFrame shall return an error when Frame.ID exceeds MaxID (0x3F).", "asil": "ASIL-B", - "rationale": "SG-05: prevents an invalid identifier from being transmitted or processed.", + "rationale": "SG-02: prevents an invalid identifier from being transmitted or processed.", "tags": ["core", "validation"] }, { @@ -14,7 +14,7 @@ "title": "ValidateFrame rejects empty data", "description": "ValidateFrame shall return an error when Frame.Data has zero length.", "asil": "ASIL-B", - "rationale": "SG-05: a zero-length LIN frame is malformed and has no defined wire format.", + "rationale": "SG-01: a zero-length LIN frame is malformed and has no defined wire format.", "tags": ["core", "validation"] }, { @@ -22,7 +22,7 @@ "title": "ValidateFrame rejects oversized data", "description": "ValidateFrame shall return an error when len(Frame.Data) exceeds MaxDataLen (8).", "asil": "ASIL-B", - "rationale": "SG-05: LIN 2.x limits payloads to 8 bytes; exceeding this is a protocol violation.", + "rationale": "SG-01: LIN 2.x limits payloads to 8 bytes; exceeding this is a protocol violation.", "tags": ["core", "validation"] }, { @@ -1104,6 +1104,62 @@ "asil": "ASIL-B", "rationale": "RELAY §3.2: robust protocol string parsing with two variants for exception/error-code styles.", "tags": ["relay", "protocol"] + }, + { + "id": "REQ-CLI-001", + "title": "version command prints tool and spec version", + "description": "The CLI `version` command shall print the tool and spec version as JSON (or text with --format text).", + "asil": "QM", + "rationale": "RELAY §11.1: mandatory version command for conformance self-reporting.", + "tags": ["cli"] + }, + { + "id": "REQ-CLI-002", + "title": "capabilities command prints supported protocols and commands", + "description": "The CLI `capabilities` command shall print supported protocols, transports, commands and interfaces as JSON.", + "asil": "QM", + "rationale": "RELAY §11.2/§12: mandatory capabilities self-report.", + "tags": ["cli"] + }, + { + "id": "REQ-CLI-003", + "title": "status command prints health status", + "description": "The CLI `status` command shall print the health status as JSON (or text with --format text).", + "asil": "QM", + "rationale": "RELAY §11.1: mandatory status command.", + "tags": ["cli"] + }, + { + "id": "REQ-CLI-004", + "title": "CLI exits 2 on invalid arguments", + "description": "The CLI shall exit with code 2 and write an error name to stderr on invalid or unknown arguments.", + "asil": "QM", + "rationale": "RELAY §11.3: standardised CLI exit codes.", + "tags": ["cli"] + }, + { + "id": "REQ-CLI-005", + "title": "CLI --format flag selects text or json output", + "description": "Commands accepting --format shall support 'text' and 'json' and reject other values with exit code 2.", + "asil": "QM", + "rationale": "RELAY §11.1: --format text|json handling.", + "tags": ["cli"] + }, + { + "id": "REQ-CLI-006", + "title": "convert command bridges lin.Frame to relay.Message JSON", + "description": "The CLI `convert` command shall read a lin.Frame JSON from stdin, validate it, and write the corresponding relay.Message JSON to stdout, writing the sentinel error name to stderr on failure.", + "asil": "QM", + "rationale": "RELAY §11.2: cross-protocol conversion command.", + "tags": ["cli"] + }, + { + "id": "REQ-SEC-013", + "title": "Bounded channel try_send rejects when full", + "description": "Chan::try_send shall return SendResult::Full when the channel is at capacity rather than growing unboundedly, bounding memory under back-pressure.", + "asil": "ASIL-B", + "rationale": "SG-04: no unbounded memory growth under load / denial-of-service resistance.", + "tags": ["security", "channel"] } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ca9f2d9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog + +All notable changes to cpp-LIN are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.4.1] +### Added +- `verify_checksum()`: validates a received checksum byte against the + locally-computed value (ISO 17987 frame reception). +- `lin::Errc::no_response`, a distinct "no slave answered" sentinel that + compares equivalent to a relay timeout, replacing the previous generic + timeout return from `send_header`. +- `CHANGELOG.md` (this file). +### Changed +- Declared RELAY spec version bumped 1.11 → 2.0 across headers, CLI JSON, + README, HARA, and requirements. +- `master::Node::set_schedule` now accepts an empty schedule (disables + scheduled transmission), matching the virtual bus and the spec. +- `Chan::send_drop_oldest` reports whether it evicted an item so DropOldest + back-pressure now increments the drop counter instead of the delivery + counter on eviction. +- `Frame::checksum_type` now defaults to Classic (the enum's zero value), + matching the canonical default used by the other language bindings. +- `capabilities` no longer duplicates `virtual` between `features` and + `transports`. +- E2E `Receiver::unwrap` no longer advances its sequence counter on a + rejected (out-of-order) frame, so a persistent shifted stream keeps being + flagged instead of resyncing after a single error. +### Fixed +- `from_message` now rejects an unrecognized `lin.checksum_type` instead of + silently coercing it to Classic. +- `testdata/relay-vectors/lin-frame.json` now encodes `lin.checksum_type` as + `"enhanced"` instead of the numeral `"1"`. +- HARA SG-03 status corrected from "Implemented" to "Partial" pending a + verified-checksum code path in a real (non-virtual) bus backend. +- Corrected the Safety Goal cross-references on three `.fusa-reqs.json` + requirement rationales (SG-02/SG-01 instead of SG-05) and added the + previously-undefined `REQ-CLI-001..006` / `REQ-SEC-013` requirement + entries for existing `fusa:req` trace tags. +- HARA citation for error handling corrected from spec §15 to §5. + +## [0.4.0] +### Added +- RELAY ecosystem audit fixes. + +## [0.3.0] +### Added +- RELAY spec v1.11 conformance (see ROADMAP.md for detail). + +## [0.2.0] +### Added +- Full safety and cyber pack: TARA, FMEA, SAS, boundary diagram, Safety + Manual, IEC 62443 artifacts (see ROADMAP.md for detail). + +## [0.1.0] +### Added +- Initial C++ LIN bus port: core frame types, PID/checksum, virtual bus, + master/slave nodes, E2E safety layer, LDF parser, and RELAY adapter/CLI. diff --git a/CMakeLists.txt b/CMakeLists.txt index 387df6d..ae5aa7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21) project(cpplin - VERSION 0.4.0 + VERSION 0.4.1 DESCRIPTION "C++ LIN bus library — RELAY-conformant, ISO 26262 ASIL-B" HOMEPAGE_URL "https://github.com/SoundMatt/cpp-LIN" LANGUAGES CXX diff --git a/HARA.md b/HARA.md index 05ee1da..684c2d0 100644 --- a/HARA.md +++ b/HARA.md @@ -109,7 +109,7 @@ ASIL-D at system level if required. |----|-------------|------|--------| | SG-01 | No data corruption during TX/RX | ASIL-B | Implemented (checksum, validation) | | SG-02 | Reject frames with ID > 0x3F | ASIL-B | Implemented (validate_frame) | -| SG-03 | Detect incorrect checksums | ASIL-B | Implemented (calc_checksum, verify_pid) | +| SG-03 | Detect incorrect checksums | ASIL-B | Partial (verify_checksum compares a received checksum; calc_checksum/verify_pid alone only compute a checksum / check PID parity, not the data checksum) | | SG-04 | No unbounded memory growth | ASIL-B | Implemented (Chan capacity, drop policy) | | SG-05 | Detect E2E protection errors | ASIL-B | Implemented (E2E CRC-16, seq check) | | SG-06 | Diagnostic frames use Classic checksum | ASIL-B | Implemented (validate_frame) | @@ -131,6 +131,6 @@ ASIL-D at system level if required. - ISO 26262:2018 Part 3 — Concept phase - LIN Specification Package Rev 2.2A — Section 2 (Frame structure) -- RELAY Specification v1.11 — §15 (Error handling) +- RELAY Specification v2.0 — §5 (Error handling) - `SEOOC.md` — Assumptions on use - `SAFETY_PLAN.md` — Development process diff --git a/README.md b/README.md index c02b0f9..74c174c 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,14 @@ C++ LIN bus library — RELAY-conformant, ISO 26262 ASIL-B. [![CI](https://github.com/SoundMatt/cpp-LIN/actions/workflows/ci.yml/badge.svg)](https://github.com/SoundMatt/cpp-LIN/actions/workflows/ci.yml) -[![RELAY Conformant](https://img.shields.io/badge/RELAY-v1.11%20conformant-blue)](https://github.com/SoundMatt/RELAY) +[![RELAY Conformant](https://img.shields.io/badge/RELAY-v2.0%20conformant-blue)](https://github.com/SoundMatt/RELAY) [![ASIL-B](https://img.shields.io/badge/ISO%2026262-ASIL--B-orange)](SAFETY_PLAN.md) [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](LICENSE) ## Overview cpp-LIN is a production-quality C++ implementation of the LIN (Local Interconnect -Network) protocol, following the RELAY specification v1.11. It is the C++ sibling +Network) protocol, following the RELAY specification v2.0. It is the C++ sibling of [go-LIN](https://github.com/SoundMatt/go-LIN) and is patterned after [cpp-CAN](https://github.com/SoundMatt/cpp-CAN). @@ -50,7 +50,7 @@ ctest --test-dir build --output-on-failure ``` include/lin/ channel.hpp — Chan: bounded, thread-safe FIFO - relay.hpp — RELAY v1.11 types (Protocol, Message, INode, …) + relay.hpp — RELAY v2.0 types (Protocol, Message, INode, …) lin.hpp — IBus, IMasterBus, Frame, Filter, free functions virtual/bus.hpp — In-process virtual LIN bus mock/mock.hpp — RELAY spec §13.7.1 canonical `mock` module (alias of virtual::Bus) @@ -97,7 +97,7 @@ P1 = NOT(ID1 ^ ID3 ^ ID4 ^ ID5) (bit 7) ## RELAY Integration -cpp-LIN exposes a RELAY v1.11 `INode` adapter: +cpp-LIN exposes a RELAY v2.0 `INode` adapter: ```cpp #include diff --git a/ROADMAP.md b/ROADMAP.md index d3c1507..210083b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,7 +33,7 @@ - [x] `RELAY_BUILD_CLI` CMake alias (§17.7, since removed — see v0.4.0) - [x] Test vectors updated to spec_version 1.11 -## v0.4.0 — RELAY ecosystem audit fixes (current) +## v0.4.0 — RELAY ecosystem audit fixes - [x] LIN-specific error category (`lin::Errc::invalid_frame`) distinct from `ErrPayloadTooLarge` for out-of-range frame IDs (spec §5.3) @@ -50,6 +50,26 @@ convention), matching peers like `go-can` - [x] `lin::mock` canonical module (spec §13.7.1), aliasing `lin::virt::Bus` +## v0.4.1 — RELAY ecosystem audit fixes, pass 2 (current) + +- [x] Declared RELAY spec version bumped 1.11 → 2.0 +- [x] `verify_checksum()` added; HARA SG-03 downgraded from "Implemented" to + "Partial" pending a verified-checksum path in a real (non-virtual) bus + backend +- [x] `lin::Errc::no_response` distinct sentinel replaces the generic + timeout previously returned by `send_header` on no slave response +- [x] `from_message` rejects unrecognized `lin.checksum_type` instead of + silently defaulting to Classic +- [x] `master::Node::set_schedule` accepts an empty schedule (spec §8.3) +- [x] DropOldest back-pressure counts an eviction as a drop, not a delivery +- [x] `Frame::checksum_type` defaults to Classic (enum zero value) +- [x] E2E `Receiver::unwrap` no longer resyncs its sequence counter after a + single rejected frame +- [x] Requirement/HARA traceability corrections: `REQ-CLI-001..006` / + `REQ-SEC-013` defined, three misattributed SG rationale references + fixed, HARA §15→§5 citation fixed +- [x] `CHANGELOG.md` added + ## v0.5.0 — Enhanced Bus Features - [ ] Sleep/wake frame support (LIN 2.x go-to-sleep command) diff --git a/cli/json.hpp b/cli/json.hpp index 1d6dbd8..3ea3c6d 100644 --- a/cli/json.hpp +++ b/cli/json.hpp @@ -182,7 +182,7 @@ inline std::string version_json() { "\"protocol\":\"LIN\"," "\"protocol_int\":3," "\"version\":\"" + std::string(kToolVersion) + "\"," - "\"spec_version\":\"1.11\"," + "\"spec_version\":\"2.0\"," "\"language\":\"cpp\"," "\"runtime\":\"c++17\"" "}"; @@ -195,7 +195,7 @@ inline std::string version_text() { o << "tool: cpp-lin\n" << "protocol: LIN\n" << "version: " << kToolVersion << "\n" - << "spec: 1.11\n" + << "spec: 2.0\n" << "language: cpp\n" << "runtime: c++17\n"; return o.str(); @@ -209,10 +209,10 @@ inline std::string capabilities_json() { "\"protocol\":\"LIN\"," "\"protocol_int\":3," "\"version\":\"" + std::string(kToolVersion) + "\"," - "\"spec_version\":\"1.11\"," + "\"spec_version\":\"2.0\"," "\"commands\":[\"version\",\"capabilities\",\"status\",\"convert\"]," "\"transports\":[\"virtual\"]," - "\"features\":[\"ldf\",\"e2e\",\"master\",\"slave\",\"virtual\",\"mock\"]," + "\"features\":[\"ldf\",\"e2e\",\"master\",\"slave\",\"mock\"]," "\"interfaces\":[\"IBus\",\"IMasterBus\",\"INode\"]," "\"optional_interfaces\":[\"IHealthProvider\",\"IMetricsProvider\",\"IDrainer\"]," "\"adapt\":true" diff --git a/include/lin/channel.hpp b/include/lin/channel.hpp index d45761b..cadf061 100644 --- a/include/lin/channel.hpp +++ b/include/lin/channel.hpp @@ -24,7 +24,7 @@ namespace lin { template class Chan { public: - enum class SendResult { Ok, Full, Closed }; + enum class SendResult { Ok, Full, Closed, Evicted }; explicit Chan(std::size_t capacity = 64) : capacity_(capacity) {} @@ -54,14 +54,16 @@ class Chan { } // Send with DropOldest policy — evicts head when at capacity. - // Returns false if closed. - bool send_drop_oldest(T value) { + // Returns Closed if closed, Evicted if an item was discarded to make room + // (the new item is still enqueued), or Ok if enqueued without eviction. + SendResult send_drop_oldest(T value) { std::lock_guard lk(mu_); - if (closed_) return false; - if (buf_.size() >= capacity_) buf_.pop_front(); + if (closed_) return SendResult::Closed; + bool evicted = false; + if (buf_.size() >= capacity_) { buf_.pop_front(); evicted = true; } buf_.push_back(std::move(value)); cv_not_empty_.notify_one(); - return true; + return evicted ? SendResult::Evicted : SendResult::Ok; } // Blocking recv — returns nullopt only when closed and empty. diff --git a/include/lin/lin.hpp b/include/lin/lin.hpp index ba38d71..82a2a97 100644 --- a/include/lin/lin.hpp +++ b/include/lin/lin.hpp @@ -28,7 +28,7 @@ namespace lin { // ── Spec version ───────────────────────────────────────────────────────────── -inline constexpr const char* kSpecVersion = "1.11"; +inline constexpr const char* kSpecVersion = "2.0"; // ── Constants ───────────────────────────────────────────────────────────────── @@ -53,7 +53,7 @@ struct Frame { uint8_t id{}; // 6-bit frame identifier (0x00–0x3F) std::vector data; // payload (1–8 bytes) uint8_t checksum{}; // wire checksum byte - ChecksumType checksum_type{ChecksumType::Enhanced}; + ChecksumType checksum_type{ChecksumType::Classic}; // §18.2 canonical default (enum zero value) }; // ── Filter ──────────────────────────────────────────────────────────────────── @@ -116,9 +116,13 @@ inline std::error_code ErrPayloadTooLarge() noexcept { return relay::ErrPayloadT // not the relay.Node interface, so — like CAN's ErrInvalidFrame (§5.4, "not a // relay sentinel") — this code is not mapped to any relay::Errc value. -// fusa:req REQ-LIN-015 +// fusa:req REQ-LIN-015 REQ-LIN-021 enum class Errc : int { invalid_frame = 1, + // Distinct "no slave answered" sentinel (§5.4). Its error category maps it + // as equivalent to relay::Errc::timeout so callers that only test for a + // deadline expiry still match, while the distinct condition is preserved. + no_response = 2, }; const std::error_category& error_category() noexcept; @@ -147,6 +151,14 @@ uint8_t verify_pid(uint8_t pid); // fusa:req REQ-LIN-008 REQ-LIN-009 REQ-LIN-010 uint8_t calc_checksum(uint8_t pid, const std::vector& data, ChecksumType ct) noexcept; +// Verifies a received checksum byte against the locally-computed value for the +// given PID and data (ISO 17987 frame reception). Returns true when received +// equals the recomputed checksum, false on a mismatch (corrupt frame — H-01). +// +// fusa:req REQ-LIN-008 REQ-LIN-009 REQ-LIN-010 +bool verify_checksum(uint8_t pid, const std::vector& data, + uint8_t received, ChecksumType ct) noexcept; + // Validates f against LIN protocol constraints. // Throws ErrInvalidFrame if any constraint is violated. // diff --git a/include/lin/relay.hpp b/include/lin/relay.hpp index 1b061d0..29231ed 100644 --- a/include/lin/relay.hpp +++ b/include/lin/relay.hpp @@ -4,7 +4,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. // relay.hpp — RELAY spec types shared across all protocol implementations. -// Mirrors the RELAY Go package spec v1.11. +// Mirrors the RELAY Go package spec v2.0. #pragma once @@ -24,7 +24,7 @@ namespace relay { // ── Spec version ───────────────────────────────────────────────────────────── // fusa:req REQ-RELAY-020 -inline constexpr const char* kSpecVersion = "1.11"; +inline constexpr const char* kSpecVersion = "2.0"; // ── Protocol ───────────────────────────────────────────────────────────────── diff --git a/requirements/requirements.json b/requirements/requirements.json index 03121d6..c899dd7 100644 --- a/requirements/requirements.json +++ b/requirements/requirements.json @@ -5,7 +5,7 @@ "language": "cpp", "standard": "iso26262", "asil": "ASIL-B", - "spec": "RELAY v1.11" + "spec": "RELAY v2.0" }, "requirements": [ {"id": "REQ-LIN-001", "title": "ValidateFrame rejects ID > 0x3F", "asil": "ASIL-B", "tags": ["core", "validation"]}, diff --git a/src/lin.cpp b/src/lin.cpp index 2bece40..36418c5 100644 --- a/src/lin.cpp +++ b/src/lin.cpp @@ -52,6 +52,13 @@ uint8_t calc_checksum(uint8_t pid, const std::vector& data, ChecksumTyp return static_cast(0xFF - static_cast(sum)); } +// ── verify_checksum ── REQ-LIN-008 REQ-LIN-009 REQ-LIN-010 ─────────────────── + +bool verify_checksum(uint8_t pid, const std::vector& data, + uint8_t received, ChecksumType ct) noexcept { + return calc_checksum(pid, data, ct) == received; +} + // ── validate_frame ── REQ-LIN-001 REQ-LIN-002 REQ-LIN-003 REQ-LIN-015 REQ-LIN-016 REQ-LIN-017 void validate_frame(const Frame& f) { @@ -76,9 +83,18 @@ class LinErrorCategory : public std::error_category { std::string message(int ev) const override { switch (static_cast(ev)) { case Errc::invalid_frame: return "lin: invalid frame"; + case Errc::no_response: return "lin: no slave response"; default: return "lin: unknown error"; } } + + // no_response wraps a deadline expiry: it is equivalent to relay's timeout + // condition (§5.4) so callers testing for relay::ErrTimeout still match. + bool equivalent(int code, const std::error_condition& cond) const noexcept override { + if (static_cast(code) == Errc::no_response) + return cond == relay::ErrTimeout().default_error_condition(); + return std::error_category::equivalent(code, cond); + } }; } // anonymous namespace @@ -119,10 +135,12 @@ Frame from_message(const relay::Message& m) { f.data = m.payload; auto it = m.meta.find("lin.checksum_type"); - if (it != m.meta.end() && it->second == "enhanced") + if (it == m.meta.end() || it->second.empty() || it->second == "classic") + f.checksum_type = ChecksumType::Classic; + else if (it->second == "enhanced") f.checksum_type = ChecksumType::Enhanced; else - f.checksum_type = ChecksumType::Classic; + throw ErrInvalidFrame("unknown lin.checksum_type: " + it->second); it = m.meta.find("lin.checksum"); if (it != m.meta.end() && !it->second.empty()) { diff --git a/src/master/node.cpp b/src/master/node.cpp index a1dbf00..97905d1 100644 --- a/src/master/node.cpp +++ b/src/master/node.cpp @@ -21,9 +21,8 @@ Node::Node(std::shared_ptr bus) // fusa:req REQ-MASTER-010 REQ-MASTER-011 REQ-MASTER-012 std::error_code Node::set_schedule(std::vector entries) { - if (entries.empty()) - return lin::make_error_code(lin::Errc::invalid_frame); // empty schedule - + // An empty schedule is valid (RELAY §8.3) and disables scheduled + // transmission; run() early-returns on an empty schedule. for (const auto& e : entries) { if (e.id > kLINMaxID) return lin::make_error_code(lin::Errc::invalid_frame); diff --git a/src/safety/e2e.cpp b/src/safety/e2e.cpp index 8b1b1e1..ab9cfe9 100644 --- a/src/safety/e2e.cpp +++ b/src/safety/e2e.cpp @@ -116,7 +116,9 @@ std::vector Receiver::unwrap(const std::vector& data) { std::lock_guard lk(mu_); if (!first_ && seq != last_seq_ + 1) { uint32_t expected_seq = last_seq_ + 1; - last_seq_ = seq; + // Do NOT advance last_seq_ on a rejected frame: a persistent stream at + // a shifted counter must keep being flagged, not silently resync to an + // attacker-/fault-supplied counter after a single error (H-04/SG-05). throw E2EError(E2EErrorKind::SequenceGap, seq, "expected " + std::to_string(expected_seq) + ", got " + std::to_string(seq)); diff --git a/src/virtual/bus.cpp b/src/virtual/bus.cpp index 2c7626a..91e8f7c 100644 --- a/src/virtual/bus.cpp +++ b/src/virtual/bus.cpp @@ -77,7 +77,7 @@ std::pair Bus::send_header(uint8_t id) { auto it = responses_.find(id); if (it == responses_.end()) { error_count_.fetch_add(1); - return {Frame{}, relay::ErrTimeout()}; // ErrNoResponse maps to timeout + return {Frame{}, lin::make_error_code(lin::Errc::no_response)}; } uint8_t pid = protect_id(id); @@ -109,11 +109,17 @@ std::pair Bus::send_header(uint8_t id) { } break; } - case relay::BackPressurePolicy::DropOldest: - s.ch->send_drop_oldest(f); - deliver_count_.fetch_add(1); - bytes_delivered_.fetch_add(f.data.size()); + case relay::BackPressurePolicy::DropOldest: { + auto r = s.ch->send_drop_oldest(f); + if (r == Chan::SendResult::Evicted) { + // §9.1: an evicted (discarded) sample is a drop, not a delivery. + drop_count_.fetch_add(1); + } else if (r == Chan::SendResult::Ok) { + deliver_count_.fetch_add(1); + bytes_delivered_.fetch_add(f.data.size()); + } break; + } case relay::BackPressurePolicy::Block: s.ch->send(f); deliver_count_.fetch_add(1); diff --git a/testdata/relay-vectors/lin-frame.json b/testdata/relay-vectors/lin-frame.json index da4aefd..081f524 100644 --- a/testdata/relay-vectors/lin-frame.json +++ b/testdata/relay-vectors/lin-frame.json @@ -5,7 +5,7 @@ "payload": "qrs=", "meta": { "lin.checksum": "73", - "lin.checksum_type": "1" + "lin.checksum_type": "enhanced" }, - "spec_version": "1.11" + "spec_version": "2.0" } diff --git a/tests/test_master.cpp b/tests/test_master.cpp index 23e3739..df4471f 100644 --- a/tests/test_master.cpp +++ b/tests/test_master.cpp @@ -25,11 +25,11 @@ TEST_CASE("Node is constructible from a bus", "[master][REQ-MASTER-001]") { (void)bus->close(); } -TEST_CASE("set_schedule rejects empty schedule", "[master][REQ-MASTER-010]") { +TEST_CASE("set_schedule accepts empty schedule (disables scheduled TX)", "[master][REQ-MASTER-010]") { auto bus = Bus::create(); Node node(bus); auto err = node.set_schedule({}); - CHECK(err); + CHECK_FALSE(err); // RELAY §8.3: an empty table is valid and disables scheduled transmission (void)bus->close(); }