Safe API Framework
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
Loading...
Searching...
No Matches
ADR-032: Setup-phase lock coverage extended to backend registration and remaining setup-only constructors

Status: Accepted Date: 2026-08-20 Applies to: src/timer/sapi_timer.c, src/ipc/sapi_ipc.c, src/task/sapi_task.c, src/netlink/sapi_netlink.c, src/nvm/sapi_nvm.c, src/log/sapi_log.c, src/clocksync/sapi_clocksync.c, src/reboot/sapi_reboot.c, src/memory/sapi_memory.c, src/safestate/sapi_safestate.c, src/ipc/sapi_ipc_pubsub.c, src/ipc/sapi_ipc_request_reply.c.

1. Context

A downstream integrator (safeAPIRBC2oo2) asked this framework to review itself end to end for exactly the class of gap ADR-026 introduced sapi_lifecycle_check_setup_allowed() to close: "can a resource with process-lifetime scope still be created/registered after the application has started running, when it should only ever be creatable during setup." ADR-026 §2.2 enumerates the constructors it gated as a closed list (sapi_timer_create, sapi_channel_init, sapi_voter_init/ _register_channel, sapi_cross_comparator_init/_register_channel, sapi_watchdog_create) and a separate, deliberate exclusion list (sapi_netlink_open, sapi_dual_channel_init, sapi_dual_negotiator_init

  • legitimately re-invoked post-lock for reconnect). Neither list mentions any sapi_*_register_backend() function, sapi_mem_pool_create(), sapi_safestate_register_handler(), or the two sapi_ipc_pubsub/ sapi_ipc_request_reply creation functions - these were simply not considered when ADR-026 was written, not intentionally left open. A repository-wide audit against ADR-026's own criteria ("is this a one-time 'wire this up' call, or a per-cycle 'do this now' call") confirmed all of them are the former.

This matters specifically for the _register_backend() family: a backend vtable is process-lifetime, integrator-supplied wiring - exactly ADR-026's own "resource an application's own design did not already account for" concern (§2.2). Before this ADR, nothing stopped execute()/pre_execute() from swapping a running application's memory- pool or NVM backend mid-run, which is a considerably sharper hazard than adding a new timer would be (it can silently redirect already-in-flight state to a different backend implementation).

2. Decision

Extend ADR-026 §2.2's gated list with the same guard, in the same place (after NULL/param validation, before any real work), returning the same SAPI_STATUS_INVALID_STATE on a locked setup phase:

sapi_nvm_open() and sapi_log_init() were evaluated and NOT gated - see the "Deferred" note below.

No new malloc-based default backend was added anywhere in the framework as part of this ADR. This was raised as a candidate (a pluggable allocator, falling back to malloc() if no backend is registered) but rejected once a repository search found safeAPIRBC2oo2/src/posix_backend/sapi_posix_backend_memory.c already implements a complete, working sapi_mem_pool_backend_t using a static compile-time-sized arena (g_arena[SAPI_POSIX_MEM_ARENA_SIZE]) with a bump allocator and intrusive free list - zero malloc()/free() calls. Adding a malloc-backed default to the framework itself would reintroduce dynamic allocation exactly where this framework's own CLAUDE.md ("No dynamic memory: malloc, free, and realloc are strictly banned") and ADR-005's "backend is integrator-supplied" philosophy both already keep it out - a real regression in rigor for a convenience an already- compliant integrator does not need. CLAUDE.md's existing rule required no amendment: it remains categorically true (zero malloc/free/ realloc call sites anywhere in include//src/) rather than needing a new stated exception.

Deferred: sapi_nvm_open(), sapi_log_init()

Both are structurally the initialization half of a _register_backend() pair, but neither has a settled answer to "is a post-lock re-open ever legitimate" the way sapi_netlink_open() does (ADR-026 §2.2's own reconnect rationale, backed by a real downstream consumer that does it). Gating them speculatively risks the same kind of break ADR-026 §2.2 already had to reason carefully about for netlink/dual - left ungated until a real use case (or its absence) is confirmed, rather than guessed at here.

3. Consequences

  • Positive: closes the gap the audit found - every remaining one-time "wire this up" call in the framework now enforces the same fixed-and- auditable-resource-set property ADR-026 established for timers/ channels/voters/cross-comparators/watchdogs.
  • Positive: the _register_backend() gating in particular closes a sharper hazard than ADR-026's original list did (a live backend swap mid-run, not just a new resource of an already-used kind).
  • Negative: sapi_ipc_pubsub_topic_create()/sapi_ipc_rr_server_create()/ _client_create() are gated ahead of having real bodies - low present risk (nothing to protect yet) but means the guard's placement was chosen without a concrete implementation to validate ordering against; worth re-checking once those modules gain real backend dispatch.
  • Neutral: sapi_nvm_open()/sapi_log_init() remain exactly as permissive as before this ADR - not a regression, just an explicitly acknowledged open question rather than a silent gap.

4. Verification

  • cmake --build build + ctest --test-dir build --output-on-failure: full framework rebuild and test suite, confirming no gated function's existing legitimate call site (all of which already run during init(), per ADR-026 §3's own "zero source changes required for any existing consumer" finding) regressed.
  • safeAPIRBC2oo2 (downstream consumer): full clean rebuild, ctest, and .claude/skills/run-safeAPIRBC2oo2/smoke.sh matching its established baseline - confirms none of its own _register_backend()/ sapi_mem_pool_create()/etc. call sites (all setup-phase, per the same ADR-026 §3 finding) are affected either.

5. Location

  • The fourteen functions enumerated in §2 above, across the ten files listed at the top of this document.
  • docs/architecture/ADR-026-application-setup-phase-lock.md (base decision this one extends - not itself edited, kept as the historical record of what was gated when).