Updating MCUBoot with TFM activated

Hi,

We have a nRF5340-DK board with the following setup:

  • b0 as the first stage immutable bootloader
  • MCUBoot as the second stage upgradable bootloader, which needs to be upgradable
  • A NS application running with TFM activated.
  • If possible only have one application slot, as our real application is too big to use the dual slot.

We are using NCS 2.6.1 on Ubuntu 22.04.

Here is my prj.conf:

6204.prj.conf

Here is my mcuboot.conf:

8360.mcuboot.conf

Our goal is to upgrade MCUBoot from the application itself. Our final device will have a SD Card to store the upgrade files, but for now we are using the DK so we send the upgrade using plain UART. The file sending is working fine and I could test upgrading my application without any issue (with two slots for testing purpose).

Here is the current "upgrader" app code:

#include <stdio.h>
#include <string.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/crc.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/reboot.h>

#include <devicetree_generated.h>
#include <dfu/dfu_target.h>
#include <dfu/dfu_target_mcuboot.h>
#include <dfu/dfu_target_stream.h>
#include <zephyr/device.h>

#include <fw_info.h>

#if defined(CONFIG_TRUSTED_EXECUTION_NONSECURE)
#include <tfm_ns_interface.h>
#include <tfm_ioctl_api.h>
#endif

#include <pm_config.h>

#include <errno.h>

#include <zephyr/device.h>
#include <zephyr/drivers/flash.h>
#include <zephyr/storage/flash_map.h>


#define VERSION 			4
#define CHUNK_SZ			1024

#define MCUBOOT_BUF_SZ		512


static const struct device *uart_dev;

/* The buffer to receive  */
static char mcuboot_buf[MCUBOOT_BUF_SZ] __aligned(4);



/**
 * @brief Helper to read the update size using UART
 *
 * @param uart_dev the UART device to use to read
 * @return int32_t >= 0 on success, -1 otherwise
 */
int32_t read_update_size_uart(const struct device *uart_dev) {

	int err;
	uint32_t prg_size = 0;
	uint32_t bytes_read = 0;
	uint8_t byte;


	do {

		err = uart_poll_in(uart_dev, &byte);

		if (err == 0) {

			prg_size |= (int)byte << (bytes_read * 8);
			bytes_read++;

		} else if (err == -1) {

			continue;
		} else {
			printf("Error detected %d", err);
			return -1;
		}

	} while (bytes_read != sizeof(uint32_t));


	return prg_size;
}

/**
 * @brief Callback handler for DFUTarget events
 *
 * @param evt the event which called the callback
 */
static void dfu_target_callback_handler(enum dfu_target_evt_id evt) {

	switch (evt) {

	case DFU_TARGET_EVT_TIMEOUT:

		printf("DFU_TARGET_EVT_TIMEOUT");
		break;

	case DFU_TARGET_EVT_ERASE_DONE:

		printf("DFU_TARGET_EVT_ERASE_DONE");
		break;

	case DFU_TARGET_EVT_ERASE_PENDING:

		printf("DFU_TARGET_EVT_ERASE_PENDING");
		break;
	}
}

void print_fw_info(struct fw_info *info, const char *name) {
	printf("FW_info%s:\n", name);
	printf("\tversion %d\n", info->version);
	printf("\tsize %d\n", info->size);
}

static int read_s0_active(uint32_t s0_address, uint32_t s1_address,
			  bool *const s0_active)
{
	const struct fw_info *s0;
	const struct fw_info *s1;

	if (!s0_active) {
		return -EINVAL;
	}

	s0 = fw_info_find(s0_address);
	if (s0 == NULL) {
		return -EFAULT;
	}
	print_fw_info(s0, "s0");

	s1 = fw_info_find(s1_address);
	if (s1 == NULL) {
		/* No s1 found, s0 is active */
		*s0_active = true;
	} else {
		print_fw_info(s1, "s1");
		/* Both s0 and s1 found, check who is active */
		*s0_active = s0->version >= s1->version;
	}

	return 0;
}


