|
Safe API Framework 0.1.0 build 384e6ae
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
|
Date: 2026-08-02 Status: Draft, consolidated from REQ-* traceability tags already present in the header comments under include/rte/. This document is the canonical source those tags cite; if a requirement's wording ever needs to change, change it here first, then update the corresponding code comment to match. Design rationale for each requirement lives in the referenced ADR under docs/architecture/; this document states what is required, the ADRs explain why.
Format: REQ-<AREA>-<MODULE>-<NUMBER>. AREA is OAL (OS Abstraction Layer) or COMMON (layer-agnostic) — a conceptual grouping, not a directory: since ADR-007 each module below lives in its own include/rte/<feature>/ + src/<feature>/ pair rather than a shared common//os/ folder.
| ID | Requirement |
|---|---|
| REQ-OAL-COMMON-001 | Every fallible API function in the framework shall return rte_status_t and shall not use exceptions or errno-style side channels. |
| REQ-OAL-COMMON-002 | rte_status_to_string() shall return a short, static, human-readable string for every defined status code, for diagnostics/logging use only — never on a safety-decision path. |
| ID | Requirement |
|---|---|
| REQ-OAL-COMMON-010 | No dynamic memory allocation is used by the framework after initialization; every stateful object is created from caller-supplied storage. |
| ID | Requirement |
|---|---|
| REQ-COMMON-BUF-001 | rte_buffer_t shall never allocate memory; the caller owns the backing storage for the lifetime of the buffer view. |
| REQ-COMMON-BUF-002 | Every buffer copy operation shall be bounds-checked against the declared capacity and shall never write past it. |
| REQ-COMMON-BUF-010 | rte_buffer_init() shall bind a buffer view to caller-owned storage with an initial length of 0. |
| REQ-COMMON-BUF-011 | rte_buffer_clear() shall reset length to 0 without altering capacity or data. |
| REQ-COMMON-BUF-012 | rte_buffer_set_length() shall reject a length greater than capacity with RTE_STATUS_RESOURCE_EXHAUSTED. |
| REQ-COMMON-BUF-013 | rte_buffer_copy_in() shall perform a bounds-checked copy of external data into the buffer and update length; a source exceeding capacity shall be rejected without partial modification. |
| REQ-COMMON-BUF-014 | rte_buffer_copy_out() shall perform a bounds-checked copy of the buffer's valid bytes to an external destination and report the number of bytes copied. |
| REQ-COMMON-BUF-015 | rte_buffer_as_const() shall produce a read-only view of the buffer's currently valid bytes. |
| REQ-COMMON-BUF-016 | rte_buffer_is_valid() shall perform a defensive check that data is non-NULL and length does not exceed capacity. |
| REQ-COMMON-BUF-020..025 | rte_buffer_write_u16/u32/u64_le/be() shall append the value's bytes in the stated byte order at the buffer's current length and advance it; byte order is always explicit at the call site (ADR-006 §2.5, no default). |
| REQ-COMMON-BUF-026..031 | rte_buffer_read_u16/u32/u64_le/be() shall decode a value in the stated byte order from the given offset without mutating the buffer. |
| ID | Requirement |
|---|---|
| REQ-COMMON-CAST-001 | A checked cast function shall never write to *out when the source value does not fit the destination type; the caller's variable is left exactly as passed in. |
| REQ-COMMON-CAST-002 | out == NULL shall yield RTE_STATUS_INVALID_PARAM without dereferencing out. |
| REQ-COMMON-CAST-003 | A value that does not fit the destination type's range shall yield RTE_STATUS_VALUE_OUT_OF_RANGE. |
Applies uniformly to all 72 rte_cast_<from>_to_<to> functions covering every ordered pair among int8_t/int16_t/int32_t/int64_t/ uint8_t/uint16_t/uint32_t/uint64_t/size_t — see ADR-003 §2.1 for the full type matrix; not enumerated as 72 separate IDs since all 72 satisfy the same three requirements by construction.
| REQ-COMMON-CAST-004 | rte_cast_checked_add_u32()/_sub_u32()/_mul_u32() and the size_t equivalents shall detect overflow/underflow and yield RTE_STATUS_VALUE_OUT_OF_RANGE instead of wrapping silently; *out is left untouched on failure (REQ-COMMON-CAST-001). | | REQ-COMMON-CAST-005 | rte_cast_bounds_check(index, count) shall return RTE_STATUS_OK if index < count, else RTE_STATUS_VALUE_OUT_OF_RANGE - a checked helper standardizing the array/index-bounds check pattern already hand-rolled ad hoc across this codebase's consumers. |
Added during the framework module reorg (module directories nested under common/oal/redundancy/app tiers - see this session's own commit history/CLAUDE.md for the reorg itself, unrelated to these two new requirements): overflow-checked arithmetic and a bounds-check helper, the two "extra MISRA-safety helpers" requested alongside the safe-pointer wrapper (2.3 below) and the already-existing RTE_ASSERT() (1.5 below).
| ID | Requirement |
|---|---|
| REQ-COMMON-SAFESTATE-001 | Handler storage shall be a fixed array of 3 slots (one per rte_safestate_level_t value); no dynamic allocation. |
| REQ-COMMON-SAFESTATE-002 | RTE_SAFESTATE_LEVEL_SAFE and RTE_SAFESTATE_LEVEL_REBOOT shall never return control to the caller — even if no handler is registered, the registered handler itself returns, or the requested level value is invalid — falling back to a defensive infinite loop. |
| REQ-COMMON-SAFESTATE-010 | rte_safestate_register_handler() shall reject a NULL handler or an unrecognized level with RTE_STATUS_INVALID_PARAM; re-registering for the same level shall replace the previous handler. |
| REQ-COMMON-SAFESTATE-011 | rte_safestate_enter() shall invoke the registered handler for the given level (if any) before applying REQ-COMMON-SAFESTATE-002's non-return guarantee. |
RTE_ASSERT is always active in every build configuration (no NDEBUG-style compile-out) and, on a false condition, calls rte_safestate_enter(RTE_SAFESTATE_LEVEL_SAFE, RTE_SAFESTATE_REASON_ASSERT_FAILED, ...) — this behavior is a consequence of REQ-COMMON-SAFESTATE-002/011 applied to the SAFE level and is not tracked as a separate requirement ID.
| ID | Requirement |
|---|---|
| REQ-COMMON-SAFETYVIOLATION-001 | Handler storage shall be a single fixed slot (no dynamic allocation) — a lower-frequency, broader-scope hook than rte_safestate's own per-level slots, since one registered handler covers all three instrumented primitive families (safe pointer, checked cast arithmetic, bounds check). |
| REQ-COMMON-SAFETYVIOLATION-002 | rte_safety_violation_report() shall be a no-op when no handler is registered — reporting a violation shall never itself become a new failure mode. |
Deliberately a separate, opt-in mechanism from rte_safestate (1.5 above): unlike a checkpoint timeout or voter disagreement (always genuine faults, escalated unconditionally through rte_safestate_enter()), the three primitives this module instruments are also invoked during ordinary, expected control flow (e.g. rte_cast_bounds_check() used to ask "is this the last valid index" is not itself a fault) — forcing every failure through rte_safestate_enter() would trigger REQ-COMMON-SAFESTATE-002's permanent halt on entirely normal code paths. Instrumented call sites: rte_safe_ptr_get()/_offset() (REQ-OAL-SAFEPTR-001/002, kind RTE_SAFETY_VIOLATION_CORRUPTION/_OUT_OF_RANGE), rte_cast_checked_add_u32()/_sub_u32()/_mul_u32()/_add_size()/ _sub_size()/_mul_size() (REQ-COMMON-CAST-004, kind RTE_SAFETY_VIOLATION_OVERFLOW), and rte_cast_bounds_check() (REQ-COMMON-CAST-005, kind RTE_SAFETY_VIOLATION_OUT_OF_RANGE). The reported file/line is the detection site inside this framework's own implementation, not the ultimate caller's site — these three primitives are ordinary functions, not macros, so they cannot capture the caller's __FILE__/__LINE__ the way RTE_ASSERT/RTE_SAFESTATE do.
| ID | Requirement |
|---|---|
| REQ-COMMON-STR-001 | No dynamic allocation; the caller owns the backing storage for the lifetime of the string. |
| REQ-COMMON-STR-002 | rte_string_copy() shall never call strlen() on its source; it scans for a NUL only up to the destination's capacity and fails rather than reading past it. |
| REQ-COMMON-STR-003 | Content is treated as raw bytes/ASCII; no multi-byte/UTF-8-aware operations are provided. |
| REQ-COMMON-STR-010 | rte_string_init() shall bind a string to caller-owned storage with an initial length of 0. |
| REQ-COMMON-STR-011 | rte_string_clear() shall reset a string to empty without altering capacity or storage. |
| REQ-COMMON-STR-012 | rte_string_length() shall report the current length (not counting a NUL terminator), returning 0 for a NULL input. |
| REQ-COMMON-STR-013 | rte_string_c_str() shall ensure a NUL terminator is present within capacity (without incrementing length) and shall fail with RTE_STATUS_RESOURCE_EXHAUSTED rather than write past capacity. |
| REQ-COMMON-STR-014 | rte_string_copy() shall replace dest's content per REQ-COMMON-STR-002, leaving dest unmodified on failure. |
| REQ-COMMON-STR-015 | rte_string_copy_n() shall copy an exact-length, not-necessarily-NUL-terminated source without scanning it. |
| REQ-COMMON-STR-016 | rte_string_concat() shall append to dest's content, scanning src for a NUL only up to dest's remaining capacity, leaving dest unmodified on failure. |
| REQ-COMMON-STR-017 | rte_string_compare() shall compare up to the shorter string's length, then by length, reporting the result via an output parameter. |
| REQ-COMMON-STR-018 | rte_string_find_char() shall report "not found" as a normal (non-error) outcome via an output parameter. |
| REQ-COMMON-STR-019 | rte_string_find_substr() shall report "not found" as a normal outcome; an empty needle is always found at index 0. |
| REQ-COMMON-STR-020 | rte_string_split_next() shall use only caller-owned cursor state (no hidden/global state, unlike strtok), producing a zero-copy view of each token. |
| REQ-COMMON-STR-021..024 | rte_string_from_u32/i32/u64/i64() shall format the value as base-10 ASCII, replacing dest's content. |
| REQ-COMMON-STR-025..028 | rte_string_to_u32/i32/u64/i64() shall parse base-10 ASCII, yielding RTE_STATUS_INVALID_PARAM for malformed input and RTE_STATUS_VALUE_OUT_OF_RANGE for a value that doesn't fit the destination type. |
| REQ-COMMON-STR-029..032 | rte_string_append_u32/i32/u64/i64() shall append the value as base-10 ASCII to dest's existing content (advancing its length), scanning nothing, leaving dest unmodified and yielding RTE_STATUS_RESOURCE_EXHAUSTED when the digits do not fit the remaining capacity. |
| REQ-COMMON-STR-033 | rte_string_append_hex_u32() shall append the value as lowercase hexadecimal (no 0x prefix) with a caller-supplied minimum digit count clamped to 1..8, never truncating a value that needs more digits, under the same bounds/immutability contract as REQ-COMMON-STR-029..032. |
Every OAL service below shares the osadapter-registration contract defined once in ADR-005 rather than repeating it per service. Note: REQ-OAL-BACKEND-001/002/003 below are a documentation-only consolidation for readability — unlike the other IDs in this SRS, they are by design not** a tag in any header comment (see the traceability caveat in section 4, which also lists the IDs that are untagged by omission); the behavior they describe is what each per-service REQ-OAL-<SERVICE>-01X rte_osadapter_<service>_register() entry already requires:
REQ-OAL-BACKEND-001 (ADR-005 §2.1, applies to every OAL service): rte_osadapter_<service>_register() shall reject a NULL OSAdapter with RTE_STATUS_INVALID_PARAM; re-registering shall replace the previous OSAdapter. Registration is setup-only: once the setup phase is locked (REQ-LIFECYCLE-001, ADR-026) it shall return RTE_STATUS_INVALID_STATE and keep the previous OSAdapter.
REQ-OAL-BACKEND-002 (ADR-005 §2.2, applies to every OAL service except rte_log): every public function shall validate its own parameters before consulting the OSAdapter; with no OSAdapter registered it shall return RTE_STATUS_NOT_INITIALIZED; with an OSAdapter registered but the corresponding vtable slot NULL, it shall return RTE_STATUS_NOT_SUPPORTED.
REQ-OAL-BACKEND-003 (ADR-005 §2.5, rte_log only): an unregistered OSAdapter is not an error; rte_log_write() with no OSAdapter registered shall silently do nothing.
| ID | Requirement |
|---|---|
| REQ-OAL-TIMER-001 | No dynamic allocation; caller supplies storage for each timer. |
| REQ-OAL-TIMER-002 | Callback execution time is the caller's responsibility to bound; the timer service itself must not block. |
| REQ-OAL-TIMER-003 | The expiry callback executes in a bounded-time, non-blocking context, documented per OSAdapter. |
| REQ-OAL-TIMER-010 | rte_timer_create() shall bind a timer to caller-owned storage without starting it. |
| REQ-OAL-TIMER-011 | rte_timer_start() shall start or restart a created timer. |
| REQ-OAL-TIMER-012 | rte_timer_stop() shall stop a running timer and be safe to call on an already-stopped timer. |
| REQ-OAL-TIMER-013 | rte_timer_destroy() shall release any OSAdapter resources bound to the timer. |
| REQ-OAL-TIMER-014 | rte_timer_now() shall report the current monotonic time base used by all timers. |
| REQ-OAL-TIMER-015 | rte_osadapter_timer_register() per REQ-OAL-BACKEND-001. |
| ID | Requirement |
|---|---|
| REQ-OAL-NVM-001 | Every read shall be integrity-checked (e.g. CRC or redundant-copy voting) before data is returned to the caller. |
| REQ-OAL-NVM-002 | No dynamic allocation; caller supplies storage/buffers. |
| REQ-OAL-NVM-010 | rte_nvm_open() shall open (creating if necessary) a named region. |
| REQ-OAL-NVM-011 | rte_nvm_read() shall read and integrity-check data, returning RTE_STATUS_DATA_CORRUPTION on integrity failure with undefined output buffer content. |
| REQ-OAL-NVM-012 | rte_nvm_write() shall write data and its integrity metadata. |
| REQ-OAL-NVM-013 | rte_nvm_sync() shall force any buffered writes to durable storage. |
| REQ-OAL-NVM-014 | rte_nvm_close() shall close a region handle. |
| REQ-OAL-NVM-015 | rte_osadapter_nvm_register() per REQ-OAL-BACKEND-001. |
| ID | Requirement |
|---|---|
| REQ-OAL-MEM-001 | All pools are reserved during system initialization; reservation after init is osadapter-defined and may be refused. |
| REQ-OAL-MEM-010 | rte_mem_pool_create() shall reserve a fixed-size-block pool. |
| REQ-OAL-MEM-011 | rte_mem_pool_acquire() shall return RTE_STATUS_RESOURCE_EXHAUSTED when no blocks remain. |
| REQ-OAL-MEM-012 | rte_mem_pool_release() shall return a previously acquired block to its pool. |
| REQ-OAL-MEM-013 | rte_mem_pool_stats() shall report current free/used block counts. |
| REQ-OAL-MEM-014 | rte_osadapter_memory_register() per REQ-OAL-BACKEND-001. |
| REQ-OAL-MEM-015 | An OSAdapter that guards its pools with an internal lock shall bound every wait for that lock (OSA rule R2: every wait bounded, OSA-ARCH-CAP 0.1). When the bound expires, rte_mem_pool_create(), rte_mem_pool_acquire(), rte_mem_pool_release() and rte_mem_pool_stats() shall return RTE_STATUS_TIMEOUT and leave the pool unchanged: create reserves nothing, acquire hands out no block (*out_block NULL), release does not return the block (it stays with the caller, who may retry), stats reads no counts. The OAL wrappers pass the status through unchanged. |
Bounds + NULL + corruption-canary checked access to a raw memory region a caller already owns (a static buffer, a rte_mem_pool_acquire() block, ...) - rte_safe_ptr_t never allocates anything itself.
| ID | Requirement |
|---|---|
| REQ-OAL-SAFEPTR-001 | Every access function (rte_safe_ptr_get()/_offset()) shall verify the canary first; a corrupted wrapper yields RTE_STATUS_DATA_CORRUPTION before any other check runs. |
| REQ-OAL-SAFEPTR-002 | rte_safe_ptr_offset() shall verify offset + length <= size (using overflow-checked addition - REQ-COMMON-CAST-004) before computing the resulting pointer, never raw ptr + offset at the call site. |
| REQ-OAL-SAFEPTR-003 | rte_safe_ptr_invalidate() shall clear both the wrapped pointer and the canary, so a subsequent access on the same wrapper fails RTE_STATUS_DATA_CORRUPTION rather than silently succeeding against a logically-released region. |
| ID | Requirement |
|---|---|
| REQ-OAL-TASK-001 | No dynamic allocation; caller supplies storage and a fixed-size stack/context region. |
| REQ-OAL-TASK-002 | Priorities are fixed at creation time; dynamic priority inheritance/inversion handling is an OSAdapter/RTOS concern, not assumed by this API. |
| REQ-OAL-TASK-010 | rte_task_create() shall create a task in the suspended state. |
| REQ-OAL-TASK-011 | rte_task_start() shall start a created task. |
| REQ-OAL-TASK-012 | rte_task_suspend() shall suspend a running task. |
| REQ-OAL-TASK-013 | rte_task_destroy() shall terminate and destroy a task. |
| REQ-OAL-TASK-014 | rte_osadapter_task_register() per REQ-OAL-BACKEND-001. |
| ID | Requirement |
|---|---|
| REQ-OAL-IPC-001 | Queues are bounded and statically sized at creation; no unbounded growth. |
| REQ-OAL-IPC-002 | Send/receive shall accept an explicit timeout and shall never block indefinitely by default. |
| REQ-OAL-IPC-010 | rte_ipc_create() shall create a bounded message channel. |
| REQ-OAL-IPC-011 | rte_ipc_send() shall block at most timeout_ms, returning RTE_STATUS_TIMEOUT if the queue stays full for the whole timeout. |
| REQ-OAL-IPC-012 | rte_ipc_receive() shall block at most timeout_ms, returning RTE_STATUS_TIMEOUT if no message arrives. |
| REQ-OAL-IPC-013 | rte_ipc_destroy() shall destroy a message channel. |
| REQ-OAL-IPC-014 | rte_osadapter_ipc_register() per REQ-OAL-BACKEND-001. |
| ID | Requirement |
|---|---|
| REQ-OAL-LOG-001 | Log calls are best-effort and non-blocking; a full OSAdapter buffer silently drops the newest entries rather than blocking or erroring the caller's control flow. This service shall never sit on a safety execution path. |
| REQ-OAL-LOG-010 | rte_log_init() shall be safe to call once at startup. |
| REQ-OAL-LOG-011 | rte_log_write() shall be non-blocking and never fail the caller's control flow. |
| REQ-OAL-LOG-012 | rte_osadapter_log_register() per REQ-OAL-BACKEND-001, with the REQ-OAL-BACKEND-003 exception for the unregistered case. |
| REQ-OAL-LOG-013 | rte_log_level_to_string() shall return a fixed, non-NULL string for every rte_log_level_t value, including an unrecognized one ("UNKNOWN"). |
| REQ-OAL-LOG-014 | rte_log_write_event() shall emit Site=<site> Timestamp=<ms> Level=<LEVEL> Cycle=<n> Source=<src> Destination=<dst> Type=<type> Info=<info>[ <extra_fields>] (space-separated Key=Value pairs, fixed order) as the message passed to the registered OSAdapter's write(), with source as that call's tag; Timestamp shall degrade to 0 (never block or skip the event) if no rte_timer OSAdapter is registered or rte_timer_now() fails; the whole call shall be a silent no-op under the same conditions as rte_log_write() (REQ-OAL-LOG-001) when no rte_log OSAdapter is registered. |
| REQ-OAL-LOG-015 | rte_log_set_level(min_level) shall set a process-global minimum severity, below which rte_log_write() and rte_log_write_event() drop the call before dispatching to the OSAdapter (and before any formatting work); rte_log_get_level() shall return the current value. The default is RTE_LOG_LEVEL_DEBUG (nothing filtered). It is callable at any time from any thread (not setup-only); a min_level outside RTE_LOG_LEVEL_DEBUG..RTE_LOG_LEVEL_ERROR leaves the threshold unchanged. A concurrent change can only cause one in-flight best-effort log line to be kept or dropped unexpectedly (REQ-OAL-LOG-001), never a torn read. |
| REQ-OAL-LOG-016 | rte_log_level_from_string(name, out_level) shall parse exactly the four canonical spellings rte_log_level_to_string() renders ("DEBUG"/"INFO"/"WARNING"/"ERROR", case-sensitive) into *out_level and return RTE_STATUS_OK; any other name, or a NULL name/out_level, shall return RTE_STATUS_INVALID_PARAM and leave *out_level unchanged. |
| REQ-OAL-LOG-017 | A rte_log_fields_t builder shall let a caller accumulate space-separated key=value pairs — typed via rte_log_fields_add_str/_u32/_i32/_u64/_i64/_hex_u32/_bool() — for use as rte_log_write_event()'s extra_fields (or info) argument without hand-rolling rte_string chains and without a variadic (MISRA C:2012 Rule 17.1). It shall use only in-struct storage (no allocation), place exactly one separating space between pairs and none at the ends, truncate rather than reject over-long content (REQ-OAL-LOG-001), tolerate a NULL builder / NULL key at every entry point as a silent no-op, and expose the accumulated text as a never-NULL C string via rte_log_fields_c_str() ("" when empty). rte_log_write_event_fields(...) shall behave as rte_log_write_event(...) with the builder's text as extra_fields, an empty or NULL builder being identical to a NULL extra_fields. rte_log_write_event() shall treat an empty-string extra_fields identically to NULL (no field, no trailing space). |
| ID | Requirement |
|---|---|
| REQ-OAL-REBOOT-001 | rte_reboot_request() is not expected to return on success; a return only occurs if the OSAdapter cannot perform the reboot. |
| REQ-OAL-REBOOT-010 | rte_reboot_request() shall request a controlled system restart via the registered OSAdapter. |
| REQ-OAL-REBOOT-011 | rte_osadapter_reboot_register() per REQ-OAL-BACKEND-001. |
Framework ships the interface and validate-then-dispatch layer only; a concrete OSAdapter (e.g. POSIX UDP sockets) is integrator-supplied and lives with the application that registers it — see RBC_GP's src/posix_osadapter/rte_posix_osadapter_netlink.c. This table was backfilled alongside ADR-027 (TCP → UDP migration) — the requirement IDs were already cited in rte_netlink.h's own header comments beforehand, but had no matching table entry here; the wording below reflects the now-transport-agnostic contract, not the earlier TCP-specific one.
| ID | Requirement |
|---|---|
| REQ-OAL-NETLINK-001 | No dynamic allocation; caller supplies storage (rte_netlink_storage_t). |
| REQ-OAL-NETLINK-002 | rte_netlink_open() shall never block longer than config->connect_timeout_ms. |
| REQ-OAL-NETLINK-003 | Send/receive shall accept an explicit timeout and shall never block indefinitely by default. |
| REQ-OAL-NETLINK-010 | rte_netlink_open() shall establish a point-to-point link per config->role (LISTEN binds and waits for its one peer; CONNECT dials), returning RTE_STATUS_TIMEOUT if not established within connect_timeout_ms. |
| REQ-OAL-NETLINK-011 | rte_netlink_send() shall send one fixed-size message, blocking at most timeout_ms; RTE_STATUS_HARDWARE_FAULT is returned only when the OSAdapter can positively confirm the peer is gone — a guarantee no OSAdapter can make on every failure mode (e.g. a lost/silently-dropped peer on an unreliable transport), so callers must not treat its absence as proof of liveness. |
| REQ-OAL-NETLINK-012 | rte_netlink_receive() shall receive one fixed-size message, blocking at most timeout_ms; RTE_STATUS_DATA_CORRUPTION is returned if the OSAdapter can detect the received message violated this link's wire contract (e.g. wrong length) but not necessarily its content. |
| REQ-OAL-NETLINK-013 | rte_netlink_close() shall close a link; the handle is invalid to use afterward. |
| REQ-OAL-NETLINK-014 | This service provides no message ordering, deduplication, or delivery guarantee of its own — an OSAdapter may be built on an unreliable transport (e.g. UDP). Any such guarantee is the caller's responsibility (rte_dual_msgchannel/rte_dual_channel, ADR-020, is the reusable sequence+CRC+ACK layer for callers that need one). |
| REQ-OAL-NETLINK-015 | A non-zero config->rcvbuf_bytes / config->sndbuf_bytes shall be applied to the link's socket before the link is handed out (at rte_netlink_open(), and at every socket (re)init of an OSAdapter that reuses rte_netlink_config_t, e.g. the POSIX channel service); a refusal shall fail the open (RTE_STATUS_INTERNAL_ERROR, no handle is returned (*out_handle is NULL)); a confirmed size below the request shall keep the link and be reported (RTE_SAFESTATE_LEVEL_DEGRADED / RTE_SAFESTATE_REASON_OSA_RT_ATTRIBUTE). 0 keeps the OS default and sets nothing. OSA rules R5, R6. |
Framework ships the interface and validate-then-dispatch layer only; a concrete OSAdapter (e.g. POSIX mlockall() + SCHED_FIFO) is integrator-supplied and lives with Platform_OS_POSIX. Exists so application startup code can ask for real-time bring-up through the OAL rather than calling an OSAdapter symbol directly (the layering ADR-001 §3.5 requires).
| ID | Requirement |
|---|---|
| REQ-OAL-PLATFORM-001 | rte_platform_realtime_init() is best-effort: an OSAdapter that cannot obtain some or all of the requested capabilities (unprivileged host, no RT scheduler, a bounded lockable-memory limit) shall still return RTE_STATUS_OK, having applied what it could, and shall not leave the process in a state that prevents later timer/task thread creation. |
| REQ-OAL-PLATFORM-010 | rte_platform_realtime_init(rt_priority) shall reject rt_priority > 99 with RTE_STATUS_INVALID_PARAM before any OSAdapter dispatch, then request the registered OSAdapter apply memory-residency configuration and (for rt_priority > 0) a real-time scheduling policy/priority for the calling process/thread. |
| REQ-OAL-PLATFORM-011 | rte_osadapter_platform_register() per REQ-OAL-BACKEND-001. |
The seam for unpredictable random bytes (e.g. the EuroRadio responder random, Subset-037-2), so application code never calls the OS for them. Framework ships the interface and validate-then-dispatch layer; the POSIX OSAdapter backs it with the kernel CSPRNG (getentropy()).
| ID | Requirement |
|---|---|
| REQ-OAL-RANDOM-001 | The registered OSAdapter shall return bytes from a cryptographically secure source, or fail with RTE_STATUS_HARDWARE_FAULT; it shall never fall back to a predictable generator. |
| REQ-OAL-RANDOM-010 | rte_random_fill(out, len) shall reject a NULL out or len outside 1..RTE_RANDOM_MAX_REQUEST (256) with RTE_STATUS_INVALID_PARAM before any OSAdapter dispatch; return RTE_STATUS_NOT_INITIALIZED with no OSAdapter registered and RTE_STATUS_NOT_SUPPORTED when its fill slot is NULL; otherwise return the OSAdapter's result unchanged. |
| REQ-OAL-RANDOM-011 | rte_osadapter_random_register() per REQ-OAL-BACKEND-001. |
Framework ships the interface and validate-then-dispatch layer; the POSIX OSAdapter backs it with pthread_mutex_t. The rows for the tags already in rte_mutex.h were backfilled on 2026-10-03, when REQ-OAL-MUTEX-004 was added (OSA rule R2, OSA-ARCH-CAP 0.1).
| ID | Requirement |
|---|---|
| REQ-OAL-MUTEX-001 | An application shall never call a platform threading primitive (pthread_*, a raw RTOS semaphore API) directly to guard its own state; it uses rte_mutex. |
| REQ-OAL-MUTEX-002 | No dynamic allocation; caller supplies storage. |
| REQ-OAL-MUTEX-003 | Non-recursive: locking twice from the same task without an intervening unlock is undefined behaviour. |
| REQ-OAL-MUTEX-004 | Every mutex the registered OSAdapter creates shall use priority inheritance. An OSAdapter that cannot create a priority-inheritance mutex shall fail the create with RTE_STATUS_INTERNAL_ERROR, leave *out_handle unwritten and report the refusal through rte_safestate_enter(RTE_SAFESTATE_LEVEL_DEGRADED, RTE_SAFESTATE_REASON_OSA_RT_ATTRIBUTE, ...); it shall never fall back to a mutex without priority inheritance. |
| REQ-OAL-MUTEX-010 | rte_mutex_create() shall reject a NULL storage or out_handle with RTE_STATUS_INVALID_PARAM, refuse once the setup phase is locked (REQ-LIFECYCLE-001), return RTE_STATUS_NOT_INITIALIZED with no OSAdapter registered and RTE_STATUS_NOT_SUPPORTED when its create slot is NULL; otherwise create an unlocked priority-inheritance mutex (REQ-OAL-MUTEX-004) in the caller's storage and return the OSAdapter's result unchanged. |
| REQ-OAL-MUTEX-011 | rte_mutex_lock() shall block the calling task until it holds the mutex. |
| REQ-OAL-MUTEX-012 | rte_mutex_unlock() shall release a mutex the calling task holds. |
| REQ-OAL-MUTEX-013 | rte_mutex_destroy() shall release any OSAdapter resources bound to the mutex. |
| REQ-OAL-MUTEX-014 | rte_mutex_lock_timed(handle, timeout_ms) shall wait at most timeout_ms for the mutex (OSA rule R2: every wait bounded): return RTE_STATUS_OK with the lock held, or RTE_STATUS_TIMEOUT with the lock not held when it was not acquired within timeout_ms; timeout_ms == 0 is one non-blocking try. It shall reject a NULL handle with RTE_STATUS_INVALID_PARAM, return RTE_STATUS_NOT_INITIALIZED with no OSAdapter registered and RTE_STATUS_NOT_SUPPORTED when its lock_timed slot is NULL, and otherwise return the OSAdapter's result unchanged; the OSAdapter returns RTE_STATUS_INTERNAL_ERROR for any other platform failure and never falls back to an unbounded wait. Not gated by the setup-phase lock (an operational call, like rte_mutex_lock()). |
The RTE side of the Fail-Safe Output Unit seam (Capella IF-SCP-02/03, baseline rule 4): a per-cycle grant and a revoke dispatched to an optional OSAdapter. An unregistered OSAdapter means no FSOU (the container build); the software checkpoint then stays the only output gate. Nothing in RTE calls the grant or revoke yet (ADR-041 is proposed; the checkpoint hook waits for its acceptance).
| ID | Requirement |
|---|---|
| REQ-FSOU-001 | rte_osadapter_fsou_register() shall reject a NULL adapter with RTE_STATUS_INVALID_PARAM and shall be setup-only: once the setup phase is locked it shall return RTE_STATUS_INVALID_STATE and keep the previous registration (REQ-LIFECYCLE-001). rte_osadapter_register_all() shall register the bundle's fsou member only when it is non-NULL. |
| REQ-FSOU-002 | rte_fsou_grant(cycle_id) and rte_fsou_revoke(reason) shall return RTE_STATUS_NOT_INITIALIZED with no FSOU OSAdapter registered and RTE_STATUS_NOT_SUPPORTED when the matching slot is NULL; otherwise they shall pass the argument to the OSAdapter and return its result unchanged. |
| REQ-FSOU-003 | rte_fsou_report_presence() shall log (INFO, tag FSOU output permission) FSOU output permission present or no FSOU output permission registered on its first call only; every later call in the same process shall log nothing. |
The raw socket vtable (rte_os_socket_ops_t) an OSAdapter registers with rte_osadapter_register_socket_ops(), and the built-in raw UDP and TCP ProtocolAdapters (rte_protocol_adapter_get_udp() / _tcp()) that consume it. OSA rule R5 (non-blocking I/O with a timeout) for this seam; added 2026-10-03 (G4 item 6 b). No production code calls the socket vtable or these ProtocolAdapters yet (netlink and the channel service open their own sockets).
| ID | Requirement |
|---|---|
| REQ-OAL-SOCKET-001 | The registered OSAdapter's open(type, out_handle) shall return RTE_STATUS_OK only with a non-blocking socket in *out_handle. If it cannot make the socket non-blocking it shall release the socket and return RTE_STATUS_INTERNAL_ERROR with *out_handle unwritten; it shall never hand out a blocking socket. A NULL out_handle shall yield RTE_STATUS_INVALID_PARAM and an unknown type RTE_STATUS_NOT_SUPPORTED, both with *out_handle unwritten. set_nonblocking(handle, false) may still switch an opened socket to blocking. |
| REQ-OAL-SOCKET-002 | The raw UDP and TCP ProtocolAdapters' open shall check the result of set_nonblocking(sock, true) on the socket just opened when that vtable slot is non-NULL (a NULL slot is accepted, the socket being non-blocking by REQ-OAL-SOCKET-001). On a failure they shall close that socket, leave the socket in their state storage at RTE_OS_SOCKET_INVALID_HANDLE, perform no bind or connect, and return the status set_nonblocking returned, with the ProtocolAdapter's *out_handle unwritten. |
A name-addressed publish/subscribe or request/response message endpoint, shaped after the flows.h sketch in Annex A.1 of the OCORA PI API specification (fl_open/fl_close/fl_send/fl_receive/fl_getattr/fl_setattr) under this project's rte_ prefix. Framework ships the interface and validate-then-dispatch layer; the OSAdapter vtable (rte_osadapter_flow_t) and rte_osadapter_flow_register() live in include/rte_osadapter/flow/rte_osadapter_flow.h (ADR-021 seam). Number 2.14 stays unused: that section moved to 3d.5.
Rows backfilled on 2026-10-04 (G4 item 6 g-1) from the tags already in rte_flow.h; the wording restates the header text and adds no requirement content. Unit tests: tests/flow/test_rte_flow.c.
| ID | Requirement |
|---|---|
| REQ-OAL-FLOW-001 | No dynamic allocation; caller supplies storage (rte_flow_storage_t). |
| REQ-OAL-FLOW-002 | rte_flow_open() shall never block longer than config->open_timeout_ms. |
| REQ-OAL-FLOW-003 | Send/receive shall accept an explicit timeout and shall never block indefinitely by default — a deliberate strengthening beyond the PI API sketch (which relies on O_NONBLOCK alone), matching every other blocking call in this framework (e.g. REQ-OAL-NETLINK-003). RTE_FLOW_O_NONBLOCK is an OSAdapter-defined hint only; callers should still pass an explicit timeout_ms rather than relying on this flag alone. |
| REQ-OAL-FLOW-004 | This service provides no message ordering, deduplication, or delivery guarantee of its own, same posture as rte_netlink (REQ-OAL-NETLINK-014). |
| REQ-OAL-FLOW-010 | rte_flow_open() shall open (join, register or subscribe to) the named Flow. It shall return RTE_STATUS_INVALID_PARAM for a NULL storage, config, config->name or out_handle, a zero config->message_size, or config->oflags not setting exactly one of the PUBLISHER, SUBSCRIBER, REQUESTER and RESPONDER flags; RTE_STATUS_TIMEOUT if the Flow is not opened within config->open_timeout_ms; RTE_STATUS_NOT_INITIALIZED with no OSAdapter registered and RTE_STATUS_NOT_SUPPORTED when the registered OSAdapter does not implement open. |
| REQ-OAL-FLOW-011 | rte_flow_send() shall send one fixed-size message on the given channel (RTE_FLOW_CHANNEL_USER or _CTRL), blocking at most timeout_ms, and return RTE_STATUS_TIMEOUT if the send does not complete in time; RTE_STATUS_HARDWARE_FAULT if the OSAdapter can positively confirm no peer is reachable (not guaranteed on every OSAdapter, see REQ-OAL-FLOW-004); RTE_STATUS_INTERNAL_ERROR for any other OSAdapter-level failure (OSAdapter-dependent: the Platform_Protocol_DDS stub passes it through from rte_netlink_send(), where the Platform_OS_POSIX netlink OSAdapter returns it for a poll() errno other than the retried EINTR, a send() errno other than the retried EINTR/EAGAIN/EWOULDBLOCK and the RTE_STATUS_HARDWARE_FAULT-mapped ECONNREFUSED, or for a short datagram write). A NULL handle or data or a zero data_size shall yield RTE_STATUS_INVALID_PARAM; RTE_STATUS_NOT_INITIALIZED/RTE_STATUS_NOT_SUPPORTED as in REQ-OAL-FLOW-010. |
| REQ-OAL-FLOW-012 | rte_flow_receive() shall receive one fixed-size message, blocking at most timeout_ms, report the channel it arrived on through out_channel when that is non-NULL, and return RTE_STATUS_TIMEOUT if no message arrives in time; RTE_STATUS_HARDWARE_FAULT if the OSAdapter can positively confirm no peer is reachable; RTE_STATUS_DATA_CORRUPTION if the OSAdapter received a message whose length differs from the Flow's message_size (both OSAdapter-dependent, not guaranteed on every OSAdapter, see REQ-OAL-FLOW-004; the Platform_Protocol_DDS stub passes both through from rte_netlink_receive(). On Darwin the Platform_OS_POSIX netlink OSAdapter calls recvfrom() without MSG_TRUNC, so a datagram longer than message_size is cut to message_size and returned as RTE_STATUS_OK, the excess bytes discarded; only a shorter one (other than a stray 1-byte handshake datagram, which is dropped) yields RTE_STATUS_DATA_CORRUPTION. Every other build passes MSG_TRUNC, which on Linux reports the real length, so there a longer one is RTE_STATUS_DATA_CORRUPTION too); RTE_STATUS_INTERNAL_ERROR for any other OSAdapter-level failure (OSAdapter-dependent: passed through by the Platform_Protocol_DDS stub; the Platform_OS_POSIX netlink OSAdapter returns it for a poll() errno other than the retried EINTR, or a recvfrom() errno other than the retried EINTR/EAGAIN/EWOULDBLOCK and the RTE_STATUS_HARDWARE_FAULT-mapped ECONNREFUSED). A NULL handle or out_data or a zero buffer_size shall yield RTE_STATUS_INVALID_PARAM, as does (Platform_Protocol_DDS stub) a buffer_size smaller than the Flow's message_size; RTE_STATUS_NOT_INITIALIZED/RTE_STATUS_NOT_SUPPORTED as in REQ-OAL-FLOW-010. |
| REQ-OAL-FLOW-013 | rte_flow_close() shall close (leave, unregister or unsubscribe from) a Flow; the handle is invalid to use afterward. A NULL handle shall yield RTE_STATUS_INVALID_PARAM; RTE_STATUS_NOT_INITIALIZED/RTE_STATUS_NOT_SUPPORTED as in REQ-OAL-FLOW-010. |
| REQ-OAL-FLOW-014 | rte_flow_getattr() shall report the Flow's current static and dynamic attributes (rte_flow_attr_t: message_size, oflags, is_connected). A NULL handle or out_attr shall yield RTE_STATUS_INVALID_PARAM; RTE_STATUS_NOT_INITIALIZED/RTE_STATUS_NOT_SUPPORTED as in REQ-OAL-FLOW-010. |
| REQ-OAL-FLOW-015 | rte_flow_setattr() shall apply the Flow's mutable attributes from new_attr (which fields are mutable is OSAdapter-defined; an immutable field such as message_size is ignored, not rejected) and report the attributes as they were before the call through out_old_attr when that is non-NULL. A NULL handle or new_attr shall yield RTE_STATUS_INVALID_PARAM; RTE_STATUS_NOT_INITIALIZED/RTE_STATUS_NOT_SUPPORTED as in REQ-OAL-FLOW-010. |
These apply across every module above and are enforced by convention and the MISRA compliance review (docs/MISRA_COMPLIANCE_REPORT.dox) rather than an individual REQ-* tag on every function:
This section covers the two modules added by ADR-017. Modules added later have their own sections below (e.g. rte_checksum in section 3g, REQ-CHECKSUM-001..008).
| ID | Requirement |
|---|---|
| REQ-CHECKPOINT-001 | rte_channel_checkpoint() shall never block longer than config->max_delay_ms. |
| REQ-CHECKPOINT-002 | A checkpoint-arrival reply that fails CRC verification or carries a different checkpoint_id shall not count toward expected_node_count. |
| REQ-CHECKPOINT-003 | If fewer than expected_node_count valid replies arrive within max_delay_ms, rte_channel_checkpoint() shall call rte_safestate_enter() at RTE_SAFESTATE_LEVEL_SAFE with RTE_SAFESTATE_REASON_CHECKPOINT_TIMEOUT before returning RTE_STATUS_TIMEOUT. |
ADR-034 note (does not change the three requirements above):** rte_channel_checkpoint() itself is unchanged — still exact checkpoint_id equality plus CRC verification, still the same bounded-retry-loop/watchdog-kick behavior. What changed is upstream, in rte_appmanager_run()'s built-in checkpoint integration (3c below): checkpoint_id is no longer a bare per-cycle counter, it is computed by folding RTE_CHECKPOINT_MARK() calls into a running signature (REQ-APPMANAGER-014). A RBC_GP caller not going through rte_appmanager's built-in integration still owns checkpoint_id entirely itself and these three requirements are the complete contract it needs.
| ID | Requirement |
|---|---|
| REQ-CLOCKSYNC-001 | rte_clocksync_get_offset_ms() and rte_clocksync_get_quality() shall return RTE_STATUS_NOT_INITIALIZED if no OSAdapter has been registered. |
| REQ-CLOCKSYNC-002 | This module shall never be called from, or influence the outcome of, rte_channel_checkpoint() or any other vital comparison — checkpoint-ID rendezvous, not clock agreement, is the basis of comparison correctness (ADR-017 §2.3). |
Not a full backfill of this module (see 3a's own note on the pre-existing gap) - just the RTE_WATCHDOG_ACTION_FAILOVER action, added when a real integrator (RBC_GP's dual-channel A/B link and SITE<->SITE heartbeat) needed a "my redundant peer stopped responding" reaction and found the action a documented dead stub, plus the time-base contract (REQ-WATCHDOG-003/-004, ISS-036 step 1): the module used to ignore a failed rte_timer_now(), arming and kicking against a made-up time of 0.
| ID | Requirement |
|---|---|
| REQ-WATCHDOG-001 | rte_watchdog_create() shall return RTE_STATUS_INVALID_PARAM if config->action is RTE_WATCHDOG_ACTION_FAILOVER and config->custom_action is NULL (same requirement already in force for RTE_WATCHDOG_ACTION_CUSTOM). |
| REQ-WATCHDOG-002 | On timeout, a watchdog configured with RTE_WATCHDOG_ACTION_FAILOVER shall invoke config->custom_action(config->context) — identical dispatch to RTE_WATCHDOG_ACTION_CUSTOM — and shall not itself decide what the timeout means; that decision belongs to the integrator's custom_action. |
| REQ-WATCHDOG-003 | rte_watchdog_start(), rte_watchdog_kick() and rte_watchdog_get_status() shall return the rte_timer_now() status unchanged and leave the watchdog unchanged when that status is not RTE_STATUS_OK: start does not arm, kick moves no deadline and counts no kick, get_status writes nothing to *status_out (no partial read-out). (ISS-036 step 1, 2026-10-06.) |
| REQ-WATCHDOG-004 | rte_watchdog_timer_tick() shall enter RTE_SAFESTATE_LEVEL_SAFE with reason RTE_SAFESTATE_REASON_CLOCK_FAILED (after one ERROR log line "watchdog clock failed - entering SAFE state" under the first such watchdog's name) when rte_timer_now() fails while any started, not yet fired watchdog exists — a watchdog whose clock has failed can never expire, which is the silent failure it exists to prevent. A tick with a failed clock and no such watchdog is a no-op. (ISS-036 step 1, 2026-10-06.) |
rte_appmanager was one of the modules 3a flagged as not yet backfilled; this section starts that backfill with the REQ-APPMANAGER-* IDs introduced or already present in the header as of ADR-019/ADR-034, not a full retroactive pass over every pre-existing behavior of the module.
ADR-034 (checkpoint-signature marks, stage reorder):** found live in RBC_GP (A/WEST and B/WEST cycling reboots roughly every 40s, never stabilizing): checkpoint_id used to be iteration_count, a process-local counter that resets to 0 on every reboot — two independently-rebooting channels' counters have no reason to ever coincide again after either one reboots alone, so REQ-CHECKPOINT-002 correctly (but uselessly) kept rejecting every reply as "wrong checkpoint_id" until, by chance, both channels next rebooted together. Fixed by having application code call RTE_CHECKPOINT_MARK() (or the labelled variant) at meaningful decision/preparation points during pre_execute()/execute()/post_execute(); each call folds a CRC64 hash of its call site into a running per-cycle signature (see REQ-APPMANAGER-014), which becomes checkpoint_id — naturally equal on both sides whenever they actually took the same program path this cycle, regardless of either side's reboot history. This also moved the checkpoint stage from first (before pre_execute()) to last (after post_execute(), REQ-APPMANAGER-012), since marks made during a cycle can only be compared once that cycle's own stages have run — which in turn made REQ-APPMANAGER-008's pacing workaround unnecessary (see that entry's own note) and enabled a genuine safety improvement, stage-then-commit output (REQ-APPMANAGER-013): an application can queue output during the cycle and defer actually transmitting it until the checkpoint confirms both channels agree, so a diverged cycle's output is never sent at all, instead of only being noticed after the fact.
| ID | Requirement |
|---|---|
| REQ-APPMANAGER-001 | Applications shall use the Application Manager (rte_appmanager_run()) for controlled initialization, execution, and shutdown lifecycle. |
| REQ-APPMANAGER-002 | Applications shall implement all mandatory operations in rte_appmanager_operations_t (init, execute, shutdown, get_name, get_version); pre_execute, post_execute, and on_checkpoint_result are optional and may be left NULL. |
| REQ-APPMANAGER-006 | rte_appmanager_run() shall treat a NULL pre_execute or post_execute as "skip this stage", not an error, and shall not call it. |
| REQ-APPMANAGER-007 | rte_appmanager_run() shall handle the checkpoint stage's result identically to pre_execute/execute/post_execute: on non-RTE_STATUS_OK, log it, increment error_count, and check error_threshold — no separate reaction path for a checkpoint failure/timeout. (ADR-034: the checkpoint stage is now the LAST stage of a cycle — REQ-APPMANAGER-012 — so this is the last opportunity for a cycle to be counted as an error, not the first.) |
| REQ-APPMANAGER-008 | Superseded by ADR-034, kept for history; documentation-only, not tagged. Originally: a genuine checkpoint-stage failure shall not be retried faster than max_delay_ms, via rte_appmanager_pace_failed_checkpoint() — needed because the checkpoint stage used to run before pre_execute(), the only place a consumer's own cycle pacing lived, so a fast-failing checkpoint could spin the loop at ~90000 iterations/second. ADR-034 moved the checkpoint stage to run LAST (REQ-APPMANAGER-012): pre_execute() now always runs before checkpoint even has a chance to fail, so it already paces every cycle regardless of that cycle's own checkpoint outcome — the starvation this requirement guarded against can no longer occur by construction. rte_appmanager_pace_failed_checkpoint() was removed accordingly. |
| REQ-APPMANAGER-009 | rte_appmanager_run() is the single entry point for an application's lifecycle (REQ-APPMANAGER-001) and shall refuse re-entry: a call arriving while a previous call is still mid-lifecycle (RTE_APP_STATE_INITIALIZING/_RUNNING/_SHUTTING_DOWN) shall return EXIT_FAILURE immediately, without altering any state belonging to the call already in progress. A new call made only after a previous one has fully returned (state RTE_APP_STATE_SHUTDOWN/_ERROR) is unaffected — this framework's own test suite relies on exactly that sequential-call pattern (ADR-026). |
| REQ-APPMANAGER-010 | The moment ops->init() returns RTE_STATUS_OK, rte_appmanager_run() shall lock the application's setup phase (rte_lifecycle_lock()) for the remainder of that run, and shall unlock it (rte_lifecycle_unlock()) both at the start of every call and the moment that call's own execution phase ends — see ADR-026 and REQ-LIFECYCLE-001. |
| REQ-APPMANAGER-011 | rte_appmanager_run() shall treat config->checkpoint->Voter (N-way channel voting) == NULL identically to config->checkpoint == NULL: skip the rte_channel_checkpoint() call entirely for that cycle (no pacing, no error counted) — never as a failed stage. (ADR-034: since the checkpoint stage now runs LAST — REQ-APPMANAGER-012 — pre_execute()/execute()/post_execute() already ran unconditionally before this check, so a paused checkpoint can no longer starve them by construction; this requirement's original "...and proceed to pre_execute()/execute()/post_execute() normally" clause is now vacuous, kept here only as historical context for why the check exists at all.) Found live (ADR-027 Phase 3, RBC_GP): before this fix, a caller-paused checkpoint (Voter (N-way channel voting) toggled to NULL while its own underlying link is known down — a normal, documented pattern, not rare) was fed to rte_channel_checkpoint(), got back RTE_STATUS_INVALID_PARAM, and had that treated as a failed stage — paced (REQ-APPMANAGER-008) and continued, which (under the pre-ADR-034 stage order) skipped every later stage for as long as Voter (N-way channel voting) stayed NULL, silently starving every one of a consumer's own per-cycle safety checks too, including rte_watchdog_timer_tick(). |
| REQ-APPMANAGER-012 | (ADR-034) rte_appmanager_run() shall run the checkpoint stage, if configured, as the LAST stage of a cycle — after pre_execute()/execute()/post_execute() have all had the opportunity to run — using the signature accumulated by RTE_CHECKPOINT_MARK() calls made during those stages THIS cycle (REQ-APPMANAGER-014) as checkpoint_id, not the previous stage order's iteration_count. |
| REQ-APPMANAGER-013 | (ADR-034) If ops->on_checkpoint_result is non-NULL, rte_appmanager_run() shall call it exactly once per cycle, immediately after the checkpoint stage, with committed = true when config->checkpoint is NULL, config->checkpoint->Voter (N-way channel voting) is NULL (paused), or the checkpoint rendezvous succeeded, and committed = false only when rte_channel_checkpoint() itself returned non-RTE_STATUS_OK (REQ-CHECKPOINT-003 has already driven rte_safestate_enter() by that point). Its own non-RTE_STATUS_OK return is handled identically to every other stage (REQ-APPMANAGER-007). |
| REQ-APPMANAGER-014 | (ADR-034) rte_appmanager_checkpoint_mark(file, line, label) — normally invoked via RTE_CHECKPOINT_MARK()/RTE_CHECKPOINT_MARK_LABEL() — shall, when called during an active rte_appmanager_run() cycle, fold a CRC64 hash of label (if non-NULL) or file:line into that cycle's running signature via signature = crc64(encode_le(signature) || encode_le(mark_hash)), and shall be a documented no-op (the signature untouched) when called outside an active cycle (before the loop starts, or after it ends, including from init()/shutdown()). The signature shall reset to the fixed seed RTE_APPMANAGER_CHECKPOINT_SIGNATURE_SEED at the start of every cycle, before pre_execute() runs. rte_appmanager_checkpoint_fold_signature(uint64_t) shall be a pure function (no reference to any in-progress cycle) computing the same 64-to-32-bit fold rte_appmanager_run() uses internally, so a caller can independently compute the expected checkpoint_id for a known signature value (e.g. a no-marks cycle always folds to 0, since the seed's own two 32-bit halves are equal). |
| ID | Requirement |
|---|---|
| REQ-LIFECYCLE-001 | Every setup-only constructor this framework ships (rte_timer_create(), rte_channel_init(), rte_voter_init()/_register_channel(), rte_cross_comparator_init()/_register_channel(), rte_watchdog_create(), rte_mutex_create(), rte_mem_pool_create(), rte_ipc_rr_server_create(), rte_ipc_rr_client_create(), rte_ipc_pubsub_topic_create() (these three: declared in installed headers, not implemented in any shipped library; see root ISSUES.dox ISS-034)), and each of the 14 OAL OSAdapter registration functions (rte_osadapter_clocksync_register(), rte_osadapter_flow_register(), rte_osadapter_fsou_register(), rte_osadapter_ipc_register(), rte_osadapter_log_register(), rte_osadapter_memory_register(), rte_osadapter_mutex_register(), rte_osadapter_netlink_register(), rte_osadapter_nvm_register(), rte_osadapter_platform_register(), rte_osadapter_random_register(), rte_osadapter_reboot_register(), rte_osadapter_task_register(), rte_osadapter_timer_register(); ADR-005, REQ-OAL-BACKEND-001: a rejected registration keeps the previous OSAdapter), shall reject its call with RTE_STATUS_INVALID_STATE once rte_lifecycle_lock() has been called and rte_lifecycle_unlock() has not been called since — i.e. once the application's setup phase is locked (see REQ-APPMANAGER-010). rte_netlink_open(), rte_dual_channel_init(), and rte_dual_negotiator_init() are deliberately not gated by this lock: all three are legitimately re-invoked after the setup phase locks by an application's own reconnect-after-link-loss logic, re-establishing a link the application already owns rather than adding a new one its own design never accounted for. Open exceptions (root ISSUES.dox ISS-032, owner decision open): rte_osadapter_register_socket_ops() (section 2.13) and rte_osadapter_channel_service_register() (section 3d.5, rte_channel_service.h) are not gated today — after the lock each still returns RTE_STATUS_OK and replaces the registered ops. |
| REQ-LIFECYCLE-002 | The setup-phase lock shall be a single, process-wide flag (no dynamic allocation, no OS dependency, no per-rte_appmanager_config_t instance) — this framework has no concept of more than one concurrently-running application per process, matching rte_appmanager's own existing g_app_state single-instance assumption. |
| REQ-LIFECYCLE-003 | A rejected setup-only call shall be reported through rte_safestate_enter(RTE_SAFESTATE_LEVEL_DEGRADED, RTE_SAFESTATE_REASON_SETUP_AFTER_INIT, ...) before RTE_STATUS_INVALID_STATE is returned (Capella OSA model: a resource created after initialisation is a critical error reported to the platform). Also gated (2026-10-02): rte_task_create(), rte_task_start(), rte_timer_start(), rte_ipc_create(), rte_nvm_open(), rte_channel_service_setup(). Gated since before that date: rte_mutex_create(), rte_mem_pool_create(), rte_ipc_rr_server_create(), rte_ipc_rr_client_create(), rte_ipc_pubsub_topic_create() (named in REQ-LIFECYCLE-001), which report the same way. The three request-reply/pub-sub constructors are TODO stubs (src/oal/ipc/rte_ipc_request_reply.c, rte_ipc_pubsub.c) compiled into no library target, and they do not compile as they stand (they include rte/log.h, which does not exist), so their gate is verified by review only; their headers are installed, so a consumer gets the declarations and a link error (root ISSUES.dox ISS-034, owner decision open: implement or remove); the eight other constructor/start calls listed in this row (rte_task_create(), rte_task_start(), rte_timer_start(), rte_ipc_create(), rte_nvm_open(), rte_channel_service_setup(), rte_mutex_create(), rte_mem_pool_create()) are verified by tests/lifecycle/test_rte_lifecycle_osa_gates.c. The 14 OAL OSAdapter registration functions named in REQ-LIFECYCLE-001 report the same way (each calls rte_lifecycle_check_setup_allowed(), which makes the report); after the lock rte_osadapter_fsou_register() is verified by tests/fsou/test_rte_fsou.c, the other 13 by tests/lifecycle/test_rte_lifecycle_osa_gates.c (each refused with RTE_STATUS_INVALID_STATE, one report each, the previous OSAdapter kept). |
ADR-025 split what this section previously described (a single rte_channel type combining a redundant transport link with N-way voting logic, channel_count/voting_strategy included in its own init config) into three modules with a clean responsibility boundary: rte_channel is now a single point-to-point link only, rte_voter does N-way 2oo2/2oo3/NMR voting over channels registered into it, and rte_cross_comparator does 2-way peer comparison. channel_count and voting_strategy moved off rte_channel_init()'s config entirely and onto rte_voter_init()'s — the floor this section used to describe (previously relaxed by ADR-019 §5.1 to allow a single-channel NMR voter with quorum_size == 1) now lives there instead, unchanged in substance: RTE_VOTING_2OO2 requires exactly 2 registered channels, RTE_VOTING_2OO3 exactly 3, RTE_VOTING_NMR at least 1 with 1 <= quorum_size <= channel_count.
| ID | Requirement |
|---|---|
| REQ-CHANNEL-001 | No dynamic allocation; caller supplies storage for every rte_channel_t. |
| REQ-CHANNEL-002 | rte_channel_init() shall return RTE_STATUS_INVALID_PARAM if config->send or config->recv is NULL — a channel with no way to move data is a construction-time error, not a deferred one. |
| REQ-CHANNEL-003 | rte_channel_send()/_receive() shall dispatch to config->send/config->recv and update health.send_count/health.receive_count (or the matching _error_count, plus health.last_error) on every call, regardless of outcome. |
| REQ-CHANNEL-004 | is_healthy shall default to true at rte_channel_init() and shall never be cleared automatically by a send/receive failure — only an explicit rte_channel_set_healthy(handle, false) call by the channel's owner (e.g. rte_voter, rte_cross_comparator) may mark it unhealthy. A single transient I/O failure alone does not condemn a link; that judgment belongs to whichever component is tracking the pattern of failures across calls. |
| REQ-CHANNEL-005 | rte_channel_config_t::name (e.g. "ChannelAtoB") is optional (may be NULL) and is not copied - same caller-owned-pointer convention as rte_watchdog_config_t::name - so it must outlive the channel. rte_channel_get_name() shall return it verbatim (NULL if handle is NULL or no name was configured). |
| ID | Requirement |
|---|---|
| REQ-VOTER-001 | No dynamic allocation; every rte_voter_t holds a fixed array of at most RTE_VOTER_MAX_CHANNELS (8) registered rte_channel_t * pointers. |
| REQ-VOTER-002 | rte_voter_init() shall return RTE_STATUS_INVALID_PARAM for a voting_strategy other than RTE_VOTING_2OO2/_2OO3/_NMR, or for RTE_VOTING_NMR with quorum_size == 0. |
| REQ-VOTER-003 | rte_voter_send()/_receive() shall return RTE_STATUS_INVALID_PARAM unless the number of currently registered channels matches the configured strategy's required count (RTE_VOTING_2OO2 = exactly 2, RTE_VOTING_2OO3 = exactly 3, RTE_VOTING_NMR = at least quorum_size). |
| REQ-VOTER-004 | rte_voter_receive() shall group every successfully-received, per-channel payload into equality classes (via config->compare if registered, otherwise memcmp) and select the largest class, reporting RTE_VOTING_AGREED with that class's data if its size meets the strategy's required quorum (voter_required_quorum()), or RTE_VOTING_DISAGREED otherwise. This is a majority vote across all registered channels, not a pairwise comparison against a single reference channel — see ADR-025 §1 for the bug this replaced. |
| REQ-VOTER-005 | On RTE_VOTING_DISAGREED, rte_voter_receive() shall call rte_safestate_enter() at RTE_SAFESTATE_LEVEL_SAFE when config->trigger_safestate_on_disagreement is true (the default), and shall always invoke config->on_disagreement (if registered) regardless of that flag. |
| REQ-VOTER-006 | rte_voter_get_channel_by_name(voter, name) shall return the first registered channel whose own rte_channel_get_name() exactly (strcmp()) matches name, or NULL if Voter (N-way channel voting)/name is NULL or no registered channel's name matches (including a channel whose own name is itself NULL - never matched by any lookup, REQ-CHANNEL-005). |
| ID | Requirement |
|---|---|
| REQ-CROSSCOMPARATOR-001 | No dynamic allocation; every rte_cross_comparator_t holds exactly 2 registered rte_channel_t * slots. |
| REQ-CROSSCOMPARATOR-002 | A 3rd rte_cross_comparator_register_channel() call on an already-fully-registered comparator shall return RTE_STATUS_RESOURCE_EXHAUSTED without disturbing the 2 already-registered channels. |
| REQ-CROSSCOMPARATOR-003 | rte_cross_comparator_execute() shall require both registered channels to be healthy and to successfully receive data_size bytes before comparing; any unhealthy channel or receive failure shall short-circuit to RTE_VOTING_TIMEOUT/RTE_VOTING_INSUFFICIENT_QUORUM as appropriate without invoking the compare step. |
| REQ-CROSSCOMPARATOR-004 | The comparison itself shall use config->compare if registered, otherwise a full memcmp() of the two channels' received payloads — CRC-64 transport-integrity verification (rte_checksum) is a separate, already-applied concern and is never itself treated as "the comparison" (ADR-025 §2.4). |
Only the requirement added with the socket buffer keys is listed here; the rest of this module (topology, replicas, quorum, roles, standby mode, the other channel keys) is not backfilled yet.
| ID | Requirement |
|---|---|
| REQ-REDCFG-001 | rte_redundancy_config_load() (rte_redundancy_config.h) shall read the optional channel-object keys "rcvbuf_bytes" and "sndbuf_bytes" into rte_channel_def_t::rcvbuf_bytes / ::sndbuf_bytes, the deployment-file source of the REQ-OAL-NETLINK-015 sizes. An absent key shall give 0 (OS default). A present value that is not an unsigned decimal integer (no digit, a sign, a fraction, an exponent, or anything other than whitespace between the digits and the following , or the object end, e.g. 1 000 000 or 256 * 1024) or is above UINT32_MAX shall fail the whole load with RTE_STATUS_INVALID_STATE and leave *out_config untouched; it shall never be silently replaced by 0 or by a wrapped value. The consumer is the integrator, which copies the two values into rte_netlink_config_t for REQ-OAL-NETLINK-015. OSA rules R5, R6. |
Moved here from section 2.14 on 2026-10-04 (G4 item 6): the header lives in include/rte/redundancy/channel_service/, not in the OS Abstraction Layer. Requirement text unchanged.
The application-facing named channel service and its OSAdapter vtable (rte_osadapter_channel_service_t). Only the requirement added with RTE_STATUS_NOT_CONNECTED is listed here (2026-10-04, G4 item 6 f-1); the rest of this module (setup, setup by id, read, read_ex, close, the setup-phase gate already in REQ-LIFECYCLE-003) is not backfilled yet.
| ID | Requirement |
|---|---|
| REQ-CHANSVC-001 | On a listen-role channel-service link whose peer address is learned from the first datagram the link receives, a send (the OSAdapter's vtable send, reached through rte_channel_service_send()) before any peer is known shall transmit nothing and shall return RTE_STATUS_NOT_CONNECTED, never RTE_STATUS_OK; rte_channel_service_send() shall return that status unchanged. An OSAdapter that keeps the link unopened until its peer is present (the flow-backed channel service, which returns RTE_STATUS_TIMEOUT for an absent peer) is not affected. OSA rule R5. Status: Implemented by Platform_OS_POSIX's channel service since 2026-10-04 (G4 item 6 f-2); verified by RBC_GP test_rte_posix_osadapter_channel_service_listen. |
Generalizes rte_dual_negotiator's pairwise older-startup-timestamp-wins rule (3e.4) to up to RTE_REDUNDANCY_CONFIG_MAX_REPLICAS candidates, for the 2oo3/NMR topologies rte_dual_negotiator cannot express. A pure state computation over a candidate snapshot the caller gathers each cycle (same posture as rte_voter); it owns no transport and is not called by rte_dual_negotiator or rte_cross_comparator. Rows backfilled on 2026-10-04 (G4 item 6 g-1) from the tags already in rte_election.h; the wording restates the header text and adds no requirement content. Unit tests: tests/election/test_rte_election.c.
| ID | Requirement |
|---|---|
| REQ-ELECTION-001 | No dynamic allocation; caller supplies storage and, on each rte_election_execute() call, a caller-gathered candidate snapshot. |
| REQ-ELECTION-002 | The winner shall be the candidate with the oldest (smallest) startup_timestamp_ms among all candidates currently in contact (REQ-ELECTION-004); on an exact timestamp tie the smallest id wins — the rule rte_dual_negotiator uses (REQ-DUAL-NEGOTIATOR-003), generalized from 2 to N candidates. |
| REQ-ELECTION-003 | rte_election_execute() shall never call rte_safestate_enter() itself — same "no automatic safety reaction" posture as rte_dual_negotiator (REQ-DUAL-NEGOTIATOR-002): deciding what a sustained lack of quorum means for safety stays an application policy decision. |
| REQ-ELECTION-004 | A candidate not present in the snapshot passed to the current rte_election_execute() call (including one this instance saw earlier that has since dropped out) shall be treated as not currently in contact and cannot win that round; callers omit rather than mark timed-out candidates. |
| REQ-ELECTION-005 | Every non-winning candidate that is in contact shall be reported RTE_DUAL_STATE_HOTSTANDBY if the winner's own degraded flag is false and RTE_DUAL_STATE_COLDSTANDBY if it is true — the winner's degradation, never a standby's own self-report, sets every standby's HOT/COLD label, the rule rte_dual_negotiator uses (REQ-DUAL-NEGOTIATOR-004), generalized from one standby to N-1. |
Declarative registry of application state that must survive a STANDBY to ONLINE promotion: the application registers each field (name, pointer, size, optionally a custom encoder/decoder pair) and the module packs or unpacks all of them into or from one bounds-checked buffer. It does not decide when a transfer happens or over what transport. Rows backfilled on 2026-10-04 (G4 item 6 g-1) from the tags already in rte_state_transfer.h; the wording restates the header text and adds no requirement content. Unit tests: tests/state_transfer/test_rte_state_transfer.c.
| ID | Requirement |
|---|---|
| REQ-STATE-TRANSFER-001 | No dynamic allocation; a fixed-size static field table (RTE_STATE_TRANSFER_MAX_FIELDS entries per registry) and caller-owned buffers for encode/decode. |
| REQ-STATE-TRANSFER-002 | rte_state_transfer_encode()/_decode() shall pack/unpack the registered fields in registration order, byte-for-byte, with no padding inserted between them. |
Three files under include/rte/dual/: rte_dual_msgchannel.h (Layer 1 "Channel", one EN 50159-defended message channel over one rte_netlink_handle_t), rte_dual_channel.h ("DualChannel", 1..N redundant Layer-1 links with always-send + bounded-ACK-wait delivery and connection-status tracking), and rte_dual_negotiator.h (own/peer rte_dual_state_t negotiation driven over an attached DualChannel). rte_dual_types.h and rte_dual_frames.h hold shared enums/wire structs with no behavior of their own beyond what the three modules above require.
| ID | Requirement |
|---|---|
| REQ-DUAL-TYPES-001 | rte_dual_state_to_string() and rte_dual_channel_status_to_string() shall return a non-NULL, static string for every defined enum value and a defensive "UNKNOWN_STATE"/"UNKNOWN_STATUS" respectively for an unrecognized one (same posture as rte_log_level_to_string(), REQ-OAL-LOG-013). |
| ID | Requirement |
|---|---|
| REQ-DUAL-MSGCHANNEL-001 | No dynamic allocation; caller supplies storage for every rte_dual_msgchannel_t. |
| REQ-DUAL-MSGCHANNEL-002 | rte_dual_msgchannel_send()/_receive() shall never block longer than the caller-supplied timeout waiting on rte_netlink_send()/_receive(). |
| REQ-DUAL-MSGCHANNEL-003 | rte_checksum_crc64_init() must already have been called (process-global, call-once) before any rte_dual_msgchannel_t send/receive is used. |
| REQ-DUAL-MSGCHANNEL-004 | rte_dual_msgchannel_receive() shall check the received frame's sender_id against expected_peer_id before verifying its CRC/sequence, and shall reject a mismatch with RTE_STATUS_HARDWARE_FAULT without advancing expected_sequence — the masquerade defense EN 50159 requires that rte_checksum_vital_message_verify() alone does not provide. |
| REQ-DUAL-MSGCHANNEL-005 | A CRC or sequence-continuity failure on receive shall yield RTE_STATUS_DATA_CORRUPTION and leave expected_sequence unchanged (the failed frame is not consumed into the sequence stream). |
| ID | Requirement |
|---|---|
| REQ-DUAL-CHANNEL-001 | No dynamic allocation; every rte_dual_channel_t holds a fixed array of at most RTE_DUAL_CHANNEL_MAX_LINKS redundant links. |
| REQ-DUAL-CHANNEL-002 | rte_dual_channel_send() shall always transmit the DATA frame on every configured link, regardless of any negotiated rte_dual_state_t — it is never gated on state (ADR-020 §2). |
| REQ-DUAL-CHANNEL-003 | After every configured link has been tried, rte_dual_channel_send() shall recompute the aggregate rte_dual_channel_status_t (FULL = all links up, DEGRADED = some, DOWN = none) and invoke config->status_callback only when the aggregate value actually changed. |
| REQ-DUAL-CHANNEL-004 | DATA traffic (rte_dual_channel_send()/_receive()) and STATE-beacon traffic (_send_state_frame()/_receive_state_frame()) share the same redundant links and the same up/down bookkeeping, but a STATE frame is fire-and-forget (no ACK wait) and shall never be counted toward or against DATA's own ACK accounting. |
| REQ-DUAL-CHANNEL-005 | rte_dual_channel_receive() and _receive_state_frame() shall poll every configured link on every call, even after an earlier link in the same sweep already staged a frame — stopping early would let one link (e.g. one with consistently shorter latency) starve every other redundant link of its own auto-ACK indefinitely. |
| REQ-DUAL-CHANNEL-006 | An inbound frame shorter than this layer's own 4-byte rte_dual_frame_header_t, or shorter than the full fixed frame its kind implies, shall be reported as RTE_STATUS_DATA_CORRUPTION rather than silently ignored or misinterpreted. |
| REQ-DUAL-CHANNEL-007 | rte_dual_channel_send()'s per-link ACK-wait loop shall keep polling for further frames within config->ack_timeout_ms even when rte_timer_now() shows no measurable progress between polls (a real round trip may legitimately complete within a single timer tick) — bounded by a fixed cap (RTE_DUAL_CHANNEL_STALL_POLL_LIMIT) on consecutive no-progress polls, so a link with a genuinely non-advancing or absent timer OSAdapter still cannot spin unboundedly. Added post-acceptance after a live run over a real transport (ADR-022's SITE migration) surfaced that the prior behavior gave up after exactly one poll — see ADR-020's "Post-acceptance fix" section. |
| REQ-DUAL-CHANNEL-008 | If no link produces a usable frame/ACK before rte_dual_channel_send()/_receive() return, and at least one link's own underlying send/receive reported RTE_STATUS_HARDWARE_FAULT (a closed/reset connection, not just "nothing arrived within this poll"), that status shall be returned instead of the generic RTE_STATUS_TIMEOUT every other "nothing usable this call" case returns. Found via a RBC_GP container-topology failover test: rte_dual_channel_send()/_receive() previously collapsed every non-success outcome — a genuinely dead TCP connection (peer container restarted) exactly as much as an ordinary "peer hasn't answered yet" — into the same RTE_STATUS_TIMEOUT, so a consumer's own reconnect logic (ab_gp_channel_negotiate_execute(), today through rte_site_role_execute(), REQ-SITEROLE-013, gating link teardown on "status is neither OK nor TIMEOUT") could never distinguish the two and never reconnected, leaving one side listening forever for a peer that had already come back up on a fresh socket. A malformed/corrupted frame (RTE_STATUS_DATA_CORRUPTION) on an otherwise-healthy link is deliberately NOT included in this escalation — it does not indicate a broken transport, only a defended-integrity rejection of one bad frame (REQ-DUAL-CHANNEL-006), and continues to fold into the generic RTE_STATUS_TIMEOUT as before. |
| ID | Requirement |
|---|---|
| REQ-DUAL-NEGOTIATOR-001 | No dynamic allocation; caller supplies storage and an already-initialized rte_dual_channel_t. |
| REQ-DUAL-NEGOTIATOR-002 | rte_dual_negotiator_execute() shall never call rte_safestate_enter() itself — deciding what a sustained RTE_DUAL_STATE_UNKNOWN means for safety stays an application policy decision (ADR-020 §4's "no automatic safety reaction" non-goal). |
| REQ-DUAL-NEGOTIATOR-003 | The initial ONLINE-vs-STANDBY decision shall use an older-startup-timestamp-wins rule, with each side's configured own_id/peer_id as a deterministic fallback only on an exact timestamp tie (the rule of the former RBC_GP site.c decide_online(); consumer today: rte_site_role_start(), ADR-040). |
| REQ-DUAL-NEGOTIATOR-004 | The HOT/COLD determination for whichever side is currently STANDBY shall always be derived from the ONLINE side's own channel-degradation bit — never from the STANDBY side's own self-reported degradation, and never from the ONLINE side's opinion of its own label. This applies symmetrically regardless of which side (own or peer) is the one currently ONLINE. |
| REQ-DUAL-NEGOTIATOR-005 | Loss of peer contact for longer than config->peer_lost_timeout_ms shall set peer_state to RTE_DUAL_STATE_UNKNOWN; own_state shall degrade to RTE_DUAL_STATE_UNKNOWN too unless it was already RTE_DUAL_STATE_ONLINE, in which case it shall remain RTE_DUAL_STATE_ONLINE (an active instance keeps acting without needing continuous peer confirmation). |
Pure decision function and timing predicates of site-role negotiation (ONLINE/STANDBY), moved from RBC_GP in ADR-040 step 1. No I/O and no state of its own.
| ID | Requirement |
|---|---|
| REQ-SITEROLE-001 | rte_site_role_decide() shall evaluate its rules in the documented order (both FAULTED; counterpart FAULTED; recovered; dispatcher-confirmed takeover; dual ONLINE; both STANDBY; follow the ONLINE A/B sibling) and return the action of the first that applies. |
| REQ-SITEROLE-002 | A PROMOTE shall be turned into WAIT unless this channel is ready and its A/B sibling is ONLINE, ready, or unreachable. |
| REQ-SITEROLE-003 | Silence alone shall never promote: rte_site_role_takeover_allowed() is true only for a STANDBY, non-FAULTED channel with an unresponsive counterpart and a dispatcher confirmation at most RTE_SITE_ROLE_TAKEOVER_CONFIRM_VALID_CYCLES old. |
| REQ-SITEROLE-004 | A cycle counter that restarted since a confirmation or snapshot was stamped shall make the confirmation expired and the snapshot stale. |
| REQ-SITEROLE-010 | rte_site_role (ADR-040 step 2) shall use no dynamic allocation; the caller supplies the service storage and both frame buffers. |
| REQ-SITEROLE-011 | The role shall change only through rte_site_role_decide() or the startup negotiation, and every change shall be reported to the application's change callback before the call that made it returns. |
| REQ-SITEROLE-012 | A per-cycle reconnect attempt shall be bounded by reconnect_attempt_ms; only rte_site_role_start() blocks, bounded by startup_timeout_ms. |
| REQ-SITEROLE-013 | The link shall be closed for reconnect after send_miss_threshold consecutive unacknowledged sends or on any receive failure other than a timeout; a STANDBY channel then marks the counterpart unresponsive and stays STANDBY. |
| REQ-SITEROLE-014 | The role of a channel and of its A/B sibling shall agree in {online, single_mode}; RTE_SITE_ROLE_SIBLING_MISMATCH_LIMIT (3) consecutive disagreeing checks shall be reported as a fault (RTE_SITE_ROLE_SIBLING_FAULT, NEGOTIATION_MISMATCH event); one agreeing check resets the count; no sibling state yet is no verdict. |
| REQ-SITEROLE-015 | With a MAC verifier configured, a dispatcher takeover confirmation shall be accepted only as a token whose version, site id and channel id match the channel, whose challenge equals the channel's current challenge and whose MAC the verifier accepts. The challenge shall be drawn from rte_random, replaced after every accepted token and after challenge_lifetime_cycles; while no challenge is held no token shall be accepted. The unauthenticated confirmation shall then be refused. Every rejection shall be logged. |
| REQ-SITEROLE-016 | With a sibling source configured, rte_site_role shall poll it once per rte_site_role_execute(), derive the A/B sibling's online, ready and reachable facts itself (reachable = a frame within sibling_reach_cycles, default 3, cycles), run the REQ-SITEROLE-014 check after the role decision of the same cycle and expose the verdict through rte_site_role_sibling_verdict(); without a source the application-supplied facts and rte_site_role_check_sibling() apply unchanged. |
| REQ-SITEROLE-017 | With a sibling source configured, rte_site_role_start() shall poll it once before the inter-site negotiation; when a sibling frame is known the channel shall start in the sibling's role - ONLINE if the sibling reports ONLINE, STANDBY otherwise - with the negotiator seeded to that state (resume_own_state), so this channel does not evaluate the REQ-DUAL-NEGOTIATOR-003 tie-break; the frame's ready and single_mode shall not take part (REQ-SITEROLE-002 gates promotions, REQ-SITEROLE-014 checks single_mode from the first execute); without a frame, or without a source, the tie-break applies unchanged and the startup timestamp is never changed by this rule. |
Hides rte_netlink/rte_ipc from application code: an application opens one rte_safechannel_t by type and host/port endpoints and never holds a rte_netlink_handle_t itself.
| ID | Requirement |
|---|---|
| REQ-SAFECHANNEL-001 | rte_safechannel_open() shall open every configured endpoint itself via the registered rte_netlink OSAdapter; the caller shall never need to call rte_netlink_open() or hold a rte_netlink_handle_t. |
| REQ-SAFECHANNEL-002 | No dynamic allocation; all storage (rte_safechannel_t, including its opened links and wrapped rte_dual_channel_t/rte_channel_t) is caller-owned and fixed-size, sized to RTE_SAFECHANNEL_MAX_LINKS. |
| REQ-SAFECHANNEL-003 | rte_safechannel_send()/_receive() shall behave identically to the caller regardless of config.type — a uniform facade over rte_dual_channel_t/rte_channel_t. |
CRC-64 computation and a "vital message" envelope (sequence + sender + CRC-64) used by rte_dual_msgchannel (REQ-DUAL-MSGCHANNEL-003) and rte_checkpoint for cross-channel/cross-site data integrity. Depends only on rte_timer (best-effort message timestamping — see REQ-CHECKSUM-005's note that a missing timer OSAdapter degrades gracefully, not a hard failure of message creation), not on rte_log or rte_safestate despite once #include-ing both unused.
| ID | Requirement |
|---|---|
| REQ-CHECKSUM-001 | rte_checksum_crc64_init() shall be callable exactly once; a subsequent call before any re-init mechanism exists shall return RTE_STATUS_ALREADY_INITIALIZED and leave the already-selected table/polynomial unchanged. |
| REQ-CHECKSUM-002 | rte_checksum_crc64() shall return 0 — never dereferencing data — if the module is not yet initialized, if its lookup table is unset, or if data is NULL while size is nonzero. 0 is not a valid checksum; the status-returning API shall not report success in this state (REQ-CHECKSUM-009). |
| REQ-CHECKSUM-003 | rte_checksum_crc64() shall be deterministic and O(n) in size, using a precomputed 256-entry lookup table. |
| REQ-CHECKSUM-004 | rte_checksum_crc64_verify() shall report RTE_STATUS_DATA_CORRUPTION (not merely a boolean) on mismatch and increment stats.verification_failures; on match it shall return RTE_STATUS_OK and increment stats.verification_passes. Before rte_checksum_crc64_init() it shall return RTE_STATUS_NOT_INITIALIZED with result_out->match == 0 and no stats change. |
| REQ-CHECKSUM-005 | rte_checksum_vital_message_create() shall reject a payload larger than sizeof(rte_vital_message_t::payload) with RTE_STATUS_INVALID_PARAM, incrementing stats.payload_oversize, without writing msg_out. |
| REQ-CHECKSUM-006 | rte_checksum_vital_message_verify() shall verify the message's CRC-64 before trusting any other field, and report RTE_STATUS_DATA_CORRUPTION on a CRC mismatch or a sequence_number that does not equal the caller-supplied expected_sequence (incrementing stats.sequence_errors in the latter case), and forward RTE_STATUS_NOT_INITIALIZED from the CRC check, in both cases without writing payload_out/payload_size_out. |
| REQ-CHECKSUM-007 | rte_checksum_vital_message_verify() shall reject a decoded payload_size exceeding the caller's payload_max_size with RTE_STATUS_INVALID_PARAM, incrementing stats.payload_oversize, without copying into payload_out. |
| REQ-CHECKSUM-008 | rte_checksum_get_stats()/_reset_stats() are diagnostics-only (never on a safety-decision path); _get_stats() returns RTE_STATUS_INVALID_PARAM for a NULL stats_out, otherwise both always return RTE_STATUS_OK. |
| REQ-CHECKSUM-009 | rte_checksum_crc64_verify(), rte_checksum_vital_message_create() and rte_checksum_vital_message_verify() shall return RTE_STATUS_NOT_INITIALIZED and write neither msg_out nor the payload outputs nor any stats counter while the module is not initialized (ISS-037). (ISS-037 step 1, 2026-10-06.) |
Entirely RBC_GP-side (no framework header changes) - kept here per this document's own cross-repo convention (REQ-APPMANAGER-011 already set this precedent for a RBC_GP-discovered requirement). See ADR-029 for the full design and the live debugging that produced several of these.
These are RBC_GP requirements: their tags live in RBC_GP's sources, not in this product's include/ (section 4). As of 2026-10-04: REQ-RBC-001 in src/application/AB/common/rbc_wire_types.h, common_config.h and src/application/AB/GP/train/ab_gp_train.c; REQ-RBC-002 in ab_gp_train.c, src/application/AB/GP/ertms_procedures/ab_gp_proc_dispatch.h/.c and src/application/AB/GP/ertms/train/ab_gp_ertms_train_message_codec.c; REQ-RBC-003/-004 in rbc_wire_types.h; REQ-RBC-006 in src/application/AB/GP/com/ab_gp_channel_negotiate.c, src/application/AB/GP/train/ab_gp_train.h and common_config.h; REQ-RBC-007 in ab_gp_train.h and ab_gp_channel_negotiate.c (written there as REQ-RBC-006/007); REQ-RBC-008 in src/application/AB/GP/com/ab_gp_channel_crosscompare.c; REQ-RBC-005 in ab_gp_train.c (train_id_in_range()), src/application/AB/GP/ertms_procedures/ab_gp_ertms_engine.c, ab_gp_proc_dispatch.c, src/application/AB/GP/com/ab_gp_saf_train.c (slot_of(), ab_gp_saf_train_is_connected()) and src/application/AB/GP/db/ab_gp_db.c (route connect/disconnect, per-train accessors); REQ-RBC-009 in src/application/C/gateway_c_common.c and src/application/C/gateway_c_train_handler.c. RBC_GA's IL and CTC Gateway handlers (gateway_c_il_handler.c, gateway_c_ctc_handler.c) set up their sim channels the same way and carry no tag.
| ID | Requirement |
|---|---|
| REQ-RBC-001 | REVISED (this session, wording only — see status note below). The RBC envelope (rbc_envelope_t, rbc_wire_types.h) shall carry a real Subset-026-style header — NID_MESSAGE (message identity), L_MESSAGE (message length in bytes, genuinely variable per message kind), T_TRAIN (message timestamp) — and MAY be variable-length per message kind, decoded/encoded via L_MESSAGE rather than a single fixed frame size for every kind. This supersedes the prior fixed-28-byte-for-every-kind mandate to allow real Subset-026 message/packet shapes (nested/N_ITER-repeated packets, e.g. Packet 15 Movement Authority) that do not fit a flat fixed-size struct — see RBC_Test_Sim_Train/src/train/message_catalog.json's _subset026Reference/_subset026PacketsReference for the real field/packet data this now needs to support. Status: partially implemented (this session). rbc_wire_types.h/rbc_wire.c (C) and RBC_Test_Sim_Core/src/simcore/rbc_wire.py (Python mirror) now carry a real, always-fully-populated 80-byte envelope (RBC_ENVELOPE_WIRE_SIZE/ENVELOPE_SIZE) with genuine NID_MESSAGE/L_MESSAGE/T_TRAIN header fields, plus the real flat Subset-026 content fields for message 146 (t_train_ack) and message 136 (the ten real Packet 0 fields — nid_lrbg, q_dirlrbg, q_dlrbg, l_doubtover, l_doubtunder, q_length, v_train, q_dirtrain, m_mode, m_level). L_MESSAGE genuinely varies by kind (content_size_for_kind()/build_envelope()'s own calculation) and is checked on decode, but the frame itself is NOT actually variable-length on the wire — every kind still occupies the same fixed 80-byte slot, with fields not meaningful for a given kind simply left zero, specifically to avoid the C/Python offset-aliasing bug this approach was chosen to sidestep (see rbc_wire.c's own header comment). Byte-for-byte C/Python encode parity verified for both message 146 and 136. Still NOT done: a genuinely variable-length wire frame (so an unrelated kind doesn't pay for fields it never uses), and the nested/N_ITER-repeated packet content (e.g. Packet 15 Movement Authority for message 3) — both remain separately-scoped follow-on work; check rbc_wire_types.h's own header comment for the current exact field layout before relying on either doc. |
| REQ-RBC-002 | rbc_wire_decode() shall reject a frame whose leading kind byte is not a recognized rbc_msg_kind_t value, returning false and leaving *out_env unmodified, rather than casting an out-of-range byte into the enum. |
| REQ-RBC-003 | Every link that can carry more than one distinct envelope within a single report cycle (C's Train/IL/A/B links, A/B's own link from C) shall demultiplex arrivals through a fixed-capacity single-producer/single-consumer queue (rbc_envelope_queue_t, RBC_ENVELOPE_QUEUE_CAPACITY) drained fully every cycle, not a single-slot "latest value only" primitive - a link carrying interleaved multi-train traffic can legitimately receive more than one distinct event before the next drain. |
| REQ-RBC-004 | rbc_envelope_queue_push() shall return false (dropping the new entry) rather than growing dynamically when the queue is full (RBC_ENVELOPE_QUEUE_CAPACITY unread entries already pending); the caller shall log the drop rather than fail silently. |
| REQ-RBC-005 | Any code path that indexes a per-train array (ctx->sessions[], db->trains[], s_train_contexts[], etc.) by a wire-supplied train_id shall validate 1 <= train_id <= RTE_EXAMPLE_MAX_TRAINS first and drop (logged) an out-of-range value, never indexing out of bounds with unchecked wire input. |
| REQ-RBC-006 | On a STANDBY-to-ONLINE promotion (ab_gp_channel_negotiate.c's apply_state_transfer()), the whole train-session table (ctx->sessions[]) shall be overwritten from the transferred snapshot unconditionally - unlike the transferred cycle counter (still gated by RTE_EXAMPLE_TRANSFER_POLICY_ENV), there is no operator-configurable "restart" policy for live train sessions: a promoted site refusing to remember an in-flight Movement Authority would be unsafe, not a preference. |
| REQ-RBC-007 | Cross-compare (ab_gp_channel_crosscompare.c) shall vote on the whole per-train session state relevant to the RBC's own decisions (in_use/train_id/cycle/d_lrbg/granted_length/ma_seq/ma_acked), not a single scalar value - ma_pending_send and the CTC-notification-sent flags are local scratch only and shall be excluded, since they carry no cross-compare-relevant decision content. The compared payload shall be the wire-encoded form, not the raw train_session_t struct, since the cross-comparator (rte_cross_comparator_execute_buffers(), as GP calls it, with no compare callback configured) compares via raw memcmp() and the struct's mixed-width members admit compiler-inserted padding a raw comparison would treat as significant. |
| REQ-RBC-008 | ab_gp_channel_crosscompare_execute() shall skip (not disagree) cross-comparing a given train for up to RTE_EXAMPLE_XCOMPARE_SYNC_SKIP_LIMIT consecutive cycles while its local and peer session snapshots do not yet field-match, to tolerate the ordinary asynchronous-arrival timing skew between two independently-cycling channels; past that bound it shall fall through to the real comparator so a genuine, persistent divergence (e.g. a relay datagram lost to only one channel) is still detected and reacted to, not silently skipped forever. |
| REQ-RBC-009 | REVISED 2026-10-04 (G4 item 6 g-3; the implementation reference changed, the intent did not). The start-up of a role-C Gateway service (gateway_c_init() in src/application/C/gateway_c_common.c, which calls the service's handler_init, e.g. gateway_c_train_handler_init()) shall NOT wait for an external Train/IL/CTC simulator, since such a sim is a genuinely external, opportunistically-connecting client that may not be running yet. Each sim channel (c-sim-train, c-sim-il, c-sim-ctc; listen role in etc/config/c_{west,east}.json) shall be set up without a peer (the POSIX channel service only binds the local port, non-blocking), shall take its peer from the datagrams it receives (the most recent sender), so a sim that starts after C, or restarts on a new source port, is reached without restarting C, and a send before the first datagram returns RTE_STATUS_NOT_CONNECTED (REQ-CHANSVC-001), which the handler shall treat like RTE_STATUS_OK (nothing sent, nothing logged). The A/B relay links are outside this requirement. The previous wording named monitor_c_init(), whose per-link background rx tasks started with a NULL handle and connected lazily on a reconnect-forever loop (ADR-029 section 2.2); that function no longer exists in RBC_GP. |
Every REQ-* ID in this document is meant to appear verbatim in the Doxygen comment of the header that declares the realising function, under include/ (both include/rte/ and the OSAdapter vtable headers in include/rte_osadapter/). A range row (e.g. REQ-COMMON-BUF-020..025) stands for every ID in its range. Exceptions, which have no tag in include/ by design:
REQ-RBC-001..009 (section 3h): RBC_GP requirements, tagged in RBC_GP's sources (see the note above that table), not here.
Re-check of 2026-10-04 (G4 item 6 g-2), range rows expanded:** 237 distinct IDs are tagged in include/; this document holds 251 IDs (248 in its tables plus the three REQ-OAL-BACKEND-* IDs of section 2).
A comparison that does not expand range rows also reports REQ-COMMON-BUF-021..025, -027..031 and REQ-COMMON-STR-022..024, -026..028, -030..032 as missing (the range-start IDs BUF-020, BUF-026, STR-021, STR-025, STR-029 match their rows literally); the range rows of sections 1.3 and 1.6 cover them.
2026-10-06 (ISS-036 step 1):** REQ-WATCHDOG-003 and REQ-WATCHDOG-004 added (section 3b) and tagged in rte_watchdog.h / rte_safestate.h: 239 distinct IDs tagged in include/, 253 IDs here (250 in the tables plus the three REQ-OAL-BACKEND-* IDs); the 14 row-without-tag IDs are unchanged.
2026-10-06 (ISS-037 step 1):** REQ-CHECKSUM-009 added (section 3g) and tagged in rte_checksum.h: 240 distinct IDs tagged in include/, 254 IDs here (251 in the tables plus the three REQ-OAL-BACKEND-* IDs); the 14 row-without-tag IDs are unchanged.
2026-10-06 (G1 step 5b-1):** REQ-SITEROLE-016 added (section 3e, site role) and tagged in rte_site_role.h: 241 distinct IDs tagged in include/, 255 IDs here (252 in the tables plus the three REQ-OAL-BACKEND-* IDs); the 14 row-without-tag IDs are unchanged.
2026-10-06 (G1 step 5b-3a):** REQ-SITEROLE-017 added (section 3e, site role) and tagged in rte_site_role.h: 242 distinct IDs tagged in include/, 256 IDs here (253 in the tables plus the three REQ-OAL-BACKEND-* IDs); the 14 row-without-tag IDs are unchanged.
The check before 2026-10-04 used REQ-[A-Z]+-[A-Z]+-[0-9]+, which matches only three-part IDs and so missed every two-part ID such as REQ-VOTER-001 or REQ-CHANSVC-001 (162 IDs found instead of 221 at the time).
Regenerate the source-side list (grep -rhoE "REQ-[A-Z]+(-[A-Z]+)*-[0-9]+" include | sort -u) whenever a module is added or changed, and update both this file and the source comment together — they are required to stay in sync by inspection, since there is currently no automated cross-check between the two (a natural next step: a CI script that fails if a REQ-* ID appears in code but not here, or vice versa, ignoring the documented exceptions above).