From 0da75023e27ec3d6dfc08fa744a7a39663fd1aad Mon Sep 17 00:00:00 2001 From: Alex J Lennon Date: Tue, 4 Aug 2026 21:34:44 +0100 Subject: [PATCH 1/4] fix: address Preloop review findings for RAM staging Gate Zephyr RAM staging behind Kconfig, use mender_malloc, add unit-tested helpers, and fix the ESP-IDF partition-label cmake mismatch plus log truncation. Co-authored-by: Cursor --- CHANGELOG.md | 9 ++ README.md | 17 ++++ cmake/mender_mcu_sources.txt | 1 + scripts/bump-consumer-pins.sh | 22 +++-- scripts/check-consumer-pin.sh | 8 +- src/core/image-ram-stage.c | 80 +++++++++++++++++ src/include/image-ram-stage.h | 66 ++++++++++++++ src/platform/log/esp-idf/log.c | 44 +++++++++- .../zephyr/image/update-module.c | 85 ++++++++----------- target/esp-idf/component.cmake | 4 +- target/zephyr/Kconfig | 19 +++++ tests/unit/CMakeLists.txt | 7 +- tests/unit/core/image_ram_stage_test.cpp | 69 +++++++++++++++ 13 files changed, 368 insertions(+), 63 deletions(-) create mode 100644 src/core/image-ram-stage.c create mode 100644 src/include/image-ram-stage.h create mode 100644 tests/unit/core/image_ram_stage_test.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 8db44ce5..26e76d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ --- +## Unreleased + +* Zephyr `zephyr-image` update module: optional RAM staging before secondary-slot + write (XIP-safe on FlexSPI), with `CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE` and + `CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE_MAX_BYTES`, `mender_malloc` staging + buffer, direct-to-flash fallback, and unit tests for the staging helpers. +* ESP-IDF: fix `MENDER_STORAGE_PARTITION_LABEL` compile definition mismatch in + `component.cmake`; grow log formatting beyond a fixed 256-byte stack buffer. + ## 1.0.0 - 2026-04-17 * The first stable release diff --git a/README.md b/README.md index d37b1d16..366333a1 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ > Machine SoT: [`DD_PIN`](./DD_PIN) · tag `dd-pin-64c10fa` · > [`scripts/check-consumer-pin.sh`](./scripts/check-consumer-pin.sh) · > [`scripts/bump-consumer-pins.sh`](./scripts/bump-consumer-pins.sh). +> +> Pin scripts accept `MENDER_MCU_PIN_BRANCH`, `DD_ROOT`, and +> `MENDER_MCU_CONSUMERS` overrides (see script headers). ## Overview @@ -32,6 +35,20 @@ The decision to fork the original mender-mcu-client was made to: * **Provide Official Support**: Ensure that the project receives the necessary attention and resources from Northern.tech to meet the needs of the community and enterprise users. +## Zephyr RAM staging (optional) + +When `CONFIG_MENDER_ZEPHYR_IMAGE_UPDATE_MODULE` is enabled, the default +`CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE` path accumulates the full artifact payload +in RAM (via `mender_malloc`) during download, then writes the secondary slot in +one pass at close. That avoids FlexSPI XIP stalls when the running image, OTA +slot, and MCUboot share the same NOR (for example i.MX RT). + +- Disable with `CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE=n` for direct-to-flash writes. +- Cap staging with `CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE_MAX_BYTES` (0 = no cap). +- If allocation fails or the image exceeds the cap, the module falls back to + writing each chunk directly to flash. MCUboot still verifies the image on + boot; staging does not bypass signature checks. + ## Get started This guide is based on our Zephyr reference application [mender-mcu-integration](https://github.com/mendersoftware/mender-mcu-integration). diff --git a/cmake/mender_mcu_sources.txt b/cmake/mender_mcu_sources.txt index 5849511b..ac645927 100755 --- a/cmake/mender_mcu_sources.txt +++ b/cmake/mender_mcu_sources.txt @@ -182,6 +182,7 @@ file(GLOB MENDER_MCU_SOURCES "${MENDER_MCU_ROOT}/src/core/client.c" "${MENDER_MCU_ROOT}/src/core/deployment-data.c" "${MENDER_MCU_ROOT}/src/core/error-counters.c" + "${MENDER_MCU_ROOT}/src/core/image-ram-stage.c" "${MENDER_MCU_ROOT}/src/core/update-module.c" "${MENDER_MCU_ROOT}/src/core/utils.c" "${MENDER_MCU_ROOT}/src/platform/log/${CONFIG_MENDER_PLATFORM_LOG_TYPE}/log.c" diff --git a/scripts/bump-consumer-pins.sh b/scripts/bump-consumer-pins.sh index 6c9e768b..5231fec0 100755 --- a/scripts/bump-consumer-pins.sh +++ b/scripts/bump-consumer-pins.sh @@ -5,10 +5,14 @@ # ./scripts/bump-consumer-pins.sh # dry-run # ./scripts/bump-consumer-pins.sh --apply # write files # -# Expects sibling checkouts under DD_ROOT (default /data_drive/dd): -# zephyr-rt1186-f1 -# zephyr-rt1170-room-display -# zephyr-rt1170-eink +# Env: +# DD_ROOT parent of consumer checkouts (default: /data_drive/dd) +# MENDER_MCU_CONSUMERS override consumer list as "repo|rel[,repo|rel...]" +# +# Default consumers (under DD_ROOT): +# zephyr-rt1186-f1|f1-controller/west.yml +# zephyr-rt1170-room-display|room-display/west.yml +# zephyr-rt1170-eink|mender-mcu-integration/west.yml # # Does not git commit/push — review, then commit each repo (and retag dd-pin-*). set -euo pipefail @@ -27,15 +31,23 @@ SHORT=${NEW:0:7} ROOT=$(cd "$(dirname "$0")/.." && pwd) DD_ROOT=${DD_ROOT:-/data_drive/dd} -CONSUMERS=( +DEFAULT_CONSUMERS=( "zephyr-rt1186-f1|f1-controller/west.yml" "zephyr-rt1170-room-display|room-display/west.yml" "zephyr-rt1170-eink|mender-mcu-integration/west.yml" ) +CONSUMERS=() +if [[ -n "${MENDER_MCU_CONSUMERS:-}" ]]; then + IFS=',' read -r -a CONSUMERS <<<"$MENDER_MCU_CONSUMERS" +else + CONSUMERS=("${DEFAULT_CONSUMERS[@]}") +fi + OLD=$(tr -d '[:space:]' < "$ROOT/DD_PIN" || true) echo "mender-mcu pin: ${OLD:-"(none)"} → $NEW" echo "tag suggestion: dd-pin-$SHORT (annotate at $NEW)" +echo "DD_ROOT=$DD_ROOT" echo replace_revision() { diff --git a/scripts/check-consumer-pin.sh b/scripts/check-consumer-pin.sh index 6d15da30..48709520 100755 --- a/scripts/check-consumer-pin.sh +++ b/scripts/check-consumer-pin.sh @@ -6,12 +6,16 @@ # curl -fsSL …/scripts/check-consumer-pin.sh | bash -s -- f1-controller/west.yml # # Env: +# MENDER_MCU_PIN_BRANCH branch hosting DD_PIN / PIN-POLICY (default: feature/zephyr-ram-stage-on-main) # MENDER_MCU_DD_PIN_URL override raw DD_PIN URL # MENDER_MCU_DD_PIN_FILE read pin from this file instead of URL +# MENDER_MCU_PIN_POLICY_URL override policy URL shown on drift (optional) set -euo pipefail WEST_YML=${1:?usage: check-consumer-pin.sh } -PIN_URL=${MENDER_MCU_DD_PIN_URL:-https://raw.githubusercontent.com/DynamicDevices/mender-mcu/feature/zephyr-ram-stage-on-main/DD_PIN} +PIN_BRANCH=${MENDER_MCU_PIN_BRANCH:-feature/zephyr-ram-stage-on-main} +PIN_URL=${MENDER_MCU_DD_PIN_URL:-https://raw.githubusercontent.com/DynamicDevices/mender-mcu/${PIN_BRANCH}/DD_PIN} +POLICY_URL=${MENDER_MCU_PIN_POLICY_URL:-https://github.com/DynamicDevices/mender-mcu/blob/${PIN_BRANCH}/PIN-POLICY.md} if [[ ! -f "$WEST_YML" ]]; then echo "error: west.yml not found: $WEST_YML" >&2 @@ -70,7 +74,7 @@ if [[ "$ACTUAL" != "$EXPECTED" ]]; then echo "error: mender-mcu pin drift" >&2 echo " west.yml ($WEST_YML): $ACTUAL" >&2 echo " DD_PIN ($PIN_SRC): $EXPECTED" >&2 - echo " policy: https://github.com/DynamicDevices/mender-mcu/blob/feature/zephyr-ram-stage-on-main/PIN-POLICY.md" >&2 + echo " policy: $POLICY_URL" >&2 exit 5 fi diff --git a/src/core/image-ram-stage.c b/src/core/image-ram-stage.c new file mode 100644 index 00000000..5ee112a2 --- /dev/null +++ b/src/core/image-ram-stage.c @@ -0,0 +1,80 @@ +/** + * @file image-ram-stage.c + * @brief Whole-image RAM staging helpers (platform-independent) + * + * Copyright Northern.tech AS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "alloc.h" +#include "image-ram-stage.h" + +mender_err_t +mender_image_ram_stage_begin(mender_image_ram_stage_t *stage, size_t size, bool enabled, size_t max_size) { + if (NULL == stage) { + return MENDER_FAIL; + } + + mender_image_ram_stage_reset(stage); + + if (!enabled || 0 == size) { + return MENDER_OK; + } + if ((max_size > 0) && (size > max_size)) { + return MENDER_OK; + } + + stage->buf = mender_malloc(size); + if (NULL == stage->buf) { + return MENDER_OK; + } + stage->capacity = size; + stage->length = 0; + return MENDER_OK; +} + +bool +mender_image_ram_stage_active(const mender_image_ram_stage_t *stage) { + return (NULL != stage) && (NULL != stage->buf); +} + +mender_err_t +mender_image_ram_stage_write(mender_image_ram_stage_t *stage, const void *data, size_t index, size_t length) { + if ((NULL == stage) || (NULL == stage->buf) || (NULL == data)) { + return MENDER_FAIL; + } + if (index + length > stage->capacity) { + return MENDER_FAIL; + } + memcpy(stage->buf + index, data, length); + if (index + length > stage->length) { + stage->length = index + length; + } + return MENDER_OK; +} + +void +mender_image_ram_stage_reset(mender_image_ram_stage_t *stage) { + if (NULL == stage) { + return; + } + if (NULL != stage->buf) { + mender_free(stage->buf); + } + stage->buf = NULL; + stage->capacity = 0; + stage->length = 0; +} diff --git a/src/include/image-ram-stage.h b/src/include/image-ram-stage.h new file mode 100644 index 00000000..3cb19de3 --- /dev/null +++ b/src/include/image-ram-stage.h @@ -0,0 +1,66 @@ +/** + * @file image-ram-stage.h + * @brief Whole-image RAM staging helpers (platform-independent) + * + * Copyright Northern.tech AS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __MENDER_IMAGE_RAM_STAGE_PRIV_H__ +#define __MENDER_IMAGE_RAM_STAGE_PRIV_H__ + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#include +#include +#include + +#include + +typedef struct mender_image_ram_stage { + uint8_t *buf; + size_t capacity; + size_t length; +} mender_image_ram_stage_t; + +/** + * @brief Try to allocate a staging buffer for @p size bytes + * @param stage Staging state (must not be NULL) + * @param size Artifact payload size + * @param enabled When false, leave stage inactive (direct-to-flash) + * @param max_size When > 0 and size > max_size, leave stage inactive + * @return MENDER_OK always (inactive stage is a valid outcome); MENDER_FAIL on bad args + * + * Allocation uses mender_malloc. Failure to allocate leaves the stage inactive. + */ +mender_err_t mender_image_ram_stage_begin(mender_image_ram_stage_t *stage, size_t size, bool enabled, size_t max_size); + +bool mender_image_ram_stage_active(const mender_image_ram_stage_t *stage); + +/** + * @brief Copy a download chunk into the staging buffer + * @return MENDER_OK, or MENDER_FAIL on overflow / inactive stage / bad args + */ +mender_err_t mender_image_ram_stage_write(mender_image_ram_stage_t *stage, const void *data, size_t index, size_t length); + +/** Free staging buffer and clear state. */ +void mender_image_ram_stage_reset(mender_image_ram_stage_t *stage); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* __MENDER_IMAGE_RAM_STAGE_PRIV_H__ */ diff --git a/src/platform/log/esp-idf/log.c b/src/platform/log/esp-idf/log.c index 6dbd31f7..56bf8f2e 100644 --- a/src/platform/log/esp-idf/log.c +++ b/src/platform/log/esp-idf/log.c @@ -22,8 +22,12 @@ #include "esp_log.h" +#include "alloc.h" #include "log.h" +/* Cap heap-backed messages so a runaway format cannot exhaust memory. */ +#define MENDER_ESP_LOG_MAX_MSG 2048 + mender_err_t mender_log_init(void) { /* Nothing to do */ @@ -31,11 +35,41 @@ mender_log_init(void) { } mender_err_t -mender_log_print(uint8_t level, MENDER_ARG_UNUSED const char *filename, const char *function, int line, char *format, ...) { - char msg[256] = ""; +mender_log_print(uint8_t level, const char *filename, const char *function, int line, char *format, ...) { + char stack_msg[256]; + char *msg = stack_msg; + char *heap_msg = NULL; va_list args; + va_list args_copy; + int needed; + va_start(args, format); - vsnprintf(msg, sizeof(msg), format, args); + va_copy(args_copy, args); + needed = vsnprintf(NULL, 0, format, args_copy); + va_end(args_copy); + + if (needed < 0) { + va_end(args); + return MENDER_FAIL; + } + + if ((size_t)needed + 1 > sizeof(stack_msg)) { + size_t alloc_len = (size_t)needed + 1; + if (alloc_len > MENDER_ESP_LOG_MAX_MSG) { + alloc_len = MENDER_ESP_LOG_MAX_MSG; + } + heap_msg = mender_malloc(alloc_len); + if (NULL != heap_msg) { + msg = heap_msg; + vsnprintf(msg, alloc_len, format, args); + } else { + /* Fall back to truncated stack buffer if heap is exhausted. */ + vsnprintf(stack_msg, sizeof(stack_msg), format, args); + msg = stack_msg; + } + } else { + vsnprintf(stack_msg, sizeof(stack_msg), format, args); + } va_end(args); esp_log_level_t esp_level; @@ -55,8 +89,10 @@ mender_log_print(uint8_t level, MENDER_ARG_UNUSED const char *filename, const ch break; } - ESP_LOG_LEVEL(esp_level, "mender", "%s:%d: %s", function, line, msg); + /* Include source filename in the tag path so it is not dropped. */ + ESP_LOG_LEVEL(esp_level, "mender", "%s:%s:%d: %s", filename ? filename : "?", function, line, msg); + mender_free(heap_msg); return MENDER_OK; } diff --git a/src/platform/update_modules/zephyr/image/update-module.c b/src/platform/update_modules/zephyr/image/update-module.c index 145aa924..71b732f1 100644 --- a/src/platform/update_modules/zephyr/image/update-module.c +++ b/src/platform/update_modules/zephyr/image/update-module.c @@ -19,13 +19,13 @@ */ #include -#include #include #include #include #include #include "client.h" +#include "image-ram-stage.h" #include "log.h" #include "update-module.h" #include "utils.h" @@ -47,17 +47,17 @@ static struct flash_img_context *mcu_boot_flash_handle = NULL; static bool artifact_had_payload; /* - * RAM staging buffer (Dynamic Devices / Josef Holzmayr sdram-stage-download). - * - * On i.MX RT FlexSPI XIP parts, slot0 (running), slot1 (OTA target) and MCUboot - * often share the same NOR. Writing each downloaded chunk straight to slot1 - * suspends XIP — including Ethernet RX — and can stall TLS mid-transfer. - * Accumulate the image in RAM during download, then one-pass write at close. - * Falls back to direct-to-flash if malloc fails. + * RAM staging (FlexSPI XIP-safe download): accumulate the image in RAM during + * download, then one-pass write at close. Falls back to direct-to-flash when + * disabled, over the size cap, or if allocation fails. */ -static uint8_t *stage_buf = NULL; /* NULL => direct-to-flash fallback */ -static size_t stage_cap = 0; -static size_t stage_len = 0; +static mender_image_ram_stage_t ram_stage; + +#ifdef CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE_MAX_BYTES +#define MENDER_RAM_STAGE_MAX_BYTES ((size_t)CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE_MAX_BYTES) +#else +#define MENDER_RAM_STAGE_MAX_BYTES ((size_t)0) +#endif static mender_err_t mender_flash_open(const char *name, size_t size, struct flash_img_context **handle) { @@ -78,21 +78,19 @@ mender_flash_open(const char *name, size_t size, struct flash_img_context **hand /* Begin deployment with sequential writes */ if (0 != (result = flash_img_init(*handle))) { mender_log_error("flash_img_init failed (%d)", -result); + FREE_AND_NULL(*handle); return MENDER_FAIL; } - /* Stage whole image in RAM when possible (FlexSPI XIP-safe download). */ - stage_buf = NULL; - stage_cap = 0; - stage_len = 0; - if (size > 0) { - stage_buf = malloc(size); - if (NULL != stage_buf) { - stage_cap = size; - mender_log_info("Staging %u bytes in RAM before flashing", (unsigned)size); - } else { - mender_log_warning("Unable to stage %u bytes in RAM; writing directly to flash", (unsigned)size); - } + if (MENDER_OK + != mender_image_ram_stage_begin(&ram_stage, size, IS_ENABLED(CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE), MENDER_RAM_STAGE_MAX_BYTES)) { + FREE_AND_NULL(*handle); + return MENDER_FAIL; + } + if (mender_image_ram_stage_active(&ram_stage)) { + mender_log_info("Staging %u bytes in RAM before flashing", (unsigned)size); + } else if ((size > 0) && IS_ENABLED(CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE)) { + mender_log_warning("Unable to stage %u bytes in RAM; writing directly to flash", (unsigned)size); } return MENDER_OK; @@ -102,26 +100,23 @@ static mender_err_t mender_flash_write(struct flash_img_context *handle, const void *data, size_t index, size_t length) { int result; + if (NULL == handle) { + mender_log_error("Invalid flash handle"); + return MENDER_FAIL; + } + /* Staged path: no flash I/O during download. */ - if (NULL != stage_buf) { - if (index + length > stage_cap) { - mender_log_error("Staged image overflow: %u > %u", (unsigned)(index + length), (unsigned)stage_cap); + if (mender_image_ram_stage_active(&ram_stage)) { + if (MENDER_OK != mender_image_ram_stage_write(&ram_stage, data, index, length)) { + mender_log_error("Staged image overflow: %u > %u", (unsigned)(index + length), (unsigned)ram_stage.capacity); return MENDER_FAIL; } - memcpy(stage_buf + index, data, length); - if (index + length > stage_len) { - stage_len = index + length; - } return MENDER_OK; } (void)index; /* Fallback: write data received directly to the update partition */ - if (NULL == handle) { - mender_log_error("Invalid flash handle"); - return MENDER_FAIL; - } if (0 != (result = flash_img_buffered_write(handle, (const uint8_t *)data, length, false))) { mender_log_error("flash_img_buffered_write failed (%d)", -result); return MENDER_FAIL; @@ -137,21 +132,15 @@ mender_flash_close(struct flash_img_context *handle) { /* Check flash handle */ if (NULL == handle) { mender_log_error("Invalid flash handle"); - if (NULL != stage_buf) { - free(stage_buf); - stage_buf = NULL; - stage_cap = stage_len = 0; - } + mender_image_ram_stage_reset(&ram_stage); return MENDER_FAIL; } /* Staged path: one-pass write after download completes. */ - if (NULL != stage_buf) { - mender_log_info("Writing %u staged bytes to flash", (unsigned)stage_len); - result = flash_img_buffered_write(handle, stage_buf, stage_len, true); - free(stage_buf); - stage_buf = NULL; - stage_cap = stage_len = 0; + if (mender_image_ram_stage_active(&ram_stage)) { + mender_log_info("Writing %u staged bytes to flash", (unsigned)ram_stage.length); + result = flash_img_buffered_write(handle, ram_stage.buf, ram_stage.length, true); + mender_image_ram_stage_reset(&ram_stage); if (0 != result) { mender_log_error("flash_img_buffered_write failed (%d)", -result); return MENDER_FAIL; @@ -195,11 +184,7 @@ mender_flash_set_pending_image(struct flash_img_context **handle) { static mender_err_t mender_flash_abort_deployment(struct flash_img_context **handle) { - if (NULL != stage_buf) { - free(stage_buf); - stage_buf = NULL; - stage_cap = stage_len = 0; - } + mender_image_ram_stage_reset(&ram_stage); /* Release memory */ FREE_AND_NULL(*handle); diff --git a/target/esp-idf/component.cmake b/target/esp-idf/component.cmake index de302bc2..0b17207a 100644 --- a/target/esp-idf/component.cmake +++ b/target/esp-idf/component.cmake @@ -102,7 +102,9 @@ if(CONFIG_MENDER_SCHEDULER_WORK_QUEUE_LENGTH) target_compile_definitions(${COMPONENT_LIB} PRIVATE CONFIG_MENDER_SCHEDULER_WORK_QUEUE_LENGTH=${CONFIG_MENDER_SCHEDULER_WORK_QUEUE_LENGTH}) endif() if(CONFIG_MENDER_STORAGE_PARTITION_LABEL) - target_compile_definitions(${COMPONENT_LIB} PRIVATE CONFIG_MENDER_PARTITION_LABEL=${CONFIG_MENDER_PARTITION_LABEL}) + # Must match storage.c (#ifdef CONFIG_MENDER_STORAGE_PARTITION_LABEL) and + # the string-quoting style used for other Kconfig strings above. + target_compile_definitions(${COMPONENT_LIB} PRIVATE CONFIG_MENDER_STORAGE_PARTITION_LABEL=\"${CONFIG_MENDER_STORAGE_PARTITION_LABEL}\") endif() # TODO: figure out how to dynamically get the version. # The component manager only fetches the staged files, so no git information is available diff --git a/target/zephyr/Kconfig b/target/zephyr/Kconfig index da82c015..0c41949e 100755 --- a/target/zephyr/Kconfig +++ b/target/zephyr/Kconfig @@ -204,6 +204,25 @@ if MENDER_MCU_CLIENT and `MCUBOOT_MODE_SWAP_USING_MOVE (`MCUBOOT_MODE_SWAP_WITHOUT_SCRATCH` for Zephyr versions below 4.1)` (or another swap algorithm) must be enabled in the bootloader configuration, e.g. `sysbuild.conf` if you're using sysbuild. + + config MENDER_ZEPHYR_IMAGE_RAM_STAGE + bool "RAM-stage zephyr-image before flashing (XIP-safe)" + default y + depends on MENDER_ZEPHYR_IMAGE_UPDATE_MODULE + help + Accumulate the full artifact payload in RAM during download, then write it to the + secondary slot in one pass at close. This avoids FlexSPI XIP stalls when the running + image, OTA slot, and MCUboot share the same NOR (e.g. i.MX RT). When allocation fails, + or the image exceeds MENDER_ZEPHYR_IMAGE_RAM_STAGE_MAX_BYTES, the module falls back + to writing each chunk directly to flash. + + config MENDER_ZEPHYR_IMAGE_RAM_STAGE_MAX_BYTES + int "Maximum payload size to stage in RAM (bytes)" + default 0 + depends on MENDER_ZEPHYR_IMAGE_RAM_STAGE + help + Soft cap for RAM staging. 0 means no cap beyond available heap. Images larger than + this value skip staging and use direct-to-flash writes. endmenu menuconfig MENDER_ARTIFACT_GENERATE diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 1907c36c..d45c2bf1 100755 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -119,7 +119,12 @@ target_compile_options(main_test PRIVATE ${PLATFORM_SPECIFIC_COMPILE_OPTIONS}) add_dependencies(tests main_test) # test for core source files -add_executable(core_test EXCLUDE_FROM_ALL core/utils_test.cpp core/artifact_test.cpp core/client_test.cpp) +add_executable(core_test EXCLUDE_FROM_ALL + core/utils_test.cpp + core/artifact_test.cpp + core/client_test.cpp + core/image_ram_stage_test.cpp +) target_include_directories(core_test PRIVATE ${CMAKE_SOURCE_DIR}/../../src/include) target_link_libraries(core_test PUBLIC mender-mcu-client diff --git a/tests/unit/core/image_ram_stage_test.cpp b/tests/unit/core/image_ram_stage_test.cpp new file mode 100644 index 00000000..eb611aec --- /dev/null +++ b/tests/unit/core/image_ram_stage_test.cpp @@ -0,0 +1,69 @@ +/** + * @file image_ram_stage_test.cpp + * @brief Unit Tests for image-ram-stage.c + * + * Copyright Northern.tech AS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +extern "C" { +#include "image-ram-stage.h" +} + +TEST(ImageRamStage, BeginDisabledLeavesInactive) { + mender_image_ram_stage_t stage = {}; + ASSERT_EQ(MENDER_OK, mender_image_ram_stage_begin(&stage, 64, false, 0)); + EXPECT_FALSE(mender_image_ram_stage_active(&stage)); + mender_image_ram_stage_reset(&stage); +} + +TEST(ImageRamStage, BeginOverMaxLeavesInactive) { + mender_image_ram_stage_t stage = {}; + ASSERT_EQ(MENDER_OK, mender_image_ram_stage_begin(&stage, 128, true, 64)); + EXPECT_FALSE(mender_image_ram_stage_active(&stage)); + mender_image_ram_stage_reset(&stage); +} + +TEST(ImageRamStage, WriteAndLength) { + mender_image_ram_stage_t stage = {}; + uint8_t chunk[4] = { 1, 2, 3, 4 }; + + ASSERT_EQ(MENDER_OK, mender_image_ram_stage_begin(&stage, 8, true, 0)); + ASSERT_TRUE(mender_image_ram_stage_active(&stage)); + ASSERT_EQ(MENDER_OK, mender_image_ram_stage_write(&stage, chunk, 0, 4)); + EXPECT_EQ(4u, stage.length); + ASSERT_EQ(MENDER_OK, mender_image_ram_stage_write(&stage, chunk, 4, 4)); + EXPECT_EQ(8u, stage.length); + EXPECT_EQ(0, memcmp(stage.buf, "\x1\x2\x3\x4\x1\x2\x3\x4", 8)); + mender_image_ram_stage_reset(&stage); + EXPECT_FALSE(mender_image_ram_stage_active(&stage)); +} + +TEST(ImageRamStage, OverflowFails) { + mender_image_ram_stage_t stage = {}; + uint8_t chunk[4] = { 9, 9, 9, 9 }; + + ASSERT_EQ(MENDER_OK, mender_image_ram_stage_begin(&stage, 4, true, 0)); + EXPECT_EQ(MENDER_FAIL, mender_image_ram_stage_write(&stage, chunk, 2, 4)); + mender_image_ram_stage_reset(&stage); +} + +TEST(ImageRamStage, NullArgsFail) { + EXPECT_EQ(MENDER_FAIL, mender_image_ram_stage_begin(NULL, 4, true, 0)); + EXPECT_FALSE(mender_image_ram_stage_active(NULL)); + EXPECT_EQ(MENDER_FAIL, mender_image_ram_stage_write(NULL, "x", 0, 1)); +} From ea2ab407c01e77a2e96e5f00735c8f688db88d80 Mon Sep 17 00:00:00 2001 From: Alex J Lennon Date: Tue, 4 Aug 2026 22:25:35 +0100 Subject: [PATCH 2/4] chore: retrigger Preloop check after local-only reviewer config Co-authored-by: Cursor From dfa84b297cf0630b08c09577726be33a38bb6ab6 Mon Sep 17 00:00:00 2001 From: Alex J Lennon Date: Tue, 4 Aug 2026 22:31:09 +0100 Subject: [PATCH 3/4] chore: retrigger Preloop check on Gemini agent From 422a1eb667339616f5a5be15c394431d151b4473 Mon Sep 17 00:00:00 2001 From: Alex J Lennon Date: Tue, 4 Aug 2026 22:45:39 +0100 Subject: [PATCH 4/4] chore: retrigger Preloop GitHub status check (recheck 2) Co-authored-by: Cursor