Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 103 additions & 2 deletions boot/boot_serial/src/boot_serial.c
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@

#include "boot_serial/boot_serial.h"
#include "boot_serial_priv.h"
#include "cobs.h"
#include "mcuboot_config/mcuboot_config.h"
#include "../src/bootutil_priv.h"

Expand Down Expand Up @@ -160,7 +161,18 @@ BOOT_LOG_MODULE_DECLARE(mcuboot);
#define SWAP_USING_OFFSET_SECTOR_UPDATE_BEGIN 1
#define BOOT_DIRECT_UPLOAD_SECONDARY_SLOT_ID_REMAINDER 0

#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL_COBS
/*
* The receive buffer must hold the COBS-encoded form of a maximum-size packet
* (header plus payload, bounded by MCUBOOT_SERIAL_MAX_RECEIVE_SIZE) together
* with its 2-byte CRC16 and the trailing 0x00 frame delimiter.
*/
#define BOOT_SERIAL_COBS_PRE_LEN (MCUBOOT_SERIAL_MAX_RECEIVE_SIZE + 2)
#define BOOT_SERIAL_COBS_BUF_SIZE (COBS_ENCODED_MAX(BOOT_SERIAL_COBS_PRE_LEN) + 1)
static char in_buf[BOOT_SERIAL_COBS_BUF_SIZE];
#else
static char in_buf[MCUBOOT_SERIAL_MAX_RECEIVE_SIZE + 1];
#endif
#ifndef MCUBOOT_SERIAL_RAW_PROTOCOL
static char dec_buf[MCUBOOT_SERIAL_MAX_RECEIVE_SIZE + 1];
#endif
Expand Down Expand Up @@ -1431,8 +1443,28 @@ boot_serial_output(void)
bs_hdr->nh_group = htons(bs_hdr->nh_group);

#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL
#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL_COBS
uint16_t crc = crc16_itu_t(CRC16_INITIAL_CRC, (uint8_t *)bs_hdr, sizeof(*bs_hdr));
crc = htons(crc16_itu_t(crc, data, len));
uint8_t cobs_frame[BOOT_SERIAL_OUT_MAX + sizeof(*bs_hdr) + sizeof(crc)];
uint8_t cobs_out[COBS_ENCODED_MAX(sizeof(cobs_frame)) + 1];
size_t frame_len = 0;
size_t out_len;

memcpy(&cobs_frame[frame_len], bs_hdr, sizeof(*bs_hdr));
frame_len += sizeof(*bs_hdr);
memcpy(&cobs_frame[frame_len], data, len);
frame_len += len;
memcpy(&cobs_frame[frame_len], &crc, sizeof(crc));
frame_len += sizeof(crc);

out_len = cobs_encode(frame_len, cobs_frame, cobs_out);
cobs_out[out_len++] = 0;
boot_uf->write((const char *)cobs_out, out_len);
#else
boot_uf->write((const char *)bs_hdr, sizeof(*bs_hdr));
boot_uf->write(data, len);
#endif
#else
#ifdef __ZEPHYR__
crc = crc16_itu_t(CRC16_INITIAL_CRC, (uint8_t *)bs_hdr, sizeof(*bs_hdr));
Expand Down Expand Up @@ -1551,7 +1583,7 @@ boot_serial_in_dec(char *in, int inlen, char *out, int *out_off, int maxout)
}
#endif /* !MCUBOOT_SERIAL_RAW_PROTOCOL */

#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL
#if defined(MCUBOOT_SERIAL_RAW_PROTOCOL) && !defined(MCUBOOT_SERIAL_RAW_PROTOCOL_COBS)
/* Dispatch every complete raw SMP packet at the front of "buf" (length from the
* SMP header, capacity "max_input") via boot_serial_input(), keeping any
* trailing bytes; returns the count of unconsumed bytes left in "buf".
Expand Down Expand Up @@ -1591,7 +1623,58 @@ boot_serial_input_raw(char *buf, int len, int max_input)

return len;
}
#endif /* MCUBOOT_SERIAL_RAW_PROTOCOL */
#endif /* MCUBOOT_SERIAL_RAW_PROTOCOL && !MCUBOOT_SERIAL_RAW_PROTOCOL_COBS */

