ASIL-B UDS diagnostics on Zephyr RTOS — how EDS enforces safety properties at build time
Most UDS implementations on Zephyr are built for one project, by one team, under schedule pressure. They work. They are not ASIL-B — and retrofitting safety properties into a running diagnostics stack costs more than building them in from the start.
Getting from "the UDS stack passes integration tests" to "the UDS stack meets the software design principles that ISO 26262 Part 6 calls for" requires a specific set of architectural choices that are very hard to retrofit. This post describes exactly what those choices are, why they matter, and how we implemented them in Xaloqi EDS — an open-source ISO 14229 UDS stack for Zephyr RTOS and FreeRTOS, built around YAML-driven code generation and ISO-TP transport.
What software used in ASIL-B developments must provide — a diagnostics stack perspective
ISO 26262 Part 6 doesn't mandate a particular implementation — it mandates properties: determinism, bounded execution, traceability, and verifiable freedom from interference. The choices that bite UDS implementations most often:
Static allocation. Dynamic memory allocation is avoided in safety-relevant software not because ISO 26262 prohibits it, but because it complicates timing analysis, introduces fragmentation risk, and makes worst-case execution time difficult to bound. EDS uses a flat array sized by GEN_DID_COUNT (a compile-time constant in generated_config.h) — no runtime allocation, no unbounded traversal, WCET analyzable at build time.
No recursion. Recursion is prohibited by MISRA C:2012 (Rule 17.2) and most automotive coding guidelines because bounded stack usage is difficult to demonstrate when call depth depends on input. EDS has no recursive paths in any service handler or safety module.
Pre-start self-test. Startup self-tests are commonly used in safety architectures as a safety mechanism (ISO 26262-6:2018 §7.4.12) to detect certain fault classes before normal operation begins. For a UDS stack, this means verifying the safety module itself — null checks, bounds checks, violation counter behaviour — before accepting any diagnostic session.
Safety violation accounting. Every rejected request due to a safety check failure — wrong session, insufficient security level, out-of-bounds length — must be counted and preserved. You cannot reset these counters in production. They are evidence.
Controlled DID access. Every DID read and write must pass through a validated access chain: existence check, session gate, security level gate, permission gate, length gate — in that order, every time. You cannot shortcut it. You cannot let a service handler call the DID database directly.
That last requirement is the one that causes the most architectural headaches. It means every service handler (0x22 ReadDataByIdentifier, 0x2E WriteDataByIdentifier, etc.) needs a generated safety wrapper around every DID — not a runtime check, a compile-time-verified, statically-allocated wrapper.
The five-step access chain
ISO 14229-1:2026 defines the order in which servers must check conditions and return NRCs. We implement this as a structural constraint because architectural enforcement is stronger than relying on coding convention — the unsafe path is architecturally unavailable, not merely guarded by a check that a future developer might bypass. EDS enforces both the UDS protocol ordering and the structural constraint.
A runtime check can be skipped by a developer who misunderstands the architecture. A structural constraint that removes the unsafe path entirely cannot be.
In EDS, every DID access — read or write — routes through five checkpoints in core/uds_safety.c:
Step 1: DID exists in database uds_safety_find_did() → NRC 0x31 Step 2: Current session is allowed uds_safety_validate_session() → NRC 0x7F Step 3: Security level is sufficient uds_safety_validate_did_access() → NRC 0x33 Step 4: Access permission is correct uds_safety_validate_did_access() → NRC 0x31 Step 5: Data length is valid uds_safety_check_did_data_length() → NRC 0x13
Service handlers in core/uds_services/ never call did_database_find() directly. This is enforced structurally — the DID database lookup function isn't exported from the safety layer. Every DID access goes through the safety module. This maps to requirements REQ-SAFE-001 through REQ-SAFE-007 in core/uds_safety.h. Each requirement traces through the full lifecycle: software safety requirement → generated safety wrapper → verification test → RTM entry. That chain is what makes the traceability auditable, not just the requirement ID.
The implementation is static. Every table entry is a compile-time constant. The safety module itself has no dynamic allocation. Total stack depth for the worst-case service call (a multi-frame write with SecurityAccess) is analyzable to a fixed number.
The code generation approach
The access chain is not enough on its own. You also need the per-DID safety wrappers — the C functions that implement the five steps for each specific DID in your configuration.
Writing those by hand is where teams fall down. You have 20 DIDs. Some are read-only in default session, some need SecurityAccess level 1 for write, some are extended-session only. Writing 20 × 5 = 100 safety checks correctly, maintaining them when the config changes, and keeping the RTM synchronized is a full-time job.
EDS solves this with codegen.py. You describe your ECU's diagnostic interface in YAML:
dids:
- id: "0xF190"
name: "VIN"
data_length: 17
access: [read]
min_session: default
read_security_level: 0
write_security_level: 0
- id: "0xF187"
name: "SparePart"
data_length: 11
access: [read, write]
min_session: extended
read_security_level: 0
write_security_level: 1
The generator produces did_safety_wrappers.c and did_safety_wrappers.h — one wrapper per DID, each implementing the full five-step chain with the correct session/security/length constants for that specific DID.
Critically, the generator enforces the ASIL-B constraint at generation time:
SAFETY [HIGH-1]: dids[1] (id='0xF187') 'SparePart' has write access but write_security_level=0. FIX: set write_security_level >= 1 in diagnostics_config.yaml
This is the key property: the misconfiguration is caught at generation time, not at runtime, not in a code review. A write-capable DID with no security requirement is a generator error, not a runtime warning. The build fails. There is no path from a misconfigured YAML to a shipped binary that bypasses security on a write DID.
Static allocation in practice
The DID and DTC databases are statically allocated arrays sized by the generator from the YAML:
/* generated/generated_config.h — do not edit */ #define GEN_DID_COUNT (5U) #define GEN_DTC_COUNT (2U)
These constants feed the database init functions. No runtime allocation. No linked lists. Stack depth for uds_server_process_request() is bounded by the longest service call path, which the Zephyr thread config can set with a fixed stack size.
The NVM mirror for DTCs — storing which DTCs have fired, which are confirmed, which are pending — is a static ring buffer. Size is a compile-time constant derived from UDS_MAX_DTC_COUNT (the maximum DTC capacity defined in uds_types.h). No heap.
The pre-start self-test
uds_safety_self_test() is called at Step 1.1 in the generated init sequence, after uds_safety_init() and before any database initialization. It tests the safety module's own null pointer checks, bounds checks, and violation counter saturation — using known-bad inputs and verifying that the correct errors are returned.
/* generated/uds_init.c — do not edit */
status = uds_safety_init();
if (status != UDS_STATUS_OK) { return status; }
/* Step 1.1 — EDS pre-start safety mechanism (ISO 26262-6:2018 §7.4.12) */
{
uds_safety_result_t rc = uds_safety_self_test();
if (rc != UDS_STATUS_OK) { return rc; }
}
If the self-test fails — which covers integration defects such as corrupted generated configuration tables, incorrect platform adaptation, incompatible library versions, or a misconfigured memory map — the ECU enters a safe state before accepting any diagnostic session.
This step is generated, not handwritten. If it's removed from the template, the generator's own test suite fails. The CI gate that checks for uds_safety_self_test() in the generated output catches any regression.
Running this on Zephyr native_sim
The complete ASIL-B stack — safety module, session FSM, security access, DID dispatch, DTC persistence, ISO-TP framing — runs unmodified on Zephyr's native_sim target. CI builds and runs the basic_ecu example on every push:
west build -b native_sim examples/basic_ecu west build -t run
The native_sim target uses a CAN loopback socket. The same binary, the same UDS handlers, the same ASIL-B wrappers that run in CI are what you flash to your STM32 or Nordic SoC on your bench. Not a simulator mode. Not reduced functionality. The platform abstraction layer is four callbacks — can_send, can_recv, nvm_read, nvm_write — and Zephyr provides the native_sim implementations automatically.
The complete stack is on GitHub — clone it and run the basic_ecu example in under 15 minutes: github.com/Xaloqi/EDS
What this is not
This is not a claim that EDS alone makes your product ASIL-B. ASIL-B is a system-level requirement. It covers hardware fault tolerance, FMEA, HARA, verification activities, management processes. EDS handles the software architecture for the diagnostics subsystem. The Professional tier ships a Safety Manual, HARA extract, Requirements Traceability Matrix, and MISRA C:2012 deviation log to give your audit the substrate it needs.
Security gates are not safety mechanisms — but they overlap. The session gate and SecurityAccess requirement in EDS serve two purposes at once: they protect against unauthorised diagnostic access (a cybersecurity objective, ISO/SAE 21434) and they prevent unsafe state changes through the diagnostic interface (a functional safety objective, ISO 26262). These are related but distinct. A safety engineer will ask: "what happens if the security gate fails?" A cybersecurity engineer will ask: "what prevents an attacker from reaching the gate?" EDS addresses the first question. The second is an integration obligation — OEM key provisioning, CAN network segmentation, secure boot — documented in the Safety Manual assumptions of use.
Freedom from interference is the largest open item for Zephyr integration. EDS enforces its own internal FFI — the QM DID database can't corrupt the ASIL-B safety wrapper logic because all access goes through the generated wrappers. But the bigger FFI question for Zephyr is at the OS level: does the diagnostic task have memory isolation from other tasks? Is the RTOS tick reliable under load? Does a misbehaving application callback have a path to corrupt the UDS stack's state? These require Zephyr MPU configuration, RTOS tick stability analysis, and callback interface discipline — all of which are integrator responsibilities documented as assumptions of use in the Safety Manual, not properties EDS can guarantee on its own.
Fault coverage is not quantified. EDS identifies and implements twelve named safety mechanisms (SM-01 through SM-12 in the Safety Manual). What it does not yet provide is a quantitative diagnostic coverage analysis — single-point fault metrics, latent fault metrics, or a FMEDA. Those are required before a formal production ASIL-B claim and are the integrator's responsibility at item level.
What EDS does give you is a diagnostics stack where the ASIL-B software properties are not assertions — they are enforced by the build system, verified by the generator's own tests, and traceable to named requirements. Starting from that baseline is a different engineering project than starting from scratch or from a library that wasn't designed for it.
The generator test that proved it
The most useful single test we have is this one, in the harness suite:
/* Group C — ReadDataByIdentifier */
TEST("0x22 in wrong session returns NRC 0x7F") {
/* Set session to default. Attempt to read a DID that requires extended. */
result = uds_tester_send_read_did(&ctx, 0xF187);
ASSERT_NRC(result, 0x7F); /* serviceNotSupportedInActiveSession — session gate */
}
It doesn't test that the session gate might work. It tests that the session gate cannot be bypassed — that the five-step chain is always in the path, for this specific DID, on this specific service call, under these specific conditions. That test runs in CI on every commit. If any change to the service handler, the safety wrapper, or the DID table breaks the gate, the test fails before the branch merges.
That's what "ASIL-B-ready" means in practice for a diagnostics stack. Not a certification. A property that the build system enforces and the test suite verifies — every time, automatically.
Standards referenced
- ISO 14229-1:2026 — Unified Diagnostic Services — NRC priority ordering for server responses
- ISO 14229-1:2026 — Unified Diagnostic Services — Session state machine requirements
- ISO 26262-6:2018 — Software unit design and implementation: determinism, bounded WCET, analyzable call depth; MISRA C:2012 (Rule 17.2) prohibits recursion
- ISO 26262-6:2018 §7.4.12 — Safety mechanisms: pre-start self-test as a safety mechanism to detect faults before normal operation
native_sim today. The Developer and Professional tiers add the codegen templates, AI tooling, and safety documentation package your audit will need. See pricing →