Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions SAFETY_MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,49 @@ process_frame(hw_frame);

### 4.2 E2E Protection for Safety-Critical Payloads (SG-05)

Use `lin::safety::Protector` and `Receiver` for all ASIL-B data paths:
**`lin::safety::Protector`/`Receiver` cannot protect a payload published as a
single raw LIN frame.** `Protector::protect()` always prepends a fixed
`kHeaderSize` (10-byte) header to the payload (`include/lin/safety/e2e.hpp`),
so its output is always at least 10 bytes — but a LIN frame's data field is
capped at `kLINMaxDataLen` (8 bytes). This is true for *any* input payload,
including a zero-byte one: there is no payload size for which
`protect()`'s output fits in a single LIN frame. `Bus::do_publish()` and
`slave::Node::set_response()` both reject any non-empty payload longer than
`kLINMaxDataLen` with `ErrInvalidFrame` (and the RELAY adapter's `send()`
surfaces the equivalent `ErrPayloadTooLarge`) specifically so this mistake
fails loudly instead of silently truncating or corrupting the frame on the
wire — **do not** work around that rejection (e.g. by truncating
`protect()`'s output yourself); a truncated E2E-protected payload is not a
valid protected payload.

As of this release, `lin::safety::Protector`/`Receiver` are **not usable
for direct single-frame LIN publish** and must not be wired into
`Bus::do_publish()` / `slave::Node::set_response()` this way. The module is
retained for callers who protect a payload that travels over a transport
capable of carrying more than 8 bytes per logical message — e.g. a future
multi-frame LIN diagnostic transport (see `ROADMAP.md`'s planned
`UDS (ISO 14229) over LIN TP adapter`, not yet implemented) — where the
protected payload is fragmented across several physical LIN frames by that
transport layer, not published directly. Round-trip `protect()`/`unwrap()`
usage that never goes through `IBus::publish()` (e.g. protecting a payload
before handing it to your own transport) remains safe and thread-safe:

```cpp
// Sender side (e.g., sensor ECU)
lin::safety::Config cfg{.data_id = 0x0042, .source_id = 0x0001};
lin::safety::Protector protector{cfg};
lin::safety::Receiver receiver{cfg};

auto raw_payload = read_sensor_value();
auto safe_payload = protector.protect(raw_payload);
bus->publish(FRAME_ID_SENSOR, safe_payload);
auto raw_payload = read_sensor_value();
auto safe_payload = protector.protect(raw_payload); // >= 10 bytes — do NOT
// pass this to
// Bus::publish()/
// set_response()
// directly

// ... safe_payload travels over your own multi-frame-capable transport ...

// Receiver side (e.g., actuator ECU)
lin::safety::Receiver receiver{cfg};
try {
auto verified = receiver.unwrap(frame.data);
auto verified = receiver.unwrap(safe_payload);
actuate(verified);
} catch (const lin::safety::E2EError& e) {
// mandatory safe state on E2E failure
Expand Down Expand Up @@ -245,7 +273,7 @@ must ensure:
4. The `id` field in `relay::Message` carries the decimal string representation
of the LIN frame ID (e.g., `"16"` for frame 0x10).

See `RELAY Spec v1.11 §8.3` for the complete LIN-over-RELAY envelope specification.
See `RELAY Spec v1.14 §8.3` for the complete LIN-over-RELAY envelope specification.

---

Expand Down Expand Up @@ -290,4 +318,4 @@ Reproducing from `SEOOC.md` for convenience:
- `sas.md` — Software Architecture Specification
- ISO 26262:2018 Part 6 §7 — Software integration and verification
- ISO 26262:2018 Part 10 §9 — Safety element out of context
- RELAY Specification v1.11 §8.3 — LIN bus binding
- RELAY Specification v1.14 §8.3 — LIN bus binding
9 changes: 9 additions & 0 deletions src/lin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ class LinAdapter : public relay::INode {
} catch (const ErrInvalidFrame&) {
return relay::make_error_code(relay::Errc::payload_too_large);
}
// Same reasoning as the ID-validation comment above: bus_->publish()
// (IBus::publish() -> Bus::do_publish()) correctly rejects an
// over-length payload with lin::Errc::invalid_frame, but that isn't
// one of relay.Node::send()'s four documented sentinels, so an
// oversized payload is translated to ErrPayloadTooLarge here rather
// than forwarding IBus's raw error code (cpp-LIN#17).
if (f.data.size() > kLINMaxDataLen) {
return relay::make_error_code(relay::Errc::payload_too_large);
}
return bus_->publish(f.id, std::move(f.data));
}

Expand Down
12 changes: 12 additions & 0 deletions src/virtual/bus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ std::error_code Bus::do_publish(uint8_t id, std::vector<uint8_t> data, ChecksumT
return lin::make_error_code(lin::Errc::invalid_frame);
}

// An empty payload is a legitimate "unregister this ID's response"
// signal (handled below), not a length violation — only a non-empty,
// over-length payload is rejected. Without this, callers following
// SAFETY_MANUAL.md's E2E-protected publish pattern (whose
// safety::Protector::protect() output is always >= kHeaderSize (10)
// bytes, already exceeding kLINMaxDataLen (8)) would have an oversized
// frame silently accepted here instead of rejected (cpp-LIN#17).
if (!data.empty() && data.size() > kLINMaxDataLen) {
error_count_.fetch_add(1);
return lin::make_error_code(lin::Errc::invalid_frame);
}

std::unique_lock<std::shared_mutex> lk(mu_);
if (closed_) {
error_count_.fetch_add(1);
Expand Down
16 changes: 16 additions & 0 deletions tests/test_relay_adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ TEST_CASE("adapt: send rejects non-numeric ID string", "[adapter][REQ-ADAPT-003]
(void)bus->close();
}

TEST_CASE("adapt: send rejects oversized payload with ErrPayloadTooLarge", "[adapter][REQ-ADAPT-002][REQ-ADAPT-003]") {
// relay::INode::send() (§10.1) may only return ErrClosed, ErrNotConnected,
// ErrTimeout, or ErrPayloadTooLarge — an oversized payload must surface
// as ErrPayloadTooLarge specifically, not the lin::Errc::invalid_frame
// that IBus::publish() returns for the same condition (cpp-LIN#17).
auto bus = Bus::create();
auto node = adapt(bus);

relay::Message msg;
msg.id = "16";
msg.payload = std::vector<uint8_t>(kLINMaxDataLen + 1, 0xAA);
auto err = node->send(msg);
CHECK(err == relay::ErrPayloadTooLarge());
(void)bus->close();
}

TEST_CASE("adapt: subscribe delivers frames as relay::Message", "[adapter][REQ-ADAPT-004]") {
auto bus = Bus::create();
auto node = adapt(bus);
Expand Down
12 changes: 12 additions & 0 deletions tests/test_slave.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ TEST_CASE("set_response rejects ID > 0x3F", "[slave][REQ-SLAVE-004]") {
bus->close();
}

TEST_CASE("set_response rejects payload longer than kLINMaxDataLen", "[slave][REQ-SLAVE-002][REQ-SLAVE-004]") {
// set_response() delegates to bus_->publish() (cpp-LIN#17): an oversized
// payload must be rejected here too, not just at the virtual::Bus level.
auto bus = Bus::create();
Node node(bus);
std::vector<uint8_t> oversized(kLINMaxDataLen + 1, 0xAA);
auto err = node.set_response(0x10, oversized);
CHECK(err);
CHECK(node.registered_ids().empty());
bus->close();
}

TEST_CASE("registered_ids reflects current state", "[slave][REQ-SLAVE-005]") {
auto bus = Bus::create();
Node node(bus);
Expand Down
18 changes: 18 additions & 0 deletions tests/test_virtual.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,24 @@ TEST_CASE("publish rejects ID > 0x3F", "[virtual][REQ-VIRT-004]") {
bus->close();
}

TEST_CASE("publish rejects payload longer than kLINMaxDataLen", "[virtual][REQ-VIRT-004]") {
auto bus = Bus::create();
std::vector<uint8_t> oversized(kLINMaxDataLen + 1, 0xAA);
auto err = bus->publish(0x10, oversized);
CHECK(err); // must reject, not silently accept
// and must not have registered a (truncated or otherwise) response
auto [f, herr] = bus->send_header(0x10);
CHECK(herr); // ErrNoResponse: nothing was ever published for 0x10
bus->close();
}

TEST_CASE("publish accepts payload exactly kLINMaxDataLen", "[virtual][REQ-VIRT-004]") {
auto bus = Bus::create();
std::vector<uint8_t> maxsize(kLINMaxDataLen, 0xAA);
CHECK_FALSE(bus->publish(0x10, maxsize));
bus->close();
}

TEST_CASE("publish(id, {}) removes registration", "[virtual][REQ-VIRT-003]") {
auto bus = Bus::create();
REQUIRE_FALSE(bus->publish(0x10, {0xAA}));
Expand Down
Loading