int receive_and_flash_image_stream(void) {
	const struct device *flash_dev = DEVICE_DT_GET(DT_NODELABEL(flash_controller));
	static char prg_data_buf[CHUNK_SZ];
	char *prg_data = prg_data_buf;
    int update_size_full = 0;
    int total_bytes_read = 0;
    int bytes_read = 0;
    int err = 0;

	/* Retrieve the update size */
    update_size_full = read_update_size_uart(uart_dev);
    if (update_size_full == -1) {
        printf("Error while getting update size!\n");
        return -1;
    }
    printf("Update size: %d bytes\n", update_size_full);

	struct dfu_target_stream_init param = {
		.id = "s1_image",
		.fdev = flash_dev,
		.buf = mcuboot_buf,
		.len = MCUBOOT_BUF_SZ,
		.offset = PM_S1_ADDRESS,
		.size = PM_S1_IMAGE_SIZE,
		.cb = NULL
	};
	err = dfu_target_stream_init(&param);
	if (err < 0) {
		printf("dfu_target_stream_init failed %d", err);
		return err;
	}

	printf("stream params:\n \
		id: %s\n \
		fdev: 0x%08X (name = %s)\n \
		buf: 0x%08X\n \
		len: %d\n \
		offset: 0x%08X\n \
		size: %d\n",
		param.id,
		param.fdev,
		param.fdev->name,
		param.buf,
		param.len,
		param.offset,
		param.size
	);

	while (1) {
		err = uart_poll_in(uart_dev, prg_data);

		if (err == 0) {

			prg_data++;
			total_bytes_read++;

			if (total_bytes_read == update_size_full) {

				printf("Received the full file!\n");

				dfu_target_stream_write(prg_data_buf, total_bytes_read % CHUNK_SZ);

				break;
			}


			if (total_bytes_read % CHUNK_SZ == 0) {
				int ret = 0;
				if ((ret = dfu_target_stream_write(prg_data_buf, CHUNK_SZ)) != 0) {
					printf("Error writing: %d\n", ret);
				}
				prg_data = prg_data_buf;
			}

		} else if (err == -ENOSYS) {

			printf("Error RX");

			return 1;
		} else if (err == -EBUSY) {

			printf("Error UART busy!\n");

			return 1;
		}
	}

    /* Finalize the DFU target after successfully receiving the data */
    err = dfu_target_stream_done(true);
    if (err != 0) {
        printf("dfu_target_stream_done failed: %d\n", err);
        return -1;
    }

    printf("Firmware received and flashed successfully to S1 slot.\n");
    return 0;
}

/**
 * @brief Wrapper function to receive an image and flash it
 *
 * @return int 0 on success, -1 otherwise
 */
int receive_and_flash_image(void) {
	static char prg_data_buf[CHUNK_SZ];
	char *prg_data;
	int update_size_full;
	int err;
	int total_bytes_read;

	prg_data = prg_data_buf;
	total_bytes_read = 0;
	update_size_full = 0;

	bool s0_active = false;

	int ret = 0;

#if defined(CONFIG_TRUSTED_EXECUTION_NONSECURE)
	if (tfm_platform_s0_active(PM_S0_ADDRESS, PM_S1_ADDRESS, &s0_active) != 0) {
		printf("Error while reding s0 active with TFM!\n");
		return -1;
	}
#else
	if ((ret = read_s0_active(PM_S0_ADDRESS, PM_S1_ADDRESS, &s0_active)) != 0) {
		printf("Error while reading s0_acitve: %d\n", ret);
	}
#endif
	printf("S0 active: %d\n", s0_active);

	/* Retrieve the update size */
	update_size_full = read_update_size_uart(uart_dev);
	if (update_size_full == -1) {

		printf("Error while getting update size!");

		return -1;
	}

	printf("Update size: %d\n", update_size_full);

	/* Set the buffer used by DFUTarget in MCUBoot mode */
	if ((err = dfu_target_mcuboot_set_buf(mcuboot_buf, MCUBOOT_BUF_SZ)) != 0) {

		printf("dfu_target_mcuboot_set_buf error: %d", err);

		return -1;
	}

	/* Initialize the DFUTarget library with our image info */
	if ((err = dfu_target_init(DFU_TARGET_IMAGE_TYPE_ANY_APPLICATION, 0, update_size_full, dfu_target_callback_handler)) != 0) {

		printf("dfu_target_init error: %d", err);

		return -1;
	}


	while (1) {
		err = uart_poll_in(uart_dev, prg_data);
		if (err == 0) {

			prg_data++;
			total_bytes_read++;

			if (total_bytes_read == update_size_full) {

				printf("Received the full file!\n");

				dfu_target_write(prg_data_buf, total_bytes_read % CHUNK_SZ);
				break;
			}

			if (total_bytes_read % CHUNK_SZ == 0) {
				int ret = 0;
				if ((ret = dfu_target_write(prg_data_buf, CHUNK_SZ)) != 0) {
					printf("Error writing: %d\n", ret);
				}
				prg_data = prg_data_buf;
			}

		} else if (err == -ENOSYS) {

			printf("Error RX");

			return 1;
		} else if (err == -EBUSY) {

			printf("Error UART busy!\n");

			return 1;
		}
	}

	/* Ask DFU target to clean its data */
	if ((err = dfu_target_done(true)) != 0) {

		printf("dfu_target_done failed: %d", err);

		return 1;
	} else {
		printf("DFU target download done!\n");
	}


	return 0;
}

int main(void) {

	int err = 0;
	bool s0_active = false;

	uart_dev = DEVICE_DT_GET(DT_NODELABEL(uart0));

	k_msleep(1000);


	printf("App version %d, board %s, compiled at %s\n", VERSION, CONFIG_BOARD, __TIMESTAMP__);
	printf("Waiting for app image...\n");

#if 1 /* Toggle to enable either dfu_target_mcuboot or dfu_target_stream */
	receive_and_flash_image();
	/* Indicate to DFUTarget that the update is ready
	and reboot to boot into the updated app if successful */
	if ((err = dfu_target_schedule_update(-1)) != 0) {

		printf("dfu_target_schedule_update failed: %d", err);

		return 1;
	} else {

		printf("DFU target successfully scheduled the update!\n");

		sys_reboot(SYS_REBOOT_WARM);
	}
#else
	receive_and_flash_image_stream();

	sys_reboot(SYS_REBOOT_WARM);
#endif

	return 0;
}

Here is what has been tested for now:

Updating MCUBoot using DFUTarget MCUBoot:

  1. Send the correct s1 image (signed_by_mcuboot_and_b0_s1_image_update.bin) to the device, with an incremented CONFIG_FW_INFO_FIRMWARE_VERSION
  2. Initialize DFUTarget with our image size, specifying it is an MCUBoot style update
  3. Flash chunk by chunk the image using dfu_target_write()
  4. Call dfu_target_done(true)
  5. Call dfu_target_schedule_update(-1)
  6. Reboot
  7. The b0n boots but the flash region for S1 is not changed, since it reads the same version for both s0/s1. Upgrade failed

Updating MCUBoot by flashing directly S1 using dfu_target_stream without TFM activated and without MCUBoot FPROTECT:

  1. Send the correct s1 image (signed_by_mcuboot_and_b0_s1_image_update.bin) to the device, with an incremented CONFIG_FW_INFO_FIRMWARE_VERSION
  2. Initialize DFUTarget stream by specifying our buffer, offset and flash device. Here we specify the S1 address directly.
  3. Write the chunk into flash using df_target_stream_write
  4. Call dfu_target_stream_done
  5. Reboot
  6. The b0n sees the newer FW version in S1 and boots it. Upgrade is successful

If we enable the app NS config again, it obviously fails at writing the chunks as the application has no access to the S1 flash region.

My questions are:

  1. Is it possible to upgrade MCUBoot with our current setup?
  2. If so, what is the preferred way to do so?
  3. DFUTarget MCUBoot seems to only use the application secondary slot to write to flash, but who is responsible to swap it to S0/S1 after the flash is done?
  4. Is there a way to use DFUTarget to update MCUBoot from the app using a NS configuration?

Thanks for your time!

Best regards

Dax-id

  • Hi,

    Is it possible to upgrade MCUBoot with our current setup?

    No. The applicion is not allowed to write to the MCUboot slots.

    DFUTarget MCUBoot seems to only use the application secondary slot to write to flash, but who is responsible to swap it to S0/S1 after the flash is done?

    Yes, that is correct. MCUboot check the secondary applicatino slot to see if there are updates that should be handles. If it is an MCUboot update it will copy the update to the other MCUslot (not the one currently cunning), and reset. Then the first stage immutable  bootloader (NSIB) will check which MCUboot slot has the highest version (which in this case is the new one). validate and start that.

    Is there a way to use DFUTarget to update MCUBoot from the app using a NS configuration?

    You may be able to modify permissions to allow the application to write to the bootlodaer partitions, but that would break the security in the sense that an application could modify itself and also modify the bootloader to start it anyway. If you have a bit space left on your device another approach could be to make a secondary application slot that is not large enough to do applicaiton updates, but is large enough to hold MCUboot, allowing MCUboot updates in the normal way. That is demonstrated by this unofficial sample.

  • Hi Einar,

    Thanks for your reply. It was indeed the solution we started investigating just after writing the post. We now have to see if the partitions/flash size are big enough for our needs, which may not be the case, but that is something we'll look at on our side!

Related