From 76e7ad7369bb6d34948ceac7d9add5cdf269b304 Mon Sep 17 00:00:00 2001 From: swoiszwillo Date: Wed, 24 Jun 2026 10:48:39 -0600 Subject: [PATCH 1/3] fix(zkernel): mark ISR-reachable list helpers K_ISR_SAFE (IRAM) k_sem_give() is K_ISR_SAFE (IRAM_ATTR) so it can run from an ESP-IDF IRAM ISR while the flash cache is disabled, but on its hot path it called z_sem_pop_waiter(), a flash-resident static helper. When the give came from an IRAM ISR during a concurrent flash op (e.g. an IRAM GPIO ISR submitting work -> k_work_submit -> k_sem_give), the fetch of z_sem_pop_waiter faulted with a cache-access error and the chip panicked (issue #53, found and fixed locally by a downstream). Mark z_sem_pop_waiter K_ISR_SAFE so it relocates into IRAM. It is pure list-walking over the caller-owned waiter list with no FreeRTOS calls, so it is safe in IRAM. Its other caller, k_sem_reset(), is flash-resident, but flash code calling an IRAM helper is fine. Sweeping the rest of the K_ISR_SAFE call graph for the same class (an IRAM function calling a flash-resident static helper) turned up one sibling: z_event_match() in k_event.c, called on both ISR-safe paths -- the post family (z_event_post_internal) and the wait family fast path (z_event_wait_internal). Mark it K_ISR_SAFE too; it is pure arithmetic with no FreeRTOS calls. No behavioral change; host (linux target) suite unaffected (224/224), clang-format clean. The fault is a target-only memory-placement issue the host build cannot observe. Co-Authored-By: Claude Opus 4.8 --- components/zkernel/src/k_event.c | 8 +++++++- components/zkernel/src/k_sem.c | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/components/zkernel/src/k_event.c b/components/zkernel/src/k_event.c index ca16c2f..b3aa932 100644 --- a/components/zkernel/src/k_event.c +++ b/components/zkernel/src/k_event.c @@ -29,7 +29,13 @@ struct z_event_waiter { bool woken; /* a post/set targeted this waiter */ }; -static uint32_t z_event_match(uint32_t current, uint32_t mask, bool all) +/* K_ISR_SAFE (IRAM): called on both ISR-safe paths -- the post family + * (z_event_post_internal) and the wait family's fast path + * (z_event_wait_internal). A flash-resident helper here would fault with + * a cache-access error when reached from an IRAM ISR during a concurrent + * flash op (the z_sem_pop_waiter class, issue #53). Pure arithmetic, no + * FreeRTOS calls -- safe in IRAM. */ +static uint32_t K_ISR_SAFE z_event_match(uint32_t current, uint32_t mask, bool all) { uint32_t hit = current & mask; diff --git a/components/zkernel/src/k_sem.c b/components/zkernel/src/k_sem.c index 7f6847f..666add9 100644 --- a/components/zkernel/src/k_sem.c +++ b/components/zkernel/src/k_sem.c @@ -44,8 +44,14 @@ int k_sem_init(struct k_sem *sem, unsigned int initial_count, unsigned int limit /* Pop the wake target: highest cached priority, FIFO among equals * (upstream wakes the highest-priority waiter). Caller holds the lock. - * Plain code over the caller-owned list -- no FreeRTOS calls. */ -static struct z_sem_waiter *z_sem_pop_waiter(struct k_sem *sem) + * Plain code over the caller-owned list -- no FreeRTOS calls. + * + * K_ISR_SAFE (IRAM): on k_sem_give's hot path, which is ISR-safe and may + * run while the flash cache is disabled. A flash-resident helper here + * would fault with a cache-access error when the give comes from an IRAM + * ISR during a concurrent flash op (issue #53). k_sem_reset is + * flash-resident, but flash code calling an IRAM helper is fine. */ +static K_ISR_SAFE struct z_sem_waiter *z_sem_pop_waiter(struct k_sem *sem) { struct z_sem_waiter *best = NULL; sys_dnode_t *n; From 4769c0890503957b6b9a1407b5d78cad27957a76 Mon Sep 17 00:00:00 2001 From: swoiszwillo Date: Wed, 24 Jun 2026 10:52:00 -0600 Subject: [PATCH 2/3] test(zkernel): add IRAM symbol-placement guard for K_ISR_SAFE helpers The issue-#53 fault is invisible to the host (linux) suite: it is a target-only memory-placement property. This guard runs against a real target ELF and fails if any K_ISR_SAFE helper landed in a flash-mapped text section instead of .iram0.text -- catching the exact regression where an IRAM_ATTR caller reaches a flash-resident static helper. Section-name based (.iram0.text vs .flash.text), so it is ISA-agnostic across Xtensa and RISC-V targets. Validated on esp32s3: z_sem_pop_waiter and z_event_match resolve into .iram0.text, while non-ISR helpers (k_sem_reset, k_timer_start) remain in .flash.text. Co-Authored-By: Claude Opus 4.8 --- tools/check_iram_symbols.sh | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100755 tools/check_iram_symbols.sh diff --git a/tools/check_iram_symbols.sh b/tools/check_iram_symbols.sh new file mode 100755 index 0000000..b1123e6 --- /dev/null +++ b/tools/check_iram_symbols.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Intercreate +# +# Assert that K_ISR_SAFE helpers land in IRAM (.iram0.text), not in a +# flash-mapped text section. A K_ISR_SAFE (IRAM_ATTR) function that calls +# a flash-resident helper faults with a cache-access error when reached +# from an IRAM ISR during a flash-cache-disabled window (issue #53). +# +# The host (linux) build cannot observe this -- it is a target-only +# memory-placement property -- so this guard runs against a real target +# ELF. Section names (.iram0.text vs .flash.text/.text) are consistent +# across Xtensa and RISC-V targets, so the check is ISA-agnostic. +# +# Usage: tools/check_iram_symbols.sh +# OBJDUMP=-objdump may be set to override toolchain autodetect. + +set -euo pipefail + +ELF="${1:?usage: check_iram_symbols.sh }" + +# Symbols that MUST be IRAM-resident: each carries K_ISR_SAFE and sits on +# a call path reachable from an IRAM ISR while the flash cache may be off. +# Keep in sync with the K_ISR_SAFE definitions in components/*/src. +REQUIRED_IRAM=( + # k_sem.c + z_sem_pop_waiter + k_sem_give + k_sem_take + # k_event.c + z_event_match + z_event_post_internal + z_event_wait_internal + # k_work.c + k_work_submit_internal + k_work_submit_to_queue + # k_msgq.c + k_msgq_put +) + +# Pick the toolchain objdump if not supplied. +if [ -z "${OBJDUMP:-}" ]; then + case "$(file -b "$ELF")" in + *RISC-V*) OBJDUMP=riscv32-esp-elf-objdump ;; + *Tensilica* | *Xtensa*) OBJDUMP="xtensa-${IDF_TARGET:-esp32s3}-elf-objdump" ;; + *) + echo "error: cannot determine toolchain for $ELF (set OBJDUMP=)" >&2 + exit 2 + ;; + esac +fi +command -v "$OBJDUMP" >/dev/null || { + echo "error: $OBJDUMP not on PATH (run . \$IDF_PATH/export.sh)" >&2 + exit 2 +} + +# One symbol-table dump, reused per symbol. +SYMTAB="$("$OBJDUMP" -t "$ELF")" + +fail=0 +for sym in "${REQUIRED_IRAM[@]}"; do + # A symbol-table entry's line names exactly one section. + line="$(printf '%s\n' "$SYMTAB" | grep -E "[[:space:]]${sym}\$" || true)" + + if [ -z "$line" ]; then + # Inlined into its IRAM caller (no out-of-line symbol) or not + # linked into this image -- both safe. Report, don't fail. + echo "skip $sym (no out-of-line symbol -- inlined or not linked)" + elif [[ "$line" == *".iram0.text"* ]]; then + echo "ok $sym -> .iram0.text" + else + sec="$(printf '%s\n' "$line" | awk '{print $(NF-1)}')" + echo "FAIL $sym -> ${sec} (expected .iram0.text; flash-resident would cache-fault from an IRAM ISR)" + fail=1 + fi +done + +exit "$fail" From 72f40716be486b5c09e8e525cd0ee8388b7933be Mon Sep 17 00:00:00 2001 From: swoiszwillo Date: Thu, 25 Jun 2026 16:25:43 -0600 Subject: [PATCH 3/3] ci(zkernel): run IRAM-placement guard on the esp32s3 build Wire tools/check_iram_symbols.sh into the existing build-esp32s3 job so every PR verifies that K_ISR_SAFE helpers are IRAM-resident on a real target ELF -- the issue-#53 regression the host (linux) suite cannot observe. Reuses the target build already produced by that job, so no extra CI cost. Also make the script derive its symbol set from source (every K_ISR_SAFE definition in components/*/src) instead of a hand-maintained list, so a newly-added ISR-safe helper is covered automatically. Inlined/gc'd symbols report "skip" (safe); only an out-of-line, flash-resident symbol fails. Validated on esp32s3: 27 symbols derived, 25 confirmed in .iram0.text (incl. z_sem_pop_waiter and z_event_match), 2 skipped. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 12 ++++++++ tools/check_iram_symbols.sh | 60 +++++++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec41f8f..a303a7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,3 +61,15 @@ jobs: cd test idf.py set-target esp32s3 idf.py build + + - name: Assert K_ISR_SAFE helpers are IRAM-resident (issue #53) + # A K_ISR_SAFE (IRAM_ATTR) function that reaches a flash-resident + # helper cache-faults when called from an IRAM ISR during a flash + # op. The host suite cannot see this; verify symbol placement on + # the target ELF. OBJDUMP is set explicitly so the check does not + # depend on `file` being present in the image. + shell: bash + run: | + . /opt/esp/idf/export.sh + OBJDUMP=xtensa-esp32s3-elf-objdump \ + tools/check_iram_symbols.sh test/build/boreas_test.elf diff --git a/tools/check_iram_symbols.sh b/tools/check_iram_symbols.sh index b1123e6..b56a226 100755 --- a/tools/check_iram_symbols.sh +++ b/tools/check_iram_symbols.sh @@ -13,31 +13,39 @@ # ELF. Section names (.iram0.text vs .flash.text/.text) are consistent # across Xtensa and RISC-V targets, so the check is ISA-agnostic. # -# Usage: tools/check_iram_symbols.sh -# OBJDUMP=-objdump may be set to override toolchain autodetect. +# The symbol set is DERIVED from source: every function carrying the +# K_ISR_SAFE attribute in components/*/src/*.c must be IRAM-resident, so a +# newly-added ISR-safe helper is covered automatically with no edit here. +# A symbol the compiler inlined into its IRAM caller has no out-of-line +# symbol and is reported as "skip" (safe). Override the derived set with +# IRAM_SYMBOLS="a b c" if you ever need to. +# +# Usage: tools/check_iram_symbols.sh [src-root] +# OBJDUMP=-objdump override toolchain autodetect +# IRAM_SYMBOLS="sym1 sym2" override the derived symbol set set -euo pipefail -ELF="${1:?usage: check_iram_symbols.sh }" +ELF="${1:?usage: check_iram_symbols.sh [src-root]}" +SRC_ROOT="${2:-$(cd "$(dirname "$0")/.." && pwd)}" + +# Derive the K_ISR_SAFE function set from source. A definition line +# carries the attribute and opens a parameter list; the function name is +# the identifier just before '('. Drop comment lines and the macro +# #define (the only other places the token appears). +derive_symbols() { + grep -hE 'K_ISR_SAFE' "$SRC_ROOT"/components/*/src/*.c | + grep -vE '^[[:space:]]*([*]|/[*]|//|#)' | + grep -E '\(' | + sed -E 's/.*[^A-Za-z0-9_]([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*\(.*/\1/' | + sort -u +} -# Symbols that MUST be IRAM-resident: each carries K_ISR_SAFE and sits on -# a call path reachable from an IRAM ISR while the flash cache may be off. -# Keep in sync with the K_ISR_SAFE definitions in components/*/src. -REQUIRED_IRAM=( - # k_sem.c - z_sem_pop_waiter - k_sem_give - k_sem_take - # k_event.c - z_event_match - z_event_post_internal - z_event_wait_internal - # k_work.c - k_work_submit_internal - k_work_submit_to_queue - # k_msgq.c - k_msgq_put -) +SYMBOLS="${IRAM_SYMBOLS:-$(derive_symbols)}" +if [ -z "${SYMBOLS//[[:space:]]/}" ]; then + echo "error: no K_ISR_SAFE symbols found under $SRC_ROOT/components" >&2 + exit 2 +fi # Pick the toolchain objdump if not supplied. if [ -z "${OBJDUMP:-}" ]; then @@ -59,7 +67,11 @@ command -v "$OBJDUMP" >/dev/null || { SYMTAB="$("$OBJDUMP" -t "$ELF")" fail=0 -for sym in "${REQUIRED_IRAM[@]}"; do +checked=0 +while IFS= read -r sym; do + [ -n "$sym" ] || continue + checked=$((checked + 1)) + # A symbol-table entry's line names exactly one section. line="$(printf '%s\n' "$SYMTAB" | grep -E "[[:space:]]${sym}\$" || true)" @@ -74,6 +86,8 @@ for sym in "${REQUIRED_IRAM[@]}"; do echo "FAIL $sym -> ${sec} (expected .iram0.text; flash-resident would cache-fault from an IRAM ISR)" fail=1 fi -done +done <<<"$SYMBOLS" +echo "---" +echo "checked $checked K_ISR_SAFE symbol(s) in $(basename "$ELF")" exit "$fail"