RTE OSAdapter POSIX 0.1.0 build 41b3eb3
POSIX/Linux OAL OSAdapter for Platform_RTE
Loading...
Searching...
No Matches
OSA rules audit (2026-10-02)

The OSA Capella model (OSA-ARCH-CAP 0.1, 2026-10-02) sets six rules for the OS layer under RTE. This page records how this adapter stands against them (gap G4 of the root docs/architecture/CAPELLA_ARCHITECTURE_BASELINE.dox). Read-only audit of every source file; the two plain bugs it found are fixed (section "Fixed"), the rest is open.

Rules and verdicts

Rule Verdict Main evidence
R1 Threads created only at init, static priority, fixed stack Not met pthread_create happens in start (task.c, timer.c), which restarts the thread; task threads: SCHED_FIFO, priority and stack size set on the attributes before creation since 2026-10-03, every return code checked, refusal reported with the ADR-018 inherited-scheduling fallback (see "Fixed"); timer threads: the creating thread's own policy and priority (explicit) and a fixed stack size (RTE_POSIX_TIMER_STACK_SIZE, 8 MiB) set on the attributes before creation since 2026-10-03, same checks and fallback (see "Fixed"), no priority of their own; task stack size optional (0 = default stack)
R2 Priority-inheritance mutexes; every wait bounded Not met PI mandatory in mutex.c since 2026-10-03 (create fails, no fallback); timed lock in the vtable since 2026-10-03 (lock_timed; Linux deadline on CLOCK_REALTIME, see "Fixed"); lock (pthread_mutex_lock) stays unbounded but has no production caller; memory-pool lock PI, created at init, and bounded (RTE_POSIX_MEM_LOCK_TIMEOUT_MS, default 50 ms) since 2026-10-03; every timer and task thread joined, one-shot and run-once included, join result checked, since 2026-10-04 (item 7 a, see "Fixed"); stop/suspend/destroy wake a timer or periodic task thread that waits for its deadline (one process-wide PI mutex + condition variable) instead of waiting up to one period, since 2026-10-04 (item 7 b-1, see "Fixed"), but pthread_join still waits with no bound for a callback/entry that is running (item 7 b-2); fprintf(stderr) logging takes the stdio lock
R3 Monotonic clock for cycles and supervision Met for cycles; one known limit Timer and periodic task threads paced by an absolute CLOCK_MONOTONIC deadline on every host since 2026-10-04 (item 8; since item 7 b-1 a timed condition wait: non-Apple hosts pthread_cond_timedwait() to the absolute deadline on a CLOCK_MONOTONIC condition variable, macOS pthread_cond_timedwait_relative_np() of the time left, the clock read again after every wake-up), no drift by the callback's run time; a failed clock read or wait is reported and stops the loop, never used as time 0 (items 8 and 7 b-1, see "Fixed"); IPC, netlink and channel-service deadlines on CLOCK_MONOTONIC. Known limit: the Linux timed-lock deadline is on CLOCK_REALTIME (R2 row, pthread_mutex_clocklock not usable on a PI mutex); the wake-mutex bound of item 7 b-1 inherits it, but the expiry is decided on CLOCK_MONOTONIC
R4 No dynamic allocation in operation Partly met No malloc/free. But getaddrinfo runs in netlink connect (reconnects are allowed after init by ADR-026) and in channel-service lazy re-init on any cycle while the socket is not open; pthread_create on each task/timer restart; rte_ipc_create, rte_nvm_open not gated
R5 Fixed-size IPC, non-blocking with timeout Partly met Netlink and channel service are non-blocking with deadlines; SO_RCVBUF/SO_SNDBUF from the link config (rcvbuf_bytes/sndbuf_bytes), set and confirmed before the link is handed out since 2026-10-03, but no production config sets them yet (see "Fixed"); IPC: queue_depth enforced, whole fixed-size messages only and pipe capacity checked at create since 2026-10-03 (see "Fixed"); posix_socket_open sets O_NONBLOCK since 2026-10-03 (checked, see "Fixed"); a channel-service listen-role send with no known peer sends nothing and returns RTE_STATUS_NOT_CONNECTED since 2026-10-04 (Platform_RTE REQ-CHANSVC-001, see "Fixed")
R6 Violations reported to RTE diagnostics as critical Not met The adapter never calls rte_lifecycle_* or rte_safestate_*; failed RT attributes are ignored with (void); RTE's lifecycle lock only returns INVALID_STATE and does not escalate

RTE has no SYS_INIT/SYS_OPERATIONAL names; the equivalent boundary is rte_lifecycle (locked by rte_appmanager_run once init succeeds).

