From ee033ad04a0a9ad2aed16a1729ff063e683943ec Mon Sep 17 00:00:00 2001 From: Matt <47545907+SoundMatt@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:36:28 -0700 Subject: [PATCH 1/2] fix(safety-docs): correct ASIL declaration + RxSequenceGuard/Zone-model staleness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes three confirmed findings from the cpp-RCP v3.0.0 deep audit. Finding A (HIGH) — .fusa.json project ASIL contradicted .fusa-hara.json: - .fusa.json's project.asil was ASIL-B; H-001 (and SG-001/SG-003/SG-007) in .fusa-hara.json are ASIL-C, with HARA.md's H-001 rationale explicitly stating no decomposition is claimed. Corrected to ASIL-C. - Discovered en route: the pinned cpp-FuSa v0.18.0 binary's config loader (src/config/config.cpp) only reads a top-level "asil"/ "standard" key, never the nested "project.asil"/"project.standard" this repo's .fusa.json schema used — so the HARA005 rule was silently falling back to its own hardcoded "ASIL-B" default regardless of what project.asil said (the previous ASIL-B value only ever "matched" by coincidence). Added top-level "asil"/"standard" fields (matching cpp-FuSa's own dogfooded .fusa.json) alongside the existing nested ones so the pinned tool actually reads the corrected value. Verified: `cpfusa check` before this fix reproduces "HARA005: Hazard ASIL ASIL-C exceeds project ASIL ASIL-B" even after editing only the nested field; after adding the top-level fields, `cpfusa check` reports [iso26262/ASIL-C] and HARA005 no longer fires (0 errors, 1 pre-existing unrelated warning: FUSA004, missing evidence file). - Propagated the same correction, with a one-line justification citing HARA.md/H-001, to the other project-wide "target ASIL" declarations found by grepping the repo: README.md, AUDIT_PACK.md (doc metadata + intro sentence only — its ASIL-D-derogation/decomposition table and math predate the correction and are explicitly flagged in-document as a follow-up needing dedicated safety-case re-derivation, not resolved here), TARA-ANALYSIS.md, and SAFETY_PLAN.md (also part of Finding C's fix). - Left narrowly-scoped ASIL-B claims alone (SG-002/004/005/006/008/010/ 011 stay ASIL-B, matching .fusa-hara.json). Flagged as ambiguous and deliberately NOT changed: README.md's per-header ASIL-B design-pattern labels (watchdog.hpp/redundancy.hpp table rows) and INCIDENT-RESPONSE.md's "ASIL-B requirement violation" severity example — none clearly assert the project's own target ASIL. - Not touched: safety-case.json/md, sas.json/md, sci.json, fmea.*, tooling/tara.* — these are regenerated by `cpfusa safety-case`/etc. on every tagged release (release.yml) and will pick up the corrected ASIL automatically once regenerated; hand-editing them would just be overwritten. Finding B (HIGH) — HARA.md/FORMAL_VERIFICATION.md said RxSequenceGuard was unwired; it has been wired since Phase 4 batch C: - Verified directly against include/rcp/mock.hpp: Server holds a seq_trackers_ member (std::array), and both dispatch_e2e() and dispatch_frame_e2e() call it via a shared seq_gate_admits() helper on every dispatched request/frame before CRC unwrap. Integration-tested in tests/test_mock.cpp, not just RxSequenceGuard's own standalone tests/test_e2e.cpp unit test. include/rcp/e2e.hpp's own "UPDATE (Phase 4/Phase 17 batch C...)" file header comment corroborates this as the authoritative account. - Updated HARA.md's SG-004 row, full H-004 section, and its Residual Risks table row from "not wired"/"effectively Open" to the current "Mitigated (opt-in)" state (matching c-RCP's own disposition for its equivalent mechanism), while being explicit that this covers mock::Server's own reference dispatch — a caller driving a transport Server with fully custom dispatch logic instead of wiring mock::Server::dispatch_frame()/dispatch_frame_e2e() as the FrameHandler is not automatically covered (per udp.hpp/l2.hpp's own header notes on the correct integration path). - Updated FORMAL_VERIFICATION.md's SP1/SP2 point 1 (the wiring-gap claim) to match. Left point 2 (the TLA+ spec modeling the pre-RFC-1982 algorithm) untouched — a separate, still-true, and still-open issue unrelated to this finding. - Did not touch H-004's S2/E3/C2/ASIL-B classification or any coverage/testing claim beyond what mock.hpp/test_mock.cpp actually show. Finding C (HIGH) — SAFETY_PLAN.md still documented the retired Zone/Command/Controller model: - Verified all five dead requirement ids (REQ-CTRL-025/026/027/004, REQ-ERR-011) are absent from .fusa-reqs.json, .fusa-reqs-pending.json, and include/. - "Zone mismatch detection" repointed to REQ-MOCK-010 ("dispatch to an unmapped byte_bus_id reports invalid_parameter"), the real current endpoint-addressing-validation mechanism in mock::Server::dispatch, matching HARA.md's SG-002 "Addressed By" entry. - "Deadline monitor" row's description reworded to drop "zone"/ "Config.Deadline" wording (REQ-DL-002 id was already correct). - "Payload copy-on-send", "Payload copy-on-publish", and "Context / deadline propagation" rows (also citing dead REQ-CTRL-026/027/004, which the audit finding's own text named but its concrete fix guidance didn't walk through): verified no current analog exists for any of the three in the stream/endpoint/register-map wire-protocol architecture (no in-process handler-invocation/pub-sub/context- cancellation model remains). Marked "No direct analog" per instructions, rather than inventing a false mapping. - Also fixed the Safety Goals table in the same document (SG-001/003/007 ASIL-B -> ASIL-C per .fusa-hara.json, part of Finding A; "Commands"/ "CmdWatchdog"/"Dead zone" terminology retired in favor of the current request/watchdog/RC-Server wording, part of Finding C) since it sits directly above the mechanisms table and shares the identical root causes (stale terminology + stale ASIL). Verification: `python3 -c "import json; json.load(open('.fusa.json'))"` passes. `cpfusa check`: HARA005 gone (0 errors, 1 pre-existing unrelated warning). `cpfusa lint`: 146 warnings both before and after (git stash diff) — unchanged, confirming no new findings from these doc/JSON edits. `cpfusa trace`: pre-existing dangling-test-tag/no-LLR-children warnings only, all in tests/*.cpp already unrelated to and untouched by this batch. Follow-ups noted, not resolved by this pass: - AUDIT_PACK.md §2's ASIL-D gap-analysis derogation table and its ASIL-B(D) = ASIL-A + ASIL-B decomposition math predate the ASIL-C correction and need dedicated safety-case re-derivation (flagged in-document). - AUDIT_PACK.md §3's "ASIL-C upgrade path"/"80% ASIL-C target" MC/DC wording now reads oddly against a project that already targets ASIL-C; left untouched since the underlying coverage-gap facts are unaffected and touching MC/DC threshold language felt too far outside a documentation-consistency fix to do safely here. - .fusa-hara.json itself has a latent, separate inconsistency (H-003 and H-005 are rated ASIL-B as hazards, but their own safety goals SG-003/SG-007 are rated ASIL-C) discovered while reading the file for this batch; out of scope for these three findings, noted for a future pass. - No new requirement ids were invented. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> --- .fusa.json | 4 +- AUDIT_PACK.md | 26 +++++++--- FORMAL_VERIFICATION.md | 37 ++++++++------ HARA.md | 112 +++++++++++++++++++++++------------------ README.md | 2 +- SAFETY_PLAN.md | 39 +++++++++----- TARA-ANALYSIS.md | 7 +-- 7 files changed, 139 insertions(+), 88 deletions(-) diff --git a/.fusa.json b/.fusa.json index ef3cb5b..ddcbe5b 100644 --- a/.fusa.json +++ b/.fusa.json @@ -1,11 +1,13 @@ { "version": "1", + "standard": "iso26262", + "asil": "ASIL-C", "project": { "name": "cpp-RCP", "version": "3.0.0", "module": "github.com/SoundMatt/cpp-RCP", "standard": "iso26262", - "asil": "ASIL-B" + "asil": "ASIL-C" }, "rules": { "exclude": [], diff --git a/AUDIT_PACK.md b/AUDIT_PACK.md index e7c0a27..2861b4a 100644 --- a/AUDIT_PACK.md +++ b/AUDIT_PACK.md @@ -2,7 +2,7 @@ **Document version**: 2.0.0 **Date**: 2026-07-28 -**Standards**: ISO 26262 (ASIL-B), IEC 61508 (SIL-2), ISO 21434, IEC 62443 SL-2 +**Standards**: ISO 26262 (ASIL-C — corrected from ASIL-B, see `HARA.md` H-001), IEC 61508 (SIL-2), ISO 21434, IEC 62443 SL-2 This revision (ROADMAP.md milestone 62, "Certification Refresh", v2.18.0) supersedes the pre-replacement audit pack authored at milestone 43. All @@ -28,9 +28,19 @@ across Phase 13-16 (v2.0.0-v2.17.0). ## 2. ASIL-D Gap Analysis (ISO 26262 §7) -cpp-RCP targets **ASIL-B** for the RC Client/RC Server communication -subsystem. The following table records deliberate derogations from -ASIL-D: +cpp-RCP targets **ASIL-C** for the RC Client/RC Server communication +subsystem (corrected from a previously-stated ASIL-B; see `HARA.md`'s +H-001 rationale — S3/E4/C2 maps to ASIL-C, not ASIL-B, and no +decomposition is claimed for it). The table and decomposition paragraph +below predate that correction and still frame this subsystem's +derogation from ASIL-D as an ASIL-B(D) = ASIL-A + ASIL-B decomposition — +per `HARA.md`'s own H-001 rationale, no such decomposition is actually +claimed (ASIL-C is met directly). **This section has not yet been +re-derived for an ASIL-C target and is flagged as a follow-up, not +resolved by this pass** — re-deriving which ASIL-D requirements still +warrant derogation, and on what rationale, at an ASIL-C (rather than +ASIL-B) target is a safety-case judgment call outside the scope of a +documentation-consistency fix. | ASIL-D Requirement | Derogation Rationale | ASIL-B Coverage | |--------------------|----------------------|-----------------| @@ -39,9 +49,11 @@ ASIL-D: | MISRA C++:2023 compliance | MISRA advisory rules selectively suppressed with justification | clang-tidy clean on safety-critical rules | | 100% MC/DC structural coverage | 80% branch coverage enforced in CI | 80% branch + path coverage reported by `cpfusa coverage` | -ASIL decomposition: the RC Client/RC Server link is decomposed as -ASIL-B(D) = ASIL-A + ASIL-B per ISO 26262-9 §5 (independent channel -decomposition) — see `HARA.md`'s per-hazard decomposition rationale. +ASIL decomposition (pre-dates the ASIL-C correction above — see this +section's own flagged-follow-up note): the RC Client/RC Server link was +previously described as decomposed as ASIL-B(D) = ASIL-A + ASIL-B per ISO +26262-9 §5 (independent channel decomposition). `HARA.md`'s current +per-hazard rationale for H-001 claims no such decomposition. --- diff --git a/FORMAL_VERIFICATION.md b/FORMAL_VERIFICATION.md index 15e6759..d6d11e0 100644 --- a/FORMAL_VERIFICATION.md +++ b/FORMAL_VERIFICATION.md @@ -59,20 +59,29 @@ comparison against the high-water mark. **STATUS CORRECTED 2026-08-21 (cpp-RCP issue #129 / RELAY Phase 17 Phase 2 pass) — two distinct corrections, both documentation-only:** -1. **Wiring gap, not algorithm gap.** This section previously implied - `RxSequenceGuard` — and by extension the SP1/SP2 properties verified - here — functions as a real, active mitigation for H-004. It does not: - `RxSequenceGuard` is **never instantiated anywhere outside its own unit - test** — not in `mock::Server`'s dispatch, and not in any transport - `Server`. SP1/SP2 are correctly verified properties of the - `RxSequenceGuard` *primitive itself*, but a formally-verified primitive - that nothing in this codebase calls provides no actual protection - against H-004 today. See `HARA.md`'s own corrected H-004 section for - the full account (mirroring c-RCP's own resolution of the identical - ambiguity, issues #601/#606). Wiring `RxSequenceGuard` into - `rcp/mock.hpp`'s dispatch is explicitly out of scope for this pass - (Phase 4/server-dispatch work) — this correction only makes this - section stop overstating what already-verified fact it establishes. +1. **Wiring gap, now closed for this codebase's own reference dispatch.** + This section previously stated that `RxSequenceGuard` was **never + instantiated anywhere outside its own unit test** — not in + `mock::Server`'s dispatch, and not in any transport `Server`. That was + accurate as of the 2026-08-21 pass that added this note, but is + superseded by Phase 4 batch C, "mock.hpp batch C — wire + RxSequenceGuard, StreamFaultTracker, RxWatchdog" (CHANGELOG.md, cpp-RCP + issue #129, PR #148): `rcp/mock.hpp`'s `Server` now holds a + `std::array seq_trackers_` member, and both + `Server::dispatch_e2e()` and `Server::dispatch_frame_e2e()` call it + (via a shared `seq_gate_admits()` helper) on every dispatched + request/frame before CRC unwrap — integration-tested in + `tests/test_mock.cpp` ("dispatch_e2e's sequence gate (REQ-E2E-028/029) + rejects a non-increasing sequence_num"), not merely the standalone + primitive's own `tests/test_e2e.cpp` unit test. See `HARA.md`'s own + corrected H-004 section and `include/rcp/e2e.hpp`'s file header + ("UPDATE (Phase 4/Phase 17 batch C...)") for the full account + (mirroring c-RCP's own earlier resolution of the identical ambiguity, + issues #601/#606). SP1/SP2 as verified here are therefore properties of + a primitive that **is** now wired into this codebase's own reference + dispatch — with the same "an integrator bypassing `mock::Server`'s own + dispatch is on their own" caveat every primitive in this codebase + carries (see `HARA.md`'s H-004 Residual Risks entry). 2. **This spec models the pre-Phase-2-pass algorithm.** The same Phase 2 pass that produced this correction also content-corrected `e2e::RxSequenceGuard`'s actual comparison rule against c-RCP's diff --git a/HARA.md b/HARA.md index c376eb7..f0c660c 100644 --- a/HARA.md +++ b/HARA.md @@ -53,7 +53,7 @@ Phase 13-16 work are appended as H-011/SG-011. | SG-001 | Requests to safety-critical endpoints shall be delivered within the configured watchdog period or a fault shall be signalled. | ASIL-C | `watchdog::Manager`/`StreamWatchdog`, `deadline::Monitor` | | SG-002 | Requests shall only be dispatched to the endpoint they are addressed to (stream_id + byte_bus_id); misaddressed requests shall be rejected. | ASIL-B | `acf::AcfMessageInfo` byte_bus_id decode, RC Server dispatch (e.g. `mock::Server::dispatch`) | | SG-003 | A per-stream watchdog kick shall be recorded for every accepted inbound request, regardless of request kind or safety tag. | ASIL-C | `watchdog::StreamWatchdog::kick_from_request`, `e2e::RxWatchdog` | -| SG-004 | A request stream configured with rx_enforce_seq shall reject any sequence number that is not strictly greater than the last accepted one. | ASIL-B | **Implemented, not wired** — see H-004's corrected note below. `e2e::RxSequenceGuard::evaluate`/`check` correctly implement the check as a standalone primitive, but are not called from `mock::Server`'s dispatch or any transport `Server`; this goal is not actually met until an integrator wires `RxSequenceGuard` into their own dispatch path. | +| SG-004 | A request stream configured with rx_enforce_seq shall reject any sequence number that is not strictly greater than the last accepted one. | ASIL-B | `e2e::RxSequenceGuard::evaluate`/`check`, wired into `mock::Server::dispatch_e2e()`/`dispatch_frame_e2e()` via `seq_gate_admits()` (Phase 4/Phase 17 batch C, cpp-RCP issue #129, PR #148) — see H-004's corrected note below. | | SG-005 | Cancellation and triggered requests shall never be delayed by a standard or compound request queued earlier on the same stream. | ASIL-B | `request::SequencerTable`/`RequestLedger` execution-priority ordering | | SG-006 | Transport authentication (mTLS on the UDP/IP variant, or link-layer authentication on native Ethernet) and per-endpoint access policy shall be enforced on every external RC Server connection. | ASIL-B | `tls::SecureClient`/`SecureServer`, `authz::AccessPolicy`, `discovery::DiscoveryClaim` | | SG-007 | An RC Server that stops responding shall be detected as unreachable within the configured liveness deadline. | ASIL-C | `deadline::Monitor`/`LivenessTracker` | @@ -119,53 +119,67 @@ endpoint through its own safe-state sequence. ### H-004 (ASIL-B): Request replay / out-of-order delivery -**STATUS CORRECTED 2026-08-21 (cpp-RCP issue #129 / RELAY Phase 17 Phase 2 -pass):** this section previously described `e2e::RxSequenceGuard` in -present-tense mitigating language, as if it were an active, wired -mitigation for H-004. It is not. A Phase 2 audit of this codebase found -`RxSequenceGuard` is implemented and unit-tested (`tests/test_e2e.cpp`) -but is **never instantiated anywhere outside its own unit test** — not in -`mock::Server`'s dispatch, and not in any transport `Server`. No caller -anywhere in this tree evaluates a real AVTPDU's `sequence_num` through it, -so linking this library today gives an integrator no actual protection -against H-004. - -c-RCP resolved the identical ambiguity for itself (issues #601/#606): it -found its own equivalent (`rcp_e2e_seq_evaluate()`/`rcp_e2e_seq_tracker_t`) -genuinely **is** wired into its own reference dispatch (`mock.c`'s -`frame_seq_gate_admits()`, called once per AVTPDU frame), and documented -the mitigation as "Mitigated (opt-in)" with an explicit residual-risk list -(opt-in config bits, no cross-restart persistence, the RFC 1982 -`[1,127]` forward-window bound, per-frame not per-message granularity) — -see c-RCP's `HARA.md` H-004 section and `include/rcp/e2e.h`'s file header. -cpp-RCP is not yet at that point. - -`e2e::RxSequenceGuard::evaluate()`/`check()` are a correct, content- -verified primitive as of this same pass (re-derived against c-RCP's -`rcp_e2e_seq_evaluate()`: an RFC 1982 forward-window comparison over the -8-bit AVTPDU `sequence_num` space — accept iff the forward distance from -the last accepted value lies in `[1, 127]` when `rx_enforce_seq` is set, -with a separate `rx_seq_safestate_enable`-gated discontinuity signal for -an increase-by-more-than-one gap; see `e2e.hpp`'s own doc comment for the -full rationale and what this pass corrected from a previous, non-wrapping -`uint32_t` comparison that never consulted `rx_seq_safestate_enable` at -all). But **implemented is not the same as wired**: this header provides -primitives, not a running dispatcher, same as every other header in this -codebase — deciding when to call `evaluate()` against real inbound traffic -is left to the embedding application, and no embedding application in -this tree does so yet. Wiring `RxSequenceGuard` into `rcp/mock.hpp`'s -dispatch (or any transport `Server`) is explicitly **out of scope** for -this pass — it is Phase 4 (server/dispatch) work — so this correction is -documentation-only: it makes this file accurately describe the wiring gap -that already existed, rather than closing it. - -H-004 is therefore **effectively Open** for this hazard's practical -purposes — an integrator gets no protection against replay/out-of-order -delivery by linking this library alone — notwithstanding the primitive's -existence and correctness. This does not change H-004's S2/E3/C2/ASIL-B -classification (the hazard's worst case, "no sequence check evaluated at -all," is exactly what every current caller already experiences); it -corrects SG-004's own "Addressed By" claim below to say so. +**STATUS CORRECTED 2026-08-22 (cpp-RCP issue #129 / RELAY Phase 17 Phase 4 +batch C pass):** the previous correction below (2026-08-21) stated that +`e2e::RxSequenceGuard` was implemented and content-correct but **never +instantiated anywhere outside its own unit test** — accurate at the time, +but superseded by Phase 4 batch C, "mock.hpp batch C — wire +RxSequenceGuard, StreamFaultTracker, RxWatchdog" (CHANGELOG.md, PR #148). +`rcp/mock.hpp`'s `Server` now holds a +`std::array +seq_trackers_` member, and both `Server::dispatch_e2e()` (single-member) +and `Server::dispatch_frame_e2e()` (multi-member, frame-level) call it — +via a shared `seq_gate_admits()` helper — on every dispatched +request/frame, before CRC unwrap, evaluating the real AVTPDU +`sequence_num` against `RxSequenceGuard::evaluate()`. This is real, +wired, integration-tested behavior: `tests/test_mock.cpp` includes +"dispatch_e2e's sequence gate (REQ-E2E-028/029) rejects a non-increasing +sequence_num" as a dispatch-level test, distinct from and in addition to +`RxSequenceGuard`'s own standalone `tests/test_e2e.cpp` unit test. See +`include/rcp/e2e.hpp`'s own file header ("UPDATE (Phase 4/Phase 17 batch +C...)") for the authoritative account of this wiring. + +c-RCP resolved the identical ambiguity for itself earlier (issues +#601/#606): its own equivalent (`rcp_e2e_seq_evaluate()`/ +`rcp_e2e_seq_tracker_t`) is wired into its own reference dispatch +(`mock.c`'s `frame_seq_gate_admits()`, called once per AVTPDU frame), and +documented as "Mitigated (opt-in)" with an explicit residual-risk list +(opt-in config bits, no cross-restart persistence, the RFC 1982 `[1,127]` +forward-window bound, per-frame not per-message granularity) — see +c-RCP's `HARA.md` H-004 section and `include/rcp/e2e.h`'s file header. +cpp-RCP's own `mock::Server` reference dispatch is now at that same +point, for both of its own dispatch paths (single-member and +frame-level). + +`e2e::RxSequenceGuard::evaluate()`/`check()` remain the correct, content- +verified primitive established by the prior pass (an RFC 1982 +forward-window comparison over the 8-bit AVTPDU `sequence_num` space — +accept iff the forward distance from the last accepted value lies in +`[1, 127]` when `rx_enforce_seq` is set, with a separate +`rx_seq_safestate_enable`-gated discontinuity signal for an +increase-by-more-than-one gap; see `e2e.hpp`'s own doc comment for the +full rationale). What has changed is that this primitive is no longer +merely implemented — it is now called against real inbound traffic by +this codebase's own reference dispatch. + +One caveat remains, honestly stated: being wired into THIS codebase's own +`mock::Server` reference implementation does not by itself protect an +integrator who drives a transport `Server` (`rcp/udp.hpp`, `rcp/l2.hpp`) +with their own dispatch logic instead of `mock::Server::dispatch_frame()`/ +`dispatch_frame_e2e()` as the `FrameHandler` — the same "primitives, not +a mandate" caveat every header in this codebase carries. `udp.hpp`'s and +`l2.hpp`'s own header comments recommend wiring `mock::Server`'s +frame-level dispatch as the `FrameHandler` precisely so this and other +frame-level behaviors are not silently bypassed. + +H-004 is therefore **Mitigated (opt-in)**, matching c-RCP's own +disposition, for any integrator using this codebase's `mock::Server` +reference dispatch with `rx_enforce_seq` set on the relevant stream. This +does not change H-004's S2/E3/C2/ASIL-B classification — the hazard's +worst case (no sequence check evaluated at all) is now the state of a +stream with `rx_enforce_seq` clear, a deliberate per-endpoint +configuration choice documented as an accepted residual risk below, not +this library's reference-dispatch default behavior. ### H-006 (ASIL-B): Execution-priority inversion @@ -211,7 +225,7 @@ or best-effort behavior is involved. | Risk | Likelihood | Mitigation | Status | |------|-----------|------------|--------| -| `e2e::RxSequenceGuard` is implemented and content-correct but not wired into `mock::Server`'s dispatch or any transport `Server` (H-004) | Medium | None within this library today — an integrator must call `RxSequenceGuard::evaluate()`/`check()` from their own dispatch loop, per AVTPDU frame, for H-004's SG-004 goal to actually be met; wiring it into `mock.hpp` is tracked as Phase 4 (server/dispatch) work, cpp-RCP issue #129 | **Open — documented gap, not silently overstated (see H-004's corrected note)** | +| An integrator driving a transport `Server` (`udp.hpp`/`l2.hpp`) with their own dispatch logic, instead of wiring `mock::Server::dispatch_frame()`/`dispatch_frame_e2e()` as the `FrameHandler`, bypasses the now-wired `RxSequenceGuard` gate (H-004) | Low | `udp.hpp`/`l2.hpp`'s own header comments document wiring `mock::Server`'s frame-level dispatch as the `FrameHandler` as the correct integration path; `RxSequenceGuard::evaluate()`/`check()` remain directly callable for a fully custom dispatch loop | **Mitigated (opt-in) for `mock::Server`'s own reference dispatch — wired via `seq_gate_admits()`/`dispatch_e2e()`/`dispatch_frame_e2e()`, Phase 4 batch C, cpp-RCP issue #129 (see H-004's corrected note)** | | A stream with `rx_enforce_seq` clear accepts replayed/out-of-order requests by design | Low | Per-endpoint configuration choice; safety-relevant streams are expected to set `rx_enforce_seq` | Accepted | | `tls::SecureClient`/`SecureServer` require an application-supplied OpenSSL/wolfSSL backend; the interface itself performs no cryptography | Low | Native Ethernet deployments should prefer MACsec (802.1AE) at layer 2 instead, per `tls.hpp`'s own header note | Accepted | | `mdns.hpp`'s discovery-adjacent name service is rescoped to the UDP/IP transport variant only | Low | TC18's own wire-level discovery (`discovery.hpp`, v2.2.0) covers native Ethernet deployments without a name-service layer | Accepted | diff --git a/README.md b/README.md index 8b3ad83..5af5f96 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ Errors are returned as `std::error_code` values in the `rcp` category. cpp-RCP targets deployment in automotive safety-critical environments. -- Safety standard: ISO 26262 ASIL-B / IEC 61508 SIL-2 +- Safety standard: ISO 26262 ASIL-C / IEC 61508 SIL-2 (corrected from ASIL-B; see `HARA.md`'s H-001 rationale) - Security standard: IEC 62443 SL-2 - cpp-FuSa static analysis runs in CI on every PR - All requirements are traced to tests in `.fusa-reqs.json` diff --git a/SAFETY_PLAN.md b/SAFETY_PLAN.md index b61d5a0..321d33f 100644 --- a/SAFETY_PLAN.md +++ b/SAFETY_PLAN.md @@ -2,38 +2,51 @@ ## Scope -cpp-RCP is the C++ implementation of the Remote Control Protocol for automotive zonal architecture targeting ISO 26262 ASIL-B / IEC 61508 SIL-2. +cpp-RCP is the C++ implementation of the Remote Control Protocol for automotive zonal architecture targeting ISO 26262 ASIL-C / IEC 61508 SIL-2 (corrected from a previously-stated ASIL-B; see `HARA.md`'s H-001 rationale — S3/E4/C2 maps to ASIL-C, not ASIL-B, and no decomposition is claimed). ## Safety standard | Standard | Target level | |---|---| -| ISO 26262 | ASIL-B | +| ISO 26262 | ASIL-C | | IEC 61508 | SIL-2 | | IEC 62443 | SL-2 | ## Safety goals +Descriptions and ASIL levels below are kept in sync with `.fusa-hara.json` +(the authoritative machine-readable source) and `HARA.md`. The +pre-replacement Zone/Command/Controller terminology this table previously +used ("Commands", "CmdWatchdog", "Dead zone") has been retired along with +that model — see `HARA.md`'s own note on the milestone-62 supersession. + | ID | Description | ASIL | |---|---|---| -| SG-001 | Commands delivered within watchdog period or fault signalled | ASIL-B | -| SG-002 | Misrouted commands rejected | ASIL-B | -| SG-003 | CmdWatchdog always deliverable | ASIL-B | -| SG-004 | Replayed commands detected and rejected | ASIL-B | -| SG-007 | Dead zone detected within configured deadline | ASIL-B | +| SG-001 | Requests to safety-critical endpoints shall be delivered within the configured watchdog period or a fault shall be signalled. | ASIL-C | +| SG-002 | Requests shall only be dispatched to the endpoint they are addressed to (stream_id + byte_bus_id); misaddressed requests shall be rejected. | ASIL-B | +| SG-003 | A per-stream watchdog kick shall be recorded for every accepted inbound request, regardless of request kind or safety tag. | ASIL-C | +| SG-004 | A request stream configured with rx_enforce_seq shall reject any sequence number that is not strictly greater than the last accepted one. | ASIL-B | +| SG-007 | An RC Server that stops responding shall be detected as unreachable within the configured liveness deadline. | ASIL-C | ## Safety mechanisms +The pre-replacement Zone/Command/Controller model (`REQ-CTRL-*`, +`REQ-ERR-011`) this table previously cited was retired at the milestone-62 +Certification Refresh (see `CYBERSECURITY.md`/`TARA-ANALYSIS.md`) and no +longer exists in `.fusa-reqs.json` or `include/`. Rows below are repointed +at the real, currently-existing requirements and mechanisms that replaced +it, verified directly against `.fusa-reqs.json` and the relevant headers. + | Mechanism | Requirement | Description | |---|---|---| -| Zone mismatch detection | REQ-CTRL-025, REQ-ERR-011 | Controller rejects commands addressed to a different zone | -| Payload copy-on-send | REQ-CTRL-026 | Payload is deep-copied before handler invocation | -| Payload copy-on-publish | REQ-CTRL-027 | Published payload is deep-copied before delivery to subscribers | -| Context / deadline propagation | REQ-CTRL-004 | Expired context terminates Send without invoking the handler | -| Watchdog kick | REQ-WDG-002 | Any accepted inbound request resets ("kicks") its stream's watchdog | +| Endpoint addressing validation | REQ-MOCK-010 | `mock::Server::dispatch` rejects a request whose byte_bus_id addresses no known endpoint, returning `invalid_parameter` — the current stream/endpoint addressing model; replaces the retired zone/Controller lookup, which has no direct analog in a byte_bus_id-addressed architecture | +| Watchdog kick | REQ-WDG-002 | `watchdog::StreamWatchdog::kick_from_request` resets that stream's watchdog on every accepted inbound request, regardless of request kind or safety tag | | Sequence guard | REQ-E2E-007 | RxSequenceGuard rejects a non-strictly-increasing sequence number when rx_enforce_seq is set | -| Deadline monitor | REQ-DL-002 | Monitor detects a zone going silent within Config.Deadline | +| Deadline monitor | REQ-DL-002 | `deadline::Monitor` detects an RC Server going silent within the configured liveness deadline (heartbeat or lifecycle-change signal) | | Execution-priority ordering | REQ-SEQ-002 | Cancellation and triggered requests dispatch ahead of standard/compound requests queued earlier on the same stream | +| Payload copy-on-send *(retired)* | — | No direct analog. This was the pre-replacement in-process `Controller`'s defensive deep-copy before invoking a registered handler; the current stream/endpoint/register-map wire-protocol architecture has no equivalent client-side handler-invocation step to protect | +| Payload copy-on-publish *(retired)* | — | No direct analog, for the same reason — the pre-replacement pub/sub `Controller::Publish`/subscriber model is gone. `admin.hpp`'s SSE/event broadcast (REQ-ADMIN-002) is a different, non-safety-relevant surface (in-process observability, not request payload delivery) and is not offered as a substitute | +| Context / deadline propagation *(retired)* | — | No direct analog. The pre-replacement per-call context-cancellation ("expired context terminates Send without invoking the handler") has no current equivalent; the closest current mechanisms are client-side request timeout (REQ-UDP-010) and the watchdog/deadline-monitor rows above, neither of which reproduces "suppress handler invocation on an already-expired call" | ## Verification approach diff --git a/TARA-ANALYSIS.md b/TARA-ANALYSIS.md index f000b1a..00ac463 100644 --- a/TARA-ANALYSIS.md +++ b/TARA-ANALYSIS.md @@ -2,7 +2,7 @@ ## ISO 21434 / IEC 62443 SL-2 **Document version**: 2.0.0 -**ASIL level**: ASIL-B (ISO 26262) / SIL-2 (IEC 61508) +**ASIL level**: ASIL-C (ISO 26262, corrected from ASIL-B — see `HARA.md`'s H-001 rationale) / SIL-2 (IEC 61508) **Date**: 2026-07-28 --- @@ -74,7 +74,7 @@ Ratings: Feasibility (1-5) × Impact (1-5) = Risk Score Residual risks above threshold (score ≥ 10): T-01, T-02, T-04, T-05, T-11. All are covered by implemented mitigations. Residual risk is accepted at -ASIL-B. +ASIL-C (corrected from ASIL-B; see `HARA.md`'s H-001 rationale). --- @@ -100,7 +100,8 @@ ASIL-B. All identified threats have corresponding mitigations aligned with IEC 62443 SL-2 security level requirements. The residual risk profile is -acceptable for ASIL-B / SIL-2 operation. A re-assessment is required when: +acceptable for ASIL-C / SIL-2 operation (corrected from ASIL-B; see +`HARA.md`'s H-001 rationale). A re-assessment is required when: - The threat landscape changes (new attack vectors identified) - A new protocol bridge (`mqttbr.hpp`/`ddsbr.hpp`/`someipbr.hpp`/ `restbridge.hpp`/`grpcbridge.hpp`/`udsbr.hpp`/`doipbr.hpp`) moves past its From 2962b0d614805010c36ca5e5e2e4ea432928fe32 Mon Sep 17 00:00:00 2001 From: Matt <47545907+SoundMatt@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:03:51 -0700 Subject: [PATCH 2/2] fix(reqs): backfill REQ-SHMEM-009/010, REQ-RMAP-081, REQ-TIMED-012 catalog entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior commit on this branch correctly raised .fusa.json's declared ASIL from B to C. That bump surfaced four `// fusa:test REQ-ID` tags in tests/test_shmem.cpp, tests/test_regmap.cpp, and tests/test_server.cpp whose ids had no corresponding entry in .fusa-reqs.json: - REQ-SHMEM-009 (tests/test_shmem.cpp:9, TEST_CASE ~line 482): a Channel's underlying state is freed exactly once regardless of std::shared_ptr release order. - REQ-SHMEM-010 (tests/test_shmem.cpp:10, TEST_CASE ~line 181): Channel::request() returns ErrClosed after close(), playing both the "send" and "recv" role of c-RCP's split shmem_side_send()/ shmem_side_recv() API. - REQ-RMAP-081 (tests/test_regmap.cpp:37, TEST_CASE ~line 828): already staged in .fusa-reqs-pending.json as a spec-defect gap (TC18's own EP_RESP_ON_ERROR prose reference is never given a wire field by Table 31), but never migrated into the confirmed catalog despite carrying a real, tested defensive guarantee (ep_generic_cfg::render() invents no bit for it). Migrated here with status "partial", matching the REQ-RMAP-067 precedent for a confirmed-gap-but-tested entry, and removed from the pending file per its own migration rule (real impl+test tags now exist). - REQ-TIMED-012 (tests/test_server.cpp:45, TEST_CASE ~line 1143): also staged in .fusa-reqs-pending.json, marked "confirmed absent" -- but that analysis is stale. rcp::server::Endpoint::admit()/ admit_with_ack()/admit_under_tscf_gate()/select_due() (server.hpp) already implement and test full TSCF presentation-time admission end-to-end (the pending entry's own grep for a dispatch-tscf/admit-gate equivalent evidently missed this code's actual naming). Migrated to the confirmed catalog as implemented and removed from pending. Verification (all via the pinned /tmp/cpfusa/build/cpfusa, matching the exact CI invocations in .github/workflows/ci.yml): - `cpfusa trace` (bare, the blocking "Traceability check" step): the four dangling //fusa:test references are gone -- confirmed zero remaining, and all four new ids resolve impl:[✓] test:[✓]. `cpfusa check`, `cpfusa lint`, and `cpfusa trace --req-coverage 100` are unchanged before/after (byte-identical output modulo timestamps for check/lint; trace --req-coverage 100 mirrors trace's own diff exactly). IMPORTANT CAVEAT discovered during verification: `cpfusa trace`'s exit code is still 1 after this fix, and this fix cannot make it 0. Reading cpfusa's own source (src/trace/trace.cpp, src/cmd/cli.cpp) shows the bare `trace` subcommand's only exit(1) path is `result.hlr_gate_failed`, which is driven entirely by a separate HLR/LLR two-tier decomposition check ("WARN: HLR ... has no LLR children"), promoted from warn-only to a hard gate whenever the project's declared ASIL is C or D. Dangling //fusa:test references are WARNING-level only and never gate the exit code, at any ASIL -- confirmed by grepping cli.cpp's trace callback and by reproducing before/after: reverting only .fusa.json's asil to B (not committed, diagnostic only) yields exit 0 with this fix in place, while ASIL-C + this fix still yields exit 1 with zero dangling refs. This project's .fusa-reqs.json has never populated parent_id, so every requirement is its own childless "HLR" (HLR-covered: 0/1137 both before and after this commit) -- already flagged as a known, unaddressed gap in ARCHITECTURE.md's schema-compat table ("the tool also expects an HLR/LLR two-tier hierarchy this repo's schema doesn't populate ... needs its own investigation"). Closing that gate requires a much larger, separate effort (retrofitting parent_id across the whole requirements catalog, or an explicit tooling decision) and is out of scope for this additive four-entry backfill. PR #178's CI will still fail the "Traceability check" step after this commit for that unrelated, pre-existing reason. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> --- .fusa-reqs-pending.json | 22 ---------------------- .fusa-reqs.json | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/.fusa-reqs-pending.json b/.fusa-reqs-pending.json index e80dda6..231073b 100644 --- a/.fusa-reqs-pending.json +++ b/.fusa-reqs-pending.json @@ -169,16 +169,6 @@ "tc18": "§11.2.2.6 (\"If the first request in an AVTPDU is a chain request, then there is no predecessor to chain to, thus the entire chain will be ignored. An error response with the error code 'CHAIN_ERROR' to each request will be generated\"), TC18.txt L1796-1798", "gap": "Confirmed absent (2026-08-22): RequestRecord::chained_predecessor is std::optional (request.hpp:1315), and RequestLedger::submit() (request.hpp:1354-1362) never inspects it — a Chained-opcode record with chained_predecessor == std::nullopt is accepted and tracked exactly like any other Pending record, with no special rejection path. RequestErrc (request.hpp:270-285) has no CHAIN_ERROR-equivalent enumerator at all — the closest neighbors (request_not_found, request_canceled, invalid_lifecycle_transition) are semantically unrelated. This is distinct from REQ-CHAIN-009's 'already-aborted chain' case (which cascade_cancel()/propagate_chain_completion() genuinely do handle, unconditionally cancelling every downstream chained_successor once one member aborts — see request.hpp's own file-header delta #4/judgment-call note at lines 207-217): REQ-CHAIN-008 is about a chained member that never had a predecessor relationship recorded in the first place, which nothing in RequestLedger detects or rejects today. A caller assembling chained_predecessor/chained_successors links at submission time (this codebase's own design, not c-RCP's positional has_predecessor bool) could in principle already choose not to submit a first-frame Chained request with a false chained_predecessor link, but RequestLedger itself provides no enforcement or detection of the case, and no test in test_request.cpp exercises it." }, - { - "id": "REQ-TIMED-012", - "title": "A request carried under a TSCF header is postponed until the header's presentation time", - "text": "A caller-facing admission/dispatch entry point shall, given an AVTPDU decoded under a TSCF header (carrying its own avtp_timestamp), defer executing any request in it until that timestamp is reached, using the already-implemented timed_admit()/timed_due() primitives (REQ-TIMED-007/008/011) as the gate.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B", - "tc18": "§11.2, TC18.txt L1159; §11.2.1, TC18.txt L1204", - "gap": "Confirmed absent (2026-08-22): rcp/request.hpp implements every TSCF-admission PRIMITIVE this requirement depends on (timed_admit()/timed_due()/timed_too_far(), request.hpp:1094-1125) but, by this file's own explicit design note (request.hpp:246-250, 'this header... does not implement a running scheduler thread; wiring select_next_due()'s output into an actual dispatch loop is left to the embedding application'), provides no dispatch/admission LOOP that actually calls them against a real TSCF-decoded avtp_timestamp. Grepped the whole tree (include/, src/) for a dispatch-tscf/admit-gate equivalent to c-RCP's rcp_mock_server_dispatch_tscf()/rcp_server_endpoint_admit(): none exists — rcp/l2.hpp/udp.hpp encode/decode TSCF *framing* (use_tscf bit, avtp_timestamp field) but never route a decoded timestamp into timed_admit()/timed_due(). This is a genuine, confirmed absence of end-to-end behavior (not merely an unwired primitive): nothing in cpp-RCP today defers ANY request's execution until a TSCF header's presentation time arrives. Per this file's own design note this may be intentionally out of request.hpp's charter (a library of primitives, not a server), but the requirement's own text (postponement actually happening) has no home anywhere else in the tree either." - }, { "id": "REQ-TIMED-013", "title": "Standard requests, and timed requests under a TSCF header, are sent as ACF_ABB", @@ -1302,18 +1292,6 @@ "tc18_master_id": "", "gap": "NOT a behavioral gap -- an id-collision traceability gap (2026-08-22, cpp-RCP Phase 6 batch 11 (RMAP/SRV)). The behavior itself is genuinely implemented and tested. The blocker is that this batch's REQ-REGMAP-* -> REQ-RMAP-* rename (ROADMAP.md Phase 17) claimed this id for cpp-RCP's own pre-existing, unrelated content first: this id is already taken in .fusa-reqs.json by that renamed entry. Already taken by: this catalog's own REQ-RMAP-015 (\"EndpointGenericConfig's per-endpoint E2E CRC safe-mode toggles\"). Genuinely implemented+tested: regmap::named_signal_string() -- tests/test_regmap.cpp's \"named_signal_string names are unique across the whole index\" TEST_CASE, dual-tagged [REQ-RMAP-044][REQ-RMAP-015] in this batch." }, - { - "id": "REQ-RMAP-081", - "title": "EP_RESP_ON_ERROR (Table 31 prose, IO-pin gauging error response) is a dangling spec reference", - "text": "NOT IMPLEMENTED, matching c-RCP's own disposition verbatim: TC18's own prose names a configuration parameter (EP_RESP_ON_ERROR) that Table 31 itself never actually defines a bit or field for -- a genuine, confirmed TC18 spec defect, not an addressable local gap. ep_generic_cfg::render()'s own reserved-octet spans (bits [3:1] and [7:6] of octet 1, mask 0xCE) deliberately stay zero for every input rather than inventing a bit position for this dangling reference.", - "standard": "iso26262", - "level": "QM", - "asil": "QM", - "scope": "tc18-gap", - "status": "not-implemented", - "tc18": "§13.2 Table 31 (EP_RESP_ON_ERROR prose reference, no defined bit/field)", - "gap": "Genuine spec-defect gap, matching c-RCP's own disposition verbatim (2026-08-22, cpp-RCP Phase 6 batch 11 (RMAP/SRV)): TC18 names EP_RESP_ON_ERROR in prose without ever defining where it lives on the wire. cpp-RCP additionally has a positive, already-tested defensive guarantee beyond c-RCP's own scope for this id -- ep_generic_cfg::render() never invents a bit for it (tests/test_regmap.cpp's \"ep_generic_cfg::render never sets a bit for the dangling EP_RESP_ON_ERROR reference\" TEST_CASE, tagged [REQ-RMAP-081]) -- but the underlying requirement (a real EP_RESP_ON_ERROR mechanism) remains genuinely unimplementable per the same spec silence c-RCP already confirmed, so this entry is filed as a gap rather than force-fit as implemented." - }, { "id": "REQ-RMAP-068", "title": "Register-write outcome: a read-only register succeeds silently, a write-prohibited register errors", diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 02348fe..f18a883 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -1210,6 +1210,22 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-SHMEM-009", + "title": "A Channel's state is freed exactly once regardless of shared_ptr release order", + "text": "A shmem::Channel's underlying state shall be freed exactly once, and only once every std::shared_ptr holder has released it, regardless of the order in which holders release -- constructing, copying, and releasing shared_ptr handles in either order shall neither crash nor leak, matching c-RCP's own refcounted rcp_avtp_transport_t contract (shmem.c:151-181).", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-SHMEM-010", + "title": "request() plays both the c-RCP send-side and recv-side ErrClosed role", + "text": "Channel::request() shall return ErrClosed once the Channel has been closed, regardless of which of c-RCP's own two separate shmem_side_send()/shmem_side_recv() roles a given call is standing in for -- a single request() call plays both the \"send\" and \"recv\" side of that split API, and each of c-RCP's own analogs independently reports RCP_ERR_CLOSED once its own side is closed (shmem.c:64-67/100-113,109-113).", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, { "id": "REQ-TSN-001", "title": "apply_priority sets SO_PRIORITY per RequestCategory", @@ -4988,6 +5004,17 @@ "status": "implemented", "tc18": "§11.2.2.7 (\"In any case the presentation_time is the earliest point in time, when the request shall be executed.\"), TC18.txt L1952" }, + { + "id": "REQ-TIMED-012", + "title": "A request carried under a TSCF header is postponed until its own presentation time", + "text": "rcp::server::Endpoint::admit()/admit_with_ack() shall, whenever the caller reports tv true (an enclosing TSCF header's own avtp_timestamp), postpone execution of the admitted request -- of any kind, Standard, Cancellation, or a conditional kind with its own start condition -- until avtp::extend_timestamp(avtp_timestamp, gptp_reference_now)'s reconstructed 48-bit instant is reached; select_due()/is_due() shall AND this envelope-level presentation gate with (not substitute for) each kind's own existing execution condition, and shall fail closed -- never report the request due -- whenever gptp_locked is false, regardless of how far in the past the gate's own instant already is.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2, TC18.txt L1159; §11.2.1, TC18.txt L1204" + }, { "id": "REQ-CANCEL-002", "title": "encode_clear_all() encodes a zero-payload clear-all request", @@ -11484,6 +11511,17 @@ "tc18": "§13.2 Table 28/31, TC18.txt L3570-3574", "tc18_master_id": "TC18-13.2-001" }, + { + "id": "REQ-RMAP-081", + "title": "EP_RESP_ON_ERROR (Table 31 prose, IO-pin gauging error response) is a dangling spec reference", + "text": "PARTIAL, matching c-RCP's own disposition: TC18's own prose names a configuration parameter (EP_RESP_ON_ERROR) that Table 31 itself never actually defines a bit or field for -- a genuine, confirmed TC18 spec defect, not an addressable local gap. What IS implemented and tested: ep_generic_cfg::render() deliberately invents no bit for it -- both reserved spans of octet 1 (bits [3:1] and [7:6], mask 0xCE) stay zero for every input, including inputs deliberately chosen to be non-zero/extreme everywhere else. What remains open: a real EP_RESP_ON_ERROR mechanism itself, which TC18's own spec silence makes genuinely unimplementable, not merely unwired.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18-gap", + "status": "partial", + "tc18": "§13.2 Table 31 (EP_RESP_ON_ERROR prose reference, no defined bit/field)" + }, { "id": "REQ-RMAP-082", "title": "named_signal_string() returns \"unknown\" for an out-of-range value",