Safe API Framework 0.1.0 build 384e6ae
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
Loading...
Searching...
No Matches
ADR-033: rte_mutex - a portable mutual-exclusion primitive

Status

Accepted

Context

RBC_GP (the reference application built on this framework) guards a netlink handle shared between its own cyclic-executive thread and a background relay-rx task with a mutex (ctx->peer_send_mutex, used by channel_ab_checkpoint.c's own checkpoint send/recv adapter and channel_ab_io.c's own peer-sample/relay-envelope/site-state sends). Found live, during an architecture review of every "backend"-named symbol the application uses: that mutex was declared as a raw pthread_mutex_t and manipulated with pthread_mutex_init()/_lock()/ _unlock()/_destroy() directly, with <pthread.h> included straight into an application header (channel_ab_types.h), not confined to a POSIX-backend implementation file.

This violates this project's own foundational premise (ADR-001/ADR-005): an application built on Platform_RTE should depend only on this framework's own OS-Abstraction-Layer API, never on a specific platform's threading primitives directly - the whole point of the backend-dispatch pattern every other OAL service (rte_timer, rte_task, rte_ipc, rte_netlink, ...) already follows is that swapping the platform/RTOS underneath an application should mean swapping one backend registration call, not auditing every application source file for platform-specific API usage. A pthread_mutex_t baked into an application-level struct defeats that guarantee outright: porting this application to a target with no pthreads (a bare-metal RTOS, for instance) would require rewriting application code, not just registering a different backend.

Checked before deciding on a fix: does Platform_RTE already expose any* portable synchronization primitive under a different name (a critical-section API bundled into rte_task, for instance)? It does not - the OAL surface (clocksync/ipc/log/memory/netlink/nvm/ reboot/task/timer) has no mutex/lock/semaphore module at all. So this is not "the application should have called an existing framework function instead" - the framework itself was missing the primitive, the same gap-class ADR-031 (rte_mem_util, for <string.h>) and ADR-026/032 (rte_lifecycle, for setup-phase gating) already closed this project's history.

Decision

Add rte_mutex as a new OAL-layer service, following the exact same consumer/backend split every other OAL service uses (ADR-021):

The POSIX backend implementation (rte_posix_osadapter_mutex.c, a thin wrapper over pthread_mutex_init()/_lock()/_unlock()/_destroy()) lives in RBC_GP's own src/posix_osadapter/, not in Platform_RTE - matching every other OAL backend's location per ADR-018's own "a backend is integrator-supplied, not part of the reusable framework" philosophy. This is exactly where pthread_mutex_t usage belongs: confined to the one file whose entire job is adapting a specific platform to this framework's portable API, never leaking into application code above it.

RBC_GP's own channel_ab_types.h/channel_ab.c/ channel_ab_checkpoint.c/channel_ab_io.c were migrated to rte_mutex_handle_t + rte_mutex_storage_t, dropping <pthread.h> from application code entirely (the one remaining pthread_join() use inside rte_task_destroy()'s own doc-comment cross-reference is a framework-internal detail the application only reads about, not calls directly).

Consequences

  • Every application built on this framework now has a portable mutex available without reaching for platform threading primitives directly.
  • The POSIX backend's own rte_posix_osadapter_register_all() gained one more call (rte_osadapter_mutex_register(rte_posix_osadapter_mutex())); any other backend integrator adding a new target must do the same.
  • channel_ab_negotiate.c's own direct select()/read() on STDIN_FILENO (the interactive ASK_USER transfer-policy prompt) was reviewed in the same pass and found to be a second, separate instance of the same class of violation - deliberately left out of this ADR's scope; it has no framework equivalent to migrate to yet (no portable console/stdin abstraction exists) and was called out to the user as an explicitly separate, not-yet-scoped follow-up.
  • Timed lock (2026-10-03, OSA rule R2 "every wait bounded", REQ-OAL-MUTEX-014): rte_mutex_lock_timed(handle, timeout_ms) and a fifth vtable member lock_timed, appended last so an older four-entry positional initialiser stays valid C with the slot NULL (the call then returns RTE_STATUS_NOT_SUPPORTED). RTE_STATUS_OK means the lock is held; RTE_STATUS_TIMEOUT means it was not acquired within timeout_ms and is not held; timeout_ms == 0 is one non-blocking try (the rte_ipc rule); any other platform failure, including a failed clock read, is RTE_STATUS_INTERNAL_ERROR and never an unbounded wait. Not lifecycle-gated, like rte_mutex_lock(). The POSIX OSAdapter uses pthread_mutex_timedlock() on Linux, whose deadline is absolute on CLOCK_REALTIME: a wall-clock step during a wait shortens or lengthens that one wait (pthread_mutex_clocklock(CLOCK_MONOTONIC) would avoid this, but glibc accepts it on a priority-inheritance mutex only with FUTEX_LOCK_PI2 support). Other POSIX hosts (macOS) poll with pthread_mutex_trylock() against a CLOCK_MONOTONIC deadline, without a priority-inheritance boost while polling - development hosts only. rte_mutex_lock() stays as it is; moving callers (first the POSIX memory-pool lock) to the timed lock is a later step.

Verification

  • Platform_RTE: full clean rebuild + ctest (matching the existing suite's own pass/fail baseline - no test added specifically for rte_mutex beyond compilation, matching rte_timer's own precedent of relying on downstream consumers, not a dedicated unit test, since the backend dispatch logic is identical in shape to every other already-tested OAL service).
  • RBC_GP: full clean rebuild + ctest, smoke.sh, and the real Docker-based safeAPITestEnv Robot Framework suite, after migrating off pthread_mutex_t - confirmed no behavior change (the POSIX mutex backend's own locking semantics are identical to what the application called directly before, just routed through the OAL dispatch layer).
  • Timed lock (2026-10-03): tests/mutex/test_rte_mutex.c (dispatch with a fake OSAdapter: NULL handle, no OSAdapter, NULL slot, timeout and result passed through unchanged); the real POSIX timed lock against a second thread holding the mutex in RBC_GP's tests/posix_osadapter/test_rte_posix_osadapter_mutex.c (macOS and Linux).

Location