Skip to content

Replace WAMR with WasmKit Embedded Swift runtime#18

Draft
Joannis wants to merge 6 commits into
mainfrom
jo/wasmkit-runtime-optimized
Draft

Replace WAMR with WasmKit Embedded Swift runtime#18
Joannis wants to merge 6 commits into
mainfrom
jo/wasmkit-runtime-optimized

Conversation

@Joannis

@Joannis Joannis commented Jun 22, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the WAMR C runtime with WasmKit compiled as Embedded Swift for RISC-V (riscv32-none-none-eabi). This brings the WASM runtime into the same Swift codebase as the rest of the Wendy firmware and eliminates the WAMR dependency.

What's in this PR

Runtime replacement (20adee23fe4447)

  • Multi-stage Swift compilation pipeline: WasmTypes → WasmParserCore → WasmKit → WendyWasmKit
  • All HAL host imports ported to WasmKit's typed Imports API (GPIO, I2C, Timer, NeoPixel, RMT, UART, SPI, OTel, Storage, Sys, BLE, Net)
  • Engine stack size capped to configured value (WasmKit defaults 512 KB; ESP32-C6 only has ~200 KB free DRAM)
  • Size optimisations: -O, -wmo, section GC, linker script patches → ~17 KB flash reduction vs naïve port

WASI Preview 1 (6549a8d)

  • Full wasi_snapshot_preview1 surface (44 functions) implemented in Swift via WasmKit's Imports API — no WAMR C shim
  • Working: fd_write (stdout/stderr, full iovec loop), random_get (esp_fill_random), clock_time_get/clock_res_get (esp_timer monotonic µs→ns), fd_fdstat_get (char-device for fds 0-2), proc_exit, sched_yield, environ/args (empty)
  • Correct stubs: all path_*, remaining fd_*, poll_oneoff, sock_* — registered with correct WASI parameter signatures, returning ENOSYS/EBADF/ENOTSUP
  • Removes dead wendy_wasi_shim C component (WAMR-specific; its init was never called with WasmKit)

Runtime fixes (6549a8d)

  • wendy_main.c: always call wendy_wasm_init() after reinit — prior code skipped init on subsequent WASM loads, leaving the engine in a torn-down state
  • Better instantiation error logging: pattern-match WasmKitError.kind to surface the message text
  • Heap stats logged before/after module instantiation for diagnostics

Test plan

  • Build for ESP32-C6: idf.py build with WASMKIT_ROOT pointing to the jo/embedded-runtime-v2 WasmKit branch
  • Flash and run a C blink app — verify GPIO + timer_delay_ms work
  • Run a Swift WASM app (swift_display) — verify wendy_print output reaches UART
  • Upload a new WASM binary over USB CDC while a module is running — verify stop/reload cycle works
  • Verify random_get and clock_time_get return non-zero values from a WASM app that uses them
  • Check that a WASM app compiled to wasm32-unknown-wasi instantiates without missing-import errors

🤖 Generated with Claude Code

https://claude.ai/code/session_01LysLik5cfWYSYXrDGde5yB

Joannis and others added 5 commits June 18, 2026 19:55
… memory config

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oHdvLR9P2c8GpozUx7cD9
Adds a new wendy_wasmkit component that reimplements the wendy_wasm.h
C API using WasmKit (https://github.com/swiftwasm/WasmKit, branch
jo/embedded-runtime-v2), compiled with -enable-experimental-feature
Embedded targeting riscv32-none-none-eabi for ESP32-C6.

Architecture:
- wendy_wasmkit/include/wendy_wasm.h   — same public C API as before
- wendy_wasmkit/include/wasm_export.h  — WAMR stub (allows existing
  component sources to compile without the managed component)
- wendy_wasmkit/src/Runtime.swift      — WasmKit Engine/Store/Module
  lifecycle, @_cdecl C entry points
- wendy_wasmkit/src/WendyImports.swift — all "wendy" host functions as
  WasmKit Function objects; dispatches callbacks through the WASM
  instance's wendy_handle_callback export
