diff --git a/.fusa-iec62443.json b/.fusa-iec62443.json index d5301b5..75a485c 100644 --- a/.fusa-iec62443.json +++ b/.fusa-iec62443.json @@ -1,5 +1,198 @@ { + "version": "1", + "standard": "IEC 62443-4-1:2018 / IEC 62443-4-2:2019", "target_sl": "SL-2", "component_type": "embedded-software", - "incident_resp_doc": "INCIDENT-RESPONSE.md" + "component": "cpp-LIN", + "date": "2026-06-19", + "incident_resp_doc": "INCIDENT-RESPONSE.md", + "tara_doc": "TARA.md", + "tara_json": "tara.json", + "security_requirements": [ + { + "id": "SR-IAC-01", + "category": "Identification and Authentication", + "standard_ref": "IEC 62443-4-2 CR 1.1", + "description": "cpp-LIN does not implement authentication (no network connectivity); integrating system must authenticate LDF and configuration files via secure boot hash (DSR-05).", + "status": "integrator", + "integrator_req": "DSR-05" + }, + { + "id": "SR-IAC-02", + "category": "Identification and Authentication", + "standard_ref": "IEC 62443-4-2 CR 1.5", + "description": "No default passwords or credentials exist in cpp-LIN. No authentication secrets stored in source code.", + "status": "met", + "evidence": "Source code review — no credential strings" + }, + { + "id": "SR-UC-01", + "category": "Use Control", + "standard_ref": "IEC 62443-4-2 CR 2.1", + "description": "cpp-LIN enforces frame ID boundaries (0x00–0x3F) at all API entry points. No frame is processed beyond declared protocol bounds.", + "status": "met", + "evidence": "REQ-LIN-001, REQ-VIRT-004, REQ-VIRT-010, REQ-ADAPT-003, REQ-SLAVE-004", + "fusa_reqs": ["REQ-LIN-001", "REQ-VIRT-004", "REQ-ADAPT-003"] + }, + { + "id": "SR-UC-02", + "category": "Use Control", + "standard_ref": "IEC 62443-4-2 CR 2.2", + "description": "Wireless access is not applicable; LIN is a wired single-master bus. Physical access control is integrator responsibility (ASM-09).", + "status": "integrator", + "integrator_req": "ASM-09" + }, + { + "id": "SR-SI-01", + "category": "System Integrity", + "standard_ref": "IEC 62443-4-2 CR 3.1", + "description": "Communication integrity for safety-critical payloads is enforced by CRC-16/CCITT-FALSE E2E protection header (lin::safety::Protector/Receiver).", + "status": "met", + "evidence": "REQ-SAFETY-005, REQ-SAFETY-006, REQ-SAFETY-008", + "fusa_reqs": ["REQ-SAFETY-005", "REQ-SAFETY-008"] + }, + { + "id": "SR-SI-02", + "category": "System Integrity", + "standard_ref": "IEC 62443-4-2 CR 3.2", + "description": "Replay attacks are detected by the monotonic sequence counter in the E2E safety header. Receiver rejects any non-sequential counter.", + "status": "met", + "evidence": "REQ-SAFETY-003, REQ-SAFETY-009", + "fusa_reqs": ["REQ-SAFETY-009"] + }, + { + "id": "SR-SI-03", + "category": "System Integrity", + "standard_ref": "IEC 62443-4-2 CR 3.3", + "description": "Software and information integrity for LDF files is an integrator responsibility (secure boot hash verification). cpp-LIN validates LDF parse errors but cannot verify file provenance.", + "status": "integrator", + "integrator_req": "DSR-05" + }, + { + "id": "SR-SI-04", + "category": "System Integrity", + "standard_ref": "IEC 62443-4-2 CR 3.4", + "description": "Error detection on all transmitted frames: PID parity bits (P0/P1), enhanced checksum, and optional E2E CRC protection.", + "status": "met", + "evidence": "REQ-LIN-004..010, REQ-SAFETY-001..015", + "fusa_reqs": ["REQ-LIN-004", "REQ-LIN-005", "REQ-LIN-008", "REQ-LIN-009", "REQ-SAFETY-005"] + }, + { + "id": "SR-DC-01", + "category": "Data Confidentiality", + "standard_ref": "IEC 62443-4-2 CR 4.1", + "description": "LIN bus payloads are not encrypted (SL-2 does not require encryption for low-speed automotive buses). Confidentiality is an integrator responsibility for high-sensitivity data.", + "status": "integrator", + "notes": "LIN bus confidentiality requires hardware encryption at transceiver level; outside cpp-LIN scope" + }, + { + "id": "SR-RDF-01", + "category": "Restricted Data Flow", + "standard_ref": "IEC 62443-4-2 CR 5.1", + "description": "No network communication in cpp-LIN. Data flows only over the LIN bus (physical layer) and through in-process API calls.", + "status": "met", + "evidence": "Architecture: no sockets, no HTTP, no MQTT in library code" + }, + { + "id": "SR-TR-01", + "category": "Timely Response to Events", + "standard_ref": "IEC 62443-4-2 CR 6.1", + "description": "Audit logging is not built into cpp-LIN. Integrating system must implement event logging (e.g., ErrNoResponse, E2EError) for incident detection.", + "status": "integrator", + "notes": "OnError callback (REQ-MASTER-007) provides the hook for integrator logging" + }, + { + "id": "SR-TR-02", + "category": "Timely Response to Events", + "standard_ref": "IEC 62443-4-2 CR 6.2", + "description": "Vulnerability reporting and patching follows the process defined in INCIDENT-RESPONSE.md (SLA: Critical 7d patch, High 30d patch).", + "status": "met", + "evidence": "INCIDENT-RESPONSE.md §4, SECURITY.md" + }, + { + "id": "SR-RA-01", + "category": "Resource Availability", + "standard_ref": "IEC 62443-4-2 CR 7.1", + "description": "cpp-LIN uses bounded channels (Chan with configurable depth) with backpressure policies (DropNewest, DropOldest, Block) to prevent resource exhaustion under high load.", + "status": "met", + "evidence": "REQ-VIRT-013, REQ-RELAY-015, Chan::send_drop_oldest()", + "fusa_reqs": ["REQ-VIRT-013", "REQ-RELAY-015"] + }, + { + "id": "SR-RA-02", + "category": "Resource Availability", + "standard_ref": "IEC 62443-4-2 CR 7.2", + "description": "Watchdog timer to detect schedule runner starvation is an integrating system responsibility (DSR-06). cpp-LIN provides the stop flag mechanism for graceful shutdown.", + "status": "integrator", + "integrator_req": "DSR-06" + }, + { + "id": "SR-SM-01", + "category": "Security Management", + "standard_ref": "IEC 62443-4-1 SM-2", + "description": "Security vulnerability management process defined in INCIDENT-RESPONSE.md and SECURITY.md, including private reporting, SLA, ASIL impact assessment, and coordinated disclosure.", + "status": "met", + "evidence": "INCIDENT-RESPONSE.md, SECURITY.md" + }, + { + "id": "SR-SM-02", + "category": "Security Management", + "standard_ref": "IEC 62443-4-1 SM-6", + "description": "TARA performed for cpp-LIN SEooC scope. See TARA.md and tara.json for full threat catalogue, CVSS scores, controls, and residual risk acceptance.", + "status": "met", + "evidence": "TARA.md, tara.json" + }, + { + "id": "SR-SM-03", + "category": "Security Management", + "standard_ref": "IEC 62443-4-1 SR-3", + "description": "Security requirements derived from TARA are traced to implementation via fusa:req annotations in source code and .fusa-reqs.json.", + "status": "met", + "evidence": ".fusa-reqs.json, fusa:req annotations in src/" + }, + { + "id": "SR-SD-01", + "category": "Secure Development", + "standard_ref": "IEC 62443-4-1 SD-1", + "description": "Static analysis (clang-tidy) runs on every PR, checking for bugprone-*, clang-analyzer-*, memory safety patterns. Errors are CI gates.", + "status": "met", + "evidence": ".github/workflows/ci.yml static-analysis job" + }, + { + "id": "SR-SD-02", + "category": "Secure Development", + "standard_ref": "IEC 62443-4-1 SD-4", + "description": "Dynamic analysis (ASan + UBSan + ThreadSanitizer) runs on every PR as CI gates. Halt-on-error is enabled.", + "status": "met", + "evidence": ".github/workflows/ci.yml sanitizers and tsan jobs" + }, + { + "id": "SR-SV-01", + "category": "Security Verification", + "standard_ref": "IEC 62443-4-1 SVV-1", + "description": "Security-relevant test cases are tagged with fusa:test annotations and referenced in .fusa-reqs.json. Coverage gate >=70% enforced in CI.", + "status": "met", + "evidence": "tests/test_safety.cpp, tests/test_relay_adapter.cpp, CI coverage job" + } + ], + "open_items": [ + { + "id": "OI-SEC-01", + "description": "Fuzz testing for validate_frame() and ldf::parse() with AFL++ not yet implemented", + "target_version": "1.0.0", + "priority": "High" + }, + { + "id": "OI-SEC-02", + "description": "SBOM (Software Bill of Materials) generation not yet automated in CI", + "target_version": "1.0.0", + "priority": "Medium" + }, + { + "id": "OI-SEC-03", + "description": "MISRA C++ compliance check not yet integrated", + "target_version": "1.0.0", + "priority": "Medium" + } + ] } diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 3cfa89d..970cb4c 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -848,6 +848,262 @@ "asil": "ASIL-B", "rationale": "RELAY §7 rule 4 / §13.7: a mandatory mock transport must implement the full bus contract so applications can be tested without hardware.", "tags": ["mock", "relay"] + }, + { + "id": "REQ-RELAY-001", + "title": "Protocol enum values are defined and stable", + "description": "relay::Protocol shall define CAN=1, DDS=2, LIN=3, MQTT=4, RCP=5, SOMEIP=6 as stable integer values matching RELAY spec v1.10 §3.", + "asil": "ASIL-B", + "rationale": "Protocol values are wire-format identifiers; changing them breaks cross-language interop.", + "tags": ["relay", "protocol"] + }, + { + "id": "REQ-RELAY-002", + "title": "Protocol enum is an int-backed enum class", + "description": "relay::Protocol shall be an enum class backed by int to prevent implicit integer promotion and enable type-safe switching.", + "asil": "ASIL-B", + "rationale": "Type safety prevents accidental protocol ID confusion at compile time.", + "tags": ["relay", "protocol"] + }, + { + "id": "REQ-RELAY-003", + "title": "to_string(Protocol) returns canonical name", + "description": "relay::to_string(Protocol p) shall return the canonical uppercase protocol name (e.g., 'LIN', 'CAN') and 'unknown' for unrecognised values.", + "asil": "ASIL-B", + "rationale": "RELAY §3.1: canonical names are used in log output and diagnostic fields.", + "tags": ["relay", "protocol"] + }, + { + "id": "REQ-RELAY-004", + "title": "Version struct has major/minor/patch fields", + "description": "relay::Version shall provide int fields major, minor, and patch with equality comparison.", + "asil": "ASIL-B", + "rationale": "RELAY §4: version negotiation requires structured version comparison.", + "tags": ["relay", "version"] + }, + { + "id": "REQ-RELAY-005", + "title": "Version::to_string returns 'major.minor.patch'", + "description": "Version::to_string() shall return a string formatted as '..'.", + "asil": "ASIL-B", + "rationale": "RELAY §4.1: canonical version string format for diagnostics and conformance checks.", + "tags": ["relay", "version"] + }, + { + "id": "REQ-RELAY-006", + "title": "Message struct fields: protocol, version, id, payload, timestamp, seq, meta", + "description": "relay::Message shall provide fields: Protocol protocol, Version version, std::string id, std::vector payload, system_clock::time_point timestamp, uint64_t seq, unordered_map meta.", + "asil": "ASIL-B", + "rationale": "RELAY §5: canonical message envelope for all protocol bindings.", + "tags": ["relay", "message"] + }, + { + "id": "REQ-RELAY-007", + "title": "Message id is a string (protocol-specific format)", + "description": "Message::id shall be a std::string whose format is defined per protocol binding (e.g., decimal integer for LIN, hex for CAN).", + "asil": "ASIL-B", + "rationale": "RELAY §5.2: string ID allows each protocol to use its native identifier notation.", + "tags": ["relay", "message"] + }, + { + "id": "REQ-RELAY-008", + "title": "Errc::closed is error code 1", + "description": "relay::Errc::closed shall have integer value 1.", + "asil": "ASIL-B", + "rationale": "RELAY §9.1: stable error code values required for cross-language compatibility.", + "tags": ["relay", "error"] + }, + { + "id": "REQ-RELAY-009", + "title": "Errc::not_connected is error code 2", + "description": "relay::Errc::not_connected shall have integer value 2.", + "asil": "ASIL-B", + "rationale": "RELAY §9.1: stable error code values.", + "tags": ["relay", "error"] + }, + { + "id": "REQ-RELAY-010", + "title": "Errc::timeout is error code 3", + "description": "relay::Errc::timeout shall have integer value 3.", + "asil": "ASIL-B", + "rationale": "RELAY §9.1: stable error code values.", + "tags": ["relay", "error"] + }, + { + "id": "REQ-RELAY-011", + "title": "Errc::payload_too_large is error code 4", + "description": "relay::Errc::payload_too_large shall have integer value 4.", + "asil": "ASIL-B", + "rationale": "RELAY §9.1: stable error code values.", + "tags": ["relay", "error"] + }, + { + "id": "REQ-RELAY-012", + "title": "Errc is an int-backed enum class", + "description": "relay::Errc shall be an enum class backed by int, compatible with std::error_code.", + "asil": "ASIL-B", + "rationale": "Enables implicit std::error_code construction via is_error_code_enum specialisation.", + "tags": ["relay", "error"] + }, + { + "id": "REQ-RELAY-013", + "title": "INode abstract interface: protocol(), send(), subscribe(), close()", + "description": "relay::INode shall declare pure virtual methods: protocol() const noexcept, send(Message), subscribe(vector), and close().", + "asil": "ASIL-B", + "rationale": "RELAY §6: INode is the mandatory base interface for all protocol adapters.", + "tags": ["relay", "interface"] + }, + { + "id": "REQ-RELAY-014", + "title": "ICaller extends INode with call()", + "description": "relay::ICaller shall extend INode with a call(Message req, milliseconds timeout) method returning pair.", + "asil": "ASIL-B", + "rationale": "RELAY §6.4: synchronous request/response pattern for RPC-style protocols.", + "tags": ["relay", "interface"] + }, + { + "id": "REQ-RELAY-015", + "title": "BackPressurePolicy enum: DropNewest=0, DropOldest=1, Block=2", + "description": "relay::BackPressurePolicy shall define DropNewest=0, DropOldest=1, Block=2.", + "asil": "ASIL-B", + "rationale": "RELAY §8: backpressure policies allow callers to choose between latency and reliability tradeoffs.", + "tags": ["relay", "backpressure"] + }, + { + "id": "REQ-RELAY-016", + "title": "SubscriberConfig holds channel depth, backpressure, event_id, topic_name", + "description": "relay::SubscriberConfig shall hold: int chan_depth, BackPressurePolicy back_pressure, uint32_t event_id, string topic_name.", + "asil": "ASIL-B", + "rationale": "RELAY §8.1: subscriber configuration captures all protocol-agnostic subscriber settings.", + "tags": ["relay", "subscriber"] + }, + { + "id": "REQ-RELAY-017", + "title": "SubscriberOption is a std::function", + "description": "relay::SubscriberOption shall be typedef'd as std::function.", + "asil": "ASIL-B", + "rationale": "RELAY §8.2: functional option pattern allows composable subscriber configuration.", + "tags": ["relay", "subscriber"] + }, + { + "id": "REQ-RELAY-018", + "title": "apply_options() folds options into SubscriberConfig", + "description": "relay::apply_options(opts) shall apply each option in order to a default SubscriberConfig and return the result.", + "asil": "ASIL-B", + "rationale": "RELAY §8.3: apply_options is the canonical way to construct a SubscriberConfig from options.", + "tags": ["relay", "subscriber"] + }, + { + "id": "REQ-RELAY-019", + "title": "SubscriberConfig::effective_depth returns default when chan_depth is 0", + "description": "effective_depth(default_depth) shall return chan_depth when chan_depth > 0, else default_depth.", + "asil": "ASIL-B", + "rationale": "RELAY §8.1: zero chan_depth means 'use implementation default'; explicit non-zero overrides.", + "tags": ["relay", "subscriber"] + }, + { + "id": "REQ-RELAY-020", + "title": "kSpecVersion constant equals '1.10'", + "description": "relay::kSpecVersion shall be a constexpr const char* equal to '1.10'.", + "asil": "ASIL-B", + "rationale": "RELAY §2: spec version constant enables runtime conformance checking.", + "tags": ["relay", "version"] + }, + { + "id": "REQ-RELAY-021", + "title": "error_category() and make_error_code(Errc) are noexcept", + "description": "relay::error_category() and make_error_code(Errc) shall be declared noexcept and return stable singleton/error_code values.", + "asil": "ASIL-B", + "rationale": "std::error_code comparison requires stable category identity; noexcept prevents unexpected termination.", + "tags": ["relay", "error"] + }, + { + "id": "REQ-RELAY-022", + "title": "is_error_code_enum specialisation enables implicit construction", + "description": "std::is_error_code_enum shall be specialised to true_type to enable implicit relay::Errc -> std::error_code conversion.", + "asil": "ASIL-B", + "rationale": "Enables idiomatic std::error_code usage: if (err == relay::Errc::closed) {...}", + "tags": ["relay", "error"] + }, + { + "id": "REQ-RELAY-023", + "title": "HealthStatus enum: OK=0, Degraded=1, Down=2", + "description": "relay::HealthStatus shall define OK=0, Degraded=1, Down=2.", + "asil": "ASIL-B", + "rationale": "RELAY §11: standardised health status values for monitoring integrations.", + "tags": ["relay", "health"] + }, + { + "id": "REQ-RELAY-024", + "title": "Health struct holds status and details string", + "description": "relay::Health shall hold HealthStatus status and std::string details.", + "asil": "ASIL-B", + "rationale": "RELAY §11.1: health details provide diagnostic context beyond the status enum.", + "tags": ["relay", "health"] + }, + { + "id": "REQ-RELAY-025", + "title": "IHealthProvider declares virtual health() const", + "description": "relay::IHealthProvider shall declare a pure virtual health() const method returning Health.", + "asil": "ASIL-B", + "rationale": "RELAY §11: optional capability interface for nodes that support health reporting.", + "tags": ["relay", "health"] + }, + { + "id": "REQ-RELAY-026", + "title": "Metrics struct fields: write_count, deliver_count, drop_count, bytes_written, bytes_delivered, error_count", + "description": "relay::Metrics shall hold uint64_t fields for write_count, deliver_count, drop_count, bytes_written, bytes_delivered, error_count.", + "asil": "ASIL-B", + "rationale": "RELAY §12: metrics fields provide observability for bus throughput and reliability.", + "tags": ["relay", "metrics"] + }, + { + "id": "REQ-RELAY-027", + "title": "IMetricsProvider declares virtual metrics() const", + "description": "relay::IMetricsProvider shall declare a pure virtual metrics() const method returning Metrics.", + "asil": "ASIL-B", + "rationale": "RELAY §12: optional capability interface for nodes that expose metrics.", + "tags": ["relay", "metrics"] + }, + { + "id": "REQ-RELAY-028", + "title": "IDrainer declares virtual close_with_drain(timeout)", + "description": "relay::IDrainer shall declare a pure virtual close_with_drain(std::chrono::milliseconds timeout) method returning std::error_code.", + "asil": "ASIL-B", + "rationale": "RELAY §13: drain pattern ensures in-flight messages are delivered before close.", + "tags": ["relay", "lifecycle"] + }, + { + "id": "REQ-RELAY-029", + "title": "Metrics bytes_written and bytes_delivered track total payload bytes", + "description": "bytes_written shall accumulate the total byte count of all payloads sent; bytes_delivered shall accumulate the total byte count of all payloads delivered to subscribers.", + "asil": "ASIL-B", + "rationale": "RELAY §12.1: byte counters enable bandwidth monitoring and leak detection.", + "tags": ["relay", "metrics"] + }, + { + "id": "REQ-RELAY-051", + "title": "with_channel_depth(n) option sets chan_depth", + "description": "relay::with_channel_depth(n) shall return a SubscriberOption that sets SubscriberConfig::chan_depth to n.", + "asil": "ASIL-B", + "rationale": "RELAY §8.2.1: channel depth tuning option for subscriber configuration.", + "tags": ["relay", "subscriber", "option"] + }, + { + "id": "REQ-RELAY-056", + "title": "with_back_pressure(policy) option sets back_pressure", + "description": "relay::with_back_pressure(p) shall return a SubscriberOption that sets SubscriberConfig::back_pressure to p.", + "asil": "ASIL-B", + "rationale": "RELAY §8.2.2: backpressure policy option for subscriber configuration.", + "tags": ["relay", "subscriber", "option"] + }, + { + "id": "REQ-RELAY-059", + "title": "parse_protocol() throws on unknown string; try_parse_protocol() returns false", + "description": "relay::parse_protocol(s) shall throw std::invalid_argument for unknown protocol strings. relay::try_parse_protocol(s, out) shall return false and leave out unchanged for unknown strings.", + "asil": "ASIL-B", + "rationale": "RELAY §3.2: robust protocol string parsing with two variants for exception/error-code styles.", + "tags": ["relay", "protocol"] } ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e12aa4f..053e593 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,6 +188,37 @@ jobs: UBSAN_OPTIONS: "halt_on_error=1:print_stacktrace=1" run: ctest --test-dir build-san --output-on-failure -j1 + tsan: + name: ThreadSanitizer (REQ-VIRT-018 concurrent access) + runs-on: ubuntu-22.04 + needs: build-and-test + steps: + - uses: actions/checkout@v4 + + - name: Install tools + run: | + sudo apt-get update -qq + sudo apt-get install -y cmake ninja-build gcc-12 g++-12 + + - name: Configure (ThreadSanitizer) + env: + CC: gcc-12 + CXX: g++-12 + run: | + cmake -B build-tsan \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_FLAGS="-fsanitize=thread -fno-omit-frame-pointer -O1" \ + -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread" \ + -G Ninja + + - name: Build + run: cmake --build build-tsan --parallel + + - name: Test with ThreadSanitizer + env: + TSAN_OPTIONS: "halt_on_error=1:second_deadlock_stack=1" + run: ctest --test-dir build-tsan --output-on-failure -j1 + fusa-asil-b: name: cpp-FuSa ASIL-B qualification runs-on: ubuntu-22.04 @@ -225,7 +256,7 @@ jobs: - name: cpfusa lint working-directory: cpp-LIN - run: ../cpp-FuSa/build/cpfusa lint --dir . || true + run: ../cpp-FuSa/build/cpfusa lint --dir . - name: cpfusa trace (requirements traceability) working-directory: cpp-LIN @@ -233,7 +264,7 @@ jobs: - name: cpfusa cyber working-directory: cpp-LIN - run: ../cpp-FuSa/build/cpfusa cyber --write --dir . || true + run: ../cpp-FuSa/build/cpfusa cyber --write --dir . - name: cpfusa qualify (ASIL-B gate) working-directory: cpp-LIN @@ -332,6 +363,10 @@ jobs: cpp-LIN/boundary.dot cpp-LIN/.fusa-hara.json cpp-LIN/.fusa-metrics.json + cpp-LIN/TARA.md + cpp-LIN/SAFETY_MANUAL.md + cpp-LIN/INCIDENT-RESPONSE.md + cpp-LIN/SECURITY.md static-analysis: name: Static analysis (clang-tidy) diff --git a/INCIDENT-RESPONSE.md b/INCIDENT-RESPONSE.md new file mode 100644 index 0000000..2218f0b --- /dev/null +++ b/INCIDENT-RESPONSE.md @@ -0,0 +1,130 @@ +# Incident Response Policy — cpp-LIN + +**Document ID:** IR-cpp-LIN-001 +**Revision:** 0.1 +**Date:** 2026-06-19 +**Author:** Matt Jones +**Standard:** ISO/SAE 21434:2021 §7.4, IEC 62443-4-1 SM-2, IEC/TR 62443-2-3 +**Contact:** matt@jellybaby.com + +--- + +## 1. Scope + +This document defines the incident response process for cpp-LIN, covering +vulnerability discovery, classification, notification, remediation, and public +disclosure for all versions of the library. + +--- + +## 2. Incident Classification + +| Severity | Description | Examples | +|----------|-------------|---------| +| **Critical** | Memory safety bug enabling arbitrary code execution; E2E bypass | Buffer overflow in frame parsing; CRC check skip | +| **High** | Data corruption in safety-critical path; denial-of-service via API | Integer overflow in checksum; unbounded allocation | +| **Medium** | Non-safety-critical logic error; incorrect but non-exploitable behaviour | Wrong PID parity for specific ID; schedule order drift | +| **Low** | Documentation error; minor API misalignment with RELAY spec | Typo in error message; wrong version string | + +--- + +## 3. Reporting a Vulnerability + +**Private reporting (preferred):** +Email **matt@jellybaby.com** with subject `[SECURITY] cpp-LIN `. + +Include: +- Affected version(s) +- Reproduction steps or proof-of-concept +- Potential impact assessment +- Suggested severity (Critical / High / Medium / Low) + +Do **not** open a public GitHub issue for security vulnerabilities. Public +issues should be reserved for non-security bugs and feature requests. + +**GitHub Security Advisories:** +Alternatively, use the [GitHub Security Advisory](https://github.com/SoundMatt/cpp-LIN/security/advisories/new) +draft mechanism for private coordinated disclosure. + +--- + +## 4. Response SLA + +| Severity | Acknowledgment | Initial Assessment | Patch Target | Public Disclosure | +|----------|----------------|-------------------|-------------|-------------------| +| Critical | 24 hours | 48 hours | 7 days | 14 days after patch | +| High | 48 hours | 5 days | 30 days | 30 days after patch | +| Medium | 5 days | 14 days | 90 days | 90 days after patch | +| Low | 14 days | 30 days | Next release | At release | + +All timelines are calendar days from initial validated report. If a reporter +requests a later disclosure date, we will accommodate up to 180 days for +Critical/High vulnerabilities. + +--- + +## 5. Response Process + +### Phase 1: Receipt and Acknowledgment +1. Acknowledge receipt within the SLA above. +2. Assign an internal tracking ID (format: `CVE--XXXXXXX` if applicable, otherwise `IR--NNN`). +3. Confirm or challenge the reported severity. + +### Phase 2: Assessment +1. Reproduce the issue on the latest `main` branch. +2. Assess ASIL impact: does this affect a safety-critical path (E2E, checksum, frame validation)? +3. Check whether a mitigation exists at the integrating system level (SEOOC.md assumptions). +4. Draft a CVSS 3.1 score for Critical/High findings. + +### Phase 3: Remediation +1. Develop a fix on a private branch (`security/IR--NNN`). +2. Write a regression test that triggers the vulnerability (must be in the public test suite). +3. Run full CI pipeline (build matrix, sanitizers, coverage, RELAY conformance, cpfusa ASIL-B). +4. Issue a patch release (semantic version bump, SECURITY.md advisory link added to CHANGELOG). + +### Phase 4: Disclosure +1. Publish a GitHub Security Advisory with CVE ID (if MITRE assigned). +2. Tag the patch release and update README badge. +3. Notify the reporter before public disclosure so they may publish their own writeup. +4. Add the incident to the internal FMEA traceability (fmea.json) and TARA update cycle. + +--- + +## 6. Severity Escalation Rules + +A finding is **automatically escalated to Critical** if it affects: +- `lin::safety::Protector::protect()` or `lin::safety::Receiver::unwrap()` (E2E bypass) +- `lin::protect_id()` or `lin::verify_pid()` (PID integrity) +- `lin::validate_frame()` (frame boundary checking) +- `lin::virt::Bus` concurrency primitives (data race → undefined behaviour) + +--- + +## 7. ASIL Impact Assessment + +For safety-critical (ASIL-B) vulnerabilities, the following additional steps apply: + +1. Update `HARA.md` if the vulnerability introduces a new hazardous event. +2. Update `SAFETY_PLAN.md` Confirmation Measures table with a reference to the incident. +3. Issue an **Interim Safety Notice (ISN)** to known integrators (via GitHub release notes tagged `[SAFETY]`). +4. Increment the `.fusa-reqs.json` requirement that was bypassed (add `status: "under-review"` field). + +--- + +## 8. Contact and Escalation Chain + +| Role | Contact | Availability | +|------|---------|-------------| +| Primary Security Contact | matt@jellybaby.com | Business hours UTC+0 | +| Backup / Emergency | GitHub Security Advisory form | 24/7 async | + +--- + +## 9. Policy References + +- **ISO/SAE 21434:2021** §7.4 — Vulnerability management and disclosure +- **IEC 62443-4-1:2018** SM-2 — Security management (vulnerability response) +- **IEC/TR 62443-2-3:2015** — Patch management for IACS +- **RELAY Spec v1.10** §22 — Security considerations +- `SECURITY.md` — Public-facing vulnerability disclosure policy +- `SEOOC.md` — Safety Element out of Context assumptions (integrator scope) diff --git a/SAFETY_MANUAL.md b/SAFETY_MANUAL.md new file mode 100644 index 0000000..11a2031 --- /dev/null +++ b/SAFETY_MANUAL.md @@ -0,0 +1,293 @@ +# Safety Manual — cpp-LIN + +**Document ID:** SM-cpp-LIN-001 +**Revision:** 0.1 +**Date:** 2026-06-19 +**Author:** Matt Jones +**Standard:** ISO 26262:2018 Part 6 §7 (user documentation), Part 10 §9 (SEooC) +**ASIL:** ASIL-B + +--- + +## 1. Purpose and Scope + +This Safety Manual provides guidance for system integrators incorporating +cpp-LIN into an automotive ECU or IACS component. It documents: + +- Assumptions that the integrating system must validate (from `SEOOC.md`) +- API usage patterns that preserve ASIL-B guarantees +- Anti-patterns that void the safety argument +- Integration checklist + +This manual supplements `SEOOC.md` (assumptions) and `HARA.md` (hazard analysis) +and must be read alongside those documents. + +--- + +## 2. Architecture Overview + +``` +┌─────────────────────────────────────────────────────┐ +│ Integrating System │ +│ │ +│ ┌──────────────┐ ┌──────────────────────────┐ │ +│ │ Application │ │ Safety Monitor (watchdog│ │ +│ │ Layer │ │ heap guard, MPU) │ │ +│ └──────┬───────┘ └──────────────────────────┘ │ +│ │ │ +│ ┌──────▼───────────────────────────────────────┐ │ +│ │ cpp-LIN │ │ +│ │ ┌───────────┐ ┌─────────┐ ┌───────────┐ │ │ +│ │ │ lin:: │ │lin:: │ │lin:: │ │ │ +│ │ │ validate │ │safety:: │ │master:: │ │ │ +│ │ │ protect_id │ │E2E Prot │ │slave:: │ │ │ +│ │ │ checksum │ │E2E Recv │ │ldf:: │ │ │ +│ │ └─────┬─────┘ └────┬────┘ └─────┬─────┘ │ │ +│ │ └─────────────▼─────────────┘ │ │ +│ │ lin::virt::Bus / IBus │ │ +│ └──────────────────────┬────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────▼────────────────────────┐ │ +│ │ LIN Physical Layer (integrator-owned) │ │ +│ │ (transceiver, break detection, bit timing) │ │ +│ └───────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +cpp-LIN operates **above the physical layer**. The integrating system is responsible +for all hardware I/O. cpp-LIN provides software-level frame validation, E2E +protection, scheduling, and protocol adaptation. + +--- + +## 3. Assumptions Validation Checklist + +Before deploying cpp-LIN in an ASIL-B system, validate **every** assumption: + +| ASM-ID | Assumption | How to Validate | +|--------|------------|-----------------| +| ASM-01 | C++17 runtime is standards-conformant | Compile with `-std=c++17 -Wall -Wextra -Wpedantic`; run test suite | +| ASM-02 | POSIX threading available | Verify `std::thread`, `std::mutex`, `std::condition_variable` work on target | +| ASM-03 | `std::terminate()` not suppressed | Confirm no custom `terminate_handler` swallows exceptions | +| ASM-04 | Sufficient heap capacity | Profile peak allocations; configure MPU/heap guard | +| ASM-05 | Single LIN master on bus | Verify hardware configuration; no multi-master wiring | +| ASM-06 | Monotonic clock ≥ 1 ms resolution | Measure `std::this_thread::sleep_for(1ms)` actual latency | +| ASM-07 | Hardware CRC for ASIL-C/D | Implement hardware CRC layer if system ASIL exceeds ASIL-B | +| ASM-08 | Correct shutdown sequencing | Review destructor order; call `IBus::close()` before bus destruction | +| ASM-09 | Physical bus integrity monitored | Implement open/short circuit detection in hardware layer | +| ASM-10 | LDF file integrity verified | Integrate LDF loading into secure boot manifest checking | + +--- + +## 4. Safe API Usage Patterns + +### 4.1 Frame Validation (SG-01, SG-02) + +**ALWAYS** validate frames received from external hardware before using them: + +```cpp +lin::Frame hw_frame = read_from_hardware(); +try { + lin::validate_frame(hw_frame); // throws ErrInvalidFrame if invalid +} catch (const lin::ErrInvalidFrame& e) { + // handle or log; do NOT process the frame + trigger_safe_state(); + return; +} +process_frame(hw_frame); +``` + +**NEVER** bypass `validate_frame()` for frames originating outside cpp-LIN. + +### 4.2 E2E Protection for Safety-Critical Payloads (SG-05) + +Use `lin::safety::Protector` and `Receiver` for all ASIL-B data paths: + +```cpp +// Sender side (e.g., sensor ECU) +lin::safety::Config cfg{.data_id = 0x0042, .source_id = 0x0001}; +lin::safety::Protector protector{cfg}; + +auto raw_payload = read_sensor_value(); +auto safe_payload = protector.protect(raw_payload); +bus->publish(FRAME_ID_SENSOR, safe_payload); + +// Receiver side (e.g., actuator ECU) +lin::safety::Receiver receiver{cfg}; +try { + auto verified = receiver.unwrap(frame.data); + actuate(verified); +} catch (const lin::safety::E2EError& e) { + // mandatory safe state on E2E failure + trigger_safe_state(); + log_safety_event(e.kind(), e.counter()); +} +``` + +**NEVER** ignore `E2EError` exceptions — they indicate data corruption or replay. + +### 4.3 Error Handling for ErrNoResponse (SG-01) + +A `lin::ErrNoResponse` from `send_header()` or `master::Node::run()` means a +slave did not respond. This is **not a fatal error** for cpp-LIN, but the +integrating system must decide the safe-state action: + +```cpp +node.on_error([](std::error_code err) { + if (err == lin::make_error_code(lin::Errc::no_response)) { + // slave absent — integrating system decides safe action + increment_absent_slave_counter(); + if (absent_count > MAX_ABSENT_FRAMES) { + trigger_degraded_mode(); + } + } +}); +``` + +**NEVER** silently discard `ErrNoResponse` without a counter or timeout guard. + +### 4.4 Schedule Table Management (SG-01) + +```cpp +lin::master::Node master_node(bus); +auto err = master_node.set_schedule({ + {0x10, 10}, // ID 0x10, 10 ms slot + {0x20, 10}, + {0x3C, 5}, // diagnostic ID +}); +if (err) { + // schedule rejected — programming error + log_fault(err); + return; +} + +std::atomic stop{false}; +std::thread run_thread([&]{ + (void)master_node.run(stop); +}); +// ... on shutdown: +stop.store(true); +run_thread.join(); +bus->close(); +``` + +**ALWAYS** join the run thread before destroying the bus. + +### 4.5 Shutdown Sequencing (ASM-08) + +```cpp +// Correct shutdown order: +stop_flag.store(true); // 1. Signal run() to exit +run_thread.join(); // 2. Wait for run() to return +bus->close(); // 3. Close bus (signals subscriber threads) +// 4. subscriber_thread.join() if applicable +// 5. shared_ptr goes out of scope +``` + +Incorrect order (bus destroyed before threads exit) is undefined behaviour. + +--- + +## 5. Anti-Patterns (Safety Violations) + +| Anti-Pattern | Consequence | Correct Alternative | +|-------------|-------------|---------------------| +| Skip `validate_frame()` on hardware input | Invalid ID / oversized payload silently processed | Always call `validate_frame()` at hardware boundary | +| Ignore `E2EError` from `Receiver::unwrap()` | Corrupted or replayed payload propagates to actuator | Trigger safe state; log event | +| Destroy bus while subscriber threads running | Undefined behaviour (data race, crash) | Call `bus->close()` and join threads first | +| Use `lin::virt::Bus` as production transport | Virtual bus lacks physical LIN timing/collision detection | Implement physical `IBus` adapter for production | +| Call `SetSchedule` concurrently with `Run` | Potential schedule modification mid-cycle | Set schedule before starting `Run`; stop/restart if update needed | +| Multiple `master::Node` instances on same bus | Two masters violate LIN protocol (ASM-05) | One master per bus; validate hardware topology | +| Ignore `set_schedule()` return value | Invalid schedule silently installed | Always check `err` from `set_schedule()` | + +--- + +## 6. Thread Safety Matrix + +| Operation | Thread Safe? | Notes | +|-----------|-------------|-------| +| `virt::Bus::publish()` | Yes | Protected by `shared_mutex` | +| `virt::Bus::send_header()` | Yes | Protected by `shared_mutex` | +| `virt::Bus::subscribe()` | Yes | Protected by `shared_mutex` | +| `virt::Bus::close()` | Yes | Idempotent | +| `safety::Protector::protect()` | Yes | Atomic sequence counter | +| `safety::Receiver::unwrap()` | Yes (single consumer) | Mutex-protected; share one receiver per data flow | +| `master::Node::run()` | Single thread only | Call from exactly one thread | +| `master::Node::set_schedule()` | Not during `run()` | Configure before or after; not concurrently | +| `slave::Node::set_response()` | Yes (after construction) | Delegates to bus::publish | + +--- + +## 7. Performance and Memory Budget + +| Component | Stack | Heap (peak) | Notes | +|-----------|-------|-------------|-------| +| `virt::Bus::create()` | — | ~1 KB + 64 B/subscriber | Map + vector per subscriber | +| `Chan` (depth=64) | — | ~8 KB | 64 × sizeof(Frame) ≈ 64 × 128 B | +| `safety::Protector` | ~16 B | None | Atomic counter only | +| `safety::Receiver` | ~32 B | None | mutex + two counters | +| `master::Node::run()` | ~512 B | None | Loop variables only | +| `ldf::parse()` | ~4 KB | Proportional to LDF size | One-time at startup | + +**Recommendation:** Allocate all channels at startup; do not create/destroy `Chan` +objects during safety-critical operation (ASIL-B scope). + +--- + +## 8. Integration with RELAY Router + +When using cpp-LIN as a RELAY node (via `lin::adapt()`), the integrating system +must ensure: + +1. The RELAY router is started before `adapt()` is called. +2. `linNode->close()` is called before the RELAY router shuts down. +3. The RELAY message envelope's `protocol` field is set to `relay::Protocol::LIN`. +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.10 §8.3` for the complete LIN-over-RELAY envelope specification. + +--- + +## 9. Qualification Evidence Location + +| Artifact | Location | +|----------|----------| +| ASIL-B requirements | `.fusa-reqs.json` | +| Hazard Analysis | `HARA.md` | +| SEooC Assumptions | `SEOOC.md` | +| Safety Plan | `SAFETY_PLAN.md` | +| TARA (cybersecurity) | `TARA.md`, `tara.json` | +| FMEA | `fmea.json` | +| Architecture Spec | `sas.md` | +| CI evidence | GitHub Actions (ASIL-B artifacts uploaded per run) | +| Qualification badge | CI artifact `fusa-badge.svg` | + +--- + +## 10. Derived Safety Requirements for Integrators + +Reproducing from `SEOOC.md` for convenience: + +| DSR-ID | Requirement | +|--------|-------------| +| DSR-01 | Monitor heap usage; trigger safe state on heap exhaustion | +| DSR-02 | Enforce single-master topology at hardware level | +| DSR-03 | Apply hardware CRC for ASIL-C/D data paths | +| DSR-04 | Call `IBus::close()` before destroying bus with active subscriber threads | +| DSR-05 | Verify LDF file integrity via secure boot mechanism | +| DSR-06 | Implement watchdog timer to detect schedule runner starvation | + +--- + +## 11. References + +- `HARA.md` — Hazard Analysis and Risk Assessment +- `SEOOC.md` — Safety Element out of Context assumptions +- `SAFETY_PLAN.md` — Development safety plan +- `TARA.md` — Threat Analysis and Risk Assessment +- `fmea.json` — Failure Mode and Effect Analysis +- `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.10 §8.3 — LIN bus binding diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..51093df --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,70 @@ +# Security Policy — cpp-LIN + +## Supported Versions + +| Version | Supported | +|---------|-----------| +| 0.1.x | Yes | +| < 0.1 | No | + +## Reporting a Vulnerability + +**Do not open public GitHub issues for security vulnerabilities.** + +Please report security issues by email to **matt@jellybaby.com** with subject +`[SECURITY] cpp-LIN `, or via the +[GitHub Security Advisory](https://github.com/SoundMatt/cpp-LIN/security/advisories/new) +private reporting mechanism. + +### What to Include + +- Affected version(s) and platform +- Reproduction steps or proof-of-concept +- Potential impact (is a safety-critical path affected?) +- Your preferred disclosure timeline + +## Response Timeline + +| Severity | Acknowledgment | Patch | +|----------|----------------|-------| +| Critical (ASIL-B path affected) | 24 h | 7 days | +| High | 48 h | 30 days | +| Medium | 5 days | 90 days | +| Low | 14 days | Next release | + +We follow **coordinated disclosure**: patches are published before or simultaneously +with public disclosure. The maximum embargo period is 180 days for Complex/Critical issues. + +## Scope + +In scope: +- Memory safety bugs (buffer overflow, use-after-free) in any cpp-LIN API +- E2E safety bypass: any input that causes `lin::safety::Receiver::unwrap()` to + accept a corrupted or replayed payload without throwing `E2EError` +- Data races in `lin::virt::Bus` concurrent operations +- Integer overflow in `protect_id()`, `verify_pid()`, `calc_checksum()` +- Denial-of-service via unbounded resource allocation in frame processing + +Out of scope: +- Physical LIN bus attacks (transceiver, wiring) — see `SEOOC.md` ASM-09 +- LDF file tampering — integrator responsibility (see `SEOOC.md` ASM-10) +- Issues in the RELAY CLI tool that do not affect the library +- Issues in test-only code + +## Safety Impact Disclosure + +If a vulnerability affects an ASIL-B annotated function (marked with `fusa:req`), +we will issue an **Interim Safety Notice (ISN)** alongside the patch release. +ISNs are tagged `[SAFETY]` in release notes. + +## Full Policy + +See [`INCIDENT-RESPONSE.md`](INCIDENT-RESPONSE.md) for the complete incident +response process including ASIL impact assessment, FMEA update procedure, and +escalation contacts. + +## Standards References + +- ISO/SAE 21434:2021 §7.4 — Vulnerability management +- IEC 62443-4-1:2018 SM-2 — Security management +- RELAY Spec v1.10 §22 — Security considerations diff --git a/TARA.md b/TARA.md new file mode 100644 index 0000000..ba85a36 --- /dev/null +++ b/TARA.md @@ -0,0 +1,216 @@ +# Threat Analysis and Risk Assessment (TARA) — cpp-LIN + +**Document ID:** TARA-cpp-LIN-001 +**Revision:** 0.1 +**Date:** 2026-06-19 +**Author:** Matt Jones +**Standards:** ISO/SAE 21434:2021, IEC 62443-4-1:2018 SL-2, IEC 62443-4-2:2019 +**Target Security Level:** SL-2 (resistance against intentional violation with moderate resources) + +--- + +## 1. Scope and Context + +This TARA covers the cpp-LIN library as a SEooC (Safety Element out of Context). +The library processes LIN bus frames in software. Physical bus access, transceiver +hardware, and network topology are **integrator responsibilities** (see `SEOOC.md`). + +The primary security objective is to prevent an attacker with access to the LIN bus +or the host software environment from: +1. Injecting corrupt frames that reach safety-critical actuators +2. Replaying legitimate frames out of temporal context +3. Exhausting resources and causing Denial-of-Service on the LIN schedule +4. Bypassing E2E protection to cause undetected data corruption + +--- + +## 2. Asset Identification + +| Asset ID | Asset | Confidentiality | Integrity | Availability | +|----------|-------|----------------|-----------|-------------| +| A-01 | LIN frame payload (safety-critical) | Low | **High** | **High** | +| A-02 | Frame ID / PID (routing) | Low | **High** | Medium | +| A-03 | E2E sequence counter | Low | **High** | Medium | +| A-04 | E2E CRC / header bytes | Low | **High** | Medium | +| A-05 | LDF configuration data | Low | **High** | Medium | +| A-06 | Master schedule table | Low | **High** | **High** | +| A-07 | Bus subscriber registry | Low | Medium | Medium | +| A-08 | Heap and channel buffers | Low | Medium | **High** | + +--- + +## 3. Threat Scenarios + +### THREAT-01 — LIN Bus Frame Injection + +| Field | Value | +|-------|-------| +| **ID** | THREAT-01 | +| **Assets** | A-01, A-02 | +| **STRIDE** | Spoofing, Tampering | +| **Attack vector** | Physical or logical LIN bus access | +| **Threat** | Attacker injects a crafted LIN frame with a valid-looking PID and corrupted payload, bypassing `validate_frame()` if the integrating system does not call it. | +| **CVSS 3.1** | AV:P/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:L = **5.3 (Medium)** | +| **Risk rating** | High (safety impact on ASIL-B path) | +| **Controls** | SC-01, SC-02, SC-05 | +| **Residual risk** | Low — E2E CRC detects any single-bit corruption; PID parity detects ID tampering | + +### THREAT-02 — Frame Replay Attack + +| Field | Value | +|-------|-------| +| **ID** | THREAT-02 | +| **Assets** | A-01, A-03 | +| **STRIDE** | Tampering, Elevation of Privilege | +| **Attack vector** | Physical LIN bus tap + replay device | +| **Threat** | Attacker records valid E2E-protected frames and replays them later, causing stale sensor data to appear fresh. | +| **CVSS 3.1** | AV:P/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N = **4.2 (Medium)** | +| **Risk rating** | High (E2E sequence counter is the primary defence) | +| **Controls** | SC-03 | +| **Residual risk** | Low — `safety::Receiver::unwrap()` rejects any non-sequential counter as `ErrSequenceGap` | + +### THREAT-03 — Resource Exhaustion / Denial of Service + +| Field | Value | +|-------|-------| +| **ID** | THREAT-03 | +| **Assets** | A-06, A-08 | +| **STRIDE** | Denial of Service | +| **Attack vector** | Malformed LDF file, or rapid-fire publish calls from a compromised host process | +| **Threat** | Attacker supplies a large LDF file or calls `Bus::subscribe()` many times to exhaust heap memory, starving the master schedule runner. | +| **CVSS 3.1** | AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H = **5.5 (Medium)** | +| **Risk rating** | Medium | +| **Controls** | SC-04, SC-06 | +| **Residual risk** | Medium — integrating system must enforce heap budgets (DSR-01) | + +### THREAT-04 — E2E Header Bypass (Corrupted DataID/SourceID) + +| Field | Value | +|-------|-------| +| **ID** | THREAT-04 | +| **Assets** | A-04 | +| **STRIDE** | Tampering | +| **Attack vector** | LIN bus bit-flip or software bug in protector | +| **Threat** | DataID or SourceID field is corrupted, but CRC happens to match (birthday attack) — receiver accepts a frame from the wrong source. | +| **CVSS 3.1** | AV:P/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N = **4.2 (Medium)** | +| **Risk rating** | Medium (CRC-16 has 2^-16 false-accept probability) | +| **Controls** | SC-05 | +| **Residual risk** | Low — CRC-16 false-accept rate is ≤1.5×10^-5 per frame; acceptable for ASIL-B | + +### THREAT-05 — LDF File Tampering + +| Field | Value | +|-------|-------| +| **ID** | THREAT-05 | +| **Assets** | A-05, A-06 | +| **STRIDE** | Tampering, Spoofing | +| **Attack vector** | Compromised build system or insecure boot | +| **Threat** | Attacker modifies the LDF file to change frame IDs, signal offsets, or schedule timing, causing the master to poll wrong IDs or decode signals incorrectly. | +| **CVSS 3.1** | AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:H/A:M = **4.4 (Medium)** | +| **Risk rating** | Medium | +| **Controls** | SC-07 | +| **Residual risk** | Low — integrating system must verify LDF integrity via secure boot (ASM-10, DSR-05) | + +### THREAT-06 — Timing Attack on Schedule Runner + +| Field | Value | +|-------|-------| +| **ID** | THREAT-06 | +| **Assets** | A-06 | +| **STRIDE** | Denial of Service | +| **Attack vector** | OS-level priority manipulation on host process | +| **Threat** | Attacker starves the LIN schedule runner thread by scheduling high-priority processes, causing slot timing jitter or total schedule stall. | +| **CVSS 3.1** | AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H = **4.1 (Medium)** | +| **Risk rating** | Low (requires OS-level privilege; hardware watchdog mitigates) | +| **Controls** | SC-06 | +| **Residual risk** | Low — integrating system implements watchdog (DSR-06) | + +### THREAT-07 — Data Race in Concurrent Bus Access + +| Field | Value | +|-------|-------| +| **ID** | THREAT-07 | +| **Assets** | A-01, A-07 | +| **STRIDE** | Tampering | +| **Attack vector** | Multi-threaded host application | +| **Threat** | Concurrent calls to `Bus::publish()`, `subscribe()`, and `send_header()` without proper internal locking cause undefined behaviour or data corruption. | +| **CVSS 3.1** | AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:H = **6.3 (Medium)** | +| **Risk rating** | Low (mitigated by implementation) | +| **Controls** | SC-08 | +| **Residual risk** | Low — `virt::Bus` uses `shared_mutex`; verified by ThreadSanitizer in CI | + +--- + +## 4. Security Controls + +| Control ID | Control | Standard Reference | Implementation | +|-----------|---------|-------------------|----------------| +| SC-01 | Frame ID boundary check | IEC 62443-4-2 CR 3.4 | `validate_frame()` throws on ID > 0x3F | +| SC-02 | PID parity verification | ISO 26262-6 REQ-LIN-006/007 | `verify_pid()` checks P0/P1 per LIN 2.x §2.3.1 | +| SC-03 | E2E sequence counter | IEC 62443-4-2 CR 3.2, ISO 26262-7 E2E | `safety::Receiver::unwrap()` enforces counter monotonicity | +| SC-04 | Bounded channel depth | IEC 62443-4-2 CR 7.1 | `Chan` drop-oldest / drop-newest backpressure (REQ-RELAY-015) | +| SC-05 | CRC-16/CCITT-FALSE E2E | IEC 62443-4-2 CR 3.4 | `safety::Protector::protect()` computes CRC over header+payload | +| SC-06 | Watchdog integration | IEC 62443-4-2 CR 7.2 | DSR-06: integrating system provides watchdog | +| SC-07 | LDF integrity at load | IEC 62443-4-2 CR 3.3 | ASM-10, DSR-05: secure boot hash verification | +| SC-08 | Thread-safe bus | IEC 62443-4-2 CR 2.1 | `shared_mutex` in `virt::Bus`; TSan CI gate | + +--- + +## 5. Attack Paths and Mitigations (Attack Tree Summary) + +``` +Goal: Corrupt ASIL-B actuator output +├── THREAT-01: Inject frame with valid PID +│ ├── SC-02 (PID parity): blocks forged PID +│ └── SC-05 (E2E CRC): blocks corrupted payload +├── THREAT-02: Replay valid frame +│ └── SC-03 (sequence counter): detects gap +├── THREAT-03: DoS via heap exhaustion +│ └── SC-04 (bounded channels): drops frames, does not crash +├── THREAT-04: E2E birthday attack +│ └── SC-05 (CRC-16 2^-16): acceptable residual risk +├── THREAT-05: LDF file tampering +│ └── SC-07 (secure boot): integrator responsibility +├── THREAT-06: Schedule starvation +│ └── SC-06 (watchdog): integrator responsibility +└── THREAT-07: Data race + └── SC-08 (shared_mutex + TSan): eliminated in implementation +``` + +--- + +## 6. Risk Acceptance + +| Threat | Residual Risk | Accepted By | Date | +|--------|--------------|-------------|------| +| THREAT-01 | Low | Matt Jones | 2026-06-19 | +| THREAT-02 | Low | Matt Jones | 2026-06-19 | +| THREAT-03 | Medium — integrator must enforce heap budget (DSR-01) | Matt Jones | 2026-06-19 | +| THREAT-04 | Low — CRC-16 false-accept within ASIL-B tolerance | Matt Jones | 2026-06-19 | +| THREAT-05 | Low — integrator must provide secure boot (DSR-05) | Matt Jones | 2026-06-19 | +| THREAT-06 | Low — integrator must provide watchdog (DSR-06) | Matt Jones | 2026-06-19 | +| THREAT-07 | Low — eliminated in implementation | Matt Jones | 2026-06-19 | + +--- + +## 7. Penetration Testing Scope (v1.0.0 target) + +| Test | Method | +|------|--------| +| Frame injection via virtual bus | Fuzzing with AFL++ on `validate_frame()` | +| Replay detection | Property-based test: random counter gaps must always be rejected | +| Heap exhaustion | Valgrind massif profiling with 1000 concurrent subscribers | +| Race condition | ThreadSanitizer on all bus operations (CI gate) | +| LDF parser fuzz | AFL++ on `ldf::parse()` with random inputs | + +--- + +## 8. References + +- ISO/SAE 21434:2021 §9 — Threat analysis and risk assessment +- IEC 62443-4-1:2018 SR-2, SR-3 — Security requirements +- IEC 62443-4-2:2019 CR 3.2, 3.3, 3.4, 7.1, 7.2 — Component requirements +- `HARA.md` — Hazard Analysis (safety view) +- `SEOOC.md` — Assumptions and derived safety requirements +- `INCIDENT-RESPONSE.md` — Vulnerability response process +- `tara.json` — Machine-readable TARA (cpfusa format) diff --git a/boundary.mermaid b/boundary.mermaid new file mode 100644 index 0000000..df48964 --- /dev/null +++ b/boundary.mermaid @@ -0,0 +1,88 @@ +--- +title: "cpp-LIN Software Architecture Boundary — ISO 26262-6 §7.4.1" +--- +graph TD + %% ── External environment ──────────────────────────────────────────────── + HW["LIN Transceiver / HW\n(integrator-owned)"] + OS["OS / RTOS\n(threading, clock)"] + APP["Application Layer\n(integrator-owned)"] + RELAY_ROUTER["RELAY Router\n(external)"] + LDF_FILE["LDF File\n(external input)"] + + %% ── cpp-LIN boundary ──────────────────────────────────────────────────── + subgraph CPP_LIN["cpp-LIN (ASIL-B boundary)"] + direction TB + + subgraph CORE["lin:: — Core Protocol"] + VF["validate_frame()\nREQ-LIN-001..003,015..017"] + PID["protect_id() / verify_pid()\nREQ-LIN-004..007"] + CS["calc_checksum()\nREQ-LIN-008..010"] + TM["to_message() / from_message()\nREQ-LIN-011..012"] + end + + subgraph VIRT["lin::virt:: — Virtual Transport"] + BUS["virt::Bus\nREQ-VIRT-001..019"] + end + + subgraph SAFETY["lin::safety:: — E2E Protection"] + PROT["Protector::protect()\nREQ-SAFETY-001..006"] + RECV["Receiver::unwrap()\nREQ-SAFETY-007..015"] + end + + subgraph MASTER["lin::master:: — Schedule Runner"] + MNODE["master::Node::run()\nREQ-MASTER-001..013"] + end + + subgraph SLAVE["lin::slave:: — Response Registration"] + SNODE["slave::Node\nREQ-SLAVE-001..008"] + end + + subgraph LDF["lin::ldf:: — LDF Parser"] + PARSE["ldf::parse()\nREQ-LDF-001..015"] + end + + subgraph ADAPT["lin:: — RELAY Adapter"] + ADAPT_FN["adapt(IBus)\nREQ-ADAPT-001..005"] + end + + subgraph RELAY_MOD["relay:: — RELAY Types"] + RTYPES["Protocol, Message, INode\nREQ-RELAY-001..029,051,056,059"] + end + end + + %% ── Data flows ────────────────────────────────────────────────────────── + HW -->|"raw frames\n[physical boundary]"| APP + APP -->|"hw_frame\n[ASM-02 validate first]"| VF + VF --> BUS + BUS --> PID + BUS --> CS + BUS -->|"Frame"| MNODE + BUS -->|"Frame"| SNODE + BUS -->|"Frame"| TM + + APP -->|"payload\n[safety-critical]"| PROT + PROT -->|"protected_payload"| BUS + BUS -->|"protected_payload"| RECV + RECV -->|"verified_payload"| APP + + LDF_FILE -->|"LDF text"| PARSE + PARSE -->|"DB{frames, schedule}"| MNODE + + MNODE -->|"std::error_code"| APP + SNODE -->|"RegisteredIDs"| APP + + TM -->|"relay::Message"| ADAPT_FN + ADAPT_FN -->|"INode*"| RELAY_ROUTER + RELAY_ROUTER -->|"relay::Message"| ADAPT_FN + + OS -->|"std::thread\nstd::mutex\nstd::chrono"| BUS + OS -->|"std::atomic stop"| MNODE + + %% ── Styling ───────────────────────────────────────────────────────────── + classDef boundary fill:#e8f4f8,stroke:#2c7bb6,stroke-width:2px + classDef external fill:#fff3cd,stroke:#856404,stroke-width:1px,stroke-dasharray:4 4 + classDef safety_comp fill:#d4edda,stroke:#155724,stroke-width:2px + + class CORE,VIRT,MASTER,SLAVE,LDF,ADAPT,RELAY_MOD boundary + class SAFETY safety_comp + class HW,OS,APP,RELAY_ROUTER,LDF_FILE external diff --git a/fmea.json b/fmea.json new file mode 100644 index 0000000..90c718b --- /dev/null +++ b/fmea.json @@ -0,0 +1,248 @@ +{ + "version": "1", + "standard": "ISO 26262-9:2018", + "component": "cpp-LIN", + "date": "2026-06-19", + "asil": "ASIL-B", + "fmea_entries": [ + { + "id": "FMEA-01", + "item": "validate_frame()", + "module": "M-01 Core", + "req_refs": ["REQ-LIN-001", "REQ-LIN-002", "REQ-LIN-003"], + "failure_mode": "Accepts frame with ID > 0x3F or data length 0 or > 8", + "failure_effect_local": "Invalid frame enters bus processing pipeline", + "failure_effect_system": "Downstream ECU receives corrupt data; potential actuator miscommand", + "hazard_ref": "H-01", + "severity": "S2", + "occurrence": "O1", + "detection": "D1", + "rpn": 2, + "mitigation": "Boundary tests REQ-LIN-001..003,015..017; ASan in CI", + "status": "Closed" + }, + { + "id": "FMEA-02", + "item": "protect_id()", + "module": "M-01 Core", + "req_refs": ["REQ-LIN-004", "REQ-LIN-005", "REQ-LIN-018"], + "failure_mode": "P0 or P1 parity bit computed incorrectly", + "failure_effect_local": "PID byte has wrong parity bits", + "failure_effect_system": "Slave rejects header; master gets ErrNoResponse; safety degradation", + "hazard_ref": "H-02", + "severity": "S2", + "occurrence": "O1", + "detection": "D1", + "rpn": 2, + "mitigation": "Known-vector tests REQ-LIN-004..007; verify_pid cross-check", + "status": "Closed" + }, + { + "id": "FMEA-03", + "item": "verify_pid()", + "module": "M-01 Core", + "req_refs": ["REQ-LIN-006", "REQ-LIN-007"], + "failure_mode": "Incorrect parity accepted or correct parity rejected", + "failure_effect_local": "Frame with corrupt ID accepted, or valid frame rejected", + "failure_effect_system": "Wrong frame ID processed; potential wrong actuator targeted", + "hazard_ref": "H-02", + "severity": "S2", + "occurrence": "O1", + "detection": "D1", + "rpn": 2, + "mitigation": "Unit tests for accept/reject cases; protect_id/verify_pid round-trip", + "status": "Closed" + }, + { + "id": "FMEA-04", + "item": "calc_checksum()", + "module": "M-01 Core", + "req_refs": ["REQ-LIN-008", "REQ-LIN-009", "REQ-LIN-010"], + "failure_mode": "Wrong checksum for classic or enhanced type; carry-around error", + "failure_effect_local": "Frame has incorrect checksum field", + "failure_effect_system": "Hardware slave rejects frame; protocol violation", + "hazard_ref": "H-03", + "severity": "S2", + "occurrence": "O1", + "detection": "D1", + "rpn": 2, + "mitigation": "Classic and enhanced checksum known-vector tests", + "status": "Closed" + }, + { + "id": "FMEA-05", + "item": "virt::Bus::publish()", + "module": "M-02 Virtual Bus", + "req_refs": ["REQ-VIRT-002", "REQ-VIRT-003", "REQ-VIRT-004", "REQ-VIRT-005"], + "failure_mode": "Data race: concurrent publish() corrupts response table", + "failure_effect_local": "Wrong payload stored; subsequent send_header delivers corrupt data", + "failure_effect_system": "Wrong sensor value propagated to actuator", + "hazard_ref": "H-01", + "severity": "S3", + "occurrence": "O2", + "detection": "D1", + "rpn": 6, + "mitigation": "shared_mutex exclusive-write; REQ-VIRT-018 ThreadSanitizer gate in CI", + "status": "Closed" + }, + { + "id": "FMEA-06", + "item": "virt::Bus::send_header()", + "module": "M-02 Virtual Bus", + "req_refs": ["REQ-VIRT-006", "REQ-VIRT-007", "REQ-VIRT-008", "REQ-VIRT-009"], + "failure_mode": "Frame delivered to wrong subscriber due to filter logic error", + "failure_effect_local": "Subscriber receives frame with wrong ID", + "failure_effect_system": "Application processes wrong signal; potential miscommand", + "hazard_ref": "H-01", + "severity": "S2", + "occurrence": "O1", + "detection": "D1", + "rpn": 2, + "mitigation": "Filter isolation tests REQ-VIRT-011; multiple-subscriber tests REQ-VIRT-014", + "status": "Closed" + }, + { + "id": "FMEA-07", + "item": "virt::Bus subscriber channel full", + "module": "M-02 Virtual Bus", + "req_refs": ["REQ-VIRT-013"], + "failure_mode": "Full subscriber channel blocks send_header, stalling master schedule", + "failure_effect_local": "LIN schedule stalls; no frames sent", + "failure_effect_system": "Total loss of LIN communication; safety degradation", + "hazard_ref": "H-05", + "severity": "S3", + "occurrence": "O2", + "detection": "D2", + "rpn": 12, + "mitigation": "Backpressure: full channel drops frame (REQ-VIRT-013); bounded Chan; integrator monitors drop_count metric", + "status": "Closed" + }, + { + "id": "FMEA-08", + "item": "safety::Protector::protect()", + "module": "M-03 Safety", + "req_refs": ["REQ-SAFETY-001", "REQ-SAFETY-002", "REQ-SAFETY-003", "REQ-SAFETY-004", "REQ-SAFETY-005", "REQ-SAFETY-006"], + "failure_mode": "CRC computed over wrong bytes (e.g., CRC slot not zeroed before CRC computation)", + "failure_effect_local": "CRC in header does not match Receiver's recomputation", + "failure_effect_system": "All frames rejected by Receiver; system enters safe state unnecessarily", + "hazard_ref": "H-04", + "severity": "S2", + "occurrence": "O1", + "detection": "D1", + "rpn": 2, + "mitigation": "Known-vector test (CRC-16/CCITT-FALSE = 0x29B1 for '123456789'); round-trip test REQ-SAFETY-011", + "status": "Closed" + }, + { + "id": "FMEA-09", + "item": "safety::Receiver::unwrap() — sequence check", + "module": "M-03 Safety", + "req_refs": ["REQ-SAFETY-009", "REQ-SAFETY-013"], + "failure_mode": "Gap detection logic fails: replayed frame accepted with old counter", + "failure_effect_local": "Stale or replayed payload passed to application", + "failure_effect_system": "Actuator receives outdated command; potential hazardous output", + "hazard_ref": "H-04", + "severity": "S3", + "occurrence": "O1", + "detection": "D1", + "rpn": 3, + "mitigation": "Sequence gap unit test; first-message seeding test REQ-SAFETY-013; concurrent protect test REQ-SAFETY-014", + "status": "Closed" + }, + { + "id": "FMEA-10", + "item": "master::Node::run()", + "module": "M-04 Master", + "req_refs": ["REQ-MASTER-003", "REQ-MASTER-004", "REQ-MASTER-005"], + "failure_mode": "Schedule processed out-of-order or slot skipped", + "failure_effect_local": "Wrong frame ID polled; slave not sampled in correct slot", + "failure_effect_system": "Sensor data acquired at wrong time; safety analysis invalid", + "hazard_ref": "H-01", + "severity": "S2", + "occurrence": "O1", + "detection": "D1", + "rpn": 2, + "mitigation": "In-order iteration test REQ-MASTER-003; slot call count test REQ-MASTER-004", + "status": "Closed" + }, + { + "id": "FMEA-11", + "item": "master::Node::run() — stop signal", + "module": "M-04 Master", + "req_refs": ["REQ-MASTER-008"], + "failure_mode": "run() ignores stop flag; thread cannot be joined", + "failure_effect_local": "Master thread continues running after shutdown request", + "failure_effect_system": "Use-after-free if bus destroyed first; undefined behaviour", + "hazard_ref": "H-05", + "severity": "S3", + "occurrence": "O1", + "detection": "D1", + "rpn": 3, + "mitigation": "Cancellation test REQ-MASTER-008; ASan validates no UAF after thread join", + "status": "Closed" + }, + { + "id": "FMEA-12", + "item": "ldf::parse()", + "module": "M-06 LDF", + "req_refs": ["REQ-LDF-014"], + "failure_mode": "Panic / crash on malformed or adversarial LDF input", + "failure_effect_local": "Process crash during LDF loading phase", + "failure_effect_system": "LIN system fails to initialize; startup denial of service", + "hazard_ref": "H-05", + "severity": "S2", + "occurrence": "O2", + "detection": "D1", + "rpn": 4, + "mitigation": "Fuzz-target test REQ-LDF-014; exception caught, returns partial DB + error", + "status": "Closed" + }, + { + "id": "FMEA-13", + "item": "LinAdapter::send()", + "module": "M-07 RELAY Adapter", + "req_refs": ["REQ-ADAPT-003"], + "failure_mode": "Out-of-range ID (>63) silently truncated to valid range and published", + "failure_effect_local": "Wrong frame ID published; wrong slave responds", + "failure_effect_system": "Wrong actuator targeted; potential safety miscommand", + "hazard_ref": "H-02", + "severity": "S2", + "occurrence": "O1", + "detection": "D1", + "rpn": 2, + "mitigation": "Explicit range check before publish; reject test msg.id='64' REQ-ADAPT-003", + "status": "Closed" + }, + { + "id": "FMEA-14", + "item": "Chan::recv() after close()", + "module": "M-09 Channel", + "req_refs": ["REQ-VIRT-015", "REQ-VIRT-016"], + "failure_mode": "recv() blocks indefinitely after bus close; subscriber thread never exits", + "failure_effect_local": "Thread leak; resource exhaustion", + "failure_effect_system": "Cannot shut down LIN system cleanly; heap / handle leak", + "hazard_ref": "H-05", + "severity": "S2", + "occurrence": "O1", + "detection": "D1", + "rpn": 2, + "mitigation": "Close signals all waiting recv() to return nullopt; lifecycle test REQ-VIRT-015", + "status": "Closed" + } + ], + "severity_scale": { + "S1": "No safety impact", + "S2": "Safety degradation — system enters safe state", + "S3": "Potential hazardous output without safe state" + }, + "occurrence_scale": { + "O1": "Improbable — no known instance in test / field", + "O2": "Remote — possible under specific conditions", + "O3": "Occasional — multiple occurrences possible" + }, + "detection_scale": { + "D1": "Unit test / CI catches before integration", + "D2": "Integration test or runtime monitor catches", + "D3": "Field detection only" + } +} diff --git a/sas.md b/sas.md new file mode 100644 index 0000000..03459b4 --- /dev/null +++ b/sas.md @@ -0,0 +1,370 @@ +# Software Architecture Specification (SAS) — cpp-LIN + +**Document ID:** SAS-cpp-LIN-001 +**Revision:** 0.1 +**Date:** 2026-06-19 +**Author:** Matt Jones +**Standard:** ISO 26262:2018 Part 6 §7.4, IEC 61508:2010 Part 3 §7.4 +**ASIL:** ASIL-B + +--- + +## 1. Purpose + +This SAS defines the decomposition of cpp-LIN into software modules, their +responsibilities, invariants, interfaces, and inter-module dependencies. It +serves as: + +- The primary evidence for ISO 26262-6 §7.4.1 (software architectural design) +- The basis for the FMEA (see `fmea.json`) +- The design reference for the safety manual (see `SAFETY_MANUAL.md`) + +--- + +## 2. Architectural Decomposition + +### 2.1 Module List + +| Module | Namespace | Files | ASIL | Role | +|--------|-----------|-------|------|------| +| M-01 Core | `lin::` | `src/lin.cpp`, `include/lin/lin.hpp` | ASIL-B | Frame validation, PID, checksum, message conversion | +| M-02 Virtual Bus | `lin::virt::` | `src/virtual/bus.cpp`, `include/lin/virtual/bus.hpp` | ASIL-B | Thread-safe in-memory LIN transport | +| M-03 Safety | `lin::safety::` | `src/safety.cpp`, `include/lin/safety.hpp` | ASIL-B | E2E protection (CRC-16, sequence counter) | +| M-04 Master | `lin::master::` | `src/master.cpp`, `include/lin/master.hpp` | ASIL-B | Schedule-driven frame exchange | +| M-05 Slave | `lin::slave::` | `src/slave.cpp`, `include/lin/slave.hpp` | ASIL-B | Response registration and subscription | +| M-06 LDF | `lin::ldf::` | `src/ldf.cpp`, `include/lin/ldf.hpp` | ASIL-A | LDF file parser | +| M-07 RELAY Adapter | `lin::` | `src/lin.cpp` (adapt()) | ASIL-B | Wraps IBus as relay::INode | +| M-08 RELAY Types | `relay::` | `src/relay.cpp`, `include/lin/relay.hpp` | ASIL-B | RELAY spec types, error codes, subscriber options | +| M-09 Channel | `lin::` | `include/lin/channel.hpp` | ASIL-B | Type-safe bounded SPSC/MPSC channel | + +### 2.2 Module Dependencies + +``` +M-04 (Master) ──► M-01 (Core) ──► M-08 (RELAY Types) +M-05 (Slave) ──► M-01 (Core) +M-02 (Virt) ──► M-01 (Core) +M-07 (Adapt) ──► M-01 (Core) ──► M-08 (RELAY Types) +M-07 (Adapt) ──► M-09 (Channel) +M-03 (Safety) ──► (none — standalone, no LIN dependency) +M-06 (LDF) ──► (none — parse-only, no bus dependency) +``` + +No circular dependencies. M-03 and M-06 are independently usable. + +--- + +## 3. Module Specifications + +### M-01 — Core (`lin::`) + +**Responsibilities:** +- Frame validation (`validate_frame`) +- PID computation and verification (`protect_id`, `verify_pid`) +- Checksum computation (`calc_checksum`, `ChecksumType`) +- Message conversion (`to_message`, `from_message`) +- IBus abstract interface definition +- Error code definition (`Errc`, `ErrInvalidFrame`, `ErrNoResponse`) + +**Invariants:** +- `validate_frame(f)` never modifies `f` +- `protect_id(id)` is a pure function: same input → same output, no side effects +- `protect_id(protect_id(id) & 0x3F)` is idempotent for any valid id +- `calc_checksum` never throws; result is always in [0x00, 0xFF] + +**Public Interface:** + +```cpp +// Boundary validation +void validate_frame(const Frame& f); // throws ErrInvalidFrame + +// PID +uint8_t protect_id(uint8_t id) noexcept; +uint8_t verify_pid(uint8_t pid); // throws ErrInvalidFrame + +// Checksum +uint8_t calc_checksum(uint8_t pid, const std::vector& data, + ChecksumType ct) noexcept; + +// Message conversion +relay::Message to_message(const Frame& f); +Frame from_message(const relay::Message& m); // throws ErrInvalidFrame + +// Bus abstraction +class IBus { ... }; +std::unique_ptr adapt(std::shared_ptr bus); +``` + +**Requirements:** REQ-LIN-001..021, REQ-ADAPT-001..005 + +--- + +### M-02 — Virtual Bus (`lin::virt::`) + +**Responsibilities:** +- Thread-safe in-memory simulation of a LIN bus +- Response registration and send_header frame exchange +- Subscriber fan-out with backpressure (drop-full-channel) +- Health reporting and metrics collection +- Close/drain lifecycle + +**Invariants:** +- `virt::Bus` is safe for concurrent access from any number of threads +- `close()` is idempotent — calling it twice has no effect +- After `close()`, all subscriber channels are closed; blocking `Chan::recv()` returns `nullopt` +- `publish(id, data)` stores a defensive copy — mutations to caller's data do not affect stored response + +**Internal Design:** +- `std::shared_mutex` for read/write access to response table and subscriber list +- Subscriber channels: `std::shared_ptr>` with bounded capacity (default 64) +- Health/Metrics: atomic counters, no lock required for reading + +**Public Interface:** + +```cpp +static std::shared_ptr create(); + +std::error_code publish(uint8_t id, std::vector data); +std::pair send_header(uint8_t id); +std::pair>, std::error_code> + subscribe(std::vector filters, std::vector opts); +std::error_code close(); +Health health() const; +Metrics metrics() const; +std::error_code close_with_drain(std::chrono::milliseconds timeout); +``` + +**Requirements:** REQ-VIRT-001..019 + +--- + +### M-03 — Safety (`lin::safety::`) + +**Responsibilities:** +- E2E protection: CRC-16/CCITT-FALSE over 10-byte header + payload +- Sequence counter (monotonically increasing, atomic) +- Header embedding: DataID (bytes 0-1), SourceID (bytes 2-3), counter (bytes 4-7), CRC (bytes 8-9) +- E2E verification: CRC check, sequence gap detection + +**Invariants:** +- `Protector::protect(payload)` output length is exactly `10 + payload.size()` +- `Protector::protect()` is thread-safe (uses `std::atomic` for counter) +- `Receiver::unwrap()` never returns partial results — either full payload or throws `E2EError` +- `E2EError::kind()` is always one of: `ErrHeaderTooShort`, `ErrCRCMismatch`, `ErrSequenceGap` + +**CRC Algorithm:** +- Polynomial: 0x1021 (CRC-16/CCITT-FALSE) +- Initial value: 0xFFFF +- Input reflection: No; Output reflection: No; XOR out: 0x0000 +- CRC computed over `[header with CRC bytes zeroed] || payload` + +**Public Interface:** + +```cpp +struct Config { uint16_t data_id; uint16_t source_id; }; + +class Protector { + explicit Protector(Config cfg); + std::vector protect(const std::vector& payload); +}; + +class E2EError : public std::exception { ... }; + +class Receiver { + explicit Receiver(Config cfg); + std::vector unwrap(const std::vector& protected_data); +}; +``` + +**Requirements:** REQ-SAFETY-001..015 + +--- + +### M-04 — Master (`lin::master::`) + +**Responsibilities:** +- Schedule-driven LIN frame exchange: iterate table in order, loop +- Per-slot timing: `std::this_thread::sleep_for(DelayMs)` +- Callback dispatch: `OnFrame` on success, `OnError` on error +- Graceful shutdown via `std::atomic& stop` + +**Invariants:** +- `run()` processes slots strictly in the order they appear in the schedule table +- `run()` returns without stopping the schedule on per-slot `ErrNoResponse` (REQ-MASTER-013) +- `run()` only returns when `stop.load()` is true or the bus is closed +- `set_schedule()` stores a defensive copy + +**Public Interface:** + +```cpp +class Node { + explicit Node(std::shared_ptr bus); + std::error_code set_schedule(std::vector entries); + std::error_code run(const std::atomic& stop); + void on_frame(std::function cb); + void on_error(std::function cb); + std::pair send_header(uint8_t id); +}; +``` + +**Requirements:** REQ-MASTER-001..013 + +--- + +### M-05 — Slave (`lin::slave::`) + +**Responsibilities:** +- Response registration via `IBus::publish()` +- Tracking registered IDs for `RegisteredIDs()` diagnostic +- Subscription delegation to `IBus::subscribe()` + +**Invariants:** +- `set_response(id, nullptr)` removes `id` from the registered set +- `RegisteredIDs()` returns an empty vector (not null) when no responses registered +- `set_response` validates id ≤ 0x3F before calling `bus->publish()` + +**Requirements:** REQ-SLAVE-001..008 + +--- + +### M-06 — LDF Parser (`lin::ldf::`) + +**Responsibilities:** +- Parse LIN Description File (LDF) text +- Populate `DB` struct with: protocol version, baud rate, node names, frame + descriptors, signal definitions, schedule tables +- Never panic on arbitrary input (REQ-LDF-014) + +**Invariants:** +- `ldf::parse()` always returns a non-null `DB`; empty fields for unparseable sections +- `DB::Frame(id)` returns null for unknown IDs (not a crash) +- `DB::Signal(name)` returns null for unknown names (not a crash) +- `DB::Frames()` returns a defensive copy + +**Requirements:** REQ-LDF-001..015 + +--- + +### M-07 — RELAY Adapter (`lin:: adapt()`) + +**Responsibilities:** +- Wraps an `IBus` as a `relay::INode` +- `send()`: decodes frame ID from `relay::Message::id`, publishes to bus +- `subscribe()`: spawns a background thread that converts `Chan` to `Chan` +- `close()`: delegates to `IBus::close()` + +**Invariants:** +- `protocol()` always returns `relay::Protocol::LIN` +- `send()` rejects non-numeric or out-of-range ID strings without publishing +- Background subscriber threads exit when the underlying `Chan` is closed +- `seq` counter in relay::Messages is strictly monotonically increasing per `LinAdapter` instance + +**Requirements:** REQ-ADAPT-001..005 + +--- + +### M-08 — RELAY Types (`relay::`) + +**Responsibilities:** +- Protocol enum (CAN=1, DDS=2, LIN=3, MQTT=4, RCP=5, SOMEIP=6) +- `Message` envelope struct +- `Errc` error codes and `std::error_category` +- `BackPressurePolicy` enum +- `SubscriberOption` / `SubscriberConfig` / `apply_options()` +- `INode`, `ICaller` abstract interfaces +- Optional capability interfaces: `IHealthProvider`, `IMetricsProvider`, `IDrainer` +- kSpecVersion constant "1.10" + +**Requirements:** REQ-RELAY-001..029, REQ-RELAY-051, REQ-RELAY-056, REQ-RELAY-059 + +--- + +### M-09 — Channel (`lin::Chan`) + +**Responsibilities:** +- Bounded, thread-safe, multi-producer / multi-consumer channel +- Blocking `send()` / `recv()` and non-blocking `try_send()` / `try_recv()` +- Drop-oldest policy: `send_drop_oldest()` +- `close()`: unblocks all waiters; subsequent `recv()` drains remaining items then returns `nullopt` + +**Invariants:** +- `Chan` is constructed with a fixed capacity; it never grows dynamically +- After `close()`, `send()` returns immediately (channel closed) +- Items pushed before `close()` are still retrievable by `recv()` until drained + +--- + +## 4. Safety Architecture Decisions + +### 4.1 No Dynamic Dispatch in Safety-Critical Path + +`validate_frame()`, `protect_id()`, `verify_pid()`, and `calc_checksum()` are +free functions with no virtual dispatch. This ensures deterministic execution +time and avoids virtual table corruption hazards. + +### 4.2 E2E at Application, Not Transport, Layer + +E2E protection (M-03) is applied above the bus transport (M-02). This ensures +protection even when using the virtual bus for test, and is consistent with +ISO 26262-6 E2E profile placement. + +### 4.3 Single-Writer Channel Design + +`Chan` is designed for single-writer (producer) / single-reader (consumer) +use per channel instance. Multi-subscriber fan-out is implemented by the bus +layer (M-02), which holds one channel per subscriber. + +### 4.4 Separation of Physical and Software Concerns + +cpp-LIN deliberately provides no physical layer implementation. `IBus` is the +boundary. All hardware I/O is the integrating system's responsibility (ASM-09). +This enforces a clean separation that enables independent ASIL qualification. + +--- + +## 5. Data Flow for Safety-Critical Path + +``` +Sensor ECU (slave) + │ + ▼ lin::safety::Protector::protect(raw_payload) + │ → header{DataID, SourceID, counter, CRC} + raw_payload + │ + ▼ lin::virt::Bus::publish(frame_id, protected_payload) + │ → stored in response table + │ + ▼ lin::master::Node::run() + │ → calls bus::send_header(frame_id) + │ → receives Frame{id, data=protected_payload, pid, checksum} + │ + ▼ lin::safety::Receiver::unwrap(frame.data) + │ → verifies CRC, sequence counter + │ → returns raw_payload or throws E2EError + │ + ▼ Actuator ECU (integrating system) +``` + +--- + +## 6. Memory Safety Strategy + +| Risk | Mitigation | +|------|-----------| +| Buffer overflow in frame parsing | `validate_frame()` checks payload length; no raw pointer arithmetic | +| Use-after-free on bus close | `shared_ptr` keeps bus alive while subscribers hold references | +| Stack overflow in recursive code | No recursive functions in cpp-LIN core | +| Heap exhaustion | Bounded `Chan` capacity; integrator monitors heap (DSR-01) | +| Data race | `shared_mutex` in `virt::Bus`; `atomic` for safety counter | + +**CI verification:** ASan+UBSan (sanitizers job), ThreadSanitizer (tsan job). + +--- + +## 7. References + +- `HARA.md` — Hazard Analysis +- `TARA.md` — Threat Analysis +- `fmea.json` — Failure Mode and Effect Analysis +- `SAFETY_MANUAL.md` — Integration guidance +- `SEOOC.md` — SEooC assumptions +- ISO 26262:2018 Part 6 §7.4 — Software architectural design +- IEC 61508:2010 Part 3 §7.4 — Software architecture diff --git a/src/lin.cpp b/src/lin.cpp index 257d6c4..cf4d8fd 100644 --- a/src/lin.cpp +++ b/src/lin.cpp @@ -104,25 +104,33 @@ Frame from_message(const relay::Message& m) { return f; } -// ── RELAY adapter ── REQ-LIN-011 ───────────────────────────────────────────── +// ── RELAY adapter ── REQ-ADAPT-001 REQ-ADAPT-002 REQ-ADAPT-003 REQ-ADAPT-004 REQ-ADAPT-005 namespace { +// LinAdapter wraps an IBus as a relay::INode. +// fusa:req REQ-ADAPT-001 REQ-ADAPT-002 REQ-ADAPT-003 REQ-ADAPT-004 REQ-ADAPT-005 class LinAdapter : public relay::INode { public: explicit LinAdapter(std::shared_ptr bus) : bus_(std::move(bus)) {} + // fusa:req REQ-ADAPT-001 relay::Protocol protocol() const noexcept override { return relay::Protocol::LIN; } + // fusa:req REQ-ADAPT-002 REQ-ADAPT-003 std::error_code send(relay::Message msg) override { + unsigned long long id_val{}; try { - uint8_t id = static_cast(std::stoull(msg.id)); - return bus_->publish(id, std::move(msg.payload)); + id_val = std::stoull(msg.id); } catch (...) { return relay::make_error_code(relay::Errc::payload_too_large); } + if (id_val > kLINMaxID) + return relay::make_error_code(relay::Errc::payload_too_large); + return bus_->publish(static_cast(id_val), std::move(msg.payload)); } + // fusa:req REQ-ADAPT-004 std::pair>, std::error_code> subscribe(std::vector opts = {}) override { @@ -163,6 +171,7 @@ class LinAdapter : public relay::INode { return {out, {}}; } + // fusa:req REQ-ADAPT-005 std::error_code close() override { return bus_->close(); } private: @@ -172,6 +181,7 @@ class LinAdapter : public relay::INode { } // anonymous namespace +// fusa:req REQ-ADAPT-001 std::unique_ptr adapt(std::shared_ptr bus) { return std::make_unique(std::move(bus)); } diff --git a/tara.json b/tara.json new file mode 100644 index 0000000..b0e2f92 --- /dev/null +++ b/tara.json @@ -0,0 +1,106 @@ +{ + "version": "1", + "standard": "ISO/SAE 21434:2021", + "security_level": "SL-2", + "component": "cpp-LIN", + "date": "2026-06-19", + "assets": [ + {"id": "A-01", "name": "LIN frame payload", "integrity": "High", "availability": "High", "confidentiality": "Low"}, + {"id": "A-02", "name": "Frame ID / PID", "integrity": "High", "availability": "Medium", "confidentiality": "Low"}, + {"id": "A-03", "name": "E2E sequence counter", "integrity": "High", "availability": "Medium", "confidentiality": "Low"}, + {"id": "A-04", "name": "E2E CRC / header bytes", "integrity": "High", "availability": "Medium", "confidentiality": "Low"}, + {"id": "A-05", "name": "LDF configuration data", "integrity": "High", "availability": "Medium", "confidentiality": "Low"}, + {"id": "A-06", "name": "Master schedule table", "integrity": "High", "availability": "High", "confidentiality": "Low"}, + {"id": "A-07", "name": "Bus subscriber registry", "integrity": "Medium", "availability": "Medium", "confidentiality": "Low"}, + {"id": "A-08", "name": "Heap and channel buffers", "integrity": "Medium", "availability": "High", "confidentiality": "Low"} + ], + "threats": [ + { + "id": "THREAT-01", + "name": "LIN Bus Frame Injection", + "stride": ["Spoofing", "Tampering"], + "assets": ["A-01", "A-02"], + "cvss": "AV:P/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:L", + "cvss_score": 5.3, + "risk_rating": "High", + "controls": ["SC-01", "SC-02", "SC-05"], + "residual_risk": "Low" + }, + { + "id": "THREAT-02", + "name": "Frame Replay Attack", + "stride": ["Tampering", "Elevation of Privilege"], + "assets": ["A-01", "A-03"], + "cvss": "AV:P/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N", + "cvss_score": 4.2, + "risk_rating": "High", + "controls": ["SC-03"], + "residual_risk": "Low" + }, + { + "id": "THREAT-03", + "name": "Resource Exhaustion / Denial of Service", + "stride": ["Denial of Service"], + "assets": ["A-06", "A-08"], + "cvss": "AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H", + "cvss_score": 5.5, + "risk_rating": "Medium", + "controls": ["SC-04", "SC-06"], + "residual_risk": "Medium" + }, + { + "id": "THREAT-04", + "name": "E2E Header Bypass (DataID/SourceID corruption)", + "stride": ["Tampering"], + "assets": ["A-04"], + "cvss": "AV:P/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N", + "cvss_score": 4.2, + "risk_rating": "Medium", + "controls": ["SC-05"], + "residual_risk": "Low" + }, + { + "id": "THREAT-05", + "name": "LDF File Tampering", + "stride": ["Tampering", "Spoofing"], + "assets": ["A-05", "A-06"], + "cvss": "AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:H/A:M", + "cvss_score": 4.4, + "risk_rating": "Medium", + "controls": ["SC-07"], + "residual_risk": "Low" + }, + { + "id": "THREAT-06", + "name": "Timing Attack on Schedule Runner", + "stride": ["Denial of Service"], + "assets": ["A-06"], + "cvss": "AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H", + "cvss_score": 4.1, + "risk_rating": "Low", + "controls": ["SC-06"], + "residual_risk": "Low" + }, + { + "id": "THREAT-07", + "name": "Data Race in Concurrent Bus Access", + "stride": ["Tampering"], + "assets": ["A-01", "A-07"], + "cvss": "AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:H", + "cvss_score": 6.3, + "risk_rating": "Low", + "controls": ["SC-08"], + "residual_risk": "Low" + } + ], + "controls": [ + {"id": "SC-01", "name": "Frame ID boundary check", "standard_ref": "IEC 62443-4-2 CR 3.4", "impl": "validate_frame()"}, + {"id": "SC-02", "name": "PID parity verification", "standard_ref": "ISO 26262-6 REQ-LIN-006/007", "impl": "verify_pid()"}, + {"id": "SC-03", "name": "E2E sequence counter", "standard_ref": "IEC 62443-4-2 CR 3.2", "impl": "safety::Receiver::unwrap()"}, + {"id": "SC-04", "name": "Bounded channel depth", "standard_ref": "IEC 62443-4-2 CR 7.1", "impl": "Chan backpressure"}, + {"id": "SC-05", "name": "CRC-16/CCITT-FALSE E2E", "standard_ref": "IEC 62443-4-2 CR 3.4", "impl": "safety::Protector::protect()"}, + {"id": "SC-06", "name": "Watchdog integration", "standard_ref": "IEC 62443-4-2 CR 7.2", "impl": "DSR-06 integrator watchdog"}, + {"id": "SC-07", "name": "LDF integrity at load", "standard_ref": "IEC 62443-4-2 CR 3.3", "impl": "DSR-05 secure boot hash"}, + {"id": "SC-08", "name": "Thread-safe bus operations", "standard_ref": "IEC 62443-4-2 CR 2.1", "impl": "shared_mutex + TSan CI gate"} + ] +} diff --git a/tests/test_relay_adapter.cpp b/tests/test_relay_adapter.cpp index 2823b2d..a7be2dd 100644 --- a/tests/test_relay_adapter.cpp +++ b/tests/test_relay_adapter.cpp @@ -3,6 +3,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +// fusa:test REQ-ADAPT-001 REQ-ADAPT-002 REQ-ADAPT-003 REQ-ADAPT-004 REQ-ADAPT-005 // fusa:test REQ-LIN-011 REQ-LIN-012 REQ-LIN-019 REQ-LIN-020 #include @@ -13,14 +14,14 @@ using namespace lin; using namespace lin::virt; -TEST_CASE("adapt: protocol returns LIN", "[adapter][REQ-LIN-011]") { +TEST_CASE("adapt: protocol returns LIN", "[adapter][REQ-ADAPT-001]") { auto bus = Bus::create(); auto node = adapt(bus); CHECK(node->protocol() == relay::Protocol::LIN); - bus->close(); + (void)bus->close(); } -TEST_CASE("adapt: send publishes payload to bus", "[adapter][REQ-LIN-011]") { +TEST_CASE("adapt: send publishes payload to bus", "[adapter][REQ-ADAPT-002]") { auto bus = Bus::create(); auto node = adapt(bus); @@ -31,26 +32,48 @@ TEST_CASE("adapt: send publishes payload to bus", "[adapter][REQ-LIN-011]") { auto err = node->send(msg); REQUIRE_FALSE(err); - // Verify response was registered auto [f, ferr] = bus->send_header(0x10); REQUIRE_FALSE(ferr); CHECK(f.id == 0x10); CHECK(f.data == std::vector{0xAA, 0xBB}); - bus->close(); + (void)bus->close(); } -TEST_CASE("adapt: subscribe delivers frames as relay::Message", "[adapter][REQ-LIN-012]") { +TEST_CASE("adapt: send rejects out-of-range frame ID string", "[adapter][REQ-ADAPT-003]") { + auto bus = Bus::create(); + auto node = adapt(bus); + + relay::Message msg; + msg.id = "64"; // 0x40 > kLINMaxID + msg.payload = {0x01}; + auto err = node->send(msg); + CHECK(err); // must reject + (void)bus->close(); +} + +TEST_CASE("adapt: send rejects non-numeric ID string", "[adapter][REQ-ADAPT-003]") { + auto bus = Bus::create(); + auto node = adapt(bus); + + relay::Message msg; + msg.id = "bad"; + msg.payload = {0x01}; + auto err = node->send(msg); + CHECK(err); + (void)bus->close(); +} + +TEST_CASE("adapt: subscribe delivers frames as relay::Message", "[adapter][REQ-ADAPT-004]") { auto bus = Bus::create(); auto node = adapt(bus); auto [ch, err] = node->subscribe({}); REQUIRE_FALSE(err); - bus->publish(0x20, {0x01, 0x02}); - bus->send_header(0x20); + (void)bus->publish(0x20, {0x01, 0x02}); + (void)bus->send_header(0x20); - // Give the bridge goroutine time to relay - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); auto got = ch->try_recv(); REQUIRE(got.has_value()); @@ -58,14 +81,54 @@ TEST_CASE("adapt: subscribe delivers frames as relay::Message", "[adapter][REQ-L CHECK(got->protocol == relay::Protocol::LIN); CHECK(got->payload == std::vector{0x01, 0x02}); CHECK(got->meta.at("lin.checksum_type") == "enhanced"); - bus->close(); + (void)bus->close(); +} + +TEST_CASE("adapt: subscribe assigns monotonically increasing seq", "[adapter][REQ-ADAPT-004]") { + auto bus = Bus::create(); + auto node = adapt(bus); + + auto [ch, err] = node->subscribe({}); + REQUIRE_FALSE(err); + + (void)bus->publish(0x10, {1}); + (void)bus->publish(0x20, {2}); + (void)bus->send_header(0x10); + (void)bus->send_header(0x20); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + auto m1 = ch->try_recv(); + auto m2 = ch->try_recv(); + REQUIRE(m1.has_value()); + REQUIRE(m2.has_value()); + CHECK(m2->seq > m1->seq); + (void)bus->close(); } -TEST_CASE("adapt: close closes the bus", "[adapter]") { +TEST_CASE("adapt: close closes the underlying bus", "[adapter][REQ-ADAPT-005]") { auto bus = Bus::create(); auto node = adapt(bus); auto err = node->close(); REQUIRE_FALSE(err); - // Second close should be idempotent - REQUIRE_FALSE(bus->close()); + // Bus should now be closed — subsequent publish returns error + auto perr = bus->publish(0x10, {1}); + CHECK(perr == relay::ErrClosed()); +} + +TEST_CASE("adapt: subscribe delivers frames after bus publish with nil removes registration", "[adapter][REQ-LIN-019]") { + auto bus = Bus::create(); + auto node = adapt(bus); + + // Register then remove + relay::Message reg; reg.id = "16"; reg.payload = {0xAA}; + REQUIRE_FALSE(node->send(reg)); + + relay::Message rem; rem.id = "16"; rem.payload = {}; + REQUIRE_FALSE(node->send(rem)); + + // Now send_header should return ErrNoResponse + auto [f, ferr] = bus->send_header(0x10); + CHECK(ferr); + (void)bus->close(); }