Fixed (2026-10-02)

  • rte_ipc_send/rte_ipc_receive with timeout_ms == 0 returned TIMEOUT without trying; it is now a non-blocking try, as "blocking at most timeout_ms" says. Covered by RBC_GP/tests/posix_osadapter/test_rte_posix_osadapter.c.
  • poll(..., (int)timeout_ms) turned a timeout above INT_MAX into a negative value, which poll reads as "wait forever" (channel service read/send, socket poll). Clamped by src/rte_posix_osadapter_poll.h.
  • Items 1 and 2 of the original list (no resource creation after init; report violations) (2026-10-02, chosen by the product owner): Platform_RTE now gates rte_task_create/start, rte_timer_start, rte_ipc_create, rte_nvm_open, rte_channel_service_setup with the setup-phase lock and reports every rejection as DEGRADED / RTE_SAFESTATE_REASON_SETUP_AFTER_INIT (REQ-LIFECYCLE-003); this adapter reports a mutex without priority inheritance, a refused stack size and a refused SCHED_FIFO priority as DEGRADED / RTE_SAFESTATE_REASON_OSA_RT_ATTRIBUTE (src/rte_posix_osadapter_report.h) instead of ignoring them. DEGRADED, not SAFE: the call's contract is unchanged and the integrator's handler decides (RBC_GP logs them: ERROR for setup-after-init, WARNING for RT attributes). Channel-service lazy re-init inside the adapter is a reconnect of an already set-up channel and stays allowed (see item 5). The mutex part was tightened on 2026-10-03 (next item): a mutex without priority inheritance can no longer be created.
  • Item 3 a) of the open list (PI mandatory, R2 + R6) (2026-10-03): src/rte_posix_osadapter_mutex.c requests PTHREAD_PRIO_INHERIT at run time on every host (the _POSIX_THREAD_PRIO_INHERIT > 0 compile gate is gone: macOS declares it -1 yet accepts the protocol, and on a strict-C99 Linux build the gate never requested PI either). A refusal by pthread_mutexattr_init, pthread_mutexattr_setprotocol or pthread_mutex_init fails the create with RTE_STATUS_INTERNAL_ERROR, leaves *out_handle unwritten and reports DEGRADED / RTE_SAFESTATE_REASON_OSA_RT_ATTRIBUTE ("mutex refused priority inheritance - create failed"); there is no plain-mutex fallback (Platform_RTE REQ-OAL-MUTEX-004). Covered by RBC_GP/tests/posix_osadapter/ test_rte_posix_osadapter_mutex.c (every host: PI accepted, no report) and test_rte_posix_osadapter_mutex_pi_refused.c (Linux only, --wrap fakes the refusal).
  • Item 3 b) of the open list (memory-pool lock PI, R2 + R6) (2026-10-03): src/rte_posix_osadapter_memory.c no longer initialises g_mem_lock statically with PTHREAD_MUTEX_INITIALIZER (a plain mutex). The new rte_posix_osadapter_memory_init() (declared in rte_posix_osadapter.h, composition-root wiring) creates it with PTHREAD_PRIO_INHERIT the same way mutex.c creates every mutex (no compile gate, no plain fallback); rte_posix_osadapter_register_all() calls it before registering the memory OSAdapter and returns its status on failure. A refusal returns RTE_STATUS_INTERNAL_ERROR, leaves the pool uninitialised and reports DEGRADED / RTE_SAFESTATE_REASON_OSA_RT_ATTRIBUTE ("memory pool lock refused priority inheritance - init failed"). Until init has succeeded, create/acquire/release/stats return RTE_STATUS_INVALID_STATE without touching the lock (acquire also sets *out_block to NULL); a second init returns OK and does not re-create the lock. The critical sections are unchanged. Covered by RBC_GP/tests/posix_osadapter/ test_rte_posix_osadapter_memory_mt.c (refused before init, init idempotent, concurrent use after), test_rte_posix_osadapter.c (memory case) and test_rte_posix_osadapter_mutex_pi_refused.c (Linux only: init refused, one report, no plain mutex, pool still refused).
  • Item 3 c) of the open list (timed lock, R2) (2026-10-03): the mutex vtable has a fifth member lock_timed (Platform_RTE rte_mutex_lock_timed(), REQ-OAL-MUTEX-014); src/rte_posix_osadapter_mutex.c implements it. RTE_STATUS_OK with the lock held, RTE_STATUS_TIMEOUT when not acquired within timeout_ms (lock not held), timeout_ms == 0 is one pthread_mutex_trylock(), a failed clock_gettime() returns RTE_STATUS_INTERNAL_ERROR and never waits unbounded. Linux: pthread_mutex_timedlock() with an absolute CLOCK_REALTIME deadline, a blocking wait that keeps the priority-inheritance boost. Known limit: the deadline is on the wall clock, so a clock step while a task waits shortens or lengthens that one wait; pthread_mutex_clocklock(CLOCK_MONOTONIC) is not used because glibc accepts it on a PI mutex only with FUTEX_LOCK_PI2 support. Other hosts (macOS has no pthread_mutex_timedlock): a trylock loop against a CLOCK_MONOTONIC deadline with a sleep of at most 1 ms between tries, never past the deadline; no PI boost while polling (development host only). No caller uses it yet (rte_mutex_create has no production caller; the memory-pool lock is item 3 d). Covered by RBC_GP/tests/posix_osadapter/test_rte_posix_osadapter_mutex.c (uncontended, held by a second thread -> TIMEOUT no earlier than the timeout, timeout_ms == 0 on a held lock, holder releases before the deadline -> OK, NULL handle; macOS and Linux).
  • Item 3 d) of the open list (bounded memory-pool lock, R2) (2026-10-03): src/rte_posix_osadapter_memory.c's mem_lock() no longer calls pthread_mutex_lock(); it waits at most RTE_POSIX_MEM_LOCK_TIMEOUT_MS (default 50 ms, overridable at compile time like RTE_POSIX_MEM_ARENA_SIZE: far above the hold time of a critical section, well below the 400 ms A/B checkpoint budget) through posix_osa_lock_bounded(). That helper is the timed-lock code of item 3 c), moved unchanged out of mutex.c into the internal src/rte_posix_osadapter_lock.{h,c} so the mutex OSAdapter's lock_timed and the pool lock share one implementation (same Linux CLOCK_REALTIME limit and same no-PI-while-polling limit on other hosts). When the bound expires every pool operation returns RTE_STATUS_TIMEOUT and leaves the pool unchanged: create claims no arena space, acquire sets *out_block to NULL, release leaves the block with the caller (who may retry), stats leaves its outputs unwritten (Platform_RTE REQ-OAL-MEM-015). A timeout is returned, not reported as a safe state. Covered by RBC_GP/tests/posix_osadapter/ test_rte_posix_osadapter_memory_lock_timeout.c (Linux only, --wrap=pthread_mutex_timedlock: deadline 50 ms ahead, forced ETIMEDOUT -> TIMEOUT for all four operations (acquire, release, stats also through the OAL) with the pool unchanged and no arena claimed, forced EINVAL -> INTERNAL_ERROR, normal use afterwards) and the unchanged test_rte_posix_osadapter_memory_mt.c (8 threads, macOS polling path and Linux) and test_rte_posix_osadapter_mutex.c (timed lock now through the shared helper). With this, item 3 is closed: rte_mutex_lock() stays an unbounded wait, but no production code calls rte_mutex_create()/rte_mutex_lock() (only Platform_RTE's OAL wrappers and tests); a future caller uses rte_mutex_lock_timed(). RBC_GP's DEGRADED handler comment no longer lists mutex priority inheritance among the attributes expected to be missing on an unprivileged host (a refusal fails the create or the pool init, item 3 a/b).
  • Item 4 a) of the open list (task-thread attributes, R1 + R6) (2026-10-03): src/rte_posix_osadapter_task.c's start no longer creates the thread first and calls pthread_setschedparam() afterwards (best effort). It always builds a pthread_attr_t (a failed pthread_attr_init returns RTE_STATUS_INTERNAL_ERROR, no thread), sets the stack size when stack_size > 0 (a refusal is reported as before, default stack kept), then PTHREAD_EXPLICIT_SCHED, SCHED_FIFO and the priority clamped into the SCHED_FIFO range (Platform_RTE REQ-OAL-TASK-002 "priorities fixed at creation"), each return code checked. A refused scheduling attribute, a failed sched_get_priority_min/max, or pthread_create returning EPERM with those attributes reports DEGRADED / RTE_SAFESTATE_REASON_OSA_RT_ATTRIBUTE ("task SCHED_FIFO priority not applied", once per start) and creates the thread again with PTHREAD_INHERIT_SCHED, stack size kept: the ADR-018 fallback for unprivileged hosts stays, only the order changed. Any other pthread_create error returns RTE_STATUS_INTERNAL_ERROR and the task is not running. A priority above INT_MAX now clamps to the maximum (it used to wrap to a negative value and clamp to the minimum). macOS grants SCHED_FIFO to an unprivileged thread; an unprivileged Linux container (no CAP_SYS_NICE, root or not) gets EPERM and takes the fallback. No production code calls rte_task_create() (only Platform_RTE's OAL wrapper and tests), so the RBC processes are unchanged at run time. Covered by RBC_GP/tests/posix_osadapter/test_rte_posix_osadapter_task_attr.c (Linux only, --wrap=pthread_create, pthread_attr_init, pthread_attr_setschedpolicy: the attributes handed to pthread_create for a priority below min, in range, above max and above INT_MAX, with and without a stack size; EPERM -> one report per start and a second create with inherited scheduling; EAGAIN -> INTERNAL_ERROR, no report, no thread, a later start works; EPERM then a failed fallback -> INTERNAL_ERROR; pthread_attr_init refused -> no create; policy refused -> one report, one inherited create) and the unchanged test_task in test_rte_posix_osadapter.c (real create on macOS and Linux). Timer threads are item 4 b (next entry).
  • Item 4 b) of the open list (timer-thread attributes, R1 + R6) (2026-10-03): src/rte_posix_osadapter_timer.c's start no longer calls pthread_create(..., NULL, ...). Choice: adapter-level, no Platform_RTE interface change. It builds a pthread_attr_t per start (a failed pthread_attr_init returns RTE_STATUS_INTERNAL_ERROR, no thread), sets the fixed stack size RTE_POSIX_TIMER_STACK_SIZE (default 8 MiB, compile-time overridable like RTE_POSIX_MEM_ARENA_SIZE, raised to PTHREAD_STACK_MIN if set below it; a refusal is reported, "timer stack size not applied", and the default stack is used), then reads the calling thread's policy and priority with pthread_getschedparam(pthread_self()) and sets PTHREAD_EXPLICIT_SCHED, that policy and that priority, each return code checked. A failed read, a refused attribute, or pthread_create returning EPERM with them reports DEGRADED / RTE_SAFESTATE_REASON_OSA_RT_ATTRIBUTE ("timer scheduling not applied", once per start) and creates the thread again with PTHREAD_INHERIT_SCHED, stack size kept (ADR-018 fallback). Any other pthread_create error returns RTE_STATUS_INTERNAL_ERROR and the timer is not running. Stop/restart, the sleep loop and the unbounded pthread_join (item 7) are unchanged. Run-time scheduling of the RBC is unchanged: the only production timers are the A/B cycle timer (RBC_GP ab_gp_channel_cycle_timer_setup(), called from rte_gp_run()) and the Gateway cycle timer (RBC_GP gateway_c_init(), the AppManager init callback run synchronously from gateway_c_main(); also the RBC_GA IL/CTC gateways and the status gateway), each started from the process's main thread after rte_platform_realtime_init(80U) in app_main_common_init(), which changes the scheduling of that same thread only (pthread_setschedparam(pthread_self()), Linux only). Measured on the real A/B and Gateway train processes (all six roles in a debian:bookworm container, /proc/<pid>/task/<tid>/stat), with the old and the new timer.c: without CAP_SYS_NICE (the Docker test environment) every timer thread runs SCHED_OTHER/0, with CAP_SYS_NICE as root every timer thread runs SCHED_FIFO/80, the same values before and after, no DEGRADED report. With a probe of the same calls on macOS (unprivileged, realtime init is a no-op): creator SCHED_OTHER/31, explicit copy accepted, thread SCHED_OTHER/31, as a default-attribute thread gets. The RBC Docker images run with ulimit -s 8192 KiB, so glibc's default thread stack there was already 8 MiB (probe, pthread_getattr_np: 8388608 bytes for a default-attribute thread and for the explicit 8 MiB one); on macOS the timer stack grows from the 512 KiB default to 8 MiB of virtual memory (one timer thread per RBC process). On a Linux host whose RLIMIT_STACK is not 8 MiB the timer stack is now 8 MiB instead of the limit-derived default: that is the R1 "fixed stack" point. A per-timer priority/stack field in rte_timer_config_t (Platform_RTE SRS + ADR-018) stays open for a timer that needs a priority different from its creator's. Covered by RBC_GP/tests/posix_osadapter/test_rte_posix_osadapter_timer_attr.c (Linux only, --wrap=pthread_create, pthread_attr_init, pthread_getschedparam, pthread_attr_setstacksize: creator SCHED_OTHER/0 and SCHED_FIFO/80 reach pthread_create as explicit attributes with the 8 MiB stack, read from pthread_self(), the periodic callback fires, also through rte_timer_*; EPERM -> one report per start and a second create with inherited scheduling, stack kept, timer runs; EAGAIN -> INTERNAL_ERROR, no report, no thread, a later start works; EPERM then a failed fallback -> INTERNAL_ERROR; pthread_attr_init refused -> no create; pthread_getschedparam refused -> one report, one inherited create; stack size refused -> one report, default stack; restart -> the old thread has exited before the new create, same attributes) and the unchanged test_timer in test_rte_posix_osadapter.c (real create on macOS and Linux). Item 4 is closed.
  • Item 6 a) of the open list (O_NONBLOCK at open, R5) (2026-10-03): src/rte_posix_osadapter_socket.c's posix_socket_open() makes every UDP and TCP socket non-blocking before it hands the handle out, through the file's existing posix_socket_set_nonblocking(fd, true) (fcntl(F_GETFL), then fcntl(F_SETFL, flags | O_NONBLOCK), each return code checked). If either call fails, the fd is closed, *out_handle stays unwritten and open returns RTE_STATUS_INTERNAL_ERROR. No Platform_RTE interface change: the vtable contract of open states no mode, set_nonblocking(false) still switches a socket back to blocking, send/recv/sendto/recvfrom already map EAGAIN/EWOULDBLOCK to RTE_STATUS_TIMEOUT and connect maps EINPROGRESS to OK. Run-time behaviour of the RBC is unchanged: the only callers of the socket open are Platform_RTE's raw UDP/TCP ProtocolAdapters (rte_protocol_adapter_udp.c / _tcp.c), which already call set_nonblocking(true) right after open, and no production code in RBC_GP, RBC_GA, RBC_SA, RBC_Test_CommServer or the simulators uses those ProtocolAdapters or the socket vtable (netlink and the channel service open their own sockets, already non-blocking, and are not touched). Covered by RBC_GP/tests/posix_osadapter/test_rte_posix_osadapter_socket.c (every host: UDP and TCP open -> O_NONBLOCK set; a bound UDP socket with no data -> recv/recvfrom return TIMEOUT at once, against an SO_RCVTIMEO guard a blocking socket would wait for; set_nonblocking(false) clears the flag (the same empty recv then waits for the guard) and true sets it again; a loopback UDP datagram and a non-blocking loopback TCP connect still work; the raw UDP ProtocolAdapter on top of these ops; unsupported type -> NOT_SUPPORTED, NULL out -> INVALID_PARAM; Linux also, with --wrap=fcntl and --wrap=close: F_GETFL refused and F_SETFL refused, UDP and TCP -> INTERNAL_ERROR, handle unwritten, exactly that fd closed and not leaked, the next open works). Loopback and ephemeral ports only. The rest of item 6 stays open. Item 6 b) (2026-10-03, review follow-up): the non-blocking open contract is now a Platform_RTE requirement (REQ-OAL-SOCKET-001, SRS section 2.13, stated in the open Doxygen of rte_osadapter_socket.h; posix_socket_open carries the tag), and the raw UDP/TCP ProtocolAdapters check the set_nonblocking(true) result and close the socket on a failure (REQ-OAL-SOCKET-002, mock tests in Platform_RTE test_rte_osadapter). The RBC_GP test_udp_protocol_adapter case is marked as an integration smoke only. No change in this repository's code.
  • Item 6 c) of the open list (bounded IPC queue of whole messages, R5) (2026-10-03): src/rte_posix_osadapter_ipc.c enforces queue_depth and moves whole messages only (Platform_RTE REQ-OAL-IPC-001 "bounded, statically sized at creation", REQ-OAL-IPC-011 "TIMEOUT if the queue stays full"). No Platform_RTE interface change: rte_ipc.h already says message_size is the fixed size of every message and queue_depth the maximum queued. create stores queue_depth; checks both fcntl(F_SETFL, O_NONBLOCK) results (failure: both fds closed, RTE_STATUS_INTERNAL_ERROR); refuses message_size > PIPE_BUF with RTE_STATUS_NOT_SUPPORTED (one non-blocking write() of at most PIPE_BUF bytes is all or nothing, so no partial message is ever queued: the "partial message" simplification of ADR-018 is gone from this adapter); refuses a message_size * queue_depth overflow with RTE_STATUS_INVALID_PARAM; checks the pipe holds queue_depth messages, else both fds closed and RTE_STATUS_RESOURCE_EXHAUSTED. Linux: F_SETPIPE_SZ, confirmed with F_GETPIPE_SZ, to queue_depth / (page / message_size) + 3 pages, not message_size * queue_depth bytes: a Linux pipe is a ring of page slots and a write is appended to the last slot only if it fits there whole, so a pipe sized to the byte count refuses a send below queue_depth (measured in a debian:bookworm container: 100-byte messages, depth 40, a 4096-byte pipe refused the first send after one receive; 8-byte messages, depth 2, the 511th receive/send cycle). Below queue_depth every slot but the oldest and the newest holds page / message_size messages, plus one slot for the next message; a randomized probe over 11 sizes (1 to 4096 bytes) and 10 depths (1 to 333; 3 pairs above the 1 MiB pipe-max-size skipped) found no refused send below queue_depth with this sizing (with 1 extra slot instead of 3, 120 of the pairs refused one; 2 passed the probe, 3 is the bound derived above). Other hosts (no settable pipe size): message_size * queue_depth <= RTE_POSIX_IPC_PIPE_CAPACITY (default 16384, the capacity this adapter assumes: a conservative budget, not a size the kernel guarantees - XNU allocates a pipe's buffer on demand and grows it, a non-blocking macOS pipe took 65536 bytes before EAGAIN in a probe with writes of 1 to 4096 bytes; overridable at compile time like RTE_POSIX_MEM_ARENA_SIZE; macOS keeps a byte ring, no slot waste). send: a size other than message_size -> RTE_STATUS_INVALID_PARAM; then tries until the CLOCK_MONOTONIC deadline (timeout_ms == 0 is one try): ioctl(read_fd, FIONREAD) (failure: RTE_STATUS_INTERNAL_ERROR); queue_depth messages queued -> sleep at most 1 ms, never past the deadline, and retry, RTE_STATUS_TIMEOUT at the deadline; else one write() of the whole message: complete -> OK, EAGAIN/EINTR -> retry within the deadline, a short write or another error -> RTE_STATUS_INTERNAL_ERROR. receive: buffer_size < message_size -> RTE_STATUS_INVALID_PARAM; reads exactly message_size bytes (a larger buffer no longer waits for bytes of the next message). A failed clock_gettime() for the deadline is now RTE_STATUS_INTERNAL_ERROR in both directions (it used to leave the deadline unset). Known limit: the depth check is exact for one sender per channel; N concurrent senders can overshoot by at most N-1 messages, still bounded by the pipe capacity checked at create (a write past it is EAGAIN, so that send ends in TIMEOUT). PIPE_BUF is 4096 on Linux and 512 on macOS, so a channel with messages above 512 bytes is Linux only. Run-time behaviour of the RBC is unchanged: no production code in RBC_GP, RBC_GA, RBC_SA, RBC_Test_CommServer or the simulators calls rte_ipc_* (only Platform_RTE's OAL wrapper and its mock tests, and RBC_GP's adapter tests). Covered by RBC_GP/tests/posix_osadapter/ test_rte_posix_osadapter_ipc.c (every host: depth 2 -> two sends OK, a third with 50 ms -> TIMEOUT no earlier than 50 ms, timeout_ms == 0 on the full queue -> TIMEOUT at once, one receive frees one slot; FIFO order of three messages; another send size -> INVALID_PARAM, nothing queued; a receive buffer one byte short -> INVALID_PARAM, a larger one -> exactly one message, the rest of the buffer untouched, the next message still queued; PIPE_BUF + 1 -> NOT_SUPPORTED, size * depth overflow -> INVALID_PARAM, more than INT_MAX bytes -> RESOURCE_EXHAUSTED, each with the handle unwritten and no fd leaked; PIPE_BUF itself round-trips; macOS also 16384 bytes accepted, 16896 -> RESOURCE_EXHAUSTED; a full queue kept full for 2000 receive/send cycles at four size/depth pairs; a receiver thread draining a full queue lets a blocked send finish OK before its deadline; Linux also, with --wrap=fcntl and --wrap=close: F_SETFL refused on either pipe end -> INTERNAL_ERROR, F_SETPIPE_SZ refused, F_GETPIPE_SZ failed or one page short -> RESOURCE_EXHAUSTED, each with the handle unwritten and exactly both pipe fds closed; the requested pipe size is pinned) and the unchanged test_ipc in test_rte_posix_osadapter.c. Mutation checks (fresh builds): byte-count pipe sizing, no depth check, no close on failure, reading buffer_size bytes, 2 instead of 3 extra slots, no size check, no PIPE_BUF check, an unchecked F_SETFL (Linux) and no capacity check, no depth check (macOS) each fail the new test.
  • Item 6 c) follow-up (waits above INT_MAX ms, R2 + R5) (2026-10-03): the IPC receive and every netlink wait (listen and connect open, send, receive) took the time left from a local remaining_ms() that ended in a bare (int) cast of an int64_t. rte_duration_ms_t is uint32_t, so a timeout above INT_MAX ms became a negative wait: the callers read it as "no time left" (IPC receive and connect-role open returned TIMEOUT without polling; listen-role open and netlink send/receive polled once with 0 ms), so the call returned at once instead of waiting. The two copies (src/rte_posix_osadapter_ipc.c, src/rte_posix_osadapter_netlink.c) were identical; both are replaced by one posix_remaining_ms() in the internal src/rte_posix_osadapter_poll.h, which keeps their contract and rounding (-1 if clock_gettime(CLOCK_MONOTONIC) fails, 0 once past the deadline, otherwise (deadline.tv_sec - now.tv_sec) * 1000 + (deadline.tv_nsec - now.tv_nsec) / 1000000, the nanosecond part truncated toward zero) and caps the result at INT_MAX before the cast, like posix_poll_timeout_ms() already does for a plain timeout. For a time left of at most INT_MAX ms the result is the same value as before (old and new arithmetic compared over 11 timeouts from 0 to UINT32_MAX and 9 clock phases: no difference up to INT_MAX; INT_MAX + 1, INT_MAX + 1000 and UINT32_MAX gave INT_MIN, -2147482649 and -2 before, INT_MAX now). A wait above INT_MAX ms is now one poll() of INT_MAX ms (about 24.8 days): IPC receive, netlink send/receive and listen-role open return TIMEOUT after that, connect-role open keeps polling until its own deadline. The -1-on-clock-failure handling is unchanged (item 8). Run-time behaviour of the RBC is unchanged: the netlink timeouts found in the production callers are constants or config values of at most seconds (shipped connect_timeout_ms 600 and 1000 ms in RBC_GP etc/config/, Platform_RTE safechannel send 5000 ms, RBC_Test_CommServer 2 and 200 ms), and nothing outside Platform_RTE's wrapper and tests calls rte_ipc_*; only a configured connect_timeout_ms above INT_MAX would now wait instead of failing at once. Covered by RBC_GP/tests/posix_osadapter/test_rte_posix_osadapter_ipc.c (receive with INT_MAX, INT_MAX + 1, INT_MAX + 1000 and UINT32_MAX ms on an empty channel gets a message a thread sends 50 ms later, OK and not TIMEOUT) and test_rte_posix_osadapter_netlink_restart.c (the connect side's receive with UINT32_MAX and INT_MAX + 1000 ms gets a datagram the listen side sends 50 ms later); both run under a 30 s SIGALRM watchdog, so a sender that never sends ends the run instead of hanging it. The cap removed (bare cast restored, fresh build, macOS and Linux): both new cases fail at once. The create-limit case of test_rte_posix_osadapter_ipc.c now asks for INT_MAX / 512 + 1 messages of 512 bytes (2^31 bytes, more than INT_MAX): the old depth INT_MAX overflowed message_size * queue_depth on a 32-bit size_t and got INVALID_PARAM instead of RESOURCE_EXHAUSTED.
  • Item 6 d) of the open list (socket buffer sizes, R5 + R6) (2026-10-03): Platform_RTE's rte_netlink_config_t gains rcvbuf_bytes and sndbuf_bytes (uint32_t, last members; 0 keeps the OS default) and REQ-OAL-NETLINK-015. The new internal src/rte_posix_osadapter_sockbuf.{h,c} (posix_socket_apply_buffers()) sets each non-zero size with setsockopt(SOL_SOCKET, SO_RCVBUF / SO_SNDBUF), the value capped at INT_MAX, return code checked, and reads it back with getsockopt() (return code and length checked). A refused set or a failed read-back is RTE_STATUS_INTERNAL_ERROR. A read-back below what an accepted request reports - twice the value on Linux (the kernel doubles it for bookkeeping and caps the request at net.core.rmem_max / wmem_max without an error), the value itself elsewhere - keeps the socket and reports DEGRADED / RTE_SAFESTATE_REASON_OSA_RT_ATTRIBUTE ("socket buffer size capped") once per socket, the pattern of a refused stack size (item 4). A field of 0 makes no setsockopt / getsockopt call at all. Netlink: called in open_listen_udp() after bind and in open_connect_udp() after socket, before any handshake; on an error the fd is closed (and the address list freed) and open returns the status with the handle unwritten. Channel service: called in init_listen_socket() after bind and in init_connect_socket() after socket, before state->fd is set, so on every socket (re)init; on an error the fd is closed (and the address list freed); setup returns the status, the lazy re-init in read/send keeps its existing RTE_STATUS_TIMEOUT mapping. The channel state that stores the config grows from 88 to 96 bytes (rte_netlink_config_t 40 -> 48, LP64), inside the 128-byte rte_channel_service_storage_t (the compile-time size check rte_posix_channel_state_fits_ holds); the netlink state does not store the config. Run-time behaviour of the RBC is unchanged: every rte_netlink_config_t in the workspace is zero-filled before its fields are set (memset / rte_mem_set or = {0}: Platform_RTE rte_safechannel.c, rte_site_role.c, rte_channel_service_flow_osadapter.c and its tests; Platform_Protocol_DDS rte_protocol_adapter_dds.c and its test; RBC_GP ab_gp_channel.c (three resolution paths), ab_gp_config.c, gateway_c_config.c, gateway_c_common.c; RBC_Test_CommServer src/main.c; this adapter's channel service zero-fills the config before it calls the resolver), and no production config sets the new fields, so no RBC process makes a new system call. Wiring them into the RBC_GP/RBC_GA JSON config is item 6 e. Measured on macOS (Darwin 25): an accepted size reads back exactly, and a request above 8 MiB is capped to 8 MiB without an error, not refused. Covered by RBC_GP/tests/posix_osadapter/test_rte_posix_osadapter_sockbuf.c (every host: a loopback netlink LISTEN/CONNECT pair and a channel-service listen/connect pair with both sizes 65536 and with both 0 -> OK, no report, a message round-trips both ways; UINT32_MAX on a channel-service connect setup -> OK and one report, capped by the kernel on Linux and macOS; Linux also, with --wrap=setsockopt, --wrap=getsockopt and --wrap=close: both 0 -> no SO_RCVBUF/SO_SNDBUF call; the values reach setsockopt unchanged, each to its own option, UINT32_MAX as INT_MAX, a 0 field not touched; SO_RCVBUF refused, SO_SNDBUF refused (after SO_RCVBUF was applied), read-back failed, read-back with a short length -> INTERNAL_ERROR for netlink LISTEN and CONNECT (handle unwritten) and channel-service listen and connect setup, no report, exactly that fd closed and not leaked, the next open / setup works, a failed connect channel re-inits on send (refused again -> TIMEOUT, then OK); a read-back below the request -> one report per socket, OK, the link usable) and Platform_RTE tests/netlink/test_rte_netlink.c (the fields pass through the OAL to the OSAdapter unchanged, for either role). Mutation checks (Linux, fresh builds): a call for a 0 field, no INT_MAX cap, no read-back comparison, a Linux read-back factor of 1, no report, no length check, no close on the listen error path, the netlink or the channel service ignoring the status - each fails the new test.
  • Item 6 e-1) of the open list (socket buffer sizes from the deployment file, R5 + R6) (2026-10-03): Platform_RTE's rte_channel_def_t gains rcvbuf_bytes and sndbuf_bytes (uint32_t, last members; 0 keeps the OS default), read by rte_redundancy_config_load() from the optional channel keys of the same names (REQ-REDCFG-001). A value that is not an unsigned decimal integer or is above UINT32_MAX fails the whole load; it is never silently 0 or wrapped. No change in this adapter, and no run-time change in the RBC: no shipped JSON sets the keys and RBC_GP does not copy them into its rte_netlink_config_t yet (item 6 e-2). Found while doing it: RBC_GP's A/B open their channels through Platform_RTE's flow-backed channel service (rte_flow_config_t has no buffer fields, the DDS protocol adapter builds the netlink config from its route), so for A/B the sizes only reach this adapter's netlink once that path carries them; the Gateway uses this adapter's channel service, which applies them.
  • Item 6 e-2a) of the open list (socket buffer sizes from the Gateway JSON, R5) (2026-10-04): RBC_GP's role-C config loader (gateway_c_config.c, also used by RBC_GA's IL/CTC gateways through gateway_c_common.c) reads optional per-channel rcvbuf_bytes / sndbuf_bytes with a checked parse (decimal digits only, at most UINT32_MAX, only whitespace before the , / }; anything else fails the load with RTE_STATUS_INVALID_PARAM) and copies them into the rte_netlink_config_t this adapter's channel service receives from the resolver, which applies them (item 6 d). Non-vital (role C), no change in this adapter, Platform_RTE or the A/B code. Run-time behaviour of the RBC is unchanged: no shipped JSON sets the keys. Covered by RBC_GP tests/gateway/test_gateway_c_config.c. Open in 6 e: the A/B links (flow path, see 6 e-1).
  • Item 6 f) of the open list (a specific status for a dropped listen-role send, R5) (2026-10-04): step 6 f-1 added RTE_STATUS_NOT_CONNECTED and REQ-CHANSVC-001 to Platform_RTE; step 6 f-2 is this adapter's side. The channel service's backend_send() on a listen-role link that has not yet learned its peer (the peer address comes from the first datagram a read receives) returns RTE_STATUS_NOT_CONNECTED instead of RTE_STATUS_OK; as before, nothing is sent. Order and every other result are unchanged: the lazy socket init (failure -> TIMEOUT) and the POLLOUT wait still come first, a connect-role send is not affected, and close still forgets the peer, so a new setup answers NOT_CONNECTED again until its first read. Only role C reaches this path at run time (the Gateways register this channel service; A/B use Platform_RTE's flow-backed channel service, which returns TIMEOUT for an absent peer). Every caller of rte_channel_service_send() was checked. In the same step the RBC_GP Train Gateway and the RBC_GA IL/CTC Gateways handle the new status exactly as they handled the OK it replaces: a relay send (send_to_relay()) clears the send-failure window that feeds the link-down reboot and logs nothing; a sim send (send_to_sim()) and the IL log-level broadcast (broadcast_one()) stay silent. Every other status keeps its old handling. The status Gateway (gateway_c_status_handler.cpp: send_on_link(), which also carries the KM114 file relay and the takeover-confirm tokens, and the util keepalive) and gateway_c_selfstatus.c ignore the send result ((void)), so a NOT_CONNECTED there is dropped as the OK was. Platform_Protocol_SafeComm's transport.c would map it to SAFECOMM_STATUS_TRANSPORT, but nothing in the workspace builds that transport over this adapter. Run-time behaviour of the RBC is unchanged:** the same frames are sent, dropped and logged as before, and the Gateway reboot decision is the same. Covered by RBC_GP tests/posix_osadapter/test_rte_posix_osadapter_channel_service_listen.c (loopback, through rte_channel_service_send(): a listen send before any peer -> NOT_CONNECTED with a timeout of 0 and of 100 ms, also after the connect side's setup; after the listen side reads the connect side's PING, its PONG -> OK and arrives; close + setup -> NOT_CONNECTED again; a connect-role send with nobody listening -> OK or TIMEOUT, never NOT_CONNECTED) and tests/gateway/test_gateway_c_train_handler_send.c (the Train Gateway over a scripted channel service: NOT_CONNECTED opens no window, ends a running one and logs nothing; OK clears; TIMEOUT opens the window without an event; INTERNAL_ERROR / INVALID_STATE open it with one ERROR ENVELOPE event; a repeated failure keeps the window's start; peers A and B separately). Mutation checks (macOS, fresh build directories): backend_send() returning OK again, the Train relay treating NOT_CONNECTED as a failure, NOT_CONNECTED neither clearing nor opening the window, and a logged TIMEOUT - each fails one of the two tests. The IL/CTC handlers carry the same two-line change as the Train handler and have no test harness of their own.
  • Item 7 a) of the open list (every created thread joined, R2) (2026-10-04): src/rte_posix_osadapter_timer.c and src/rte_posix_osadapter_task.c no longer keep a running flag that the timer/task thread cleared itself when its loop ended. That made stop/suspend/destroy skip pthread_join() for a one-shot timer that had fired or a run-once task whose entry had returned, so each such start left one unjoined thread behind (its stack and descriptor never reclaimed), and a restart started a new thread without joining the old one. It is replaced by bool joinable, set after a successful pthread_create() and cleared only after pthread_join() returned 0, read and written only by the controlling thread (the one calling start/stop/suspend/destroy); the thread's own loop writes no shared field. stop/suspend/destroy join whenever joinable is set and start joins before it creates (restart; Platform_RTE REQ-OAL-TIMER-011..013, REQ-OAL-TASK-011..013: start/restart, stop safe on a stopped timer, destroy releases the OSAdapter resources). The result of pthread_join() is checked (it was (void)): a failure returns RTE_STATUS_INTERNAL_ERROR with joinable kept, so a later call joins; a start that cannot join the old thread creates none. Self check: a call is from the thread's own callback/entry when the calling thread's thread-specific value under the adapter's key is that timer's/task's state. Each timer/task thread stores it (pthread_setspecific()) before its first callback/entry; the key is one per file, created by the first create in the process through pthread_once() and never deleted. The check reads nothing the controlling thread writes after pthread_create(), so it holds even while pthread_create() has not returned yet (a run-once entry runs at once; there is no window and no usage restriction). A key that cannot be created makes that create and every later one return RTE_STATUS_INTERNAL_ERROR with the storage and handle unwritten (not retried). A thread that cannot store its value (ENOMEM) reports DEGRADED / RTE_SAFESTATE_REASON_OSA_RT_ATTRIBUTE ("timer thread-specific data not set - timer not run", "task thread-specific data not set - task not run") and returns without any callback/entry; start has already returned OK, so the timer never fires / the entry never runs, and stop/suspend/destroy join the thread as usual. Known limit: that failure is visible only through the report (a production cycle timer that never fires stalls the cycle until the watchdog acts); it never runs callbacks without the self check. Self-call rule: stop/suspend/destroy from the thread's own callback/entry set should_stop and return RTE_STATUS_OK without joining (before: a pthread_join() on itself, EDEADLK ignored, the thread then left unjoined); the next start/stop/suspend/destroy from another thread joins it. should_stop is set before the self check, so the loop ends on either branch. start from the callback/entry returns RTE_STATUS_NOT_SUPPORTED (already in the documented return list of rte_timer_start()/rte_task_start()) before touching the state; before, it reset should_stop and created a second thread for the same timer while the first kept running. The timer loop also ends right after a callback during which a stop was requested (it used to sleep one more period first, without a further callback), so a stop that waits on a callback in progress waits for that callback only. Shared state: should_stop is the only field both threads write; start sets it to 0 only while no thread exists (before pthread_create(), after any join), and stop/suspend/destroy set it to 1 from either side, so two writes that can overlap both store 1. It stays volatile sig_atomic_t (C99, no <stdatomic.h>); pthread_create() and pthread_join() are the synchronisation points. The storage-size checks hold. pthread_join() stays an unbounded wait (item 7 b). Run-time behaviour of the RBC is unchanged:** the only production timers are the periodic A/B and Gateway cycle timers (RBC_GP ab_gp_channel_cycle_timer_setup() / ab_gp_channel_shutdown(), gateway_c_init() / gateway_c_shutdown(), also used by the RBC_GA IL/CTC gateways and the status gateway), created and started once from the main thread (the first create also creates the key) and stopped and destroyed from the main thread at shutdown (ab_gp_channel.c rte_timer_stop()/rte_timer_destroy(), gateway_c_common.c rte_timer_destroy(); also when reached from the checkpoint-timeout SAFE-state handler or the Gateway link-down reboot, both raised by the cycle processing on the main thread, never on the timer thread); their callbacks only count ticks and call no rte_timer_*. A periodic timer's thread was already joined there. Nothing in RBC_GP, RBC_GA, RBC_SA, Platform_Protocol_SafeComm, Platform_Protocol_JRU, Platform_Protocol_DDS, RBC_Test_CommServer or Platform_RTE's src/ calls rte_task_* (only Platform_RTE's OAL wrapper and tests). Covered by RBC_GP/tests/posix_osadapter/ test_rte_posix_osadapter.c (every host, through rte_timer_*/rte_task_*: a one-shot timer started three times fires once per start; stop from the first periodic callback -> OK, no further callback, destroy from main OK; start from the first callback -> NOT_SUPPORTED, the timer keeps firing, stop OK and no callback after it; a run-once task started again after its entry returned runs again, destroy OK), test_rte_posix_osadapter_thread_reclaim.c (Linux only, --wrap=pthread_create,pthread_join,pthread_setspecific: as many joins as creates after a one-shot fire + stop, a one-shot restart, a run-once task + destroy and a run-once restart, a periodic stop, a double stop (one join), a self stop then destroy (joined once, from the main thread, no join from the timer thread), a self start (no second create); the same for a periodic task's self suspend and self start; a refused pthread_join -> INTERNAL_ERROR from stop, suspend, a restart (no create) and destroy, the thread kept, a later stop/destroy joins it; the self stop / self start / self suspend of a timer, a periodic and a run-once task while pthread_create() is held until the thread's first callback/entry has made its self call; pthread_setspecific refused -> one report, no callback/entry, the thread joined, the next start runs normally; and a one-shot restart through rte_timer_*) and test_rte_posix_osadapter_thread_key_refused.c (Linux only, --wrap=pthread_key_create: timer and task create -> INTERNAL_ERROR, storage and handle unwritten, one key attempt per file, also through rte_timer_create()/rte_task_create()). Mutation checks (Linux, osa-build:bookworm): the previous timer.c/task.c fail both reclaim tests (joins short of creates; a self start returned OK); ignoring the pthread_join() result fails them; the self check through joinable and the thread ID fails the held-create self start; no pthread_setspecific() fails the self stop; running the loop after a failed pthread_setspecific() fails the refused-mark case. Setting should_stop only inside the self branch (not before the check) is equivalent while the self check is exact, so no test kills it alone; combined with the joinable self check it fails the held-create self stop.
  • Item 8 a) of the open list (periodic pacing by an absolute monotonic deadline on every host, R3) (2026-10-04): the duplicated ms_to_timespec() / timespec_add_ms() of src/rte_posix_osadapter_timer.c and src/rte_posix_osadapter_task.c are replaced by the internal src/rte_posix_osadapter_period.{h,c} (not installed, the same pattern as rte_posix_osadapter_lock.{h,c}): posix_period_init() reads CLOCK_MONOTONIC into a local of the thread (a failure returns RTE_STATUS_INTERNAL_ERROR with the deadline unwritten), posix_period_advance() moves it one period later (nanoseconds normalised), posix_period_sleep_until() sleeps until it - Linux: the previous clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME), restarted on EINTR only while should_stop == 0, any other error RTE_STATUS_INTERNAL_ERROR; other hosts: read CLOCK_MONOTONIC, return at once when the deadline has passed, else nanosleep() the time left and read the clock again after the wake-up (an EINTR or an early wake-up only shortens one sleep), a failed read RTE_STATUS_INTERNAL_ERROR. Before: a non-Linux host slept a fresh relative period after every callback, so each period drifted by the callback's run time (Platform_RTE REQ-OAL-TIMER-014 asks for one monotonic time base), and on Linux a failed first clock_gettime() started the deadline at 0, so the timer fired back to back until the deadline caught up with the uptime. run_timer_loop() (periodic and one-shot) and the new run_periodic_task() (the periodic branch of run_task()) call the init once when the thread starts and advance + sleep every period. A deadline already passed (the callback / entry overran) gives no sleep: the expiry is late, none is skipped (what Linux did already). A failed init or sleep reports DEGRADED / RTE_SAFESTATE_REASON_OSA_RT_ATTRIBUTE once ("timer monotonic clock failed - timer stopped", "task monotonic clock failed - task stopped") and ends the loop with no further callback / entry. start has already returned OK; stop / suspend / destroy join the thread as usual and the next start reads the clock again. Same posture as the item 7 a thread-specific-data failure: no callback without a time base, and the stalled cycle is left to the watchdog (a dedicated safe-state reason code for both is an open sub-item of item 7 b in the root TODO.dox). The 7 a design is unchanged (joinable, the self check, should_stop set before the self check, the timer loop ending right after a callback during which a stop was requested). The deadline is a local of the thread; the state structs do not grow. The run-once task and osadapter_now() are unchanged. Run-time behaviour of the RBC: on Linux (Docker, the target) unchanged - same call, same deadline arithmetic; only a clock failure or a failed sleep (a clock_nanosleep() error other than EINTR), neither ever seen, now stops the timer with a report (a failed clock read gave a burst before). On macOS the A/B and Gateway cycle timers no longer drift by the callback's run time. macOS CLOCK_MONOTONIC keeps counting while the system sleeps, so after a laptop sleep a timer delivers the expiries it missed back to back (harmless for the GP/Gateway tick counters; development host only). Covered by RBC_GP/tests/posix_osadapter/ test_rte_posix_osadapter.c (every host, through rte_timer_* / rte_task_*: a 40 ms periodic timer and a 40 ms periodic task whose callback / entry sleeps 20 ms reach their 20th expiry at least 800 ms and less than 1100 ms after start; the drifting loop needs at least 20 x (40 + 20) = 1200 ms by construction, so it fails without any host load; host load delays the correct loop, which catches up at each absolute deadline, provided the paced thread is not starved by the load, i.e. runs at or above the competing priority - the timer thread copies the creator's scheduling, and the test task asks for sched_get_priority_max(SCHED_FIFO) because a task priority of 0 is clamped to the SCHED_FIFO minimum, which on macOS (15 of 15..47, ordinary threads at 31) is starved by ordinary load; the reasoning is in the test) and test_rte_posix_osadapter_period_clock.c (Linux only, --wrap=clock_gettime,clock_nanosleep, failures armed for the timer / task thread only so osadapter_now() and the lock deadlines on the main thread keep the real clock: the first read refused for a periodic timer, a one-shot timer and a periodic task -> one report each, no callback / entry, no sleep, stop / suspend OK, the next start runs normally; clock_nanosleep returning EINVAL on the third sleep -> two callbacks / entries, one report, none after; one EINTR -> the retry sleeps to the same deadline, no expiry lost, no report; an EINTR on every sleep -> no callback / entry, and a stop / suspend ends the loop at the next EINTR). Mutation checks: the previous timer.c / task.c fail the every-host drift case on macOS (1257 ms timer, 1264 ms task); on Linux (osa-build:bookworm) a deadline of 0 on a failed read, a dropped report, and an EINTR restart that ignores should_stop each fail the Linux test. Known gap: the non-Linux clock-failure branch (a failed clock_gettime() inside the nanosleep() loop) has no forced-failure test, because macOS ld64 has no --wrap; its pacing is covered by the every-host drift case.
  • Item 7 b-1) of the open list (a stop wakes the waiting thread, R2) (2026-10-04): a timer or periodic task thread waited for its deadline in clock_nanosleep() (Linux) / nanosleep() (other hosts), which nothing interrupts, so stop/suspend/destroy (and a restart, which joins first) waited up to one period before pthread_join() returned (Platform_RTE REQ-OAL-TIMER-012/013, REQ-OAL-TASK-012/013 - their wording, the @return lists and the SRS are unchanged; no interface or ABI change). src/rte_posix_osadapter_period.{h,c} now waits on ONE process-wide wake object, a priority-inheritance mutex and a condition variable, static in period.c: a per-timer object does not fit rte_timer_storage_t / rte_task_storage_t (64 bytes, fixed by the Platform_RTE ABI). posix_period_wake_ready() creates it once per process through pthread_once() (PTHREAD_PRIO_INHERIT requested at run time on every host, as mutex.c does; non-Apple hosts pthread_condattr_setclock(CLOCK_MONOTONIC)); the timer and task create call it after the self-check key. A refusal of any of these calls reports DEGRADED / RTE_SAFESTATE_REASON_OSA_RT_ATTRIBUTE once ("period wake object refused - timer/task create failed"), destroys a mutex already created, and makes that create and every later one return RTE_STATUS_INTERNAL_ERROR with the storage and handle unwritten (not retried, the posture of the key). Wait: posix_period_sleep_until() reads CLOCK_MONOTONIC and returns when the deadline has passed; otherwise it takes the wake mutex through posix_osa_lock_bounded() bounded by the time left (rounded up to whole ms), reads should_stop under it and, if 0, waits - non-Apple hosts pthread_cond_timedwait() to the same absolute deadline as before, macOS (no condition variable clock attribute) pthread_cond_timedwait_relative_np() of the time left, read again under the mutex (_DARWIN_C_SOURCE in period.c only) - then reads should_stop again and releases the mutex. A wake-up (0) or a timeout without a stop goes round the loop: the clock is read again and the wait resumes to the same deadline (no expiry added, none lost, no drift). Deviation from the plan wording, the safer side: a timeout of the wake-mutex lock on the waiting side is not taken as "deadline reached" but sends the loop back to the clock read, because Linux measures the lock bound on CLOCK_REALTIME (a wall-clock step could end it before the monotonic deadline and give an early expiry); a real lock timeout lasts the whole time left, so the clock read then finds the deadline passed. A clock read, lock or wait error other than a timeout keeps the item 8 posture (one report, "timer monotonic clock failed - timer stopped" / "task monotonic clock failed - task stopped", the loop ends with no further callback / entry). Stop: the new posix_period_wake_all() stores 1 in should_stop while it holds the wake mutex (bounded by the new RTE_POSIX_PERIOD_WAKE_LOCK_TIMEOUT_MS, default 50 ms, compile-time overridable like RTE_POSIX_MEM_LOCK_TIMEOUT_MS) and broadcasts. stop/suspend/destroy call it where they stored 1 before (before the self check, so the 7 a self-call rule is unchanged, and in join_*_thread(), so a restart wakes the old thread too); a self call from the callback / entry takes the mutex like any other caller (the callback runs outside it). No lost wake-up: the waiter checks the flag and enters the wait under the same mutex the waker holds while it stores the flag and broadcasts, and the condition wait releases the mutex atomically, so the waker's critical section lies either wholly before the check (the waiter sees 1) or wholly after the wait began (the broadcast wakes it). If the waker cannot take the mutex in time it stores the flag without it and does not broadcast - not an error, the caller still joins, and the thread ends at its next deadline (the behaviour before 7 b-1). A broadcast wakes every waiting periodic thread of the process; one whose flag is 0 waits again to its own deadline. The 7 a design (joinable, the thread-specific-data self check, should_stop set before it, the loop ending right after a callback during which a stop was requested) and the 8 a design (absolute deadline, catch-up, no expiry skipped, failure posture) are unchanged. Known limits: pthread_join() still waits with no bound for a callback / entry that is running (item 7 b-2, needs an owner decision on the status and the safe-state reaction); the woken thread re-acquires the mutex inside pthread_cond_*wait() with no bound of the adapter's (every holder only checks a flag, broadcasts or starts a wait); glibc condition variables (since glibc 2.25) are not priority-inheritance aware, so the wake-up path itself gives no boost, only the mutex does; macOS takes a contended mutex by polling (trylock and at most 1 ms sleep), so a contended wake mutex can add about 1 ms to a wake-up or an expiry there. Run-time behaviour of the RBC: the A/B and Gateway cycle timers (and the RBC_GA IL/CTC and status gateways) run the new wait; in steady state their expiries are unchanged (same absolute deadline; per period one more CLOCK_MONOTONIC read and an uncontended lock / unlock of the wake mutex; with one timer per process the mutex is never contended and no broadcast happens). Their shutdown (ab_gp_channel_shutdown(), gateway_c_shutdown(), also when reached from the checkpoint-timeout SAFE-state handler or the Gateway link-down reboot) no longer waits up to one cycle (500 / 600 ms) in rte_timer_stop() / rte_timer_destroy(); no further callback ran in that wait before either. The first timer create of each process also creates the wake object; a refusal would now fail that create (and the process start) instead of being impossible - PI mutexes and CLOCK_MONOTONIC condition variables are accepted in the RBC Docker images and on macOS (the gate runs prove it). Covered by RBC_GP/tests/posix_osadapter/ test_rte_posix_osadapter_stop_wake.c (every host, through rte_timer_* / rte_task_*: (a) a periodic 5000 ms timer stopped, and destroyed, 50 ms after start returns in less than 500 ms with no callback (measured 0 ms on macOS and Linux); (b) a one-shot 2000 ms timer stopped early is fast with no callback, a restart fires once, no earlier than one full period after the restart (2000 / 2001 ms measured); (c) a periodic 5000 ms task: suspend and destroy fast, entry never called; (d) a 20 ms periodic timer runs while main starts and stops 10 long-period timers and 10 long-period tasks (each stop fast): no callback before its deadline (checked in the callback against the time read before start), no more callbacks than deadlines passed, and the smallest of up to 50 sampled lags (expiries due minus delivered) at most 3 - not the plan's fixed 45..51 count per second, which a moment of host load would break (ISS-029); a loop that dropped an expiry per wake-up would keep a lag of about one per broadcast; measured lag 0; (e) a stop from main during a 200 ms callback returns once that callback ends (199 / 201 ms) with no further callback, and a stop from the callback itself still returns OK, then destroy is fast), test_rte_posix_osadapter_period_clock.c (Linux only, reworked from clock_nanosleep to --wrap=clock_gettime,pthread_cond_timedwait,pthread_condattr_setclock,pthread_mutex_timedlock: the condition variable is created once, on CLOCK_MONOTONIC, and every wait gets an absolute CLOCK_MONOTONIC deadline at most one period ahead; the item 8 first-read cases unchanged (no wait); pthread_cond_timedwait returning EINVAL on the third wait (timer and task) and the timer thread's mutex lock returning EINVAL on its third try -> one report, no callback / entry and no wait after it; one early return 0 -> the next wait has the same deadline and no callback ran in between; every wait cut short to at most 1 ms (a real wait, so a broadcast still wakes it) -> no callback before its deadline, a stop still fast; the timer thread's mutex lock timing out twenty times in a row -> no report, no early callback, callbacks continue; main's mutex lock timing out in stop -> OK, the thread ends at a deadline (stop returned no earlier than (callbacks + 1) periods after start) with at most the one callback already due, no report; the EINTR cases went with clock_nanosleep: a condition wait does not return EINTR) and test_rte_posix_osadapter_period_wake_refused.c (Linux only, --wrap=pthread_mutexattr_setprotocol, pthread_condattr_setclock,pthread_cond_init,pthread_mutex_destroy, one process per refused call: timer and task create, directly and through rte_timer_create() / rte_task_create(), run-once task included -> INTERNAL_ERROR, storage and handle unwritten, one attempt, one report, no condition variable on CLOCK_REALTIME, the mutex destroyed again when the condition variable is refused). The existing every-host and Linux timer / task tests (pacing, 7 a reclaim and self calls, attributes, key refused) pass unchanged. Mutation checks (fresh builds of the test targets against a mutated copy of this adapter): the previous period.c / timer.c / task.c fail the every-host test on macOS at (a) (stop took 4943 ms), and with the stop-latency check made non-fatal also at (b), (c) and each of the 20 churn stops of (d); posix_period_wake_all() without the broadcast fails (a) on macOS and Linux (4942 ms); a wake-up taken as the deadline fails (d) (callback before its deadline) and the Linux early-return case (no wait to the same deadline); a waiting-side lock timeout taken as the deadline fails the Linux lock-timeout case (early callback); a lock error or a wait error not reported fails the Linux refusal cases (no report); no report, or no destroy of the mutex, on a refused wake object fails the Linux refusal test. The lost-wake-up window itself (a stop between the flag check and the wait) is closed by construction (the check and the wait under one mutex); no test forces that interleaving.