- wendy_wasmkit/src/WasmKitBridge.c   — thin C shim for output callback
- wendy_wasmkit/CMakeLists.txt        — custom swiftc build step for
  WasmKit + bridge sources in a single WMO compilation unit

Subsystem changes:
- wendy_callback: add wendy_callback_dequeue() for Swift callback dispatch
- wendy_uart/spi/storage/otel: add wendy_*_guest_*() public C wrappers
  callable from Swift without a wasm_exec_env_t
- All components: replace wasm-micro-runtime CMake dep with wendy_wasmkit
- wendy_wasm: neutered (no sources, no WAMR dep); kept for reference
- sdkconfig.defaults: WAMR Kconfig options removed

Build requirement: a Swift toolchain that supports
  -enable-experimental-feature Embedded -target riscv32-none-none-eabi
Set SWIFT_TOOLCHAIN_BIN=/path/to/swift/usr/bin before running idf.py.
Override WasmKit location with -DWASMKIT_ROOT=/path if needed (default:
../../../../swiftwasm/WasmKit relative to the wendy_wasmkit component).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvUkRGKaQLGvW5BwCnF6qn
All 4 Swift compilation stages now succeed and the firmware links cleanly
into a 1.5 MB binary (fits the 1.88 MB factory partition).

Build-system changes:
- CMakeLists.txt: post-configure linker script patch that inserts an explicit
  `.got.plt 0 (INFO)` section before /DISCARD/, fixing the "discarded output
  section: .got.plt" fatal GNU ld error triggered by Swift Embedded RISC-V code
- wendy_wasmkit/CMakeLists.txt:
  - 4-stage Swift compilation pipeline (WasmTypes→WasmParserCore→WasmKit→bridge)
  - -package-name WasmKit so Swift `package` access level symbols are visible
  - -fno-pic -fno-plt to prevent GOT/PLT generation
  - C compilation of _CWasmKit.c, TrapGuard.c, WasmKitBridge.c with GCC
  - Swift Unicode data tables bundled into the archive (avoids cross-archive PLT)
  - INTERFACE library for component (no SRCS = ESP-IDF creates INTERFACE target)
  - linker.lf: maps .swift_modhash sections to flash_rodata

Swift sources:
- Runtime.swift → WendyRuntime.swift (avoids WasmKit/Execution/Runtime.swift clash)
- WendyRuntime.swift: `import WasmKit; import WasmTypes`; fixed partition subtype
  enum; fixed esp_log_write (variadic) → wendy_log wrapper; relaxed gTerminateRequested
  to `internal` visibility; fixed unsafeBitCast for handle unwrapping
- WendyImports.swift: `import WasmKit; import WasmTypes`; pdMS_TO_TICKS →
  wendy_ms_to_ticks C wrapper; Value.i32/i64 use UInt32/UInt64 (not Int32/Int64);
  added Value.int32/int64 and UInt32.cInt helpers for HAL calls

Component changes:
- wendy_callback.h: added <stdbool.h> for bool; added wendy_callback_dequeue()
- wendy_otel/safety/wasi_shim CMakeLists: added esp_timer dependency
- wasm_export.h stub: added wasm_runtime_set_exception()

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvUkRGKaQLGvW5BwCnF6qn
Applies WasmKit optimizations from the paired jo/embedded-runtime-v2
branch commit 12dd701.  Net result: 1,625,008 → 1,607,392 bytes (-17 KB).

Summary of changes in WasmKit (reflected here via updated build):
- Imports: array storage to avoid String.hashValue → NFC normalization
- ExportsStorage: [(key:,value:)] array in Embedded mode for same reason
- Exports.find(function/memory:): StaticString byte-compare lookup
- Trap/TrapReason/WasmKitError description conformances guarded for Embedded
- TrapReason.Message error strings simplified (no array interpolation)
- parseName() uses String(validating:as:UTF8.self) instead of char append

