Skip to content

Case Study: Runtime Intrusion Detection on Zephyr RTOS

How the open-source EmbedIDS intrusion detection library runs inside Zephyr RTOS firmware as an optional module, with a bounded sample queue and a single detector thread, and flags command abuse, request floods, tampering and telemetry loss within two sample periods.

native_sim/native/64 Zephyr v4.2.0 · simulated

    DetectorMonitoring

    Application threads

    Device mode idle State policy
    Inference queue 0 / 8 queued Idle
    Configuration 10 work units Approved
    Outbound dispatcher no traffic Allow-list
    Telemetry producer beat every 50 ms Alive

    k_msgq

    0/32 queued

    dropped 0

    Detector · every 100 ms

    1. state_abuse 0 / 0
    2. request_rate 0 / 32
    3. queue_rejects 0 / 0
    4. config_integrity 0 / 0
    5. unknown_destination 0 / 0
    6. tx_volume 0 / 4096
    7. heartbeat_age 0 / 500
    8. telemetry_drops 0 / 0
      Replay of a recorded run of scripts/zephyr/run_scenarios.py (recorded data). Commands, alert lines, values and latencies are recorded; the replay slows each 100 ms detector cycle down. Fault commands are test-only.

      The challenge

      A microcontroller running an AI workload cannot host a Linux security agent: no processes to inspect, kilobytes of RAM, and real-time work that monitoring must never block. Detection has to live inside the firmware.

      The approach

      The EmbedIDS C core builds as a Zephyr module. Application threads and interrupts submit samples to a bounded queue without waiting; one detector thread evaluates every sample against fixed rules and prints JSON alerts.

      The result

      All seven scenarios passed in each of 10 repeated runs. Every attack was flagged within 120 ms of its trigger, and the normal workload raised no alerts. Metric history takes 640 bytes, with no heap allocation.


      The challenge: attacks on a Zephyr device

      Picture a battery-powered sensor or a small vision module: a Zephyr application accepts commands, runs inference, ships results upstream and reports its own health. Attacks against it look like this:

      ThreatWhat happens on the device
      Command and state abuseAn inference or management command arrives in a state that forbids it
      Request floodingRequests arrive faster than the inference worker can serve them, and its queue overflows
      Configuration tamperingA protected setting changes outside the approved update path
      Unexpected outbound activityData heads to an unapproved destination, or approved traffic exceeds its budget
      Monitor disruptionSecurity telemetry stops, or monitoring samples are lost

      The constraints matter as much as the threats. Detection cannot allocate memory at run time, interrupt handlers cannot wait for it, and it must never delay the inference path it protects.

      The approach: intrusion detection compiled into the firmware

      EmbedIDS joins the build as an ordinary Zephyr module, so no separate agent, operating system or cloud service is needed. The C core stays free of operating-system calls; a thin adapter connects it to Zephyr’s kernel:

      • Configured with Kconfig. Limits such as the number of metrics and the queue size reach both the library and its callers, so every structure layout matches.
      • Non-blocking ingestion. embedids_zephyr_submit(), declared in ports/zephyr/include/embedids/zephyr.h, copies a sample into a statically allocated k_msgq without waiting. It is safe from threads and interrupt handlers. A full queue counts a drop instead of blocking.
      • A single owner. Only the detector thread touches the EmbedIDS context. embedids_zephyr_poll() drains at most one queue’s worth of samples per call, so a burst cannot stall the thread indefinitely.
      • Every sample is evaluated. All rules run after each queued sample, so a normal reading that arrives later cannot hide a brief violation.
      • Unsafe configurations are refused. The adapter rejects duplicate metric names, out-of-range indices, the unfinished built-in trend algorithm and Boolean thresholds, rather than running them silently.

      Architecture of the Zephyr integration

      The replay above shows the pipeline: application threads on the left, the 32-slot queue in the middle and the detector’s rules on the right. The module itself is described by zephyr/module.yml and configured through zephyr/Kconfig. The demo enables it with these lines from its prj.conf:

      examples/zephyr/edge_ai_security/prj.conf (excerpt)
      CONFIG_EMBEDIDS=y
      CONFIG_EMBEDIDS_FLOATING_POINT=n
      CONFIG_EMBEDIDS_MAX_METRICS=8
      CONFIG_EMBEDIDS_MAX_ALGORITHMS_PER_METRIC=1
      CONFIG_EMBEDIDS_SAMPLE_QUEUE_SIZE=32

      Producers submit; the detector thread drains and evaluates. In the demo, the detector thread itself samples the application’s counters every 100 ms:

      examples/zephyr/edge_ai_security/src/main.c (simplified)
      while (true) {
      k_sleep(K_MSEC(SAMPLE_MS)); /* 100 ms */
      read_counters_under_lock(values);
      for (uint16_t i = 0; i < METRIC_COUNT; ++i) {
      embedids_zephyr_submit(&detector, i, (embedids_metric_value_t){.u32 = values[i]});
      }
      embedids_zephyr_poll(&detector, detection_result, NULL);
      }

      The full loop, rules and command parser are in src/main.c; the adapter is ports/zephyr/embedids_zephyr.c.

      The demo firmware watches eight rules, all numeric thresholds:

      RuleRaises an alert when
      state_abusea command arrives in a state that forbids it
      request_ratemore than 32 inference requests arrive in one interval
      queue_rejectsthe eight-slot inference queue rejects a request
      config_integritythe work-unit setting differs from its approved value
      unknown_destinationa send targets an unapproved destination
      tx_volumemore than 4,096 approved bytes leave in one interval
      heartbeat_agethe telemetry producer’s last heartbeat is older than 500 ms
      telemetry_dropsthe adapter’s sample queue drops a sample

      Each alert is printed once, when the rule starts failing, with the value, the limit and the sample time. A clear line follows when it recovers.

      Seeing it work: attacks through the UART console

      Every scenario boots fresh firmware and types commands into its UART console, exactly as an operator or an attacker on that interface would.

      Scenario A: a request flood

      The device is switched to active, then asked for 256 inferences at once. Nine requests are accepted, one straight to the waiting worker and eight into its queue, and 247 are rejected. The next detector sample, 100 ms after the command, raises two alerts:

      UART output
      {"type":"alert","rule":"request_rate","value":256,"limit":32,"timestamp_ms":440,"sample_ms":100}
      {"type":"alert","rule":"queue_rejects","value":247,"limit":0,"timestamp_ms":440,"sample_ms":100}

      The worker keeps serving the requests it accepted. The detector reads counters and never touches the request queue itself.

      Scenario B: configuration tampering

      The config command changes the inference work units, but only while the device is idle. The test-only fault config command bypasses that path and writes 100 over the approved 10, as memory corruption or a rogue code path would. The next sample, 10 ms later, raises config_integrity.

      This is an in-memory comparison against the approved value. It shows the detection path, not cryptographic attestation.

      Scenario C: the telemetry goes silent

      fault heartbeat stops the telemetry producer, which normally beats every 50 ms. Its age climbs until a sample reads 580 ms, past the 500 ms limit. Across ten runs the alert arrived 29 to 79 ms after the timeout expired, depending on where the silence began within the 100 ms sampling cycle.

      More scenarios

      ScenarioCommandWhat EmbedIDS raises
      State abuseinfer 1 while idlestate_abuse after 10 ms
      Unknown destinationsend unknown 128unknown_destination after 10 ms
      Outbound volumesend approved 8192tx_volume after 100 ms
      Queue overflowfault overflowtelemetry_drops after 120 ms, 104 samples dropped
      Normal workload12 paced infer 1 and send approved 256Nothing; 12 inferences completed

      The normal run also sends seven malformed commands, such as infer 257. The parser rejects them without raising alerts: bad syntax is not, by itself, evidence of an attack.

      Results: detection latency and footprint

      Trigger-to-alert latency per detection across 10 runs. Budget 200 ms; worst case 120 ms.
      ScenarioRuleLatency
      State abuse state_abuse 10 ms
      Flood request_rate 100 ms
      Flood queue_rejects 100 ms
      Config tamper config_integrity 10 ms
      Outbound unknown_destination 10 ms
      Outbound tx_volume 100 ms
      Heartbeat loss heartbeat_age 29–79 ms
      Queue overflow telemetry_drops 120 ms
      Time from each trigger to the detector sample that raised the alert, in simulated milliseconds over 10 repeated runs. All runs passed; only heartbeat loss varied. The worst case, 120 ms, is inside the two-sample budget. Data: repeat-summary.json.
      • 7 / 7 scenarios passed in each of 10 repeated runs, including the normal workload with no alerts.
      • 10–120 ms from trigger to alert sample, against a 200 ms budget of two sample periods.
      • 640 bytes of metric history: eight metrics, four samples each, 20 bytes per sample.
      • 19 / 19 tests for the Zephyr adapter, across integer and floating-point builds.
      MeasureTargetMeasured
      Scenario acceptance7 / 77 / 7 in every one of 10 runs
      Trigger to alert sample≤ 200 ms10–120 ms
      Alerts during the normal workload00
      Heap allocation in the detector and adapternoneno allocator symbols referenced
      Metric history—640 B
      Adapter state, including the 32-slot queue—872 B
      Host CPU for the whole simulated firmware—1.4–1.9 % of one core

      The host CPU figure covers the entire native_sim process on an x86-64 host. It is not a microcontroller measurement, and native executable sizes are not flash budgets.

      The integration is backed by automated checks that run in CI:

      • Adapter tests cover per-sample evaluation, interrupt-context submission, concurrent producers, overflow accounting, history wrap-around, invalid configurations, custom algorithms, and builds with floating-point and double-precision metrics.
      • The scenario runner boots fresh firmware for each scenario and drives its UART. It fails on a missing alert, an unexpected alert, or a detection slower than 200 ms.
      • A no-fault build confirms that the fault commands disappear when CONFIG_EMBEDIDS_DEMO_FAULT_INJECTION is off.
      • The core library’s existing test suites run in the same workflow.

      What we learned

      • Evaluate every sample, not just the latest. The first adapter drained the whole queue and then checked each metric once, so a normal reading could mask a violation queued just before it. The adapter now runs every rule after each sample, and a regression test pins this down.
      • A drop counter must not depend on the queue it measures. In the overflow scenario, the report of the first 96 drops was itself dropped, because the queue was full. The alert came one interval later from the next count, still within budget. The fix is to read embedids_zephyr_dropped() directly in the detector instead of routing it through the queue.
      • Counts per interval are not rates. Thresholds apply per observed interval. Under scheduling delays, intervals stretch; production firmware should measure the interval and the detector’s own deadline.
      • A monitor cannot report its own death. Heartbeat detection catches a silent producer while the detector runs. A stalled detector or device needs a hardware watchdog or an external observer.

      What this study does not show

      • Hardware behavior. Timing on native_sim is simulated and almost deterministic; a physical board adds interrupt and scheduling jitter.
      • Real inference or networking. The workload and the dispatcher model application policy; no neural network runs and no packets are inspected.
      • Protection against privileged compromise. Everything shares one trusted firmware image. There is no userspace isolation, secure-world monitor or automatic containment.
      • A field false-positive rate. A short scripted normal workload with no alerts is a regression check, not a statistical claim.

      Path to hardware

      PhaseStatusScope
      Simulation demoDoneModule, adapter, demo firmware, scenario runner and CI on native_sim
      Physical boardNextMemory, timing and power measured on a target microcontroller
      Real inferenceProposedA TensorFlow Lite Micro workload in place of the simulated one
      Network inputsProposedZephyr network statistics feeding the outbound rules
      Protected monitorProposedMonitor state isolated with userspace, plus an independent watchdog

      Try it yourself on native_sim

      The demo needs a Linux host with a Zephyr v4.2.0 development environment; no hardware is required.

      Terminal window
      west build -b native_sim/native/64 examples/zephyr/edge_ai_security -d build/zephyr-demo
      python3 scripts/zephyr/run_scenarios.py \
      --binary build/zephyr-demo/zephyr/zephyr.exe \
      --report build/zephyr-demo/scenarios.json

      The source is on GitHub: the demo firmware in examples/zephyr/edge_ai_security/, the scenario runner scripts/zephyr/run_scenarios.py and the adapter tests in tests/zephyr/adapter/.

      The Zephyr integration guide covers setup, the adapter API, resource accounting and every command in detail. For a Linux device with a GPU, the Edge AI runtime security case study runs the same detection engine as an on-device agent.


      Measurements were taken on 29 October 2026 with Zephyr v4.2.0 on native_sim/native/64 (x86-64 host, 100 ms sampling), over 10 repeated runs of scripts/zephyr/run_scenarios.py. The recorded data is in scripts/zephyr/case-study-data/; the replay at the top of this page plays one of those runs.

      Zephyr is a project of the Linux Foundation. This case study describes a simulated device and does not imply endorsement by the Zephyr Project.