#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL_COBS
/* Dispatch every complete COBS frame (terminated by a 0x00 delimiter) at the
* front of "buf" via boot_serial_input(): each frame is COBS-decoded in place,
* its trailing CRC16 verified, and the SMP packet handed on. A frame that is
* malformed, fails CRC, or decodes larger than "max_decoded" is dropped; the
* next delimiter is the resync point. Returns the count of bytes left in "buf"
* (a still-incomplete trailing frame, moved to the front).
*/
static int
boot_serial_input_cobs(char *buf, int len, int max_decoded)
{
int consumed = 0;

for (int i = 0; i < len; i++) {
if (buf[i] != 0) {
continue;
}

const int frame_len = i - consumed;
if (frame_len > 0) {
/* "dec" spans the SMP packet plus its trailing 2-byte CRC, so it
* must hold at least a header and CRC and at most a max-size packet
* plus CRC. The CRC self-checks to zero over the whole span.
*/
const size_t dec = cobs_decode(
frame_len,
&buf[consumed],
&buf[consumed]
);

if (
dec != SIZE_MAX &&
dec >= sizeof(struct nmgr_hdr) + sizeof(uint16_t) &&
dec <= (size_t)max_decoded + sizeof(uint16_t) &&
crc16_itu_t(CRC16_INITIAL_CRC, &buf[consumed], dec) == 0
) {
boot_serial_input(&buf[consumed], (int)(dec - sizeof(uint16_t)));
}
}

consumed = i + 1;
}

if (consumed > 0 && consumed < len) {
memmove(buf, &buf[consumed], (size_t)(len - consumed));
}

return len - consumed;
}
#endif /* MCUBOOT_SERIAL_RAW_PROTOCOL_COBS */