Remaining 32 KB overhead: Unicode NFC normalization tables from Swift's
String type (inherent; eliminating requires replacing String with [UInt8]
throughout WasmKit's public API).

Updated bridge code in this repo:
- exports[function: "name"] / exports[memory: "memory"] replaced throughout
  with exports.find(function:) / exports.find(memory:) using StaticString
  to avoid String subscript hashing in WendyRuntime.swift / WendyImports.swift

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvUkRGKaQLGvW5BwCnF6qn
WASI:
- Replace broken WAMR-era C shim with complete WASI snapshot_preview1
  implementation in Swift using WasmKit's Imports API (44 functions).
- fd_write: full iovec loop via withUnsafeBufferPointer, stdout/stderr only.
- random_get: esp_fill_random via withUnsafeMutableBufferPointer.
- clock_time_get / clock_res_get: esp_timer monotonic (µs→ns).
- fd_fdstat_get: 24-byte char-device stat for stdio fds 0-2.
- All path_*, remaining fd_*, sock_*, poll_oneoff: correct WASI signatures
  returning ENOSYS/EBADF/ENOTSUP — no filesystem or WASI sockets.
- Remove dead wendy_wasi_shim dependency from CMakeLists (init was never
  called with WasmKit; now eliminated entirely).
- Add esp_random.h to WendyCBridge.h for esp_fill_random.

Runtime fixes:
- Engine stack size: respect configured stackSize instead of defaulting to
  WasmKit's 512 KB (which exceeds available DRAM on ESP32-C6).
- Instantiation errors: pattern-match WasmKitError.kind to log the message
  text; log heap stats before and after instantiation for diagnostics.
- wendy_main.c init/reinit: always call wendy_wasm_init() after reinit —
  previous code skipped init on subsequent runs, leaving the engine torn down.
- WasmKitBridge.c: add wendy_format_version, wendy_format_mac, wendy_log_heap
  C helpers (variadic snprintf unavailable in Embedded Swift).
- CMakeLists: forward all CONFIG_WENDY_* Kconfig flags as Swift -D defines
  so #if CONFIG_WENDY_WASI_SHIM etc. are evaluated correctly in WendyImports.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LysLik5cfWYSYXrDGde5yB
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

AI Security Review

This pull request replaces the WAMR C-based WebAssembly runtime with WasmKit compiled as Embedded Swift for RISC-V (ESP32-C6). The changes are largely architectural — restructuring build dependencies, registering host-import functions, and exposing thin C wrapper APIs. No hardcoded secrets, credentials, or PII were identified. However, several moderate-to-high severity issues warrant attention: unsafe pointer arithmetic and integer handling in the new posix_memalign WASM host-function wrapper (potential memory corruption within the WASM linear memory), missing input validation on buffer lengths passed from WASM guest code to host functions, a linker-script patch that silently modifies generated build artifacts during configure time (supply chain / integrity concern), and path-traversal risk from WASM module sources interacting with unvalidated storage keys. Additionally, the Swift compilation pipeline accepts an externally-controlled WASMKIT_ROOT path without any integrity verification (hash or signature), introducing third-party supply chain risk. Most findings are scoped to the embedded firmware threat model, but some (especially buffer handling at the WASM host-function boundary) represent genuine exploitable vectors if untrusted WASM modules are loaded at runtime.

Security & Compliance Review — PR #18: Replace WAMR with WasmKit Embedded Swift Runtime

Executive Summary

This pull request replaces the WAMR C-based WebAssembly runtime with WasmKit compiled as Embedded Swift for RISC-V (ESP32-C6). The changes are largely architectural — restructuring build dependencies, registering host-import functions, and exposing thin C wrapper APIs. No hardcoded secrets, credentials, or PII were identified. However, several moderate-to-high severity issues warrant attention: unsafe pointer arithmetic and integer handling in the new posix_memalign WASM host-function wrapper (potential memory corruption within the WASM linear memory), missing input validation on buffer lengths passed from WASM guest code to host functions, a linker-script patch that silently modifies generated build artifacts during configure time (supply chain / integrity concern), and path-traversal risk from WASM module sources interacting with unvalidated storage keys. Additionally, the Swift compilation pipeline accepts an externally-controlled WASMKIT_ROOT path without any integrity verification (hash or signature), introducing third-party supply chain risk. Most findings are scoped to the embedded firmware threat model, but some (especially buffer handling at the WASM host-function boundary) represent genuine exploitable vectors if untrusted WASM modules are loaded at runtime.


Findings Table

Severity Standards File Line(s) Title
HIGH ISO27001-A.8, NIST-SI-10, SOC2-CC6 components/wendy_wasm/src/wendy_wasm.c posix_memalign_wrapper Unsafe pointer arithmetic may corrupt WASM linear memory
HIGH ISO27001-A.8, NIST-SI-10 components/wendy_spi/src/wendy_spi.c wendy_spi_guest_transfer WASM-guest-controlled tx_buf/rx_buf passed directly to hardware SPI without bounds-check against linear memory
HIGH ISO27001-A.8, NIST-SI-10 components/wendy_storage/src/wendy_storage.c wendy_storage_guest_* Unvalidated key/value lengths from WASM guest passed to NVS — potential OOB read and NVS corruption
HIGH ISO27001-A.8, NIST-SI-10 components/wendy_uart/src/wendy_uart.c wendy_uart_guest_write, wendy_uart_guest_read Unvalidated length from WASM guest; no upper-bound check before hardware I/O
MEDIUM SOC2-CC9, ISO27001-A.8, NIST-SC-28 components/wendy_wasmkit/CMakeLists.txt WASMKIT_ROOT resolution Externally-controlled WasmKit source tree loaded without integrity verification
MEDIUM SOC2-CC8, ISO27001-A.8 CMakeLists.txt Linker-script patch block Configure-time mutation of generated build artifacts undermines build reproducibility and audit trail
MEDIUM ISO27001-A.8, NIST-SI-10 components/wendy_otel/src/wendy_otel.c wendy_otel_guest_log WASM-guest-controlled len passed to otel_log_wrapper without explicit bounds check
MEDIUM ISO27001-A.8, NIST-SI-10 components/wendy_wasmkit/CMakeLists.txt SWIFT_TOOLCHAIN_BIN resolution Compiler binary resolved from environment variable without path validation — build-time injection risk
LOW SOC2-CC7, ISO27001-A.12 components/wendy_wasm/src/wendy_wasm.c posix_memalign_wrapper Allocation failures return ENOMEM with no log — silent failure hides resource exhaustion
LOW SOC2-CC7, ISO27001-A.12 components/wendy_callback/src/wendy_callback.c wendy_callback_dequeue New dequeue path lacks logging of dropped/dequeued event types — reduces observability
INFORMATIONAL SOC2-CC9 components/wendy_wasm/idf_component.yml WAMR removal WAMR dependency removed but no pinned WasmKit version or hash — transitive dependency risk

Detailed Findings


[HIGH-1] Unsafe Pointer Arithmetic May Corrupt WASM Linear Memory

File: components/wendy_wasm/src/wendy_wasm.c
Function: posix_memalign_wrapper

Description:
The implementation over-allocates size + (align - 1) bytes from WASM linear memory, then performs pointer arithmetic to find an aligned address within that allocation and stores the aligned WASM address back to the guest. However, neither size nor align are clamped before this arithmetic. If a WASM guest passes a very large size (e.g., near UINT32_MAX), the addition (uint32_t)size + (uint32_t)(align - 1) will integer-overflow, causing wasm_runtime_module_malloc to allocate a much smaller buffer than intended. The returned aligned pointer will then point outside the allocated region, and subsequent guest writes will silently corrupt WASM linear memory or adjacent host metadata.

Additionally, *(uint32_t *)memptr = wasm_addr + ... writes to a pointer that WAMR is supposed to have already translated to a native host pointer into linear memory — but if the WAMR auto-translation assumptions are wrong (e.g., the signature "(*ii)i" means memptr is already translated), this could write an incorrect WASM address into host memory.

uint32_t wasm_addr = wasm_runtime_module_malloc(
    inst, (uint32_t)size + (uint32_t)(align - 1), &native_ptr);
// ↑ integer overflow if size near UINT32_MAX
if (wasm_addr == 0 || !native_ptr)
    return 12; /* ENOMEM */
uintptr_t raw     = (uintptr_t)native_ptr;
uintptr_t aligned = (raw + (uintptr_t)(align - 1)) & ~(uintptr_t)(align - 1);
*(uint32_t *)memptr = wasm_addr + (uint32_t)(aligned - raw);
// ↑ no overflow check on (aligned - raw) before adding to wasm_addr

Remediation:

  1. Validate size and align before arithmetic: reject if size == 0, if size > MAX_WASM_ALLOC (e.g., 64 KB), or if align > 4096.
  2. Use checked arithmetic:
    if (size > (UINT32_MAX - (uint32_t)(align - 1))) return 12; // overflow guard
  3. Add an assertion or check that (aligned - raw) < (size_t)(align - 1) before the final store.
  4. Review whether the WAMR signature "(*ii)i" actually translates memptr to a host pointer; if so, the write target is a WASM-linear-memory location and is correct, but add a comment explaining this and validate the offset doesn't escape the allocated region.

Controls violated: ISO 27001 A.8.28 (Secure coding), NIST SP 800-53 SI-10 (Information Input Validation)


[HIGH-2] WASM-Guest-Controlled Pointers Passed Directly to Hardware SPI

File: components/wendy_spi/src/wendy_spi.c
Function: wendy_spi_guest_transfer

Description:
The new public wendy_spi_guest_transfer function accepts tx_buf and rx_buf as raw C pointers from the Swift (WasmKit) layer and passes them directly to spi_device_transmit. Unlike the WAMR wrapper path — where WAMR validates and translates WASM guest pointers into verified host linear-memory addresses — the WasmKit path calls this function with pointers the Swift runtime has translated. If the Swift/WasmKit translation is incomplete or bypassed, a malicious WASM module could pass a crafted pointer to cause:

  • Reads from arbitrary host memory regions (information disclosure to WASM guest).
  • Writes to arbitrary host memory via the SPI RX buffer.

The len validation (<= 4096) exists but does not validate that tx_buf + len and rx_buf + len stay within WASM linear memory bounds.

int wendy_spi_guest_transfer(int dev_id, int32_t len,
                              const uint8_t *tx_buf, uint8_t *rx_buf)
{
    if (dev_id < 0 || dev_id >= MAX_SPI_DEVS || !s_spi_devs[dev_id].opened) { return -1; }
    if (len <= 0 || len > 4096) { return -1; }
    spi_transaction_t trans = {
        .length    = (size_t)len * 8,
        .tx_buffer = tx_buf,   // ← no linear-memory bounds verification
        .rx_buffer = rx_buf,   // ← no linear-memory bounds verification
    };

Remediation:

  1. The WasmKit Imports binding layer (in WendyImports.swift) must perform explicit bounds checking against the WASM instance's linear memory before calling these C wrappers. Add a helper function (e.g., guard memory.isValidRange(ptr: txBuf, length: len) else { throw ... }).
  2. In the C wrapper, add a documentation comment stating the precondition that buffers must be pre-validated by the Swift caller.
  3. Alternatively, pass wasm_memory_base + offset from WasmKit with explicit offset+length validation rather than raw pointers.

Controls violated: ISO 27001 A.8.28, NIST SP 800-53 SI-10, SOC2-CC6


[HIGH-3] Unvalidated Key/Value Lengths from WASM Guest in NVS Wrappers

File: components/wendy_storage/src/wendy_storage.c
Functions: wendy_storage_guest_get, wendy_storage_guest_set, wendy_storage_guest_delete, wendy_storage_guest_exists

Description:
The new public guest-callable wrappers pass key_len and val_len directly to the underlying storage_*_wrapper functions, which use these lengths to access NVS (Non-Volatile Storage). If the underlying wrappers use key_len for memcpy or strnlen-style operations without verifying that key + key_len stays within WASM linear memory, a WASM guest can:

  • Trigger out-of-bounds reads from host memory (key/value buffers extend past linear memory).
  • Write arbitrary data into NVS by providing a val pointer and inflated val_len.
  • Cause NVS corruption affecting firmware configuration persistence.
int wendy_storage_guest_get(const char *key, int key_len, char *val, int32_t val_len) {
    return storage_get_wrapper(NULL, key, key_len, val, (int)val_len);
    // ↑ key_len and val_len not validated against WASM linear memory bounds
}

NVS keys in ESP-IDF have a maximum length of 15 bytes — passing key_len > 15 will cause undefined behavior in NVS.

Remediation:

  1. Add explicit maximum-length validation: if (key_len <= 0 || key_len > 15) return -1; (NVS key max is 15 chars).
  2. Validate val_len against a reasonable maximum (e.g., NVS string max ~4000 bytes).
  3. The WasmKit Imports layer must validate that key and val pointers + their respective lengths lie within WASM linear memory before calling these functions.
  4. Null-terminate or use explicit length-aware string operations in the underlying wrappers.

Controls violated: ISO 27001 A.8.28, NIST SP 800-53 SI-10, SOC2-CC6


[HIGH-4] Unvalidated Buffer Length in UART Guest Wrappers

File: components/wendy_uart/src/wendy_uart.c
Functions: wendy_uart_guest_write, wendy_uart_guest_read

Description:
The new UART guest wrappers pass len from the WASM guest directly to uart_write_wrapper and uart_read_wrapper without any bounds validation. A malicious or buggy WASM module can pass:

  • A very large len (e.g., INT32_MAX) causing the UART driver to attempt a massive DMA transfer, exhausting DRAM or causing a crash.
  • A len that extends data or buf past WASM linear memory, exposing host memory to read/write via hardware.
int wendy_uart_guest_write(int port, const uint8_t *data, int len) {
    return uart_write_wrapper(NULL, port, (const char *)data, len);
    // ↑ len not bounded; data not verified within WASM linear memory
}
int wendy_uart_guest_read(int port, uint8_t *buf, int len) {
    return uart_read_wrapper(NULL, port, (char *)buf, len);
    // ↑ same issue for the read-into-guest-buffer path
}

Remediation:

  1. Add length validation: if (len <= 0 || len > 4096) return -1; (or a suitable per-UART maximum).
  2. Validate that data/buf + len remain within WASM linear memory bounds (must be enforced in the WasmKit Swift layer before calling these functions).
  3. Document the precondition clearly in the header.

Controls violated: ISO 27001 A.8.28, NIST SP 800-53 SI-10, NIST SP 800-53 AU-9 (availability)


[MEDIUM-1] WasmKit Source Tree Loaded Without Integrity Verification

File: components/wendy_wasmkit/CMakeLists.txt
Variable: WASMKIT_ROOT

Description:
The build system resolves the WasmKit source tree from an environment variable or a relative path default (../../../../swiftwasm/WasmKit). No cryptographic hash, git commit SHA pinning, or signature verification is performed on any WasmKit source files before they are compiled directly into firmware. A compromised or substituted WasmKit source tree (e.g., through a supply-chain attack or accidental directory misconfiguration) would be silently compiled into production firmware. The PR description references a specific branch (jo/embedded-runtime-v2) but does not enforce it at build time.

if(NOT DEFINED WASMKIT_ROOT)
    get_filename_component(WASMKIT_ROOT
        "${CMAKE_CURRENT_LIST_DIR}/../../../../swiftwasm/WasmKit"
        ABSOLUTE)
endif()
# ↑ No integrity check; any directory named WasmKit at this path is accepted

Remediation:

  1. Pin the expected git commit SHA in the CMakeLists and verify it at configure time:
    execute_process(COMMAND git -C "${WASMKIT_ROOT}" rev-parse HEAD OUTPUT_VARIABLE _WK_SHA ...)
    if(NOT _WK_SHA STREQUAL "${EXPECTED_WASMKIT_SHA}")
        message(FATAL_ERROR "WasmKit SHA mismatch: expected ${EXPECTED_WASMKIT_SHA}, got ${_WK_SHA}")
    endif()
  2. Consider vendoring WasmKit as a git submodule with a pinned commit.
  3. Document the expected branch and commit in the README and idf_component.yml.

Controls violated: SOC2-CC9 (Third-party risk), ISO 27001 A.8.8 (Management of technical vulnerabilities), NIST SP 800-53 SC-28, SA-12


[MEDIUM-2] Configure-Time Mutation of Generated Build Artifacts

File: CMakeLists.txt
Block: Linker-script patch section

Description:
The build system modifies sections.ld and sections.ld.in — ESP-IDF generated files — at CMake configure time using file(READ) / file(WRITE). This approach:

  1. Breaks reproducible builds: the patch is applied in-place to files under CMAKE_BINARY_DIR, but re-running CMake on an already-patched tree silently skips re-patching (the _already guard), which is correct, but any build-system clean or regeneration may produce an inconsistent state.
  2. Silently modifies third-party generated artifacts: there is no record of this modification in version control, no hash of the pre/post state, and no rollback mechanism.
  3. Masks upstream changes: if ESP-IDF updates sections.ld.in to fix a related bug, the configure-time patch may silently fail or produce an incorrect result.
file(WRITE "${_f}" "${_ld_content}")
message(STATUS "wendy-lite: patched .got.plt placement in ${_f}")
# ↑ No hash verification of the original file; no record of what was changed

Remediation:

  1. Prefer a proper linker fragment (.lf file) or a custom sections.ld.in checked into the repo to override the ESP-IDF default — ESP-IDF supports project-level linker fragment overrides.
  2. If the in-place patch is unavoidable, compute and log SHA256 of the original and patched files, and verify the original matches the expected hash before patching.
  3. Add an explicit CI step that validates the linker script content after configure.

Controls violated: SOC2-CC8 (Change management, audit trail), ISO 27001 A.8.32 (Change management)


[MEDIUM-3] WASM-Guest-Controlled Length Passed to OTel Log Without Bounds Check

File: components/wendy_otel/src/wendy_otel.c
Function: wendy_otel_guest_log

Description:
The wendy_otel_guest_log wrapper passes len from the WASM guest directly to otel_log_wrapper without validating it against a maximum or against WASM linear memory bounds. A large or negative len could cause otel_log_wrapper to read past the end of the msg buffer (which is a translated WASM pointer), disclosing host memory content in log output or causing a crash.

int wendy_otel_guest_log(int32_t level, const char *msg, int32_t len) {
    return otel_log_wrapper(NULL, level, msg, (int)len);
    // ↑ len from WASM guest; msg pointer not bounds-checked against linear memory
}

Remediation:

  1. Add: if (len <= 0 || len > MAX_LOG_MESSAGE_LEN) return -1;
  2. Validate msg + len within WASM linear memory bounds in the WasmKit Swift layer before calling this function.
  3. Consider using strnlen(msg, len) rather than trusting the guest-provided length.

Controls violated: ISO 27001 A.8.28, NIST SP 800-53 SI-10


[MEDIUM-4] Compiler Binary Resolved from Environment Variable Without Validation

File: components/wendy_wasmkit/CMakeLists.txt

Description:
The Swift compiler path is taken directly from $ENV{SWIFT_TOOLCHAIN_BIN} without validating that it points to an expected binary or that the binary has expected properties (e.g., version check). In a CI/CD environment or developer machine where an attacker can control environment variables, this could allow substitution of a malicious swiftc binary that introduces backdoors into the compiled firmware at build time.

if(DEFINED ENV{SWIFT_TOOLCHAIN_BIN})
    set(SWIFTC "$ENV{SWIFT_TOOLCHAIN_BIN}/swiftc")
else()
    find_program(SWIFTC swiftc)
endif()
# ↑ No version check, no path canonicalisation, no signature verification

Remediation:

  1. After resolving SWIFTC, execute "${SWIFTC}" --version and validate that the output matches an expected version string.
  2. Document the required Swift toolchain version explicitly in the README and build documentation.
  3. In CI, pin the toolchain download URL and verify its checksum before use.

Controls violated: ISO 27001 A.8.28, SOC2-CC9 (Supply chain)


[LOW-1] Silent Failure on WASM Allocation Errors — Reduces Observability

File: components/wendy_wasm/src/wendy_wasm.c
Function: posix_memalign_wrapper

Description:
When wasm_runtime_module_malloc returns 0 or native_ptr is null, the function returns ENOMEM silently. On an embedded device with severely limited DRAM (~200 KB free as noted in the PR), allocation failures may be a leading indicator of resource exhaustion and should be logged at WARNING level to assist diagnostics.

if (wasm_addr == 0 || !native_ptr)
    return 12; /* ENOMEM */

Remediation:
Add ESP_LOGW(TAG, "posix_memalign: WASM alloc failed (size=%d, align=%d)", size, align); before returning.

Controls violated: SOC2-CC7, ISO 27001 A.12.4 (Logging and monitoring)


[LOW-2] New Callback Dequeue Path Lacks Event-Type Logging

File: components/wendy_callback/src/wendy_callback.c
Function: wendy_callback_dequeue

Description:
The new wendy_callback_dequeue function (used by the WasmKit path for event dispatch) dequeues events from the FreeRTOS queue without any logging, even at DEBUG level. The existing wendy_callback_wait_and_dispatch path presumably has logging. If the WasmKit dispatch path drops events or misroutes them, there is no observability into which events were dequeued and whether they were successfully dispatched.

bool wendy_callback_dequeue(wendy_callback_event_t *event, uint32_t timeout_ticks)
{
    if (!s_queue || !event) {
        return false;
    }
    return xQueueReceive(s_queue, event, (TickType_t)timeout_ticks) == pdTRUE;
    // ↑ no logging on dequeue success or failure
}

Remediation:
Add ESP_LOGD(TAG, "dequeued event type=%d", event->type); on success, and ESP_LOGW(TAG, "dequeue called with null queue or event"); on null guard failure.

Controls violated: SOC2-CC7, ISO 27001 A.12.4


[INFORMATIONAL] No Pinned Version or Hash for WasmKit Dependency

File: components/wendy_wasm/idf_component.yml

Description:
The WAMR dependency (espressif/wasm-micro-runtime: ~2.4.0) was version-pinned in the IDF component manifest. WasmKit is now resolved via a filesystem path with no version, commit SHA, or hash recorded in any manifest or lock file. This makes the build non-reproducible and increases supply chain risk.

# jo/wasmkit-runtime: WAMR dependency removed; wendy_wasmkit replaces this component.
dependencies:
  idf:
    version: ">=5.3.0"
# ↑ WasmKit dependency not tracked in any manifest

Remediation:
Add a comment block recording the

The full Swift runtime only builds on esp32c6 with both WasmKit sources
and a Swift toolchain present. All other cases now fall back to a minimal
C stub that returns ESP_ERR_NOT_SUPPORTED from all wendy_wasm_* functions,
so the firmware still links and boots (without WASM support).

This fixes CI: the build matrix includes esp32c5 and esp32p4 which are not
esp32c6, and the CI container has no Swift toolchain. Previously CMakeLists
used FATAL_ERROR for any of these conditions; now it warns and builds stubs.

Changes:
- CMakeLists.txt: replace FATAL_ERROR with a three-way guard (target ≠
  esp32c6, WasmKit missing, Swift missing) that registers stubs and returns.
  Also change find_program(swiftc REQUIRED) → optional (handled by guard).
- src/wendy_wasm_stub.c: new file with no-op implementations of all
  wendy_wasm_* functions declared in wendy_wasm.h.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LysLik5cfWYSYXrDGde5yB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant