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.
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.
DetectorMonitoring
Application threads
k_msgq
drop report lost in the full queue
Detector · every 100 ms
state_abuse 0 / 0 request_rate 0 / 32 queue_rejects 0 / 0 config_integrity 0 / 0 unknown_destination 0 / 0 tx_volume 0 / 4096 heartbeat_age 0 / 500 telemetry_drops 0 / 0 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.
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:
| Threat | What happens on the device |
|---|---|
| Command and state abuse | An inference or management command arrives in a state that forbids it |
| Request flooding | Requests arrive faster than the inference worker can serve them, and its queue overflows |
| Configuration tampering | A protected setting changes outside the approved update path |
| Unexpected outbound activity | Data heads to an unapproved destination, or approved traffic exceeds its budget |
| Monitor disruption | Security 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.
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:
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.embedids_zephyr_poll() drains at most one queue’s worth of samples per
call, so a burst cannot stall the thread indefinitely.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:
CONFIG_EMBEDIDS=yCONFIG_EMBEDIDS_FLOATING_POINT=nCONFIG_EMBEDIDS_MAX_METRICS=8CONFIG_EMBEDIDS_MAX_ALGORITHMS_PER_METRIC=1CONFIG_EMBEDIDS_SAMPLE_QUEUE_SIZE=32Producers submit; the detector thread drains and evaluates. In the demo, the detector thread itself samples the application’s counters every 100 ms:
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:
| Rule | Raises an alert when |
|---|---|
state_abuse | a command arrives in a state that forbids it |
request_rate | more than 32 inference requests arrive in one interval |
queue_rejects | the eight-slot inference queue rejects a request |
config_integrity | the work-unit setting differs from its approved value |
unknown_destination | a send targets an unapproved destination |
tx_volume | more than 4,096 approved bytes leave in one interval |
heartbeat_age | the telemetry producer’s last heartbeat is older than 500 ms |
telemetry_drops | the 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.
Every scenario boots fresh firmware and types commands into its UART console, exactly as an operator or an attacker on that interface would.
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:
{"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.
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.
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.
| Scenario | Command | What EmbedIDS raises |
|---|---|---|
| State abuse | infer 1 while idle | state_abuse after 10 ms |
| Unknown destination | send unknown 128 | unknown_destination after 10 ms |
| Outbound volume | send approved 8192 | tx_volume after 100 ms |
| Queue overflow | fault overflow | telemetry_drops after 120 ms, 104 samples dropped |
| Normal workload | 12 paced infer 1 and send approved 256 | Nothing; 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.
| Scenario | Rule | Latency |
|---|---|---|
| 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 |
repeat-summary.json.
| Measure | Target | Measured |
|---|---|---|
| Scenario acceptance | 7 / 7 | 7 / 7 in every one of 10 runs |
| Trigger to alert sample | ≤ 200 ms | 10–120 ms |
| Alerts during the normal workload | 0 | 0 |
| Heap allocation in the detector and adapter | none | no 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:
CONFIG_EMBEDIDS_DEMO_FAULT_INJECTION is off.embedids_zephyr_dropped()
directly in the detector instead of routing it through the queue.native_sim is simulated and almost
deterministic; a physical board adds interrupt and scheduling jitter.| Phase | Status | Scope |
|---|---|---|
| Simulation demo | Done | Module, adapter, demo firmware, scenario runner and CI on native_sim |
| Physical board | Next | Memory, timing and power measured on a target microcontroller |
| Real inference | Proposed | A TensorFlow Lite Micro workload in place of the simulated one |
| Network inputs | Proposed | Zephyr network statistics feeding the outbound rules |
| Protected monitor | Proposed | Monitor state isolated with userspace, plus an independent watchdog |
The demo needs a Linux host with a Zephyr v4.2.0 development environment; no hardware is required.
west build -b native_sim/native/64 examples/zephyr/edge_ai_security -d build/zephyr-demopython3 scripts/zephyr/run_scenarios.py \ --binary build/zephyr-demo/zephyr/zephyr.exe \ --report build/zephyr-demo/scenarios.json./build/zephyr-demo/zephyr/zephyr.exe# then type, one per line:infer 1mode activeinfer 256send unknown 128fault heartbeatstatuspython "$ZEPHYR_BASE/scripts/twister" -T tests/zephyr/adapter \ -p native_sim/native/64 -O build/zephyr-tests --inline-logs \ --extra-args="ZEPHYR_MODULES=$PWD"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.