/*
* Task which waits reading console, expecting to get image over
Expand All @@ -1617,7 +1700,11 @@ boot_serial_read_console(const struct boot_uart_funcs *f,int timeout_in_ms)
#endif

boot_uf = f;
#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL_COBS
max_input = MCUBOOT_SERIAL_MAX_RECEIVE_SIZE;
#else
max_input = sizeof(in_buf);
#endif

off = 0;
while (timeout_in_ms > 0 || bs_entry) {
Expand Down Expand Up @@ -1653,6 +1740,19 @@ boot_serial_read_console(const struct boot_uart_funcs *f,int timeout_in_ms)
}
off += rc;
#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL
#ifdef MCUBOOT_SERIAL_RAW_PROTOCOL_COBS
/*
* COBS framing: accumulate bytes until a 0x00 delimiter completes one
* or more frames, dispatch them, and keep any trailing partial frame.
* The delimiter and per-frame CRC make the stream self-synchronising,
* so no input timeout is required.
*/
off = boot_serial_input_cobs(in_buf, off, max_input);
if (off == (int)sizeof(in_buf)) {
/* Full buffer with no delimiter: oversized or garbage; resync. */
off = 0;
}
#else
/*
* Raw protocol: bytes are accumulated until one or more full SMP
* packets have been received; a single read may carry several complete
Expand All @@ -1667,6 +1767,7 @@ boot_serial_read_console(const struct boot_uart_funcs *f,int timeout_in_ms)
raw_input_start = os_uptime_get_ms_32();
#endif
off = boot_serial_input_raw(in_buf, off, max_input);
#endif /* MCUBOOT_SERIAL_RAW_PROTOCOL_COBS */
#else
if (!full_line) {
if (off == max_input) {
Expand Down
74 changes: 74 additions & 0 deletions boot/boot_serial/src/cobs.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2026 Intercreate, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/

#include "cobs.h"

enum {
/* Code byte for a maximal group: cobs_run_max literals, no implicit zero. */
cobs_code_full = cobs_run_max + 1,
};

size_t cobs_encode(
size_t src_size,
uint8_t const src[static restrict src_size],
uint8_t dst[static restrict COBS_ENCODED_MAX(src_size)]
) {
size_t dst_idx = 1;
size_t code_idx = 0;
uint8_t code = 1;

for (size_t src_idx = 0; src_idx < src_size; src_idx += 1) {
if (src[src_idx] != 0) {
dst[dst_idx] = src[src_idx];
dst_idx += 1;
code += 1;
if (code != cobs_code_full) {
continue;
}
}
dst[code_idx] = code;
code_idx = dst_idx;
dst_idx += 1;
code = 1;
}
dst[code_idx] = code;

return dst_idx;
}

size_t cobs_decode(
size_t src_size,
uint8_t const src[static src_size],
uint8_t dst[static src_size]
) {
size_t dst_idx = 0;
size_t src_idx = 0;

while (src_idx < src_size) {
uint8_t const code = src[src_idx];
src_idx += 1;

if (code == 0) {
return SIZE_MAX;
}

for (uint8_t i = 1; i < code; i += 1) {
if (src_idx >= src_size) {
return SIZE_MAX;
}
dst[dst_idx] = src[src_idx];
dst_idx += 1;
src_idx += 1;
}

if (code != cobs_code_full && src_idx < src_size) {
dst[dst_idx] = 0;
dst_idx += 1;
}
}

return dst_idx;
}
58 changes: 58 additions & 0 deletions boot/boot_serial/src/cobs.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2026 Intercreate, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/

#ifndef H_COBS_
#define H_COBS_

#include <stddef.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

enum {
/* COBS appends one code byte per run of this many non-zero input bytes. */
cobs_run_max = 254,
};

/*
* Worst-case COBS-encoded length for "len" decoded bytes, excluding any frame
* delimiter. A macro rather than a function so it can size a buffer at compile
* time; "len" is evaluated more than once.
*/
#define COBS_ENCODED_MAX(len) ((len) + (len) / cobs_run_max + 1)

/*
* Encode "src_size" bytes of "src" into "dst", replacing every 0x00 so the
* encoded output may be terminated with a lone 0x00 delimiter. The array-bound
* syntax states the contract: "dst" must hold COBS_ENCODED_MAX(src_size) bytes,
* and "restrict" means "dst" and "src" must not alias. Returns the number of
* bytes written to "dst".
*/
size_t cobs_encode(
size_t src_size,
uint8_t const src[static restrict src_size],
uint8_t dst[static restrict COBS_ENCODED_MAX(src_size)]
);

/*
* Decode "src_size" COBS bytes of "src" (with the trailing delimiter already
* removed) into "dst", which may alias "src" for in-place decoding (hence no
* "restrict"); the decoded length never exceeds "src_size". Returns the decoded
* length, or SIZE_MAX if "src" is not a well-formed COBS frame.
*/
size_t cobs_decode(
size_t src_size,
uint8_t const src[static src_size],
uint8_t dst[static src_size]
);

#ifdef __cplusplus
}
#endif

#endif /* H_COBS_ */
4 changes: 4 additions & 0 deletions boot/zephyr/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@ if(CONFIG_MCUBOOT_SERIAL)
${BOOT_DIR}/boot_serial/src/zcbor_bulk.c
)

zephyr_sources_ifdef(CONFIG_BOOT_SERIAL_RAW_PROTOCOL_COBS
${BOOT_DIR}/boot_serial/src/cobs.c
)