Open, in priority order

  1. No DNS in operation (R4): resolve once and cache the address. Design point: the channel service's lazy re-init exists because a peer container may not be resolvable at startup (see root ISSUES.dox ISS-009); removing it needs a startup-ordering answer first, so it was not changed blind.
  2. Sockets (R5): socket buffer sizes set in the RBC_GP/RBC_GA channel config (item 6 e; the adapter and the interface are done, item 6 d, the redundancy-config keys, item 6 e-1, and the Gateway JSON keys, item 6 e-2a) (O_NONBLOCK at posix_socket_open and its contract, items 6 a and 6 b, the bounded IPC queue, item 6 c, the socket buffer sizes in the link config, item 6 d, and the RTE_STATUS_NOT_CONNECTED status for a dropped listen-role send, item 6 f, are done, see "Fixed").
  3. Bounded shutdown (R2): a bounded wait instead of pthread_join in suspend/stop for a callback / entry that does not return (item 7 b-2; needs the owner decision on the status and the safe-state reaction when the deadline passes - TIMEOUT in the Platform_RTE @return lists and SRS). Every created thread is joined since item 7 a, and stop/suspend/destroy wake a waiting thread instead of waiting up to one period since item 7 b-1 (see "Fixed").
  4. Logging (R2): fixed-size ring drained by a low-priority thread created at init, instead of fprintf(stderr).