zephyr_include_directories(
${BOOT_DIR}/bootutil/include
${BOOT_DIR}/boot_serial/include
Expand Down
38 changes: 34 additions & 4 deletions boot/zephyr/Kconfig.serial_recovery
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,48 @@ config BOOT_SERIAL_RAW_PROTOCOL
length field of the SMP header. The SMP client must be configured
to use a matching raw transport.

config BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT
bool "Raw protocol input expiration"
choice BOOT_SERIAL_RAW_PROTOCOL_RECOVERY
prompt "Raw protocol packet delimiting and stall recovery"
depends on BOOT_SERIAL_RAW_PROTOCOL
default y
default BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT
help
The raw protocol carries SMP packets as binary with boundaries taken
from the SMP header length field. This selects how a truncated or
corrupt transfer is detected and recovered from. The options are
mutually exclusive.

config BOOT_SERIAL_RAW_PROTOCOL_NONE
bool "No delimiting or recovery"
help
Packet boundaries come solely from the SMP header length. A truncated
or malformed packet has no resync point and can permanently wedge
serial recovery. This matches the original raw behaviour with the input
timeout disabled.

config BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT
bool "Input expiration timeout"
help
Discard a partially received raw SMP packet after a period without new
data, so that a stalled or malformed transfer cannot permanently wedge
serial recovery. Unlike the SMP over console format, the raw protocol has
no CRC or framing to detect a truncated packet, so there is otherwise no
way to resynchronise the input stream. This mirrors Zephyr's
MCUMGR_TRANSPORT_RAW_UART_INPUT_TIMEOUT (zephyrproject-rtos/zephyr#101821)
and is enabled by default because raw recovery is unsafe without it.
and is the default because raw recovery is unsafe without it.

config BOOT_SERIAL_RAW_PROTOCOL_COBS
bool "COBS framing with CRC16"
select CRC
help
Wrap each raw SMP packet as COBS(header || payload || CRC16) followed by
a single 0x00 frame delimiter. The delimiter is an unambiguous resync
point and the CRC16 (CRC-16/XMODEM, identical to the SMP over console
protocol) detects corruption, so no input timeout is required. This adds
the COBS codec and a CRC16 and grows the receive buffer by less than 1%.
The SMP client must be configured to use a matching COBS+CRC transport;
no stock mcumgr client speaks this encoding yet.

endchoice

config BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT_MS
int "Raw protocol input expiration timeout (ms)"
Expand Down
4 changes: 4 additions & 0 deletions boot/zephyr/include/mcuboot_config/mcuboot_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@
CONFIG_BOOT_SERIAL_RAW_PROTOCOL_INPUT_TIMEOUT_MS
#endif

#ifdef CONFIG_BOOT_SERIAL_RAW_PROTOCOL_COBS
#define MCUBOOT_SERIAL_RAW_PROTOCOL_COBS
#endif

#ifdef CONFIG_MCUBOOT_SERIAL
#define MCUBOOT_SERIAL_RECOVERY
#endif
Expand Down
6 changes: 6 additions & 0 deletions boot/zephyr/sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ tests:
integration_platforms:
- nrf52840dk/nrf52840
tags: bootloader_mcuboot
sample.bootloader.mcuboot.serial_recovery_raw_cobs:
extra_args: EXTRA_CONF_FILE=serial_recovery_raw_cobs.conf
platform_allow: nrf52840dk/nrf52840
integration_platforms:
- nrf52840dk/nrf52840
tags: bootloader_mcuboot
sample.bootloader.mcuboot.serial_recovery_all_options:
extra_args: EXTRA_CONF_FILE=serial_recovery.conf
extra_configs:
Expand Down
5 changes: 5 additions & 0 deletions boot/zephyr/serial_recovery_raw_cobs.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CONFIG_MCUBOOT_SERIAL=y
CONFIG_BOOT_SERIAL_UART=y
CONFIG_BOOT_SERIAL_RAW_PROTOCOL=y
CONFIG_BOOT_SERIAL_RAW_PROTOCOL_COBS=y
CONFIG_UART_CONSOLE=n
Loading
Loading