From 83e6e83b9fa65d8b300233c690e7ba448df2697d Mon Sep 17 00:00:00 2001 From: David Garske Date: Fri, 23 Oct 2026 15:36:31 -0700 Subject: [PATCH] Add compressed firmware updates installed into an execute-in-place slot --- .github/workflows/test-build-cmake.yml | 2 +- .../workflows/test-compressed-simulator.yml | 157 +++++ CMakePresets.json | 6 +- Makefile | 1 + .../examples/sim-compressed-update-ecc.config | 31 + .../sim-compressed-update-ext-enc.config | 34 + .../examples/sim-compressed-update-ext.config | 31 + config/examples/sim-compressed-update.config | 31 + .../stm32f439zi-compressed-update.config | 46 ++ .../examples/stm32h7-compressed-update.config | 41 ++ config/examples/stm32h7.config | 6 +- docs/CMake.md | 4 +- docs/Compression.md | 107 +++ docs/README.md | 1 + docs/Signing.md | 13 + docs/Targets.md | 6 +- docs/compile.md | 11 + docs/firmware_image.md | 6 + hal/stm32f4.c | 16 + hal/stm32h7.h | 31 +- hal/uart/uart_drv_stm32f4.c | 24 +- include/gzip.h | 47 ++ include/image.h | 4 + include/wolfboot/wolfboot.h | 79 ++- options.mk | 49 +- src/gzip.c | 411 ++++++++++- src/image.c | 51 ++ src/libwolfboot.c | 16 +- src/update_flash.c | 259 ++++++- tools/config.mk | 2 + tools/keytools/Makefile | 10 + tools/keytools/sign.c | 248 ++++++- tools/scripts/sim-compressed-update.sh | 153 +++++ tools/scripts/wolfboot_cmake_full_build.sh | 2 +- tools/test.mk | 74 ++ tools/unit-tests/Makefile | 80 ++- tools/unit-tests/unit-compressed-install.c | 314 +++++++++ tools/unit-tests/unit-compressed-tlv.c | 252 +++++++ tools/unit-tests/unit-gzip-sink.c | 648 ++++++++++++++++++ tools/unit-tests/unit-sign-encrypted-output.c | 10 +- 40 files changed, 3251 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/test-compressed-simulator.yml create mode 100644 config/examples/sim-compressed-update-ecc.config create mode 100644 config/examples/sim-compressed-update-ext-enc.config create mode 100644 config/examples/sim-compressed-update-ext.config create mode 100644 config/examples/sim-compressed-update.config create mode 100644 config/examples/stm32f439zi-compressed-update.config create mode 100644 config/examples/stm32h7-compressed-update.config create mode 100644 docs/Compression.md create mode 100755 tools/scripts/sim-compressed-update.sh create mode 100644 tools/unit-tests/unit-compressed-install.c create mode 100644 tools/unit-tests/unit-compressed-tlv.c create mode 100644 tools/unit-tests/unit-gzip-sink.c diff --git a/.github/workflows/test-build-cmake.yml b/.github/workflows/test-build-cmake.yml index e73ac5c362..7527cee45d 100644 --- a/.github/workflows/test-build-cmake.yml +++ b/.github/workflows/test-build-cmake.yml @@ -30,7 +30,7 @@ jobs: - name: Run CMake build for STM32H7 run: | rm -rf ./build - cmake -B build -DWOLFBOOT_TARGET=stm32h7 -DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x8020000 -DWOLFBOOT_SECTOR_SIZE=0x20000 -DWOLFBOOT_PARTITION_SIZE=0xD0000 -DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x80F0000 -DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x81C0000 + cmake -B build -DWOLFBOOT_TARGET=stm32h7 -DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x8020000 -DWOLFBOOT_SECTOR_SIZE=0x20000 -DWOLFBOOT_PARTITION_SIZE=0xE0000 -DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x8100000 -DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x81E0000 - name: Build wolfBoot run: make -C build diff --git a/.github/workflows/test-compressed-simulator.yml b/.github/workflows/test-compressed-simulator.yml new file mode 100644 index 0000000000..b42c857970 --- /dev/null +++ b/.github/workflows/test-compressed-simulator.yml @@ -0,0 +1,157 @@ +name: Compressed images - test with simulator target + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + +jobs: + compressed_update_simulator_tests: + runs-on: ubuntu-latest + container: + image: ghcr.io/wolfssl/wolfboot-ci-sim:v1.0 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + # The sink decoder is a separate output path in src/gzip.c, so it + # has to be exercised and not merely built. The container on + # internal flash is read in place; on external flash it is pulled in + # through the read callback, which is a different path through the + # decoder's input. + sign: [ ED25519, ECC256 ] + config: [ sim-compressed-update, sim-compressed-update-ext, + sim-compressed-update-ext-enc ] + window: [ 16384 ] + # The window sizes the decoder's buffer and bounds back-references. + # Only the input path differs between the three layouts, so the + # smallest window is run on one of them. + include: + - sign: ED25519 + config: sim-compressed-update + window: 4096 + - sign: ECC256 + config: sim-compressed-update + window: 4096 + # ECC flash programs whole write units and erases whole sectors; + # the simulator enforces both for this config. + - sign: ED25519 + config: sim-compressed-update-ecc + window: 16384 + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: make clean + run: | + make distclean + + - name: Install zlib headers + run: | + apt-get update && apt-get install -y zlib1g-dev + + - name: Select config + run: | + cp config/examples/${{ matrix.config }}.config .config + sed -i "s/^SIGN?=.*/SIGN?=${{ matrix.sign }}/" .config + sed -i "s/^WOLFBOOT_COMPRESSED_WINDOW=.*//" .config + echo "WOLFBOOT_COMPRESSED_WINDOW=${{ matrix.window }}" >> .config + + - name: Build tools + run: | + make -C tools/keytools && make -C tools/bin-assemble + + # Not a bare `make`: that also assembles internal_flash.dd with the + # update partition inside it, which is wrong when the partition lives + # on the external device. The test target below builds both images. + - name: Build wolfboot.elf and test-app/image.elf + run: | + make clean && make wolfboot.elf test-app/image.elf + + - name: Run the compressed update simulator test + run: | + ./tools/scripts/sim-compressed-update.sh + + compressed_update_cross_build: + runs-on: ubuntu-latest + container: + image: ghcr.io/wolfssl/wolfboot-ci-arm:v1.0 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + config: [ stm32h7-compressed-update, stm32f439zi-compressed-update ] + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Install zlib headers + run: | + apt-get update && apt-get install -y zlib1g-dev + + - name: Select config + run: | + cp config/examples/${{ matrix.config }}.config .config + + - name: Build + run: | + make clean && make keysclean && make + + # The installer walks the boot partition a sector at a time and the + # trailer lives in its last sector, so a partition that is not a whole + # number of sectors would have its tail erased by the trailer write. + - name: Check the partition geometry + run: | + BOOT=$(grep -E '^WOLFBOOT_PARTITION_SIZE\??=' .config | tail -1 | cut -d= -f2) + UPD=$(grep -E '^WOLFBOOT_PARTITION_UPDATE_SIZE\??=' .config | tail -1 | cut -d= -f2) + SEC=$(grep -E '^WOLFBOOT_SECTOR_SIZE\??=' .config | tail -1 | cut -d= -f2) + echo "boot $BOOT, update $UPD, sector $SEC" + [ $(($BOOT % $SEC)) -eq 0 ] || { echo "boot not sector aligned"; exit 1; } + [ $(($UPD % $SEC)) -eq 0 ] || { echo "update not sector aligned"; exit 1; } + # The point of the mode: the update partition is the smaller one. + [ $(($UPD)) -lt $(($BOOT)) ] || { echo "update is not smaller than boot"; exit 1; } + echo "ok: update is $(( 100 * $UPD / $BOOT ))% of boot" + + # The window knob has to reach the decoder's buffer, which is the RAM + # the mode actually costs. + - name: Check the decoder window buffer was sized from the config + run: | + WIN=$(grep -E '^WOLFBOOT_COMPRESSED_WINDOW\??=' .config | tail -1 | cut -d= -f2) + SZ=$(arm-none-eabi-nm -S wolfboot.elf | awk '$4=="compressed_window" {print $2}') + [ -n "$SZ" ] || { echo "compressed_window not linked in"; exit 1; } + WANT=$((2 * $WIN)) + [ $((0x$SZ)) -eq $WANT ] || \ + { echo "compressed_window is $((0x$SZ)) bytes, expected $WANT"; exit 1; } + echo "ok: compressed_window is $((0x$SZ)) bytes for a $WIN window" + + # Signing has to refuse a container the update partition cannot hold, + # so a release that compresses worse than the split fails here rather + # than on the device. + - name: Check a container is produced and fits + run: | + UPD=$(grep -E '^WOLFBOOT_PARTITION_UPDATE_SIZE\??=' .config | tail -1 | cut -d= -f2) + SEC=$(grep -E '^WOLFBOOT_SECTOR_SIZE\??=' .config | tail -1 | cut -d= -f2) + SIGN=$(grep -E '^SIGN\??=' .config | tail -1 | cut -d= -f2 | tr 'A-Z' 'a-z') + HASH=$(grep -E '^HASH\??=' .config | tail -1 | cut -d= -f2 | tr 'A-Z' 'a-z') + WIN=$(grep -E '^WOLFBOOT_COMPRESSED_WINDOW\??=' .config | tail -1 | cut -d= -f2) + WOLFBOOT_PARTITION_SIZE=$(grep -E '^WOLFBOOT_PARTITION_SIZE\??=' .config | tail -1 | cut -d= -f2) \ + WOLFBOOT_PARTITION_UPDATE_SIZE=$UPD WOLFBOOT_SECTOR_SIZE=$SEC \ + WOLFBOOT_COMPRESSED_WINDOW=$WIN \ + ./tools/keytools/sign --compress --$SIGN --$HASH \ + test-app/image.elf wolfboot_signing_private_key.der 2 + SZ=$(stat -c%s test-app/image_v2_signed_compressed.bin) + LIMIT=$(( $(($UPD)) - $(($SEC)) )) + echo "container $SZ bytes, limit $LIMIT" + [ "$SZ" -le "$LIMIT" ] || { echo "container does not fit"; exit 1; } + echo "ok: container fits the update partition" diff --git a/CMakePresets.json b/CMakePresets.json index 4047a9b3f5..329a545ef3 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -347,13 +347,13 @@ "WOLFBOOT_VERSION": "ON", "SPMATH": "ON", "RAM_CODE": "OFF", - "WOLFBOOT_PARTITION_SIZE": "0xD0000", + "WOLFBOOT_PARTITION_SIZE": "0xE0000", "WOLFBOOT_SECTOR_SIZE": "0x20000", "ARCH_FLASH_OFFSET": "0x08000000", "WOLFBOOT_ORIGIN": "0x08000000", "WOLFBOOT_PARTITION_BOOT_ADDRESS": "0x8020000", - "WOLFBOOT_PARTITION_UPDATE_ADDRESS": "0x80F0000", - "WOLFBOOT_PARTITION_SWAP_ADDRESS": "0x81C0000", + "WOLFBOOT_PARTITION_UPDATE_ADDRESS": "0x8100000", + "WOLFBOOT_PARTITION_SWAP_ADDRESS": "0x81E0000", "DEBUG": "OFF", "VTOR": "ON", "V": "OFF", diff --git a/Makefile b/Makefile index 95be9779a6..278200d4c3 100644 --- a/Makefile +++ b/Makefile @@ -362,6 +362,7 @@ endif # Environment variables for sign tool SIGN_ENV=IMAGE_HEADER_SIZE=$(IMAGE_HEADER_SIZE) \ + WOLFBOOT_COMPRESSED_WINDOW=$(WOLFBOOT_COMPRESSED_WINDOW) \ WOLFBOOT_PARTITION_SIZE=$(WOLFBOOT_PARTITION_SIZE) \ WOLFBOOT_PARTITION_UPDATE_SIZE=$(WOLFBOOT_PARTITION_UPDATE_SIZE) \ WOLFBOOT_SECTOR_SIZE=$(WOLFBOOT_SECTOR_SIZE) \ diff --git a/config/examples/sim-compressed-update-ecc.config b/config/examples/sim-compressed-update-ecc.config new file mode 100644 index 0000000000..5da58c8e12 --- /dev/null +++ b/config/examples/sim-compressed-update-ecc.config @@ -0,0 +1,31 @@ +ARCH=sim +TARGET=sim +SIGN?=ED25519 +HASH?=SHA256 +WOLFBOOT_SMALL_STACK?=0 +SPI_FLASH=0 +DEBUG=1 + +# UPDATE holds a signed gzip stream whose payload is a complete signed +# image, inflated into BOOT, which keeps executing in place. +COMPRESSED_UPDATE=1 +WOLFBOOT_COMPRESSED_WINDOW=16384 + +# Trailer journal and the ECC flash rules: the simulator then rejects writes +# that are not whole, erased 16-byte units, and partial-sector erases +NVM_FLASH_JOURNAL=1 +NVM_FLASH_ECC=1 +FLASH_WRITE_UNIT=16 + +# sizes should be multiple of system page size +WOLFBOOT_PARTITION_SIZE=0x50000 +WOLFBOOT_PARTITION_UPDATE_SIZE=0x2C000 +WOLFBOOT_SECTOR_SIZE=0x1000 +WOLFBOOT_PARTITION_BOOT_ADDRESS=0x80000 +# if on external flash, it should be multiple of system page size +WOLFBOOT_PARTITION_UPDATE_ADDRESS=0x100000 +# No swap step in this mode; still reserved for the keytools. +WOLFBOOT_PARTITION_SWAP_ADDRESS=0x180000 + +# required for keytools +WOLFBOOT_FIXED_PARTITIONS=1 diff --git a/config/examples/sim-compressed-update-ext-enc.config b/config/examples/sim-compressed-update-ext-enc.config new file mode 100644 index 0000000000..376fb94db3 --- /dev/null +++ b/config/examples/sim-compressed-update-ext-enc.config @@ -0,0 +1,34 @@ +ARCH=sim +TARGET=sim +SIGN?=ED25519 +HASH?=SHA256 +WOLFBOOT_SMALL_STACK?=0 +SPI_FLASH=0 +EXT_FLASH=1 +ENCRYPT=1 +ENCRYPT_WITH_AES128=1 +DEBUG=1 + +# Compressed update partition on external flash, encrypted at rest, boot +# partition executed in place from internal flash. ENCRYPT=1 hooks the +# external flash read, which is the same seam the installer and the image +# hasher pull the container through, so it is decrypted on its way in. +COMPRESSED_UPDATE=1 + +# LZ77 window the image was compressed with. The decoder needs twice this in +# RAM and refuses an image compressed with a larger one. +WOLFBOOT_COMPRESSED_WINDOW=16384 + +# UPDATE is sized for the container alone. +# sizes should be multiple of system page size +WOLFBOOT_PARTITION_SIZE=0x50000 +WOLFBOOT_PARTITION_UPDATE_SIZE=0x2C000 +WOLFBOOT_SECTOR_SIZE=0x1000 +WOLFBOOT_PARTITION_BOOT_ADDRESS=0x80000 +# on external flash, so it starts at the base of that device +WOLFBOOT_PARTITION_UPDATE_ADDRESS=0x00000 +# No swap step in this mode; still reserved for the keytools. +WOLFBOOT_PARTITION_SWAP_ADDRESS=0x180000 + +# required for keytools +WOLFBOOT_FIXED_PARTITIONS=1 diff --git a/config/examples/sim-compressed-update-ext.config b/config/examples/sim-compressed-update-ext.config new file mode 100644 index 0000000000..1580213302 --- /dev/null +++ b/config/examples/sim-compressed-update-ext.config @@ -0,0 +1,31 @@ +ARCH=sim +TARGET=sim +SIGN?=ED25519 +HASH?=SHA256 +WOLFBOOT_SMALL_STACK?=0 +SPI_FLASH=0 +EXT_FLASH=1 +DEBUG=1 + +# Compressed update partition on external flash, boot partition executed in +# place from internal flash. The container is not addressable, so wolfBoot +# pulls it in through the external flash reader as it inflates. +COMPRESSED_UPDATE=1 + +# LZ77 window the image was compressed with. The decoder needs twice this in +# RAM and refuses an image compressed with a larger one. +WOLFBOOT_COMPRESSED_WINDOW=16384 + +# UPDATE is sized for the container alone. +# sizes should be multiple of system page size +WOLFBOOT_PARTITION_SIZE=0x50000 +WOLFBOOT_PARTITION_UPDATE_SIZE=0x2C000 +WOLFBOOT_SECTOR_SIZE=0x1000 +WOLFBOOT_PARTITION_BOOT_ADDRESS=0x80000 +# on external flash, so it starts at the base of that device +WOLFBOOT_PARTITION_UPDATE_ADDRESS=0x00000 +# No swap step in this mode; still reserved for the keytools. +WOLFBOOT_PARTITION_SWAP_ADDRESS=0x180000 + +# required for keytools +WOLFBOOT_FIXED_PARTITIONS=1 diff --git a/config/examples/sim-compressed-update.config b/config/examples/sim-compressed-update.config new file mode 100644 index 0000000000..5047044682 --- /dev/null +++ b/config/examples/sim-compressed-update.config @@ -0,0 +1,31 @@ +ARCH=sim +TARGET=sim +SIGN?=ED25519 +HASH?=SHA256 +WOLFBOOT_SMALL_STACK?=0 +SPI_FLASH=0 +DEBUG=1 + +# UPDATE holds a signed gzip stream whose payload is a complete signed +# image, inflated into BOOT, which keeps executing in place. +COMPRESSED_UPDATE=1 + +# LZ77 window the image was compressed with. The decoder needs twice this in +# RAM and refuses an image compressed with a larger one. +WOLFBOOT_COMPRESSED_WINDOW=16384 + +# UPDATE is sized for the container alone, close enough to what the test +# fixture compresses to that the signing tool's fit check is exercised near +# its real margin. +# sizes should be multiple of system page size +WOLFBOOT_PARTITION_SIZE=0x50000 +WOLFBOOT_PARTITION_UPDATE_SIZE=0x2C000 +WOLFBOOT_SECTOR_SIZE=0x1000 +WOLFBOOT_PARTITION_BOOT_ADDRESS=0x80000 +# if on external flash, it should be multiple of system page size +WOLFBOOT_PARTITION_UPDATE_ADDRESS=0x100000 +# No swap step in this mode; still reserved for the keytools. +WOLFBOOT_PARTITION_SWAP_ADDRESS=0x180000 + +# required for keytools +WOLFBOOT_FIXED_PARTITIONS=1 diff --git a/config/examples/stm32f439zi-compressed-update.config b/config/examples/stm32f439zi-compressed-update.config new file mode 100644 index 0000000000..5f705f1de7 --- /dev/null +++ b/config/examples/stm32f439zi-compressed-update.config @@ -0,0 +1,46 @@ +# NUCLEO-F439ZI with a compressed update partition, executing in place. +# +# Both partitions stay inside the first 1MB: hal/stm32f4.c describes only +# that much flash, because the second bank's sector numbers are not +# contiguous with the first's. A layout past it is refused at build time. +# +# Console is USART3 on PD8/PD9, the ST-LINK virtual COM port on the +# NUCLEO-144 boards. +ARCH?=ARM +TARGET?=stm32f4 +SIGN?=ED25519 +HASH?=SHA256 +DEBUG?=1 +DEBUG_UART?=1 +CFLAGS_EXTRA+=-DUSE_UART3 +VTOR?=1 +NVM_FLASH_WRITEONCE?=0 +SPMATH?=1 + +# 168MHz from an 8MHz HSE, the NUCLEO-144 default. +CLOCK_SPEED?=168000000 +STM32_PLLM?=8 +STM32_PLLN?=336 +STM32_PLLP?=2 +STM32_PLLQ?=7 + +# Implies DISABLE_BACKUP: no swap, and no backup of the outgoing image. +COMPRESSED_UPDATE?=1 + +# LZ77 window the image was compressed with: 32KB of bss on a part with +# 192KB of SRAM. Sign with 'sign --compress=14' to match. +WOLFBOOT_COMPRESSED_WINDOW?=16384 + +# UPDATE is sized for the container alone. The split has to cover the worst +# ratio the product will ever ship; the signing tool refuses an artifact +# that does not fit, so a bad split fails at build time, not on the device. +WOLFBOOT_SECTOR_SIZE?=0x20000 +WOLFBOOT_PARTITION_SIZE?=0x80000 +WOLFBOOT_PARTITION_UPDATE_SIZE?=0x60000 +WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x08020000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x080A0000 +# No swap step in this mode, so no swap partition is reserved. +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0 + +# required for keytools +WOLFBOOT_FIXED_PARTITIONS=1 diff --git a/config/examples/stm32h7-compressed-update.config b/config/examples/stm32h7-compressed-update.config new file mode 100644 index 0000000000..65390b049b --- /dev/null +++ b/config/examples/stm32h7-compressed-update.config @@ -0,0 +1,41 @@ +# STM32H7 with a compressed update partition, executing in place. The H743 +# has 2MB of flash in 128KB sectors. +ARCH?=ARM +TARGET?=stm32h7 +SIGN?=ECC256 +HASH?=SHA256 +DEBUG?=0 +DEBUG_UART?=0 +VTOR?=1 +NO_ASM?=0 +EXT_FLASH?=0 +SPI_FLASH?=0 +ALLOW_DOWNGRADE?=0 +NVM_FLASH_WRITEONCE?=0 +WOLFBOOT_VERSION?=1 +V?=0 +SPMATH?=1 +RAM_CODE?=0 +DUALBANK_SWAP?=0 + +# Implies DISABLE_BACKUP: no swap, and no backup of the outgoing image. +COMPRESSED_UPDATE?=1 + +# LZ77 window the image was compressed with. The decoder needs twice this in +# RAM, which fits the 128KB DTCM wolfBoot runs from here. Sign with +# 'sign --compress=14' to match. +WOLFBOOT_COMPRESSED_WINDOW?=16384 + +# UPDATE is sized for the container alone. The split has to cover the worst +# ratio the product will ever ship; the signing tool refuses an artifact +# that does not fit, so a bad split fails at build time, not on the device. +WOLFBOOT_SECTOR_SIZE?=0x20000 +WOLFBOOT_PARTITION_SIZE?=0x100000 +WOLFBOOT_PARTITION_UPDATE_SIZE?=0xE0000 +WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x8020000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x8120000 +# No swap step in this mode, so no swap partition is reserved. +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0 + +# required for keytools +WOLFBOOT_FIXED_PARTITIONS=1 diff --git a/config/examples/stm32h7.config b/config/examples/stm32h7.config index e2a1f3cb06..03d90d905d 100644 --- a/config/examples/stm32h7.config +++ b/config/examples/stm32h7.config @@ -17,8 +17,8 @@ V?=0 SPMATH?=1 RAM_CODE?=0 DUALBANK_SWAP?=0 -WOLFBOOT_PARTITION_SIZE?=0xD0000 +WOLFBOOT_PARTITION_SIZE?=0xE0000 WOLFBOOT_SECTOR_SIZE?=0x20000 WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x8020000 -WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x80F0000 -WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x81C0000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x8100000 +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x81E0000 diff --git a/docs/CMake.md b/docs/CMake.md index 94609efd67..407141c4fd 100644 --- a/docs/CMake.md +++ b/docs/CMake.md @@ -220,7 +220,7 @@ For example, to build for the stm32h7 platform: ``` $ mkdir build $ cd build -$ cmake -DWOLFBOOT_TARGET=stm32h7 -DBUILD_TEST_APPS=yes -DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x8020000 -DWOLFBOOT_SECTOR_SIZE=0x20000 -DWOLFBOOT_PARTITION_SIZE=0xD0000 -DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x80F0000 -DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x81C0000 .. +$ cmake -DWOLFBOOT_TARGET=stm32h7 -DBUILD_TEST_APPS=yes -DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x8020000 -DWOLFBOOT_SECTOR_SIZE=0x20000 -DWOLFBOOT_PARTITION_SIZE=0xE0000 -DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x8100000 -DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x81E0000 .. $ make ``` @@ -328,7 +328,7 @@ Scanning dependencies of target wolfboot_stm32h7_outputs Signing and hashing algorithms can be specified with `-DSIGN=` and `-DHASH=`. To view additional options to configuring wolfBoot, add `-LAH` to your cmake command, along with the partition specifications. ``` -$ cmake -DWOLFBOOT_TARGET=stm32h7 -DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x8020000 -DWOLFBOOT_SECTOR_SIZE=0x20000 -DWOLFBOOT_PARTITION_SIZE=0xD0000 -DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x80F0000 -DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x81C0000 -LAH .. +$ cmake -DWOLFBOOT_TARGET=stm32h7 -DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x8020000 -DWOLFBOOT_SECTOR_SIZE=0x20000 -DWOLFBOOT_PARTITION_SIZE=0xE0000 -DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x8100000 -DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x81E0000 -LAH .. ``` #### stm32f4 diff --git a/docs/Compression.md b/docs/Compression.md new file mode 100644 index 0000000000..bbb73b7bd5 --- /dev/null +++ b/docs/Compression.md @@ -0,0 +1,107 @@ +# Compressed firmware updates + +With `COMPRESSED_UPDATE=1` the update partition holds a gzip-compressed, signed image, and wolfBoot inflates it into the boot partition, which goes on executing in place. The update partition is then sized for the compressed container rather than for a second full copy of the application, and the flash that frees goes to the application instead. + +This is a different mechanism from [delta updates](firmware_update.md), which shrink the update that has to be transferred but still install into a same-size partition, and from [encrypted partitions](encrypted_partitions.md), which protect the update partition at rest. + +## What it gives up + +There is no local copy of the outgoing image. Three properties bound that: + +- The container's signature is verified **before the first erase**, so a corrupt or unauthentic image never touches the running one. +- The update partition is only ever read during an install, so a power cut leaves an intact source. The next boot resumes from the first destination sector not yet recorded, re-inflating from the beginning and discarding output below that point rather than rewriting it. +- The payload is a complete signed image, so after installing, the boot partition is an ordinary wolfBoot partition whose own manifest the normal boot path verifies. The inflater does not have to be trusted for the installed image to be authentic. + +What remains is that an image which is valid, boots, and then misbehaves cannot be rolled back on the device; it has to be replaced by pushing another update. That is the same trade [`DISABLE_BACKUP`](compile.md) makes without the flash saving. If rollback matters more than flash, use an ordinary A/B layout or delta updates, which keep it. + +## How it works + +On a boot where the update partition is armed, wolfBoot: + +1. Reads the compression tags and refuses an image whose window is larger than this build's decode buffer, or whose uncompressed length will not fit the boot partition. +2. Verifies the integrity and signature of the container. +3. Inflates it into the boot partition, erasing each sector the first time it is written and recording each one as it completes. +4. Marks the boot partition `IMG_STATE_SUCCESS` and consumes the update. +5. Boots the boot partition through the usual path, which verifies the inner manifest. + +The decoder uses a bounded LZ77 window and writes through a sink, so it needs twice the window in RAM regardless of how large the image is. + +## Manifest fields + +Three TLVs are added to the image header (see [firmware_image.md](firmware_image.md)): + +| Tag | Name | Length | Contents | +|-----|------|--------|----------| +| 0x08 | `HDR_IMG_COMPRESSION` | 2 | Algorithm id, little-endian. `0x0001` is gzip/DEFLATE. | +| 0x09 | `HDR_IMG_UNCOMPRESSED_SIZE` | 4 | Uncompressed length in bytes, little-endian. | +| 0x0A | `HDR_IMG_COMP_WINDOW` | 2 | LZ77 window as a power of two, little-endian. | + +Compression is detected by the presence of `HDR_IMG_COMPRESSION`, not by a bit in `HDR_IMG_TYPE`. A manifest carrying the tag with an unknown algorithm, or without a valid uncompressed-size tag, is rejected rather than installed as a raw payload. + +A gzip header has no field for the window the stream was compressed with, and the decoder sizes its buffer from it, so it travels in the signed manifest instead. An image whose window exceeds what the bootloader was built for is refused before the inflate starts, rather than failing part way through with a distance error that looks like corruption. + +## Preparing an image + +The signing tool compresses, because it has to sign twice: once over the image that will live in the boot partition, and once over the compressed container. + +```sh +./tools/keytools/sign --compress test-app/image.bin wolfboot_signing_private_key.der 2 +``` + +This produces `test-app/image_v2_signed_compressed.bin`, which is what gets written to the update partition. `--compress=` selects the window, 12 to 15; it defaults to whatever `WOLFBOOT_COMPRESSED_WINDOW` the bootloader was configured with, so the two cannot disagree. + +`--compress` needs the tool built against zlib, which supplies the DEFLATE encoder; the decoder on the device is wolfBoot's own `src/gzip.c`. The dependency is optional, so a tool built without the zlib headers still builds and only rejects this one option. Install them (`zlib1g-dev` on Debian and Ubuntu) and rebuild `tools/keytools`. + +## Sizing the partitions + +The boot partition holds the uncompressed image, the update partition only the container, and both must be a whole number of sectors: + +``` +WOLFBOOT_PARTITION_SIZE = boot partition, the executable budget +WOLFBOOT_PARTITION_UPDATE_SIZE = update partition, >= container + one sector +``` + +A safe update still needs the new image in flash before the running one is overwritten, so the floor is `U + C` against `2U` for an uncompressed pair: the saving is the difference between a compressed copy and a full one, not the compression ratio itself. It is largest for payloads carrying lookup tables, fonts, a filesystem or model weights, and smallest for dense code. + +Pick the split from the worst compression ratio the product will ever ship, not the best, and measure your own firmware first. The signing tool refuses to emit a container that does not fit the configured update partition, so a release that compresses worse than the split fails at build time rather than on the device - but it still fails, and the split cannot be changed in the field. + +`WOLFBOOT_PARTITION_SWAP_ADDRESS` is unused in this mode and may be left at 0: there is no swap step. + +## Build options + +| Option | Meaning | +|--------|---------| +| `COMPRESSED_UPDATE=1` | Enable the mode. Implies `GZIP=1`, the gzip sink decoder and `DISABLE_BACKUP`. | +| `WOLFBOOT_PARTITION_UPDATE_SIZE` | Update partition size, independent of and no larger than the boot partition. Defaults to the boot partition size. | +| `WOLFBOOT_COMPRESSED_WINDOW` | LZ77 window: 4096, 8192, 16384 (the default) or 32768. The decoder needs twice this in RAM, and a smaller window costs ratio. | +| `GZIP_SMALL=1` | Use the bit-serial Huffman decoder: a few KB less code and no static tables, at a fraction of the table-driven decoder's rate. | + +Rejected at build time: `DELTA_UPDATES` (delta patches the partition it is consuming, this one overwrites it from a separate slot), `WOLFBOOT_ELF`, `DUALBANK_SWAP`, `PULL_LINKER_DEFINES` (a single partition-size linker symbol serves both partitions), a self-update signed compressed (wolfBoot cannot inflate itself), and a partition that is not a whole number of sectors. + +## An update partition on external flash + +`EXT_FLASH=1` with the update partition on a serial flash is supported, and is the layout that gains most: a small internal flash for the application, a cheap external device for the incoming update, and compression shrinking what that device has to hold as well as what has to be transferred. + +The container is not addressable there, so wolfBoot pulls it in through the external flash interface as it inflates rather than reading it in place. Nothing else changes: the boot partition is still internal and still executes in place, and the same verification happens before the first erase. `config/examples/sim-compressed-update-ext.config` exercises this under the simulator. + +## Encryption at rest + +`ENCRYPT=1` composes with this mode, so the container can be stored encrypted on the external device. It needs no separate mechanism: that layer hooks the external flash read interface, which is the same seam the installer and the image hasher already pull the container through, so the plaintext appears on its way in and nothing downstream knows the difference. See [encrypted_partitions.md](encrypted_partitions.md) for the key handling, which is unchanged. + +Because it works through the external flash interface, it requires `EXT_FLASH=1`; an internal update partition is read directly and has no such seam. Note also that the boot partition holds plaintext, since it is executed in place: this protects the stored update, not the running image. `config/examples/sim-compressed-update-ext-enc.config` exercises it. + +## Flash that programs whole units + +`NVM_FLASH_ECC=1`, and the trailer journal it requires, compose with this mode. The installer programs the boot partition in aligned chunks and pads the last one out to a whole write unit, so flash that can only program a full unit is never handed a partial one. `config/examples/sim-compressed-update-ecc.config` exercises it. + +## Examples + +`config/examples/sim-compressed-update.config` runs the flow under the simulator with a deliberately asymmetric layout. `tools/scripts/sim-compressed-update.sh` asserts the install, that it is consumed rather than repeated, that a corrupt container never reaches the inflater, and that signing refuses a container too big for the update partition: + +```sh +cp config/examples/sim-compressed-update.config .config +make keysclean && make +./tools/scripts/sim-compressed-update.sh +``` + +`config/examples/stm32h7-compressed-update.config` and `config/examples/stm32f439zi-compressed-update.config` configure a 2MB STM32. Note that `hal/stm32f4.c` describes only the first 1MB of flash, so the F4 example keeps both partitions inside it; a layout past that is refused at build time. diff --git a/docs/README.md b/docs/README.md index 67be49e2c0..bcea88acd6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,7 @@ See also: [wolfBoot Product Overview](https://www.wolfssl.com/products/wolfboot/ - [**azure_keyvault.md**](./azure_keyvault.md) - Using Azure Key Vault for key management and signing with wolfBoot. - [**CMake.md**](./CMake.md) - CMake-based build setup, presets, toolchains, and tips for building wolfBoot. - [**compile.md**](./compile.md) - How to build/compile wolfBoot (toolchains, options, typical steps). +- [**Compression.md**](./Compression.md) - Compressed firmware updates: a gzip-compressed update partition installed into a boot partition that executes in place. - [**encrypted_partitions.md**](./encrypted_partitions.md) - Creating and managing encrypted firmware/data partitions. - [**firmware_image.md**](./firmware_image.md) - wolfBoot firmware image format, layout, and metadata. - [**firmware_update.md**](./firmware_update.md) - Update flow: slots, verification, rollback, and recovery. diff --git a/docs/Signing.md b/docs/Signing.md index e904c560e9..7f76288c2c 100644 --- a/docs/Signing.md +++ b/docs/Signing.md @@ -212,6 +212,19 @@ For complete documentation of the self-header feature — including configuration, update flow, and runtime verification — see [firmware_update.md](firmware_update.md#self-header-persisting-the-bootloader-manifest). +#### Compressed images + +An image for a compressed update partition is produced with: + + * `--compress[=]` sign the image, compress it, and sign the container. The payload is a + complete signed image, so the boot partition it is installed into stays an ordinary wolfBoot + partition. `` selects the LZ77 window as a power of two, 12 to 15, and defaults to the + window the bootloader was configured with. Needs the tool built against zlib. + +The output is named `_v_signed_compressed.bin`. This requires the bootloader to be built +with `COMPRESSED_UPDATE=1`, and cannot be combined with `--delta` or with a wolfBoot self-update. +See [Compression.md](Compression.md). + #### Encryption using a symmetric key Although signed to be authenticated, by default the image is not encrypted and diff --git a/docs/Targets.md b/docs/Targets.md index 53026cd31c..917e2856c3 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -1706,10 +1706,10 @@ Example 2MB partitioning on STM32-H753: ``` WOLFBOOT_SECTOR_SIZE?=0x20000 -WOLFBOOT_PARTITION_SIZE?=0xD0000 +WOLFBOOT_PARTITION_SIZE?=0xE0000 WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x8020000 -WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x80F0000 -WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x81C0000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x8100000 +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x81E0000 ``` ### Build Options diff --git a/docs/compile.md b/docs/compile.md index 305ea790e0..0ee26df773 100644 --- a/docs/compile.md +++ b/docs/compile.md @@ -206,6 +206,17 @@ differences between the old firmware to replace, currently running on the target For more information and examples, see the [firmware update](firmware_update.md) section. +### Compressed images + +wolfBoot can install a gzip-compressed update, keeping only the update partition compressed and +inflating it into the boot partition, which goes on executing in place. To enable this feature, +compile with `COMPRESSED_UPDATE=1` and set `WOLFBOOT_PARTITION_UPDATE_SIZE` smaller than +`WOLFBOOT_PARTITION_SIZE`. + +Sign with `--compress`, which compresses and signs the nested container. + +For more information and examples, see the [compression](Compression.md) section. + ### Enable debug symbols To debug the bootloader, simply compile with `DEBUG=1`. The size of the bootloader will increase diff --git a/docs/firmware_image.md b/docs/firmware_image.md index 2daded06ab..05d4f412dc 100644 --- a/docs/firmware_image.md +++ b/docs/firmware_image.md @@ -51,6 +51,12 @@ Each **Type** has a different meaning, and integrate information about the firmw - A 'firmware signature' Tag (type: 0x0020, size: 64 Bytes) used to validate the signature stored with the firmware against a known public key - A 'firmware type' Tag (type: 0x0030, size: 2 Bytes) used to identify the type of firmware, and the authentication mechanism in use. +A compressed image carries three additional Tags (see [Compression.md](Compression.md)): + + - A 'compression' Tag (type: 0x0008, size: 2 Bytes) indicating the algorithm used to compress the payload. 0x0001 is gzip/DEFLATE. The presence of this Tag is what marks the payload as compressed. + - An 'uncompressed size' Tag (type: 0x0009, size: 4 Bytes) indicating the length of the payload once decompressed. The bootloader uses it to reject an image too large for the boot partition before decompressing, and to cross-check the number of bytes actually produced. + - A 'compression window' Tag (type: 0x000A, size: 2 Bytes) giving the LZ77 window the payload was compressed with, as a power of two. A gzip header has no field for it and the decoder sizes its buffer from it, so it is signature-covered; an image needing a larger window than the bootloader was built for is refused before decompressing. + A 'public key hint digest' tag is transmitted in the header (type: 0x10, size:32 Bytes). This tag contains the SHA digest of the public key used by the signing tool. The bootloader may use this field to locate the correct public key in case of multiple keys available. diff --git a/hal/stm32f4.c b/hal/stm32f4.c index 51908330cb..2ef2d6cf27 100644 --- a/hal/stm32f4.c +++ b/hal/stm32f4.c @@ -120,6 +120,22 @@ #define FLASH_SECTOR_11 0x00E0000 /* 128 Kb */ #define FLASH_TOP 0x0100000 +/* The table above describes the first 1MB only. A 2MB part (F42x/F43x) has + * a second bank whose sector numbers are not contiguous with these, so an + * erase past FLASH_TOP resolves to no sector and silently does nothing. + * Caught here rather than at run time, where it looks like a partition that + * will not take an update. */ +#if defined(__WOLFBOOT) && defined(WOLFBOOT_PARTITION_UPDATE_ADDRESS) + #if ((WOLFBOOT_PARTITION_UPDATE_ADDRESS + 0) + \ + (WOLFBOOT_PARTITION_UPDATE_SIZE + 0)) > (ARCH_FLASH_OFFSET + FLASH_TOP) + #error "Update partition extends past the 1MB this HAL can erase" + #endif + #if ((WOLFBOOT_PARTITION_BOOT_ADDRESS + 0) + (WOLFBOOT_PARTITION_SIZE + 0)) \ + > (ARCH_FLASH_OFFSET + FLASH_TOP) + #error "Boot partition extends past the 1MB this HAL can erase" + #endif +#endif + #define FLASH_SECTORS 12 const uint32_t flash_sector[FLASH_SECTORS + 1] = { FLASH_SECTOR_0, diff --git a/hal/stm32h7.h b/hal/stm32h7.h index 05819a820a..35d3d8883c 100644 --- a/hal/stm32h7.h +++ b/hal/stm32h7.h @@ -298,8 +298,18 @@ # error "Please use a bigger WOLFBOOT_PARTITION_SIZE, since the last 128KB on each partition will be reserved for bootloader flags" #endif +/* Not WOLFBOOT_PARTITION_SIZE: the UPDATE slot is sized independently in the + * compressed and monolithic self-update modes, and the flags page has to be + * the sector its own last byte falls in. PULL_LINKER_DEFINES has no separate + * update size, so the two are equal there. */ +#ifdef WOLFBOOT_PARTITION_UPDATE_SIZE +#define STM32H7_PART_UPDATE_SIZE WOLFBOOT_PARTITION_UPDATE_SIZE +#else +#define STM32H7_PART_UPDATE_SIZE WOLFBOOT_PARTITION_SIZE +#endif + #define STM32H7_PART_BOOT_END (WOLFBOOT_PARTITION_BOOT_ADDRESS + WOLFBOOT_PARTITION_SIZE) -#define STM32H7_PART_UPDATE_END (WOLFBOOT_PARTITION_UPDATE_ADDRESS + WOLFBOOT_PARTITION_SIZE) +#define STM32H7_PART_UPDATE_END (WOLFBOOT_PARTITION_UPDATE_ADDRESS + STM32H7_PART_UPDATE_SIZE) #define STM32H7_WORD_SIZE (32) #define STM32H7_PART_BOOT_FLAGS_PAGE_ADDRESS \ (((STM32H7_PART_BOOT_END - 1) / STM32H7_SECTOR_SIZE) * STM32H7_SECTOR_SIZE) @@ -310,4 +320,23 @@ #define STM32H7_UPDATE_FLAGS_PAGE(x) \ ((x >= STM32H7_PART_UPDATE_FLAGS_PAGE_ADDRESS) && (x < STM32H7_PART_UPDATE_END)) +/* The flags page is erased in full on every partition state write, and + * hal_flash_erase() cannot clear less than a sector. A BOOT partition that + * does not end on a sector boundary therefore has its erase reach past it, + * into the UPDATE partition when that is in internal flash, taking the + * staged image's header with it. */ +#if defined(WOLFBOOT_PARTITION_SIZE) && !defined(PART_UPDATE_EXT) && \ + defined(WOLFBOOT_PARTITION_UPDATE_ADDRESS) && \ + ((STM32H7_PART_BOOT_FLAGS_PAGE_ADDRESS + STM32H7_SECTOR_SIZE) > \ + (WOLFBOOT_PARTITION_UPDATE_ADDRESS + 0)) +# error "BOOT flags page erase reaches into the UPDATE partition: align both partition addresses and WOLFBOOT_PARTITION_SIZE to 128KB" +#endif +/* Same erase, on the UPDATE side: its flags page is the sector its last byte + * falls in, so an end that is not on a sector boundary is erased past. */ +#if defined(WOLFBOOT_PARTITION_UPDATE_SIZE) && !defined(PART_UPDATE_EXT) && \ + defined(WOLFBOOT_PARTITION_UPDATE_ADDRESS) && \ + ((STM32H7_PART_UPDATE_END % STM32H7_SECTOR_SIZE) != 0) +# error "UPDATE partition does not end on a 128KB boundary: its flags page erase would reach past it" +#endif + #endif /* HAL_STM32H7_H */ diff --git a/hal/uart/uart_drv_stm32f4.c b/hal/uart/uart_drv_stm32f4.c index c2ea800de1..0acecc78f2 100644 --- a/hal/uart/uart_drv_stm32f4.c +++ b/hal/uart/uart_drv_stm32f4.c @@ -48,6 +48,14 @@ #define CLOCK_SPEED (168000000) #endif +#ifndef BAUD_RATE +#define BAUD_RATE (115200) +#endif + +/* APB == SYSCLK here: clock_pll_on() writes the 4-bit AHB encoding into the + * 3-bit PPRE fields, so both decode as /1 (RCC_CFGR 0x0001200A on F439ZI). */ +#define UART_PCLK (CLOCK_SPEED) + /* Common GPIO Config */ #define GPIO_MODE_AF (2) @@ -166,8 +174,8 @@ int uart_init(uint32_t bitrate, uint8_t data, char parity, uint8_t stop) /* Configure for TX + RX */ UART_CR1 |= (UART_CR1_TX_ENABLE | UART_CR1_RX_ENABLE); - /* Configure clock */ - UART_BRR = CLOCK_SPEED / bitrate; + /* BRR is the 16x-oversampled divisor: a rounded PCLK/baud. */ + UART_BRR = (UART_PCLK + (bitrate / 2)) / bitrate; /* Configure data bits */ if (data == 8) @@ -202,6 +210,18 @@ int uart_init(uint32_t bitrate, uint8_t data, char parity, uint8_t stop) #ifdef DEBUG_UART void uart_write(const char *buf, unsigned int len) { + /* Nothing calls the initializer above on the console path, so the first + * print used to hang. Skipped under UART_FLASH: loader.c inits it. */ +#ifndef UART_FLASH + static int uart_ready = 0; + + if (!uart_ready) { + if (uart_init(BAUD_RATE, 8, 'N', 1) != 0) { + return; + } + uart_ready = 1; + } +#endif while (len--) { uart_tx(*buf); buf++; diff --git a/include/gzip.h b/include/gzip.h index af16835121..b3c380b7dc 100644 --- a/include/gzip.h +++ b/include/gzip.h @@ -72,4 +72,51 @@ int wolfBoot_gunzip(const uint8_t *in, uint32_t in_len, uint8_t *out, uint32_t out_max, uint32_t *out_len); +#ifdef WOLFBOOT_GZIP_SINK + +/* Window the stream was compressed with, as a power of two. RFC 1951 caps + * it at 32KB; below 4KB the ratio costs more than the RAM it saves. */ +#define GZIP_SINK_WINDOW_MIN_BITS 12 +#define GZIP_SINK_WINDOW_MAX_BITS 15 + +/* Scratch wolfBoot_gunzip_sink() needs for a given window: twice the window, + * so a back-reference is always resident in a flat buffer. */ +#define GZIP_SINK_BUF_SIZE(bits) (2U << (bits)) + +/* Called with each run of decompressed bytes, in order and exactly once. + * `off` is the offset within the decompressed stream. Returns 0 on success; + * anything else aborts the inflate. */ +typedef int (*wolfBoot_gzip_write_cb)(void *ctx, uint32_t off, + const uint8_t *buf, uint32_t len); + +/* Called to fetch a run of the compressed stream, which the decoder reads + * in order but a piece at a time. `off` is the offset within the stream. + * Returns 0 on success; anything else aborts the inflate. Lets the stream + * live somewhere that cannot be addressed directly, or be transformed on + * the way in. */ +typedef int (*wolfBoot_gzip_read_cb)(void *ctx, uint32_t off, + uint8_t *buf, uint32_t len); + +/* Decompress a gzip stream to a sink rather than to a buffer, so the output + * need not be addressable and only GZIP_SINK_BUF_SIZE(window_bits) of RAM is + * used regardless of how large the stream decompresses to. + * + * The stream is read in place from `in`, or pulled in through `read_cb` when + * it is not addressable; pass exactly one of the two, and with `read_cb`, + * `in_len` is the length of the whole compressed stream. + * + * The stream must have been compressed with a window no larger than + * 1 << window_bits; a back-reference beyond it is rejected with + * WOLFBOOT_GZIP_E_DISTANCE rather than silently accepted. + * + * Returns 0 on success, negative WOLFBOOT_GZIP_E_* on error. + */ +int wolfBoot_gunzip_sink(const uint8_t *in, uint32_t in_len, + wolfBoot_gzip_read_cb read_cb, void *read_ctx, + uint8_t *scratch, uint32_t window_bits, + wolfBoot_gzip_write_cb write_cb, void *ctx, + uint32_t out_max, uint32_t *out_len); + +#endif /* WOLFBOOT_GZIP_SINK */ + #endif /* WOLFBOOT_GZIP_H */ diff --git a/include/image.h b/include/image.h index 5fcdb65ea6..c3ae1f81e0 100644 --- a/include/image.h +++ b/include/image.h @@ -1735,6 +1735,10 @@ int wolfBoot_verify_authenticity(struct wolfBoot_image *img); int wolfBoot_set_partition_state(uint8_t part, uint8_t newst); int wolfBoot_get_update_sector_flag(uint16_t sector, uint8_t *flag); int wolfBoot_set_update_sector_flag(uint16_t sector, uint8_t newflag); +#ifdef WOLFBOOT_COMPRESSED_UPDATE +int wolfBoot_image_compression(struct wolfBoot_image *img, + uint32_t *uncompressed_sz, uint32_t *window_bits); +#endif #ifdef WOLFBOOT_ELF_FLASH_SCATTER /* Support for ELF scatter/gather format */ diff --git a/include/wolfboot/wolfboot.h b/include/wolfboot/wolfboot.h index d8313809e8..65824e6dcf 100644 --- a/include/wolfboot/wolfboot.h +++ b/include/wolfboot/wolfboot.h @@ -289,6 +289,15 @@ extern "C" { #define HDR_IMG_DELTA_BASE 0x05 #define HDR_IMG_DELTA_SIZE 0x06 #define HDR_IMG_DELTA_BASE_HASH 0x07 +/* uint16 LE algorithm id. Its presence is what marks the payload as + * compressed: the HDR_IMG_TYPE 0x00F0 nibble is equality-tested, so it + * cannot carry an orthogonal flag. */ +#define HDR_IMG_COMPRESSION 0x08 +/* uint32 LE uncompressed length, bounding the inflate. */ +#define HDR_IMG_UNCOMPRESSED_SIZE 0x09 +/* uint16 LE LZ77 window, as a power of two. A gzip header has no field for + * it, and the decoder sizes its buffer from this, so it has to be signed. */ +#define HDR_IMG_COMP_WINDOW 0x0A #define HDR_PUBKEY 0x10 #define HDR_SECONDARY_CIPHER 0x11 #define HDR_SECONDARY_PUBKEY 0x12 @@ -309,6 +318,9 @@ extern "C" { #define HDR_DEVICE_TREE_DIGEST 0x35 #define HDR_PADDING 0xFF +/* HDR_IMG_COMPRESSION algorithm ids */ +#define HDR_COMP_GZIP 0x0001 /* RFC 1952 gzip / 1951 DEFLATE */ + /* Auth Key types */ #define AUTH_KEY_NONE 0x00 #define AUTH_KEY_ED25519 0x01 @@ -619,10 +631,25 @@ extern "C" { #define WOLFBOOT_PARTITION_UPDATE_SIZE WOLFBOOT_PARTITION_SIZE #endif #if !defined(WOLFBOOT_SELF_UPDATE_MONOLITHIC) && \ + !defined(WOLFBOOT_COMPRESSED_UPDATE) && \ ((WOLFBOOT_PARTITION_UPDATE_SIZE + 0) != (WOLFBOOT_PARTITION_SIZE + 0)) - #error "WOLFBOOT_PARTITION_UPDATE_SIZE != WOLFBOOT_PARTITION_SIZE requires SELF_UPDATE_MONOLITHIC=1" -#endif -#if defined(EXT_ENCRYPTED) && \ + #error "WOLFBOOT_PARTITION_UPDATE_SIZE != WOLFBOOT_PARTITION_SIZE requires SELF_UPDATE_MONOLITHIC=1 or COMPRESSED_UPDATE=1" +#endif +/* Only the compressed image is stored there, so a larger update partition + * would just be wasted flash. */ +#if defined(WOLFBOOT_COMPRESSED_UPDATE) && \ + ((WOLFBOOT_PARTITION_UPDATE_SIZE + 0) > (WOLFBOOT_PARTITION_SIZE + 0)) + #error "COMPRESSED_UPDATE: WOLFBOOT_PARTITION_UPDATE_SIZE must not exceed WOLFBOOT_PARTITION_SIZE" +#endif +/* One _wolfboot_partition_size symbol serves both partitions, and the size + * is a const rather than a macro, so the guards above cannot see it. */ +#if defined(WOLFBOOT_COMPRESSED_UPDATE) && defined(PULL_LINKER_DEFINES) + #error "COMPRESSED_UPDATE is not supported with PULL_LINKER_DEFINES" +#endif +/* START_FLAGS_OFFSET_UPDATE keeps the UPDATE trailer in the clear at an + * offset sized from that slot, so an asymmetric layout is fine here. The + * swap-based modes exchange whole slots and still need them equal. */ +#if defined(EXT_ENCRYPTED) && !defined(WOLFBOOT_COMPRESSED_UPDATE) && \ ((WOLFBOOT_PARTITION_UPDATE_SIZE + 0) != (WOLFBOOT_PARTITION_SIZE + 0)) #error "Asymmetric WOLFBOOT_PARTITION_UPDATE_SIZE is not supported with ENCRYPT" #endif @@ -659,6 +686,52 @@ extern "C" { #define WOLFBOOT_TRAILER_SECTORS 1 #endif +/* Compressed update partition, inflated into the boot partition, which is + * then executed in place. Bootloader only, as above. */ +#if defined(WOLFBOOT_COMPRESSED_UPDATE) && defined(__WOLFBOOT) + #if !defined(WOLFBOOT_GZIP) || !defined(WOLFBOOT_GZIP_SINK) + #error "WOLFBOOT_COMPRESSED_UPDATE requires the gzip sink decoder (GZIP=1)" + #endif + /* The boot partition is overwritten in place, so there is nothing to swap + * back to and no scratch sector to swap through. */ + #ifndef DISABLE_BACKUP + #error "WOLFBOOT_COMPRESSED_UPDATE requires DISABLE_BACKUP" + #endif + /* Delta patches the boot partition it is consuming; this one overwrites + * it from a separate slot. */ + #ifdef DELTA_UPDATES + #error "DELTA_UPDATES is not compatible with WOLFBOOT_COMPRESSED_UPDATE" + #endif + #ifdef WOLFBOOT_ELF + #error "WOLFBOOT_ELF is not supported with WOLFBOOT_COMPRESSED_UPDATE" + #endif + #ifdef DUALBANK_SWAP + #error "DUALBANK_SWAP is not compatible with WOLFBOOT_COMPRESSED_UPDATE" + #endif + /* The installer walks the boot partition a sector at a time and the + * trailer lives in its last sector, so a partial sector at the end would + * be erased by the trailer write. */ + #if ((WOLFBOOT_PARTITION_SIZE + 0) % (WOLFBOOT_SECTOR_SIZE + 0)) != 0 + #error "WOLFBOOT_PARTITION_SIZE must be a multiple of WOLFBOOT_SECTOR_SIZE" + #endif + #if ((WOLFBOOT_PARTITION_UPDATE_SIZE + 0) % (WOLFBOOT_SECTOR_SIZE + 0)) != 0 + #error "WOLFBOOT_PARTITION_UPDATE_SIZE must be a multiple of WOLFBOOT_SECTOR_SIZE" + #endif + /* Install progress is one nibble per destination sector, kept in the + * update partition's trailer below MAGIC (4) and the state byte (1); an + * encrypted trailer also holds the key and nonce, under 64 bytes for + * every cipher this supports. */ + #ifdef EXT_ENCRYPTED + #define WOLFBOOT_TRAILER_HEAD (5 + 64) + #else + #define WOLFBOOT_TRAILER_HEAD 5 + #endif + #if (((WOLFBOOT_PARTITION_SIZE + 0) / (WOLFBOOT_SECTOR_SIZE + 0) + 1) / 2) > \ + ((WOLFBOOT_SECTOR_SIZE + 0) - WOLFBOOT_TRAILER_HEAD) + #error "Too many boot sectors to track in the update partition trailer" + #endif +#endif /* WOLFBOOT_COMPRESSED_UPDATE */ + #define PART_BOOT 0 #define PART_UPDATE 1 #define PART_SWAP 2 diff --git a/options.mk b/options.mk index a2cb42e004..8ddc8e5b71 100644 --- a/options.mk +++ b/options.mk @@ -1293,8 +1293,23 @@ ifeq ($(CUSTOM_PARTITION_TRAILER),1) CFLAGS+= -D"CUSTOM_PARTITION_TRAILER" endif +# COMPRESSED_UPDATE=1 inflates a compressed UPDATE partition into BOOT, which +# is then executed in place. There is no swap and no backup of the outgoing +# image, so it builds on DISABLE_BACKUP, and the inflater is mandatory. Both +# are set here because their own blocks below have already run by the time +# the COMPRESSED_UPDATE block is reached. +COMPRESSED_UPDATE?=0 +ifeq ($(COMPRESSED_UPDATE),1) + DISABLE_BACKUP=1 + GZIP=1 +endif + ifeq ($(DISABLE_BACKUP),1) - $(warning DISABLE_BACKUP=1 disables power-fail-safe updates; losing power during an update can leave BOOT partially written and unrecoverable) + # Not said for COMPRESSED_UPDATE: that installer only reads the UPDATE + # partition, so an interrupted install restarts from an intact source. + ifneq ($(COMPRESSED_UPDATE),1) + $(warning DISABLE_BACKUP=1 disables power-fail-safe updates; losing power during an update can leave BOOT partially written and unrecoverable) + endif CFLAGS+= -D"DISABLE_BACKUP" WOLFBOOT_PARTITION_SWAP_ADDRESS?=0 endif @@ -1373,6 +1388,38 @@ ifeq ($(GZIP),1) endif endif +# COMPRESSED_UPDATE=1: the UPDATE partition holds a gzip-compressed, signed +# image and is inflated into BOOT, which keeps executing in place. The payload +# is the already-signed inner image, so BOOT stays an ordinary partition that +# the normal boot path verifies. Handled by update_flash.o, the default +# strategy object, so no arch.mk change is needed. +ifeq ($(COMPRESSED_UPDATE),1) + CFLAGS+=-DWOLFBOOT_COMPRESSED_UPDATE -DWOLFBOOT_GZIP_SINK + # ENCRYPT=1 composes, because it hooks the external flash read, which is + # the same seam the installer and the image hasher pull the container + # through. + ifeq ($(ENCRYPT),1) + ifneq ($(EXT_FLASH),1) + $(error ENCRYPT=1 with COMPRESSED_UPDATE needs EXT_FLASH=1: the encryption layer hooks the external flash interface) + endif + endif + # LZ77 window the image was compressed with. The decoder needs twice this + # in RAM and refuses an image compressed with a larger one. + WOLFBOOT_COMPRESSED_WINDOW?=16384 + ifeq ($(WOLFBOOT_COMPRESSED_WINDOW),4096) + COMPRESSED_WINDOW_BITS=12 + else ifeq ($(WOLFBOOT_COMPRESSED_WINDOW),8192) + COMPRESSED_WINDOW_BITS=13 + else ifeq ($(WOLFBOOT_COMPRESSED_WINDOW),16384) + COMPRESSED_WINDOW_BITS=14 + else ifeq ($(WOLFBOOT_COMPRESSED_WINDOW),32768) + COMPRESSED_WINDOW_BITS=15 + else + $(error WOLFBOOT_COMPRESSED_WINDOW must be 4096, 8192, 16384 or 32768) + endif + CFLAGS+=-DWOLFBOOT_COMPRESSED_WINDOW_BITS=$(COMPRESSED_WINDOW_BITS) +endif + # FIT_RAMDISK=1 enables FIT ramdisk (initramfs) extraction and DTB # /chosen/linux,initrd-{start,end} fixup. Compressed (gzip) ramdisks # decompress through the same path when GZIP=1. diff --git a/src/gzip.c b/src/gzip.c index 48ac2b89b0..b5aab51149 100644 --- a/src/gzip.c +++ b/src/gzip.c @@ -144,6 +144,40 @@ typedef uint32_t gz_bits_t; #endif #define GZIP_BITS_FULL (GZIP_BITS_WIDTH - 8) +#ifdef WOLFBOOT_GZIP_SINK +/* Output drain for the sink decoder; see gz_flush(). */ +typedef struct gz_sink { + int (*write)(void *ctx, uint32_t off, const uint8_t *buf, uint32_t len); + void *ctx; + uint32_t window; /* bytes of history the stream may reference */ + uint32_t out_max; /* cap on total bytes produced */ + uint32_t flushed; /* bytes already handed to write() */ + uint32_t crc; /* running, not yet final-XOR'd */ +} gz_sink_t; + +/* The block decoder stopped to let the scratch buffer drain. Not an error: + * those are all negative. */ +#define GZ_NEED_FLUSH 1 + +/* Ample for every contiguous read the parser makes, the widest being + * GZIP_HEADER_MIN_SIZE. */ +#define GZIP_SRC_BUF 512 +/* Consumed bytes kept in front of the cursor on every refill, so + * gz_align_byte() can push back over the bit buffer. */ +#define GZIP_SRC_KEEP 8 + +/* Pulls the compressed stream in through a callback. NULL in gz_state_t + * means the stream is addressable and is read in place. */ +typedef struct gz_src { + int (*read)(void *ctx, uint32_t off, uint8_t *buf, uint32_t len); + void *ctx; + uint32_t total; /* compressed bytes in the whole stream */ + uint32_t pos; /* stream offset of buf[0] */ + uint32_t have; /* valid bytes in buf */ + uint8_t buf[GZIP_SRC_BUF]; +} gz_src_t; +#endif /* WOLFBOOT_GZIP_SINK */ + typedef struct gz_state { const uint8_t *in; /* next input byte */ const uint8_t *in_end; @@ -154,11 +188,22 @@ typedef struct gz_state { uint8_t *out; /* output buffer (doubles as sliding window) */ uint8_t *out_end; uint8_t *op; /* next output byte */ +#ifdef WOLFBOOT_GZIP_SINK + gz_sink_t *sk; /* NULL for the flat-buffer entry point */ + gz_src_t *src; /* NULL when the stream is addressable */ +#endif #ifdef WOLFBOOT_GZIP_SMALL uint32_t crc32; /* running CRC32 of decompressed bytes */ #endif } gz_state_t; +#ifdef WOLFBOOT_GZIP_SINK +static int gz_src_more(gz_state_t *s, const uint8_t **in, + const uint8_t **in_end); +#else +#define gz_src_more(s, in, in_end) (-1) +#endif + #if defined(__GNUC__) typedef uint64_t gz_word_t __attribute__((__may_alias__)); #define GZIP_WORD_ACCESS 1 @@ -172,15 +217,19 @@ typedef uint64_t gz_word_t __attribute__((__may_alias__)); * registers; the struct forms serve the slow paths. */ /* Refill to more than GZIP_BITS_FULL bits; past the end of input, zero - * bytes are appended and counted in `over`. */ -#define GZ_REFILL_V(buf, nbits, over, in, in_end) \ + * bytes are appended and counted in `over`. Exhausting the buffered input + * is instead a refill when the stream is pulled in through a callback; + * gz_src_more() is (-1) without the sink decoder, which short-circuits the + * test away. */ +#define GZ_REFILL_V(s, buf, nbits, over, in, in_end) \ do { \ while ((nbits) <= GZIP_BITS_FULL) { \ - if ((in) < (in_end)) { \ - (buf) |= (gz_bits_t)(*(in)++) << (nbits); \ + if (((in) >= (in_end)) && \ + (gz_src_more((s), &(in), &(in_end)) != 0)) { \ + (over) += 8; \ } \ else { \ - (over) += 8; \ + (buf) |= (gz_bits_t)(*(in)++) << (nbits); \ } \ (nbits) += 8; \ } \ @@ -193,7 +242,7 @@ typedef uint64_t gz_word_t __attribute__((__may_alias__)); #define GZ_DROP_V(buf, nbits, n) \ do { (buf) >>= (n); (nbits) -= (n); } while (0) -#define GZ_REFILL(s) GZ_REFILL_V((s)->buf, (s)->nbits, (s)->over, \ +#define GZ_REFILL(s) GZ_REFILL_V((s), (s)->buf, (s)->nbits, (s)->over, \ (s)->in, (s)->in_end) #define GZ_OVERRUN(s) GZ_OVERRUN_V((s)->nbits, (s)->over) #define GZ_PEEK(s, n) GZ_PEEK_V((s)->buf, (n)) @@ -209,6 +258,85 @@ static uint32_t gz_get_bits(gz_state_t *s, uint32_t n) return v; } +#ifdef WOLFBOOT_GZIP_SINK +/* Pull more of the stream into the input buffer and reposition the cursor, + * keeping GZIP_SRC_KEEP consumed bytes in front of it. Returns 0 when at + * least one byte was added. */ +static int gz_src_more(gz_state_t *s, const uint8_t **in, + const uint8_t **in_end) +{ + gz_src_t *src; + uint32_t consumed, keep, tail, from, n, want, i; + + if (s->src == NULL) { + return -1; + } + src = s->src; + consumed = (uint32_t)(*in - src->buf); + if ((src->pos + consumed) >= src->total) { + return -1; + } + keep = (consumed < (uint32_t)GZIP_SRC_KEEP) ? consumed + : (uint32_t)GZIP_SRC_KEEP; + tail = src->have - consumed; + from = consumed - keep; + n = keep + tail; + for (i = 0; i < n; i++) { + src->buf[i] = src->buf[from + i]; + } + src->pos += from; + src->have = n; + /* Republished now, so the cursor stays valid against the compacted + * buffer even if the read below fails. */ + *in = src->buf + keep; + *in_end = src->buf + src->have; + + want = (uint32_t)GZIP_SRC_BUF - src->have; + if (want > (src->total - (src->pos + src->have))) { + want = src->total - (src->pos + src->have); + } + if (want == 0) { + return -1; + } + if (src->read(src->ctx, src->pos + src->have, src->buf + src->have, + want) != 0) { + return -1; + } + src->have += want; + *in_end = src->buf + src->have; + return 0; +} + +/* Guarantee n contiguous readable bytes at the cursor; n must not exceed + * GZIP_SRC_BUF - GZIP_SRC_KEEP. */ +static int gz_src_need(gz_state_t *s, uint32_t n) +{ + if (s->src == NULL) { + return ((uint32_t)(s->in_end - s->in) >= n) ? 0 : -1; + } + while ((uint32_t)(s->in_end - s->in) < n) { + if (gz_src_more(s, &s->in, &s->in_end) != 0) { + return -1; + } + } + return 0; +} + +/* Bytes of the stream still ahead of the cursor. */ +static uint32_t gz_src_left(gz_state_t *s) +{ + if (s->src == NULL) { + return (uint32_t)(s->in_end - s->in); + } + return s->src->total - + (s->src->pos + (uint32_t)(s->in - s->src->buf)); +} +#else +#define gz_src_need(s, n) \ + (((uint32_t)((s)->in_end - (s)->in) >= (uint32_t)(n)) ? 0 : -1) +#define gz_src_left(s) ((uint32_t)((s)->in_end - (s)->in)) +#endif /* WOLFBOOT_GZIP_SINK */ + /* Drop the partial byte and hand whole buffered bytes back to the cursor. */ static int gz_align_byte(gz_state_t *s) { @@ -268,10 +396,10 @@ static void gz_crc_init(void) #define GZ_CRC_BYTE(crc, b) \ ((gz_crc_table[((crc) ^ (b)) & 0xFFU]) ^ ((crc) >> 8)) -static uint32_t gz_crc32(const uint8_t *p, uint32_t len) +/* Running CRC32, neither seeded nor final-XOR'd, so a stream can be folded + * in pieces as it is flushed. */ +static uint32_t gz_crc32_ex(uint32_t crc, const uint8_t *p, uint32_t len) { - uint32_t crc = GZIP_CRC32_INIT; - #if defined(GZIP_WORD_ACCESS) && defined(__BYTE_ORDER__) && \ (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) /* Aligned words, then the bytes of each word in stream order. */ @@ -295,7 +423,12 @@ static uint32_t gz_crc32(const uint8_t *p, uint32_t len) crc = GZ_CRC_BYTE(crc, *p++); len--; } - return crc ^ GZIP_CRC32_FINAL_XOR; + return crc; +} + +static uint32_t gz_crc32(const uint8_t *p, uint32_t len) +{ + return gz_crc32_ex(GZIP_CRC32_INIT, p, len) ^ GZIP_CRC32_FINAL_XOR; } #endif /* WOLFBOOT_GZIP_SMALL */ @@ -647,6 +780,51 @@ static void gz_copy(uint8_t *op, const uint8_t *src, uint32_t len, #endif /* WOLFBOOT_GZIP_SMALL */ +#ifdef WOLFBOOT_GZIP_SINK + +/* Longest single back-reference, so the decoder can stop this far from the + * end of the scratch buffer and still emit one whole copy. */ +#define GZIP_SINK_SLACK 258 + +/* Hand everything older than one window to the sink and slide the rest to + * the front, so that op - distance never leaves the scratch buffer. `final` + * drains the window too, and is only valid once the stream has ended. */ +static int gz_flush(gz_state_t *s, int final) +{ + gz_sink_t *k = s->sk; + uint32_t have = (uint32_t)(s->op - s->out); + uint32_t keep = final ? 0 : k->window; + uint32_t n, i; + int ret; + + if (have <= keep) { + return 0; + } + n = have - keep; + /* Per flush rather than per byte: a block can only overshoot by the + * bounded scratch buffer. */ + if (n > (k->out_max - k->flushed)) { + return WOLFBOOT_GZIP_E_OUTPUT; + } +#ifndef WOLFBOOT_GZIP_SMALL + /* The small decoder already folds the CRC in as it writes each byte. */ + k->crc = gz_crc32_ex(k->crc, s->out, n); +#endif + ret = k->write(k->ctx, k->flushed, s->out, n); + if (ret != 0) { + return WOLFBOOT_GZIP_E_OUTPUT; + } + k->flushed += n; + /* Moves down, so a forward copy is safe. */ + for (i = 0; i < keep; i++) { + s->out[i] = s->out[n + i]; + } + s->op = s->out + keep; + return 0; +} + +#endif /* WOLFBOOT_GZIP_SINK */ + /* ------------------------------------------------------------------------- */ /* Block decoders */ /* ------------------------------------------------------------------------- */ @@ -655,6 +833,7 @@ static int gz_inflate_stored(gz_state_t *s) { uint32_t len, nlen; uint32_t crc = 0; + uint32_t n, room; int ret; ret = gz_align_byte(s); @@ -662,7 +841,7 @@ static int gz_inflate_stored(gz_state_t *s) return ret; } /* LEN and NLEN are little-endian 16-bit words */ - if (s->in_end - s->in < 4) { + if (gz_src_need(s, 4) != 0) { return WOLFBOOT_GZIP_E_TRUNCATED; } len = (uint32_t)s->in[0] | ((uint32_t)s->in[1] << 8); @@ -671,18 +850,51 @@ static int gz_inflate_stored(gz_state_t *s) if ((len ^ 0xFFFFU) != nlen) { return WOLFBOOT_GZIP_E_FORMAT; } - if ((uint32_t)(s->in_end - s->in) < len) { + if (gz_src_left(s) < len) { return WOLFBOOT_GZIP_E_TRUNCATED; } +#ifndef WOLFBOOT_GZIP_SINK if ((uint32_t)(s->out_end - s->op) < len) { return WOLFBOOT_GZIP_E_OUTPUT; } +#endif #ifdef WOLFBOOT_GZIP_SMALL crc = s->crc32; #endif + /* A stored block may be up to 65535 bytes, more than the sink's scratch + * buffer holds, so it is drained as it is copied. gz_flush() does not + * touch the CRC the sink accumulates, so `crc` stays live across it. */ while (len > 0) { - GZ_PUT_V(s->op, crc, *s->in++); - len--; + room = (uint32_t)(s->out_end - s->op); +#ifdef WOLFBOOT_GZIP_SINK + if ((room == 0) && (s->sk != NULL)) { + ret = gz_flush(s, 0); + if (ret != 0) { + return ret; + } + room = (uint32_t)(s->out_end - s->op); + } +#endif + if (room == 0) { + return WOLFBOOT_GZIP_E_OUTPUT; + } + n = (len < room) ? len : room; +#ifdef WOLFBOOT_GZIP_SINK + /* Bounded by what is buffered too, since the source arrives a piece + * at a time. */ + if ((s->in >= s->in_end) && + (gz_src_more(s, &s->in, &s->in_end) != 0)) { + return WOLFBOOT_GZIP_E_TRUNCATED; + } + if (n > (uint32_t)(s->in_end - s->in)) { + n = (uint32_t)(s->in_end - s->in); + } +#endif + len -= n; + while (n > 0) { + GZ_PUT_V(s->op, crc, *s->in++); + n--; + } } #ifdef WOLFBOOT_GZIP_SMALL s->crc32 = crc; @@ -707,12 +919,26 @@ static int gz_inflate_huffman(gz_state_t *s, const gz_huff_t *litlen, uint32_t length, distance, extra; int sym; int ret = 0; +#ifdef WOLFBOOT_GZIP_SINK + /* out_end on the flat path, where the test never fires. */ + uint8_t *flush_mark = (s->sk != NULL) ? (out_end - GZIP_SINK_SLACK) + : out_end; + uint32_t win = (s->sk != NULL) ? s->sk->window : 0; +#endif #ifdef WOLFBOOT_GZIP_SMALL crc = s->crc32; #endif for (;;) { - GZ_REFILL_V(buf, nbits, over, in, in_end); +#ifdef WOLFBOOT_GZIP_SINK + /* Leave between symbols so the caller can drain with the reader on + * a symbol boundary and the trees still valid. */ + if (op > flush_mark) { + ret = GZ_NEED_FLUSH; + break; + } +#endif + GZ_REFILL_V(s, buf, nbits, over, in, in_end); if (GZ_OVERRUN_V(nbits, over)) { ret = WOLFBOOT_GZIP_E_TRUNCATED; break; @@ -743,7 +969,7 @@ static int gz_inflate_huffman(gz_state_t *s, const gz_huff_t *litlen, length = gz_len_base[sym] + GZ_PEEK_V(buf, extra); GZ_DROP_V(buf, nbits, extra); - GZ_REFILL_V(buf, nbits, over, in, in_end); + GZ_REFILL_V(s, buf, nbits, over, in, in_end); GZ_DECODE_V(dist, sym, buf, nbits); if (sym < 0) { ret = sym; @@ -754,7 +980,7 @@ static int gz_inflate_huffman(gz_state_t *s, const gz_huff_t *litlen, break; } extra = gz_dist_extra[sym]; - GZ_REFILL_V(buf, nbits, over, in, in_end); + GZ_REFILL_V(s, buf, nbits, over, in, in_end); distance = gz_dist_base[sym] + GZ_PEEK_V(buf, extra); GZ_DROP_V(buf, nbits, extra); if (GZ_OVERRUN_V(nbits, over)) { @@ -766,6 +992,14 @@ static int gz_inflate_huffman(gz_state_t *s, const gz_huff_t *litlen, ret = WOLFBOOT_GZIP_E_DISTANCE; break; } +#ifdef WOLFBOOT_GZIP_SINK + /* The scratch buffer holds more than one window, so enforce the + * declared window rather than what happens to be resident. */ + if ((win != 0) && (distance > win)) { + ret = WOLFBOOT_GZIP_E_DISTANCE; + break; + } +#endif if (length > (uint32_t)(out_end - op)) { ret = WOLFBOOT_GZIP_E_OUTPUT; break; @@ -773,6 +1007,8 @@ static int gz_inflate_huffman(gz_state_t *s, const gz_huff_t *litlen, GZ_COPY_V(op, crc, op - distance, length, distance); } s->in = in; + /* A refill slides the input buffer, so the end moves with the cursor. */ + s->in_end = in_end; s->buf = buf; s->nbits = nbits; s->over = over; @@ -783,6 +1019,30 @@ static int gz_inflate_huffman(gz_state_t *s, const gz_huff_t *litlen, return ret; } +#ifdef WOLFBOOT_GZIP_SINK +/* Drain and re-enter until the block ends; the decoder stops between + * symbols, so the same trees continue the same block. */ +static int gz_run_huffman(gz_state_t *s, const gz_huff_t *litlen, + const gz_huff_t *dist) +{ + int ret; + + for (;;) { + ret = gz_inflate_huffman(s, litlen, dist); + if (ret != GZ_NEED_FLUSH) { + return ret; + } + ret = gz_flush(s, 0); + if (ret != 0) { + return ret; + } + } +} +#else +#define gz_run_huffman(s, litlen, dist) \ + gz_inflate_huffman((s), (litlen), (dist)) +#endif + /* Build the fixed Huffman trees defined in RFC 1951 Sec. 3.2.6 */ static int gz_build_fixed(gz_huff_t *litlen, gz_huff_t *dist) { @@ -907,7 +1167,7 @@ static int gz_inflate_dynamic(gz_state_t *s, gz_huff_t *litlen, ret = gz_huff_build(dist, code_lens + hlit, (int)hdist); } if (ret == 0) { - ret = gz_inflate_huffman(s, litlen, dist); + ret = gz_run_huffman(s, litlen, dist); } return ret; } @@ -949,7 +1209,7 @@ static int gz_inflate(gz_state_t *s) fixed_built = 1; } if (ret == 0) { - ret = gz_inflate_huffman(s, &litlen, &dist); + ret = gz_run_huffman(s, &litlen, &dist); } } else if (btype == 2) { @@ -969,7 +1229,7 @@ static int gz_inflate(gz_state_t *s) static int gz_skip_zstring(gz_state_t *s) { - while (s->in < s->in_end) { + while (gz_src_need(s, 1) == 0) { if (*s->in++ == 0) { return 0; } @@ -982,7 +1242,7 @@ static int gz_parse_header(gz_state_t *s) uint8_t flg; uint32_t xlen; - if (s->in_end - s->in < GZIP_HEADER_MIN_SIZE) { + if (gz_src_need(s, GZIP_HEADER_MIN_SIZE) != 0) { return WOLFBOOT_GZIP_E_TRUNCATED; } /* Magic 1F 8B, CM = 8 (DEFLATE) */ @@ -998,15 +1258,26 @@ static int gz_parse_header(gz_state_t *s) s->in += GZIP_HEADER_MIN_SIZE; if (flg & GZIP_FLG_FEXTRA) { - if (s->in_end - s->in < 2) { + if (gz_src_need(s, 2) != 0) { return WOLFBOOT_GZIP_E_TRUNCATED; } xlen = (uint32_t)s->in[0] | ((uint32_t)s->in[1] << 8); s->in += 2; - if ((uint32_t)(s->in_end - s->in) < xlen) { - return WOLFBOOT_GZIP_E_TRUNCATED; + /* xlen is a 16-bit field and can exceed the input buffer, so step + * over it rather than moving the cursor past the end. */ + while (xlen > 0) { + uint32_t skip; + + if (gz_src_need(s, 1) != 0) { + return WOLFBOOT_GZIP_E_TRUNCATED; + } + skip = (uint32_t)(s->in_end - s->in); + if (skip > xlen) { + skip = xlen; + } + s->in += skip; + xlen -= skip; } - s->in += xlen; } if ((flg & GZIP_FLG_FNAME) && (gz_skip_zstring(s) != 0)) { return WOLFBOOT_GZIP_E_TRUNCATED; @@ -1015,7 +1286,7 @@ static int gz_parse_header(gz_state_t *s) return WOLFBOOT_GZIP_E_TRUNCATED; } if (flg & GZIP_FLG_FHCRC) { - if (s->in_end - s->in < 2) { + if (gz_src_need(s, 2) != 0) { return WOLFBOOT_GZIP_E_TRUNCATED; } s->in += 2; /* header CRC; not validated */ @@ -1034,7 +1305,7 @@ static int gz_parse_trailer(gz_state_t *s, uint32_t computed_crc, if (ret != 0) { return ret; } - if (s->in_end - s->in < GZIP_TRAILER_SIZE) { + if (gz_src_need(s, GZIP_TRAILER_SIZE) != 0) { return WOLFBOOT_GZIP_E_TRUNCATED; } got_crc = (uint32_t)s->in[0] | ((uint32_t)s->in[1] << 8) | @@ -1074,6 +1345,10 @@ int wolfBoot_gunzip(const uint8_t *in, uint32_t in_len, s.out = out; s.out_end = out + out_max; s.op = out; +#ifdef WOLFBOOT_GZIP_SINK + s.sk = NULL; + s.src = NULL; +#endif #ifdef WOLFBOOT_GZIP_SMALL s.crc32 = GZIP_CRC32_INIT; #endif @@ -1097,4 +1372,86 @@ int wolfBoot_gunzip(const uint8_t *in, uint32_t in_len, return ret; } +#ifdef WOLFBOOT_GZIP_SINK + +int wolfBoot_gunzip_sink(const uint8_t *in, uint32_t in_len, + wolfBoot_gzip_read_cb read_cb, void *read_ctx, + uint8_t *scratch, uint32_t window_bits, + wolfBoot_gzip_write_cb write_cb, void *ctx, + uint32_t out_max, uint32_t *out_len) +{ + gz_state_t s; + gz_sink_t k; + gz_src_t src; + uint32_t crc; + int ret; + + /* Exactly one of the two input forms. */ + if (((in == NULL) == (read_cb == NULL)) || (scratch == NULL) || + (write_cb == NULL) || (out_len == NULL)) { + return WOLFBOOT_GZIP_E_PARAM; + } + if ((window_bits < GZIP_SINK_WINDOW_MIN_BITS) || + (window_bits > GZIP_SINK_WINDOW_MAX_BITS)) { + return WOLFBOOT_GZIP_E_PARAM; + } + *out_len = 0; + + k.write = write_cb; + k.ctx = ctx; + k.window = 1U << window_bits; + k.out_max = out_max; + k.flushed = 0; + k.crc = GZIP_CRC32_INIT; + + if (read_cb != NULL) { + src.read = read_cb; + src.ctx = read_ctx; + src.total = in_len; + src.pos = 0; + src.have = 0; + /* Empty, so the first read pulls a buffer in. */ + s.in = src.buf; + s.in_end = src.buf; + s.src = &src; + } + else { + s.in = in; + s.in_end = in + in_len; + s.src = NULL; + } + s.buf = 0; + s.nbits = 0; + s.over = 0; + s.out = scratch; + s.out_end = scratch + (k.window * 2U); + s.op = scratch; + s.sk = &k; +#ifdef WOLFBOOT_GZIP_SMALL + s.crc32 = GZIP_CRC32_INIT; +#else + gz_crc_init(); +#endif + + ret = gz_parse_header(&s); + if (ret == 0) { + ret = gz_inflate(&s); + } + if (ret == 0) { + ret = gz_flush(&s, 1); + } + if (ret == 0) { +#ifdef WOLFBOOT_GZIP_SMALL + crc = s.crc32 ^ GZIP_CRC32_FINAL_XOR; +#else + crc = k.crc ^ GZIP_CRC32_FINAL_XOR; +#endif + ret = gz_parse_trailer(&s, crc, k.flushed); + } + *out_len = k.flushed; + return ret; +} + +#endif /* WOLFBOOT_GZIP_SINK */ + #endif /* WOLFBOOT_GZIP */ diff --git a/src/image.c b/src/image.c index 12b0559883..0222a18e44 100644 --- a/src/image.c +++ b/src/image.c @@ -1628,6 +1628,57 @@ int wolfBoot_open_image_address(struct wolfBoot_image *img, uint8_t *image) return 0; } +#ifdef WOLFBOOT_COMPRESSED_UPDATE +/* Read the compression manifest fields. + * + * Returns 1 for a gzip-compressed image, 0 for an uncompressed one and -1 + * for a compression tag this build cannot act on. The last must never fall + * through to treating the payload as firmware: that would be a jump into, or + * an install of, DEFLATE data. + * + * window_bits may be NULL; it reads back 0 when the image carries no window + * tag, which only the execute-in-place installer needs. + */ +int wolfBoot_image_compression(struct wolfBoot_image *img, + uint32_t *uncompressed_sz, uint32_t *window_bits) +{ + uint8_t *tlv = NULL; + uint16_t len; + uint16_t algo; + + if (window_bits != NULL) { + *window_bits = 0; + } + len = wolfBoot_get_header(img, HDR_IMG_COMPRESSION, &tlv); + if ((len == 0) || (tlv == NULL)) { + return 0; + } + if (len != 2) { + return -1; + } + algo = (uint16_t)tlv[0] | ((uint16_t)tlv[1] << 8); + if (algo != HDR_COMP_GZIP) { + return -1; + } + len = wolfBoot_get_header(img, HDR_IMG_UNCOMPRESSED_SIZE, &tlv); + if ((len != 4) || (tlv == NULL)) { + return -1; + } + *uncompressed_sz = (uint32_t)tlv[0] | ((uint32_t)tlv[1] << 8) | + ((uint32_t)tlv[2] << 16) | ((uint32_t)tlv[3] << 24); + len = wolfBoot_get_header(img, HDR_IMG_COMP_WINDOW, &tlv); + if (len != 0) { + if ((len != 2) || (tlv == NULL)) { + return -1; + } + if (window_bits != NULL) { + *window_bits = (uint32_t)tlv[0] | ((uint32_t)tlv[1] << 8); + } + } + return 1; +} +#endif /* WOLFBOOT_COMPRESSED_UPDATE */ + #if defined(MMU) || defined(WOLFBOOT_FDT) /** diff --git a/src/libwolfboot.c b/src/libwolfboot.c index bcb5d37c10..40ba5e0e78 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -137,6 +137,14 @@ int wolfBoot_initialize_encryption(void) (2 * WOLFBOOT_SECTOR_SIZE))) /* MAGIC (4B) + PART_FLAG (1B) + (N_SECTORS / 2) */ #define START_FLAGS_OFFSET (ENCRYPT_TMP_SECRET_OFFSET - TRAILER_OVERHEAD) + /* The same cut-off for the UPDATE slot, which is where it is actually + * applied. Identical for a symmetric layout; with the slots sized + * independently the BOOT-derived one lands outside the UPDATE partition + * and its trailer would be encrypted along with the image. */ + #define TRAILER_OVERHEAD_UPDATE (4 + 1 + (WOLFBOOT_PARTITION_UPDATE_SIZE / \ + (2 * WOLFBOOT_SECTOR_SIZE))) + #define START_FLAGS_OFFSET_UPDATE \ + (ENCRYPT_TMP_SECRET_OFFSET_UPDATE - TRAILER_OVERHEAD_UPDATE) #define SECTOR_FLAGS_SIZE (WOLFBOOT_SECTOR_SIZE - (4 + 1 + \ ENCRYPT_KEY_SIZE + ENCRYPT_NONCE_SIZE)) /* MAGIC (4B) + PART_FLAG (1B) + ENCRYPT_KEY_SIZE + ENCRYPT_NONCE_SIZE */ @@ -3461,8 +3469,8 @@ int RAMFUNCTION ext_flash_encrypt_write(uintptr_t address, const uint8_t *data, iv_counter = (address - WOLFBOOT_PARTITION_UPDATE_ADDRESS) / ENCRYPT_BLOCK_SIZE; /* Do not encrypt last sector */ - if (iv_counter >= (START_FLAGS_OFFSET - ENCRYPT_BLOCK_SIZE) / - ENCRYPT_BLOCK_SIZE) { + if (iv_counter >= (START_FLAGS_OFFSET_UPDATE - + ENCRYPT_BLOCK_SIZE) / ENCRYPT_BLOCK_SIZE) { return ext_flash_write(address, data, len); } if (wolfBoot_initialize_encryption() < 0) @@ -3608,8 +3616,8 @@ int RAMFUNCTION ext_flash_decrypt_read(uintptr_t address, uint8_t *data, int len iv_counter = (address - WOLFBOOT_PARTITION_UPDATE_ADDRESS) / ENCRYPT_BLOCK_SIZE; /* Do not decrypt last sector */ - if (iv_counter >= (START_FLAGS_OFFSET - ENCRYPT_BLOCK_SIZE) / - ENCRYPT_BLOCK_SIZE) { + if (iv_counter >= (START_FLAGS_OFFSET_UPDATE - + ENCRYPT_BLOCK_SIZE) / ENCRYPT_BLOCK_SIZE) { return ext_flash_read(address, data, len); } if (!encrypt_initialized) { diff --git a/src/update_flash.c b/src/update_flash.c index 8118770814..f6171d2650 100644 --- a/src/update_flash.c +++ b/src/update_flash.c @@ -35,6 +35,9 @@ #endif #include "delta.h" +#ifdef WOLFBOOT_COMPRESSED_UPDATE +#include "gzip.h" +#endif #include "printf.h" /* Bytes at the end of each partition that hold the trailer */ @@ -48,8 +51,12 @@ #define wb_trailer_clear(img, part) \ wolfBoot_set_partition_state(part, IMG_STATE_NEW) #else +/* The trailer sits at the end of the partition being cleared, which is not + * the end of BOOT when the two are sized independently. */ #define wb_trailer_clear(img, part) \ - wb_flash_erase(img, WOLFBOOT_PARTITION_SIZE - TRAILER_LEN, TRAILER_LEN) + wb_flash_erase(img, (((part) == PART_UPDATE) ? \ + WOLFBOOT_PARTITION_UPDATE_SIZE : WOLFBOOT_PARTITION_SIZE) - \ + TRAILER_LEN, TRAILER_LEN) #endif /* The trailer sector(s) are erased independently from the image: they must @@ -1087,6 +1094,192 @@ static int wolfBoot_delta_update(struct wolfBoot_image *boot, /* Max firmware size: partition must hold header + fw + trailer sector(s) */ #define MAX_UPDATE_SIZE (size_t)((WOLFBOOT_PARTITION_SIZE - \ IMAGE_HEADER_SIZE - TRAILER_LEN)) + +#ifdef WOLFBOOT_COMPRESSED_UPDATE + +/* Programmed in whole chunks at an aligned offset, so no flash word is ever + * written twice between erases. A multiple of every programming unit in the + * tree, the widest being the STM32H7's 32 bytes. */ +#define COMPRESSED_WRITE_CHUNK 256 + +#if (WOLFBOOT_SECTOR_SIZE % COMPRESSED_WRITE_CHUNK) != 0 + #error "WOLFBOOT_SECTOR_SIZE must be a multiple of COMPRESSED_WRITE_CHUNK" +#endif +#if (COMPRESSED_WRITE_CHUNK % WOLFBOOT_FLASH_WRITE_UNIT) != 0 + #error "COMPRESSED_WRITE_CHUNK must be a multiple of WOLFBOOT_FLASH_WRITE_UNIT" +#endif + +/* Two windows, so a back-reference is always resident; see src/gzip.c. */ +static uint8_t compressed_window[ + GZIP_SINK_BUF_SIZE(WOLFBOOT_COMPRESSED_WINDOW_BITS)]; + +struct compressed_install { + struct wolfBoot_image *boot; + uint32_t next_off; /* where the next programmed chunk lands */ + uint32_t erased_upto; /* BOOT erased below this */ + uint32_t marked_upto; /* sectors below this recorded as installed */ + uint32_t skip_upto; /* output below this survived an earlier attempt */ + uint32_t pend_len; + uint8_t pend[COMPRESSED_WRITE_CHUNK]; +}; + +static int compressed_emit(struct compressed_install *ci) +{ + uint32_t end = ci->next_off + ci->pend_len; + uint32_t len = WB_UNIT_ROUNDUP(ci->pend_len); + + while (ci->erased_upto < end) { + if (wb_flash_erase(ci->boot, ci->erased_upto, WOLFBOOT_SECTOR_SIZE) + != 0) { + return -1; + } + ci->erased_upto += WOLFBOOT_SECTOR_SIZE; + } + /* An image rarely ends on a write unit, and flash that programs whole + * units only (NVM_FLASH_ECC) rejects a partial one. */ + memset(ci->pend + ci->pend_len, FLASH_BYTE_ERASED, len - ci->pend_len); + if (wb_flash_write(ci->boot, ci->next_off, ci->pend, (int)len) != 0) { + return -1; + } + ci->next_off = end; + ci->pend_len = 0; + /* Record a destination sector only once every byte of it is programmed, + * so a resume never starts inside a half-written sector. */ + while ((ci->marked_upto + WOLFBOOT_SECTOR_SIZE) <= ci->next_off) { + wolfBoot_set_update_sector_flag( + (uint16_t)(ci->marked_upto / WOLFBOOT_SECTOR_SIZE), + SECT_FLAG_UPDATED); + ci->marked_upto += WOLFBOOT_SECTOR_SIZE; + } + return 0; +} + +/* Sink for the inflater: buffers into aligned chunks, erasing each sector + * the first time it is reached. The decoder emits strictly forward and + * once only, so a sector is never revisited. */ +static int compressed_sink_write(void *ctx, uint32_t off, const uint8_t *buf, + uint32_t len) +{ + struct compressed_install *ci = (struct compressed_install *)ctx; + uint32_t n; + + /* Already programmed by an interrupted attempt. It still has to be + * inflated, to rebuild the decoder's window, but not written back. */ + if (off < ci->skip_upto) { + n = ci->skip_upto - off; + if (n >= len) { + return 0; + } + buf += n; + len -= n; + } + while (len > 0) { + n = (uint32_t)sizeof(ci->pend) - ci->pend_len; + if (n > len) { + n = len; + } + memcpy(ci->pend + ci->pend_len, buf, n); + ci->pend_len += n; + buf += n; + len -= n; + if (ci->pend_len == (uint32_t)sizeof(ci->pend)) { + if (compressed_emit(ci) != 0) { + return -1; + } + } + } + return 0; +} + +#ifdef EXT_FLASH +/* An external container is not addressable, so it is pulled in through the + * same reader the image hasher uses. */ +static int compressed_src_read(void *ctx, uint32_t off, uint8_t *buf, + uint32_t len) +{ + struct wolfBoot_image *img = (struct wolfBoot_image *)ctx; + + if (ext_flash_check_read((uintptr_t)img->fw_base + off, buf, (int)len) + != (int)len) { + return -1; + } + return 0; +} +#endif + +/* Inflate the update partition into the boot partition, returning the number + * of bytes installed or -1. The payload is a complete signed image, so what + * lands in BOOT is an ordinary wolfBoot partition that the normal boot path + * verifies; this step does not have to be trusted for the installed image to + * be authentic. */ +static int wolfBoot_inflate_update(struct wolfBoot_image *update, + struct wolfBoot_image *boot, uint32_t uncompressed_sz, + uint32_t window_bits) +{ + static struct compressed_install ci; + uint32_t out_len = 0; + uint32_t sector = 0; + uint8_t flag; + int ret; + + memset(&ci, 0, sizeof(ci)); + ci.boot = boot; + + /* Resume at the first destination sector not recorded as installed. */ + while (((sector + 1) * WOLFBOOT_SECTOR_SIZE) <= uncompressed_sz) { + flag = SECT_FLAG_NEW; + if (wolfBoot_get_update_sector_flag((uint16_t)sector, &flag) != 0) { + break; + } + if (flag != SECT_FLAG_UPDATED) { + break; + } + sector++; + } + ci.skip_upto = sector * WOLFBOOT_SECTOR_SIZE; + ci.next_off = ci.skip_upto; + ci.erased_upto = ci.skip_upto; + ci.marked_upto = ci.skip_upto; + if (ci.skip_upto > 0) { + wolfBoot_printf("Resuming install at 0x%lx\n", + (unsigned long)ci.skip_upto); + } + + wolfBoot_printf("Inflating %lu bytes to %lu...\n", + (unsigned long)update->fw_size, (unsigned long)uncompressed_sz); + +#ifdef EXT_FLASH + if (PART_IS_EXT(update)) { + ret = wolfBoot_gunzip_sink(NULL, update->fw_size, + compressed_src_read, update, compressed_window, window_bits, + compressed_sink_write, &ci, uncompressed_sz, &out_len); + } + else +#endif + { + ret = wolfBoot_gunzip_sink(update->fw_base, update->fw_size, + NULL, NULL, compressed_window, window_bits, + compressed_sink_write, &ci, uncompressed_sz, &out_len); + } + if (ret != 0) { + wolfBoot_printf("Inflate failed (%d)\n", ret); + return -1; + } + /* The gzip trailer binds out_len; this binds it to the manifest. */ + if (out_len != uncompressed_sz) { + wolfBoot_printf("Inflated %lu bytes, manifest says %lu\n", + (unsigned long)out_len, (unsigned long)uncompressed_sz); + return -1; + } + if (ci.pend_len > 0) { + if (compressed_emit(&ci) != 0) { + return -1; + } + } + return (int)ci.next_off; +} + +#endif /* WOLFBOOT_COMPRESSED_UPDATE */ #ifdef __CCRX__ #pragma section FRAM #endif @@ -1218,12 +1411,22 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) struct wolfBoot_image swap; #endif uint16_t update_type; +#ifndef DISABLE_BACKUP + /* Only the swap loop exchanges the two sizes. */ uint32_t fw_size; +#endif +#ifndef DISABLE_BACKUP uint32_t size; +#endif #if defined(DELTA_UPDATES) int inverse = 0; #endif int fallback_image = 0; +#ifdef WOLFBOOT_COMPRESSED_UPDATE + uint32_t comp_size = 0; + uint32_t comp_window = 0; + int comp = 0; +#endif #ifndef DISABLE_BACKUP int rollback_needed = 0; int bootStateRet = -1; @@ -1289,7 +1492,35 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) } /* get total size */ +#ifdef WOLFBOOT_COMPRESSED_UPDATE + /* The inflated payload is a complete signed image, so its length is the + * whole of what BOOT will hold. */ + comp = wolfBoot_image_compression(&update, &comp_size, &comp_window); + if (comp < 0) { + wolfBoot_printf("Update carries an unusable compression tag\n"); + return -1; + } + if (comp == 0) { + wolfBoot_printf("COMPRESSED_UPDATE: update is not compressed\n"); + return -1; + } + if (comp_window != WOLFBOOT_COMPRESSED_WINDOW_BITS) { + /* The window sizes this build's buffer, so a mismatch cannot be + * decoded here even though the image is otherwise valid. */ + wolfBoot_printf("Image window 2^%lu, this build has 2^%d\n", + (unsigned long)comp_window, WOLFBOOT_COMPRESSED_WINDOW_BITS); + return -1; + } + if ((comp_size <= IMAGE_HEADER_SIZE) || + (comp_size > (MAX_UPDATE_SIZE + IMAGE_HEADER_SIZE))) { + wolfBoot_printf("Inflated size %lu does not fit the boot partition\n", + (unsigned long)comp_size); + return -1; + } + total_size = comp_size; +#else total_size = wolfBoot_get_total_size(&boot, &update); +#endif if (total_size <= IMAGE_HEADER_SIZE) { wolfBoot_printf("Image total size %u invalid!\n", total_size); return -1; @@ -1633,6 +1864,26 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) wolfBoot_get_encrypt_key(key, nonce); #endif +#ifdef WOLFBOOT_COMPRESSED_UPDATE + /* Inflate the UPDATE partition into BOOT. UPDATE is only read, so an + * interrupted install still has an intact source to resume from. */ + copy_ret = wolfBoot_inflate_update(&update, &boot, comp_size, + comp_window); + if (copy_ret < 0) { + wolfBoot_printf("Inflate failed, aborting install\n"); +#ifdef EXT_FLASH + ext_flash_lock(); +#endif + hal_flash_lock(); +#ifdef EXT_ENCRYPTED + wolfBoot_zeroize(key, sizeof(key)); + wolfBoot_zeroize(nonce, sizeof(nonce)); + wolfBoot_enable_fallback_iv(0); +#endif + return -1; + } + sector = ((uint32_t)copy_ret + sector_size - 1) / sector_size; +#else /* Directly copy the content of the UPDATE partition into the BOOT * partition. */ while ((sector * sector_size) < total_size) { @@ -1653,12 +1904,10 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) } sector++; } +#endif /* WOLFBOOT_COMPRESSED_UPDATE */ /* erase remainder of partition */ -#ifdef PRINTF_ENABLED - size = WOLFBOOT_PARTITION_SIZE - (sector * sector_size); wolfBoot_printf("Erasing remainder of partition (%d sectors)...\n", - size/sector_size); -#endif + (WOLFBOOT_PARTITION_SIZE - (sector * sector_size)) / sector_size); while ((sector * sector_size) < WOLFBOOT_PARTITION_SIZE) { wb_flash_erase(&boot, sector * sector_size, sector_size); sector++; diff --git a/tools/config.mk b/tools/config.mk index 3126a8744c..9a6a5c1f7e 100644 --- a/tools/config.mk +++ b/tools/config.mk @@ -152,6 +152,8 @@ CONFIG_VARS:= ARCH TARGET SIGN HASH MCUXSDK MCUXPRESSO MCUXPRESSO_CPU MCUXPRESSO XMSS_PARAMS \ ELF BIG_ENDIAN \ GZIP FIT_RAMDISK \ + COMPRESSED_UPDATE WOLFBOOT_COMPRESSED_WINDOW \ + WOLFBOOT_RAMBOOT_MAX_SIZE \ WATCHDOG WATCHDOG_TIMEOUT_MS WATCHDOG_BASE WATCHDOG_CLK_HZ \ WATCHDOG_HANDOFF \ NXP_CUSTOM_DCD NXP_CUSTOM_DCD_OBJS \ diff --git a/tools/keytools/Makefile b/tools/keytools/Makefile index 022af071aa..9f4b97020d 100644 --- a/tools/keytools/Makefile +++ b/tools/keytools/Makefile @@ -27,6 +27,16 @@ LDFLAGS = OBJDIR = ./ LIBS = +# zlib supplies the DEFLATE encoder for 'sign --compress'; the decoder on the +# device side is wolfBoot's own src/gzip.c. Optional, because most builds of +# this tool never compress: without the headers the tool still builds and +# only that one option is unavailable. +HAVE_ZLIB := $(shell $(CC) -E -include zlib.h -x c /dev/null >/dev/null 2>&1 && echo 1) +ifeq ($(HAVE_ZLIB),1) + CFLAGS += -DHAVE_ZLIB + LDFLAGS += -lz +endif + ifeq ($(WOLFSSL_LIB),1) CFLAGS += -DWOLFSSL_USE_OPTIONS_H ifeq ($(WOLFSSL_INCDIR),) diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 946f30ed6c..08b64971e0 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -43,6 +43,9 @@ #include #include #include +#ifdef HAVE_ZLIB +#include +#endif #include "wolfboot/version.h" @@ -188,6 +191,11 @@ static inline int fp_truncate(FILE *f, size_t len) #define HDR_IMG_DELTA_INVERSE 0x15 #define HDR_IMG_DELTA_INVERSE_SIZE 0x16 +#define HDR_IMG_COMPRESSION 0x08 +#define HDR_IMG_UNCOMPRESSED_SIZE 0x09 +#define HDR_IMG_COMP_WINDOW 0x0A +#define HDR_COMP_GZIP 0x0001 + #define HDR_IMG_TYPE_AUTH_MASK 0xFF00 #define HDR_IMG_TYPE_AUTH_NONE 0xFF00 #define HDR_IMG_TYPE_WOLFBOOT 0x0000 @@ -414,6 +422,17 @@ struct cmd_options { uint32_t policy_sz; uint8_t partition_id; uint32_t custom_tlvs; + uint32_t uncompressed_sz; + uint32_t compressed_sz; + /* LZ77 window as a power of two. A gzip header has no field for it and + * the bootloader sizes its buffer from it, so it is signed into the + * manifest. */ + uint32_t comp_window_bits; + /* --compress: sign the image, compress the result, and sign that. The + * payload has to be a complete signed image so the boot partition it is + * installed into stays an ordinary wolfBoot partition. Cleared while the + * inner image is signed, so only the container carries the tags. */ + int compress; struct cmd_tlv { uint16_t tag; uint16_t len; @@ -1514,6 +1533,104 @@ static int dts_hash_file(const char *file, int hash_algo, uint8_t *out, * against header_required_size() without the auto-grow exit(1) path. */ static uint32_t test_last_header_idx; +#ifdef HAVE_ZLIB +/* Compress to a gzip stream with an explicit LZ77 window. gzip(1) always + * uses 32KB; the bootloader sizes its decode buffer from the window, so a + * smaller one is the RAM-for-ratio trade an integrator may want to make. */ +static int gzip_compress_file(const char *in_path, const char *out_path, + uint32_t window_bits, uint32_t *isize, uint32_t *csize) +{ + FILE *fin = NULL, *fout = NULL; + z_stream zs; + uint8_t inbuf[16384], outbuf[16384]; + uint32_t total_in = 0, total_out = 0; + size_t n, have; + int ret = -1, zret, flush = Z_NO_FLUSH; + + fin = fopen(in_path, "rb"); + if (fin == NULL) { + fprintf(stderr, "Cannot open %s: %s\n", in_path, strerror(errno)); + return -1; + } + fout = fopen(out_path, "wb"); + if (fout == NULL) { + fprintf(stderr, "Cannot create %s: %s\n", out_path, strerror(errno)); + fclose(fin); + return -1; + } + memset(&zs, 0, sizeof(zs)); + /* 16 + bits selects the RFC 1952 gzip wrapper over raw DEFLATE. */ + if (deflateInit2(&zs, 9, Z_DEFLATED, 16 + (int)window_bits, 8, + Z_DEFAULT_STRATEGY) != Z_OK) { + fprintf(stderr, "deflateInit2 failed\n"); + fclose(fin); + fclose(fout); + return -1; + } + do { + n = fread(inbuf, 1, sizeof(inbuf), fin); + if (ferror(fin)) { + fprintf(stderr, "Read error on %s\n", in_path); + goto exit_compress; + } + total_in += (uint32_t)n; + flush = feof(fin) ? Z_FINISH : Z_NO_FLUSH; + zs.next_in = inbuf; + zs.avail_in = (uInt)n; + do { + zs.next_out = outbuf; + zs.avail_out = (uInt)sizeof(outbuf); + zret = deflate(&zs, flush); + if (zret == Z_STREAM_ERROR) { + fprintf(stderr, "deflate failed\n"); + goto exit_compress; + } + have = sizeof(outbuf) - zs.avail_out; + if (fwrite(outbuf, 1, have, fout) != have) { + fprintf(stderr, "Write error on %s\n", out_path); + goto exit_compress; + } + total_out += (uint32_t)have; + } while (zs.avail_out == 0); + } while (flush != Z_FINISH); + + *isize = total_in; + *csize = total_out; + ret = 0; + +exit_compress: + deflateEnd(&zs); + fclose(fin); + fclose(fout); + if (ret != 0) { + unlink(out_path); + } + return ret; +} +#endif /* HAVE_ZLIB */ + +/* WOLFBOOT_COMPRESSED_WINDOW as the build sets it, as a power of two. + * Falls back to 16KB, which is the bootloader's own default. */ +static uint32_t compress_window_bits_from_env(void) +{ + const char *env = getenv("WOLFBOOT_COMPRESSED_WINDOW"); + unsigned long win; + uint32_t bits; + + if ((env == NULL) || (*env == '\0')) { + return 14; + } + win = strtoul(env, NULL, 0); + for (bits = 12; bits <= 15; bits++) { + if (win == (1UL << bits)) { + return bits; + } + } + fprintf(stderr, "WOLFBOOT_COMPRESSED_WINDOW=%s is not 4096, 8192, " + "16384 or 32768\n", env); + exit(16); +} + static uint32_t header_required_size(int is_diff, uint32_t cert_chain_sz, uint32_t secondary_key_sz) { @@ -1545,6 +1662,15 @@ static uint32_t header_required_size(int is_diff, uint32_t cert_chain_sz, } } + if (CMD.compress) { + header_size_align_4(&idx); + header_size_append_tag(&idx, 2); + header_size_align_4(&idx); + header_size_append_tag(&idx, 4); + header_size_align_4(&idx); + header_size_append_tag(&idx, 2); + } + for (i = 0; i < CMD.custom_tlvs; i++) { header_size_align_8(&idx); header_size_append_tag(&idx, CMD.custom_tlv[i].len); @@ -1791,6 +1917,19 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, } } + if (CMD.compress) { + /* Append pad bytes, so fields are 4-byte aligned */ + ALIGN_4(header_idx); + header_append_tag_u16(header, &header_idx, HDR_IMG_COMPRESSION, + HDR_COMP_GZIP); + ALIGN_4(header_idx); + header_append_tag_u32(header, &header_idx, HDR_IMG_UNCOMPRESSED_SIZE, + CMD.uncompressed_sz); + ALIGN_4(header_idx); + header_append_tag_u16(header, &header_idx, HDR_IMG_COMP_WINDOW, + (uint16_t)CMD.comp_window_bits); + } + /* Add custom TLVs */ if (CMD.custom_tlvs > 0) { uint32_t i; @@ -2371,9 +2510,10 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, const char *psize_name = "WOLFBOOT_PARTITION_SIZE"; const char *env_psize = getenv("WOLFBOOT_PARTITION_SIZE"); const char *env_ssize = getenv("WOLFBOOT_SECTOR_SIZE"); - if (CMD.self_update) { - /* self-update images are staged in the UPDATE partition, which - * may be larger than BOOT (monolithic self-update) */ + if (CMD.self_update || CMD.compress) { + /* Staged in the UPDATE partition, which is sized independently: + * larger than BOOT for a monolithic self-update, smaller than it + * for a compressed update, where it only holds the container. */ const char *env_usize = getenv("WOLFBOOT_PARTITION_UPDATE_SIZE"); if (env_usize && *env_usize) { env_psize = env_usize; @@ -3660,6 +3800,29 @@ int main(int argc, char** argv) } CMD.delta = 1; CMD.delta_base_file = argv[++i]; + } else if (strncmp(argv[i], "--compress", 10) == 0) { +#ifndef HAVE_ZLIB + fprintf(stderr, "--compress needs this tool built against zlib; " + "install the zlib development headers and rebuild\n"); + exit(16); +#endif + CMD.compress = 1; + /* Default to whatever the bootloader was configured for, so the + * two cannot disagree and have the image refused on its window + * tag. An explicit = overrides it. */ + CMD.comp_window_bits = compress_window_bits_from_env(); + if (argv[i][10] == '=') { + CMD.comp_window_bits = (uint32_t)strtoul(&argv[i][11], NULL, 0); + } + else if (argv[i][10] != '\0') { + fprintf(stderr, "Unknown option %s\n", argv[i]); + exit(16); + } + if ((CMD.comp_window_bits < 12) || (CMD.comp_window_bits > 15)) { + fprintf(stderr, "--compress window bits must be 12..15 " + "(4KB..32KB)\n"); + exit(16); + } } else if (strcmp(argv[i], "--no-base-sha") == 0) { CMD.no_base_sha = 1; } @@ -4000,6 +4163,33 @@ int main(int argc, char** argv) exit(16); } + if (CMD.compress && CMD.delta) { + fprintf(stderr, "Error: --compress excludes --delta\n"); + exit(16); + } + if (CMD.compress && CMD.self_update) { + fprintf(stderr, "Error: --compress cannot be used for a wolfBoot " + "self-update: wolfBoot cannot inflate itself\n"); + exit(16); + } + /* The output dispatch tests these first, so the container would never be + * built; refuse rather than emit a plain image under a compressed name. */ + if (CMD.compress && CMD.hybrid) { + fprintf(stderr, "Error: --compress excludes a hybrid signature\n"); + exit(16); + } + if (CMD.compress && CMD.sha_only) { + fprintf(stderr, "Error: --compress excludes --sha-only\n"); + exit(16); + } + /* The container is built from the signed image, so there is nothing to + * describe until that has happened. */ + if (CMD.compress && CMD.header_only) { + fprintf(stderr, "Error: --compress cannot be combined with " + "--header-only\n"); + exit(16); + } + memset(buf, 0, sizeof(buf)); strncpy((char*)buf, CMD.image_file, sizeof(buf)-1); tmpstr = strrchr((char*)buf, '.'); @@ -4008,14 +4198,16 @@ int main(int argc, char** argv) } { const char* artifact = - CMD.header_only ? "header" : (CMD.sha_only ? "digest" : "signed"); + CMD.header_only ? "header" : (CMD.sha_only ? "digest" : + (CMD.compress ? "signed_compressed" : "signed")); snprintf(CMD.output_image_file, sizeof(CMD.output_image_file) - 1, "%s_v%s_%s.bin", (char*)buf, CMD.fw_version, artifact); } snprintf(CMD.output_encrypted_image_file, sizeof(CMD.output_encrypted_image_file), - "%s_v%s_signed_and_encrypted.bin", + CMD.compress ? "%s_v%s_signed_compressed_and_encrypted.bin" : + "%s_v%s_signed_and_encrypted.bin", (char*)buf, CMD.fw_version); printf("Update type: %s\n", @@ -4034,6 +4226,10 @@ int main(int argc, char** argv) break; } printf("Input image: %s\n", CMD.image_file); + if (CMD.compress) { + printf("Compression: gzip, 2^%u window\n", + (unsigned)CMD.comp_window_bits); + } printf("Selected cipher: %s\n", sign_str); printf("Selected hash : %s\n", hash_str); if (CMD.sign != NO_SIGN) { @@ -4141,6 +4337,48 @@ int main(int argc, char** argv) zero_and_free(kbuf2, key_buffer_sz2); if (pubkey2) free(pubkey2); +#ifdef HAVE_ZLIB + } else if (CMD.compress) { + /* Two signatures: the inner one travels into the boot partition and + * is what the normal boot path verifies there, the outer one covers + * the compressed bytes so a corrupt payload is caught before the + * boot partition is touched. */ + char inner_path[512]; + char gz_path[512]; + int saved_encrypt = CMD.encrypt; + + if ((snprintf(inner_path, sizeof(inner_path), "%s.inner", + CMD.output_image_file) >= (int)sizeof(inner_path)) || + (snprintf(gz_path, sizeof(gz_path), "%s.gz", + CMD.output_image_file) >= (int)sizeof(gz_path))) { + fprintf(stderr, "Output path too long\n"); + ret = 1; + goto cleanup; + } + /* Only the container is encrypted; the inner image is plaintext + * once installed, because it is executed in place. */ + CMD.encrypt = ENC_OFF; + CMD.compress = 0; + ret = make_header(pubkey, pubkey_sz, CMD.image_file, inner_path); + CMD.encrypt = saved_encrypt; + CMD.compress = 1; + if (ret == 0) { + ret = gzip_compress_file(inner_path, gz_path, + CMD.comp_window_bits, &CMD.uncompressed_sz, + &CMD.compressed_sz); + } + if (ret == 0) { + printf("Compressed: %u -> %u bytes (%u%%)\n", + (unsigned)CMD.uncompressed_sz, + (unsigned)CMD.compressed_sz, + (unsigned)((CMD.compressed_sz * 100U) / + (CMD.uncompressed_sz ? CMD.uncompressed_sz : 1U))); + ret = make_header(pubkey, pubkey_sz, gz_path, + CMD.output_image_file); + } + unlink(inner_path); + unlink(gz_path); +#endif /* HAVE_ZLIB */ } else { ret = make_header(pubkey, pubkey_sz, CMD.image_file, CMD.output_image_file); diff --git a/tools/scripts/sim-compressed-update.sh b/tools/scripts/sim-compressed-update.sh new file mode 100755 index 0000000000..f312cd77f3 --- /dev/null +++ b/tools/scripts/sim-compressed-update.sh @@ -0,0 +1,153 @@ +#!/bin/bash +# Simulated end-to-end test for a compressed update partition installed into +# a boot partition that keeps executing in place. +# +# Run after: +# cp config/examples/sim-compressed-update.config .config +# make keysclean && make && make test-sim-compressed-update-with-update + +set -uo pipefail + +if [ ! -f ".config" ]; then + echo "Missing .config. Run make config first." >&2 + exit 1 +fi + +if ! grep -Eq '^(COMPRESSED_UPDATE(\?|)=1)' .config; then + echo "COMPRESSED_UPDATE=1 is required for this simulation." >&2 + exit 1 +fi + +if [ ! -x "./wolfboot.elf" ]; then + echo "wolfboot.elf not found. Build the simulator first." >&2 + exit 1 +fi + +cfg() { + grep -E "^$1\??=" .config | tail -1 | cut -d= -f2 +} + +# An external update partition is not addressable, so wolfBoot reads the +# container through the external flash interface; the layout also puts it in +# a separate image file. +if grep -Eq '^EXT_FLASH\??=1' .config; then + EXT=1 + FLASH_IMG=external_flash.dd + if grep -Eq '^ENCRYPT\??=1' .config; then + ENC=1 + MAKE_TARGET=test-sim-compressed-update-ext-enc-with-update + else + MAKE_TARGET=test-sim-compressed-update-ext-with-update + fi +else + EXT=0 + FLASH_IMG=internal_flash.dd + MAKE_TARGET=test-sim-compressed-update-with-update +fi +ENC=${ENC:-0} + +BOOT_SIZE=$(( $(cfg WOLFBOOT_PARTITION_SIZE) )) +UPDATE_SIZE=$(( $(cfg WOLFBOOT_PARTITION_UPDATE_SIZE) )) +UPDATE_OFF=$(( $(cfg WOLFBOOT_PARTITION_UPDATE_ADDRESS) )) +SECTOR=$(( $(cfg WOLFBOOT_SECTOR_SIZE) )) +V2=test-app/image_v2_signed_compressed.bin +# The sign tool cannot auto-detect the algorithm from a raw .der key, so +# pass it explicitly the way tools/test.mk does via SIGN_OPTIONS. +SIGN_FLAGS="--$(echo "$(cfg SIGN)" | tr 'A-Z' 'a-z') --$(echo "$(cfg HASH)" | tr 'A-Z' 'a-z')" + +fail() { echo "FAIL: $*" >&2; exit 1; } +ok() { echo " ok: $*"; } + +reset_flash() { + make $MAKE_TARGET >/dev/null 2>&1 || \ + fail "could not rebuild the simulated flash" + if [ "$ENC" = "1" ]; then + # The application stores the image key and arms the update; the + # test harness cannot write the key into the trailer itself. + timeout 60 ./wolfboot.elf update_trigger >/dev/null 2>&1 || \ + fail "could not arm the encrypted update" + fi +} + +run_sim() { + timeout 120 ./wolfboot.elf "$@" 2>&1 +} + +# Build the artifacts and the simulated flash up front, so the script works +# straight after a clean build. +reset_flash + +if [ "$EXT" = "1" ]; then + if [ "$ENC" = "1" ]; then + echo "Update partition is on external flash, encrypted at rest." + else + echo "Update partition is on external flash." + fi +fi + +echo "1. the update partition is smaller than the boot partition" +# The point of the feature: the flash a second full copy would have cost is +# given to the application instead. +[ "$UPDATE_SIZE" -lt "$BOOT_SIZE" ] || \ + fail "update partition ($UPDATE_SIZE) is not smaller than boot ($BOOT_SIZE)" +ok "boot $BOOT_SIZE bytes, update $UPDATE_SIZE bytes" + +[ -f "$V2" ] || fail "$V2 not found; run make test-sim-compressed-update-with-update" +csz=$(stat -c%s "$V2") +[ "$csz" -le "$((UPDATE_SIZE - SECTOR))" ] || \ + fail "container is $csz bytes, past the update partition's usable space" +ok "container is $csz bytes of $((UPDATE_SIZE - SECTOR)) usable" + +echo "2. the update is inflated into the boot partition and booted in place" +reset_flash +out=$(run_sim get_version) +echo "$out" | grep -q "Inflating" || fail "the inflate path did not run" +ver=$(echo "$out" | tail -1) +[ "$ver" = "2" ] || fail "expected version 2 after the install, got '$ver'" +ok "inflated and booted version $ver" + +echo "3. the install is consumed, not repeated on every boot" +out=$(run_sim get_version) +echo "$out" | grep -q "Inflating" && \ + fail "the update was reinstalled on the second boot" +ver=$(echo "$out" | tail -1) +[ "$ver" = "2" ] || fail "expected version 2 on the second boot, got '$ver'" +ok "second boot booted version $ver without reinstalling" + +echo "4. a corrupt container is rejected before the boot partition is touched" +reset_flash +# Flip a byte in the compressed body. The signature covers the compressed +# bytes, so this must fail verification rather than reach the inflater. +dd if=/dev/zero of=$FLASH_IMG bs=1 \ + seek=$((UPDATE_OFF + 1024)) count=16 conv=notrunc status=none +out=$(run_sim get_version) +echo "$out" | grep -q "Inflating" && \ + fail "a corrupt container reached the inflater" +ver=$(echo "$out" | tail -1) +[ "$ver" = "1" ] || fail "expected version 1 to still be running, got '$ver'" +ok "corrupt container rejected, version $ver still booting" + +echo "5. an image that will not fit the update partition is refused at signing" +cp test-app/image.elf test-app/image.toobig.elf +# Incompressible, and far larger than the update partition can hold. +dd if=/dev/urandom of=test-app/image.toobig.elf bs=1k count=$((UPDATE_SIZE / 1024)) \ + oflag=append conv=notrunc status=none +if WOLFBOOT_PARTITION_SIZE=$(cfg WOLFBOOT_PARTITION_SIZE) \ + WOLFBOOT_PARTITION_UPDATE_SIZE=$(cfg WOLFBOOT_PARTITION_UPDATE_SIZE) \ + WOLFBOOT_SECTOR_SIZE=$(cfg WOLFBOOT_SECTOR_SIZE) \ + WOLFBOOT_COMPRESSED_WINDOW=$(cfg WOLFBOOT_COMPRESSED_WINDOW) \ + ./tools/keytools/sign --compress $SIGN_FLAGS test-app/image.toobig.elf \ + wolfboot_signing_private_key.der 3 >/dev/null 2>&1; then + rm -f test-app/image.toobig.elf test-app/image_v3_signed_compressed.bin + fail "signing accepted a container larger than the update partition" +fi +rm -f test-app/image.toobig.elf test-app/image_v3_signed_compressed.bin +ok "signing refused a container that would not fit" + +reset_flash +echo +# Resume after an interrupted install is covered by unit-compressed-install, +# which can place the already-written bytes and the progress flags together, +# and on hardware by cutting power mid-install. The simulator cannot +# interrupt itself at a deterministic point. +echo "Compressed update simulation successful." diff --git a/tools/scripts/wolfboot_cmake_full_build.sh b/tools/scripts/wolfboot_cmake_full_build.sh index ffbf0b800b..eb2ac849c8 100644 --- a/tools/scripts/wolfboot_cmake_full_build.sh +++ b/tools/scripts/wolfboot_cmake_full_build.sh @@ -343,5 +343,5 @@ echo "cmake --build --preset $TARGET -j" # Reminder: Manual build # mkdir -p build # cd build -# cmake -DWOLFBOOT_TARGET=stm32h7 -DBUILD_TEST_APPS=yes -DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x8020000 -DWOLFBOOT_SECTOR_SIZE=0x20000 -DWOLFBOOT_PARTITION_SIZE=0xD0000 -DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x80F0000 -DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x81C0000 .. +# cmake -DWOLFBOOT_TARGET=stm32h7 -DBUILD_TEST_APPS=yes -DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x8020000 -DWOLFBOOT_SECTOR_SIZE=0x20000 -DWOLFBOOT_PARTITION_SIZE=0xE0000 -DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x8100000 -DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x81E0000 .. # make diff --git a/tools/test.mk b/tools/test.mk index f7d8d82fac..8f97a13afc 100644 --- a/tools/test.mk +++ b/tools/test.mk @@ -199,6 +199,80 @@ test-sim-internal-flash-with-update: wolfboot.bin test-app/image.elf FORCE $$(($(WOLFBOOT_PARTITION_UPDATE_ADDRESS)-$(ARCH_FLASH_OFFSET))) test-app/image_v$(TEST_UPDATE_VERSION)_signed.bin \ $$(($(WOLFBOOT_PARTITION_SWAP_ADDRESS)-$(ARCH_FLASH_OFFSET))) erased_sec.dd +test-sim-compressed-update-ext-enc-with-update: wolfboot.bin test-app/image.elf FORCE + # The container is encrypted at rest on the external device. ENCRYPT=1 + # hooks the external flash read, so the installer and the image hasher + # both see plaintext without knowing anything about it. The key is + # provisioned by the application, through 'wolfboot.elf update_trigger'. + @printf "0123456789abcdef0123456789abcdef0123456789abcdef" > /tmp/enc_key.der + $(Q)cp test-app/image.elf test-app/image.bak.elf + $(Q)dd if=/dev/urandom of=test-app/image.elf bs=1k count=48 oflag=append conv=notrunc + $(Q)$(SIGN_ENV) $(SIGN_TOOL) $(SIGN_OPTIONS) test-app/image.elf \ + $(PRIVATE_KEY) 1 + $(Q)cp test-app/image.bak.elf test-app/image.elf + $(Q)dd if=/dev/urandom of=test-app/image.elf bs=1k count=48 oflag=append conv=notrunc + $(Q)$(SIGN_ENV) $(SIGN_TOOL) $(SIGN_OPTIONS) --compress \ + --encrypt /tmp/enc_key.der --aes128 \ + test-app/image.elf $(PRIVATE_KEY) $(TEST_UPDATE_VERSION) + $(Q)dd if=/dev/zero bs=$$(($(WOLFBOOT_PARTITION_SIZE))) count=1 2>/dev/null $(INVERSION) > v1_part.dd + $(Q)dd if=test-app/image_v1_signed.bin of=v1_part.dd conv=notrunc + $(Q)$(BINASSEMBLE) internal_flash.dd 0 wolfboot.bin \ + $$(($(WOLFBOOT_PARTITION_BOOT_ADDRESS) - $(ARCH_FLASH_OFFSET))) v1_part.dd + # No trailer written here: update_trigger arms it and stores the key. + $(Q)dd if=/dev/zero bs=$$(($(WOLFBOOT_PARTITION_UPDATE_SIZE))) count=1 2>/dev/null $(INVERSION) > update_part.dd + $(Q)dd if=test-app/image_v$(TEST_UPDATE_VERSION)_signed_compressed_and_encrypted.bin of=update_part.dd conv=notrunc + $(Q)$(BINASSEMBLE) external_flash.dd \ + $$(($(WOLFBOOT_PARTITION_UPDATE_ADDRESS))) update_part.dd + +test-sim-compressed-update-ext-with-update: wolfboot.bin test-app/image.elf FORCE + # The container lives on external flash and is never addressable, so + # wolfBoot pulls it in through the external flash reader as it inflates. + # BOOT stays in internal flash and keeps executing in place. + $(Q)cp test-app/image.elf test-app/image.bak.elf + $(Q)dd if=/dev/urandom of=test-app/image.elf bs=1k count=96 oflag=append conv=notrunc + $(Q)$(SIGN_ENV) $(SIGN_TOOL) $(SIGN_OPTIONS) test-app/image.elf \ + $(PRIVATE_KEY) 1 + $(Q)cp test-app/image.bak.elf test-app/image.elf + $(Q)dd if=/dev/urandom of=test-app/image.elf bs=1k count=96 oflag=append conv=notrunc + $(Q)$(SIGN_ENV) $(SIGN_TOOL) $(SIGN_OPTIONS) --compress \ + test-app/image.elf $(PRIVATE_KEY) $(TEST_UPDATE_VERSION) + $(Q)dd if=/dev/zero bs=$$(($(WOLFBOOT_PARTITION_SIZE))) count=1 2>/dev/null $(INVERSION) > v1_part.dd + $(Q)dd if=test-app/image_v1_signed.bin of=v1_part.dd conv=notrunc + $(Q)$(BINASSEMBLE) internal_flash.dd 0 wolfboot.bin \ + $$(($(WOLFBOOT_PARTITION_BOOT_ADDRESS) - $(ARCH_FLASH_OFFSET))) v1_part.dd + # Arm the update: pBOOT at the end of the external update partition. + $(Q)dd if=/dev/zero bs=$$(($(WOLFBOOT_PARTITION_UPDATE_SIZE))) count=1 2>/dev/null $(INVERSION) > update_part.dd + $(Q)dd if=test-app/image_v$(TEST_UPDATE_VERSION)_signed_compressed.bin of=update_part.dd conv=notrunc + $(Q)printf "pBOOT" | dd of=update_part.dd bs=1 seek=$$(($(WOLFBOOT_PARTITION_UPDATE_SIZE) - 5)) conv=notrunc + $(Q)$(BINASSEMBLE) external_flash.dd \ + $$(($(WOLFBOOT_PARTITION_UPDATE_ADDRESS))) update_part.dd + +test-sim-compressed-update-with-update: wolfboot.bin test-app/image.elf FORCE + # BOOT holds an ordinary signed image; UPDATE holds the compressed + # container, which the installer inflates into BOOT. The container's + # payload is itself a signed image, so BOOT stays an ordinary partition. + $(Q)cp test-app/image.elf test-app/image.bak.elf + $(Q)dd if=/dev/urandom of=test-app/image.elf bs=1k count=16 oflag=append conv=notrunc + $(Q)$(SIGN_ENV) $(SIGN_TOOL) $(SIGN_OPTIONS) test-app/image.elf \ + $(PRIVATE_KEY) 1 + $(Q)cp test-app/image.bak.elf test-app/image.elf + # Enough incompressible padding that the container lands near the ratio + # real firmware achieves, so the fit against the update partition is + # tested with a realistic margin instead of a flattering one. + $(Q)dd if=/dev/urandom of=test-app/image.elf bs=1k count=96 oflag=append conv=notrunc + $(Q)$(SIGN_ENV) $(SIGN_TOOL) $(SIGN_OPTIONS) --compress \ + test-app/image.elf $(PRIVATE_KEY) $(TEST_UPDATE_VERSION) + $(Q)dd if=/dev/zero bs=$$(($(WOLFBOOT_SECTOR_SIZE))) count=1 2>/dev/null $(INVERSION) > erased_sec.dd + # Arm the update: pBOOT at the end of the (smaller) update partition. + $(Q)dd if=/dev/zero bs=$$(($(WOLFBOOT_PARTITION_UPDATE_SIZE))) count=1 2>/dev/null $(INVERSION) > update_part.dd + $(Q)dd if=test-app/image_v$(TEST_UPDATE_VERSION)_signed_compressed.bin of=update_part.dd conv=notrunc + $(Q)printf "pBOOT" | dd of=update_part.dd bs=1 seek=$$(($(WOLFBOOT_PARTITION_UPDATE_SIZE) - 5)) conv=notrunc + $(Q)$(BINASSEMBLE) internal_flash.dd \ + 0 wolfboot.bin \ + $$(($(WOLFBOOT_PARTITION_BOOT_ADDRESS) - $(ARCH_FLASH_OFFSET))) test-app/image_v1_signed.bin \ + $$(($(WOLFBOOT_PARTITION_UPDATE_ADDRESS)-$(ARCH_FLASH_OFFSET))) update_part.dd \ + $$(($(WOLFBOOT_PARTITION_SWAP_ADDRESS)-$(ARCH_FLASH_OFFSET))) erased_sec.dd + test-sim-internal-flash-with-delta-update: # This target calls test-sim-internal-flash-with-update with the delta option # The delta option causes the sign tool to produce image_v2_signed_diff.bin instead of image_v2_signed.bin diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 9b25c10166..623c114a37 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -1,5 +1,15 @@ LDFLAGS=-lcheck -lm -pthread +# The gzip sink tests build their fixtures with a chosen LZ77 window, which +# gzip(1) cannot produce, so they need zlib. Optional: without the headers +# those suites are skipped and the rest still build, matching how +# tools/keytools treats the same dependency. +HAVE_ZLIB := $(shell $(CC) -E -include zlib.h -x c /dev/null >/dev/null 2>&1 && echo 1) +ifeq ($(HAVE_ZLIB),1) + CFLAGS += -DHAVE_ZLIB + LDFLAGS += -lz +endif + UNAME_S := $(shell uname -s) ifneq ($(UNAME_S),Darwin) LDFLAGS+=-lrt -lsubunit @@ -68,7 +78,7 @@ TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ unit-update-flash-self-update \ unit-update-flash-self-update-ecc \ unit-nsc-update \ - unit-update-flash-enc unit-update-flash-enc-full unit-update-flash-enc-intswap unit-update-ram unit-update-ram-fit unit-update-ram-uboot unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-nofixed-noramboot unit-update-ram-noramboot unit-update-ram-custom-trailer unit-custom-trailer-nopart unit-update-flash-hwswap unit-update-flash-custom-trailer unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ + unit-update-flash-enc unit-update-flash-enc-full unit-update-flash-enc-intswap unit-update-ram unit-update-ram-fit unit-update-ram-uboot unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-nofixed-noramboot unit-update-ram-noramboot unit-update-ram-custom-trailer unit-compressed-tlv unit-custom-trailer-nopart unit-update-flash-hwswap unit-update-flash-custom-trailer unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ unit-update-disk unit-update-disk-confirm unit-update-disk-golden unit-update-disk-failover unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-update-disk-fit-ramdisk unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ unit-image-nopart unit-image-sha384 unit-image-sha3-384 unit-image-dts \ unit-image-dts-sha384 unit-image-dts-sha3-384 unit-store-sbrk \ @@ -95,6 +105,11 @@ TESTS+=unit-psoc-e8-flash TESTS+=unit-diagnostics-256 TESTS+=unit-fit-gzip unit-fit-nogzip TESTS+=unit-gzip-small unit-gzip-bits32 unit-gzip-root7 unit-fit-gzip-small +ifeq ($(HAVE_ZLIB),1) +TESTS+=unit-gzip-sink unit-gzip-sink-small unit-gzip-sink-bits32 \ + unit-gzip-sink-root7 +TESTS+=unit-compressed-install unit-compressed-install-ecc +endif TESTS+=unit-fit-fpga TESTS+=unit-mpusize TESTS+=unit-flash-erase-h7 @@ -436,6 +451,12 @@ unit-update-ram-fit:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUT -DWOLFBOOT_DTS_BOOT_ADDRESS=0xCF000000 \ -DWOLFBOOT_DTS_UPDATE_ADDRESS=0xCF100000 \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +# Compressed-image manifest reader and the shared slot-selection policy. +unit-compressed-tlv:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN \ + -DUNIT_TEST_AUTH -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED \ + -DWOLFBOOT_COMPRESSED_UPDATE -DWOLFBOOT_GZIP -DWOLFBOOT_GZIP_SINK \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT \ + -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE unit-update-ram:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH -DPART_UPDATE_EXT \ -DPART_SWAP_EXT -DPART_BOOT_EXT -DWOLFBOOT_DUALBOOT -DNO_XIP \ @@ -1145,6 +1166,26 @@ unit-gzip-root7: ../../include/target.h unit-gzip.c gcc -o $@ unit-gzip.c $(CFLAGS) -DWOLFBOOT_GZIP \ -DWOLFBOOT_GZIP_ROOT_BITS=7 $(LDFLAGS) +# The sink entry point, which decompresses to a callback with a bounded +# window instead of to a flat buffer. Fixtures need a chosen window size, +# which host gzip(1) cannot produce, so these link zlib. Built over the same +# decoder variants as the suite above, since the block decoders differ. +unit-gzip-sink: ../../include/target.h unit-gzip-sink.c + gcc -o $@ unit-gzip-sink.c $(CFLAGS) -DWOLFBOOT_GZIP \ + -DWOLFBOOT_GZIP_SINK $(LDFLAGS) + +unit-gzip-sink-small: ../../include/target.h unit-gzip-sink.c + gcc -o $@ unit-gzip-sink.c $(CFLAGS) -DWOLFBOOT_GZIP \ + -DWOLFBOOT_GZIP_SINK -DWOLFBOOT_GZIP_SMALL $(LDFLAGS) + +unit-gzip-sink-bits32: ../../include/target.h unit-gzip-sink.c + gcc -o $@ unit-gzip-sink.c $(CFLAGS) -DWOLFBOOT_GZIP \ + -DWOLFBOOT_GZIP_SINK -DWOLFBOOT_GZIP_BITS_32 $(LDFLAGS) + +unit-gzip-sink-root7: ../../include/target.h unit-gzip-sink.c + gcc -o $@ unit-gzip-sink.c $(CFLAGS) -DWOLFBOOT_GZIP \ + -DWOLFBOOT_GZIP_SINK -DWOLFBOOT_GZIP_ROOT_BITS=7 $(LDFLAGS) + # FIT-loader gzip / unsupported-compression branch coverage. Built twice # from the same source: once with WOLFBOOT_GZIP (success + decompress # failure paths) and once without (compile-time fail-closed path). @@ -1263,6 +1304,43 @@ unit-update-flash-enc-intswap: ../../include/target.h unit-update-flash.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/chacha.c \ $(CFLAGS) $(LDFLAGS) +# The execute-in-place installer. DISABLE_BACKUP is what COMPRESSED_UPDATE +# implies in options.mk; the asymmetric update partition is the layout the +# mode exists for, and nothing else in the tree exercises it. +unit-compressed-install:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN \ + -DUNIT_TEST_AUTH -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED \ + -DDISABLE_BACKUP -DWOLFBOOT_COMPRESSED_UPDATE \ + -DWOLFBOOT_GZIP -DWOLFBOOT_GZIP_SINK \ + -DWOLFBOOT_COMPRESSED_WINDOW_BITS=12 \ + -DWOLFBOOT_PARTITION_UPDATE_SIZE=0x4000 \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT \ + -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +# The same installer on flash that programs whole 32-byte units only, as the +# AURIX TC3xx does. 4 KB sectors, because the journal must be able to log a +# full-partition update. +unit-compressed-install-ecc:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN \ + -DUNIT_TEST_AUTH -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED \ + -DDISABLE_BACKUP -DWOLFBOOT_COMPRESSED_UPDATE \ + -DWOLFBOOT_GZIP -DWOLFBOOT_GZIP_SINK \ + -DWOLFBOOT_COMPRESSED_WINDOW_BITS=12 \ + -DWOLFBOOT_PARTITION_UPDATE_SIZE=0x4000 \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT \ + -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE \ + -DNVM_FLASH_JOURNAL -DNVM_FLASH_ECC -DWOLFBOOT_FLASH_WRITE_UNIT=32 \ + -DWOLFBOOT_SECTOR_SIZE=0x1000 -DIMAGE_HEADER_SIZE=256 + +unit-compressed-install: ../../include/target.h unit-compressed-install.c + gcc -o $@ unit-compressed-install.c ../../src/image.c ../../src/gzip.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) + +unit-compressed-install-ecc: ../../include/target.h unit-compressed-install.c + gcc -o $@ unit-compressed-install.c ../../src/image.c ../../src/gzip.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) + +unit-compressed-tlv: ../../include/target.h unit-compressed-tlv.c + gcc -o $@ unit-compressed-tlv.c ../../src/image.c ../../src/gzip.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) + unit-update-ram: ../../include/target.h unit-update-ram.c gcc -o $@ unit-update-ram.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-compressed-install.c b/tools/unit-tests/unit-compressed-install.c new file mode 100644 index 0000000000..f9bdba321c --- /dev/null +++ b/tools/unit-tests/unit-compressed-install.c @@ -0,0 +1,314 @@ +/* unit-compressed-install.c + * + * unit tests for the execute-in-place compressed installer + * (wolfBoot_inflate_update in src/update_flash.c), which inflates the update + * partition into the boot partition a chunk at a time. + * + * Covers what the simulator cannot reach directly: that the destination is + * erased before it is written and written once, that an interrupted install + * resumes from the recorded sector flags without rewriting what is already + * there, and that a payload disagreeing with the manifest is refused. + * + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +#include +#include +#include +#include +#include +#include +#include + +#define MOCK_ADDRESS_UPDATE 0xCC000000 +#define MOCK_ADDRESS_BOOT 0xCD000000 +#define MOCK_ADDRESS_SWAP 0xCE000000 + +#include "user_settings.h" +#include "wolfboot/wolfboot.h" +#include "libwolfboot.c" +#include "update_flash.c" +#include +#include "unit-mock-flash.c" + +Suite *wolfboot_suite(void); + +void do_boot(const uint32_t *address) +{ + (void)address; +} + +int hal_flash_protect(haladdr_t address, int len) +{ + (void)address; + (void)len; + return 0; +} + +void arch_reboot(void) +{ +} + +/* ------------------------------------------------------------------------- */ +/* Fixtures */ +/* ------------------------------------------------------------------------- */ + +#define TEST_WINDOW_BITS WOLFBOOT_COMPRESSED_WINDOW_BITS +#define TEST_PAYLOAD_SZ (6U * WOLFBOOT_SECTOR_SIZE) + +static uint8_t expect[TEST_PAYLOAD_SZ]; +static struct wolfBoot_image boot_img, update_img; + +/* Deterministic and partly compressible, so the stream carries both + * literals and back-references. */ +static void fill_payload(uint8_t *p, uint32_t len, unsigned seed) +{ + uint32_t i; + + for (i = 0; i < len; i++) { + seed = seed * 1103515245u + 12345u; + p[i] = (i % 64 < 48) ? (uint8_t)('a' + (i % 19)) + : (uint8_t)((seed >> 16) & 0xFF); + } +} + +static uint32_t deflate_window(const uint8_t *in, uint32_t in_len, + uint8_t *out, uint32_t out_cap, int window_bits) +{ + z_stream zs; + + memset(&zs, 0, sizeof(zs)); + ck_assert_int_eq(deflateInit2(&zs, 9, Z_DEFLATED, 16 + window_bits, 8, + Z_DEFAULT_STRATEGY), Z_OK); + zs.next_in = (Bytef *)in; + zs.avail_in = in_len; + zs.next_out = out; + zs.avail_out = out_cap; + ck_assert_int_eq(deflate(&zs, Z_FINISH), Z_STREAM_END); + ck_assert_int_eq(deflateEnd(&zs), Z_OK); + return (uint32_t)zs.total_out; +} + +/* Lay a container into the update partition and point the two image structs + * at the mocked flash. */ +static void prepare_ex(uint32_t payload_sz, int window_bits, int long_match) +{ + uint8_t *upd = (uint8_t *)WOLFBOOT_PARTITION_UPDATE_ADDRESS; + uint32_t clen; + + ck_assert_int_ge(mmap_file("/tmp/wolfboot-unit-ci-update.bin", + (uint8_t *)MOCK_ADDRESS_UPDATE, WOLFBOOT_PARTITION_SIZE, NULL), 0); + ck_assert_int_ge(mmap_file("/tmp/wolfboot-unit-ci-boot.bin", + (uint8_t *)MOCK_ADDRESS_BOOT, WOLFBOOT_PARTITION_SIZE, NULL), 0); + hal_flash_unlock(); + hal_flash_erase(WOLFBOOT_PARTITION_BOOT_ADDRESS, WOLFBOOT_PARTITION_SIZE); + hal_flash_erase(WOLFBOOT_PARTITION_UPDATE_ADDRESS, WOLFBOOT_PARTITION_SIZE); + + fill_payload(expect, payload_sz, 1); + if (long_match) { + /* Repeat an earlier run far enough back that the match can only be + * coded as a distance beyond a small window. */ + memcpy(expect + payload_sz - 1024, expect, 1024); + } + memset(upd, 0xFF, IMAGE_HEADER_SIZE); + memcpy(upd, "WOLF", 4); + clen = deflate_window(expect, payload_sz, upd + IMAGE_HEADER_SIZE, + WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE, window_bits); + + memset(&update_img, 0, sizeof(update_img)); + update_img.hdr = upd; + update_img.fw_base = upd + IMAGE_HEADER_SIZE; + update_img.not_ext = 1; + update_img.hdr_ok = 1; + update_img.part = PART_UPDATE; + update_img.fw_size = clen; + + memset(&boot_img, 0, sizeof(boot_img)); + boot_img.hdr = (uint8_t *)WOLFBOOT_PARTITION_BOOT_ADDRESS; + boot_img.fw_base = boot_img.hdr + IMAGE_HEADER_SIZE; + boot_img.not_ext = 1; + boot_img.hdr_ok = 1; + boot_img.part = PART_BOOT; +} + +static void prepare(uint32_t payload_sz, int window_bits) +{ + prepare_ex(payload_sz, window_bits, 0); +} + +static void cleanup(void) +{ +#ifdef NVM_FLASH_ECC + /* No write that was not a whole, erased write unit, and no partial + * sector erase, reached the boot partition. */ + ck_assert_int_eq(ecc_violations, 0); +#endif + hal_flash_lock(); + munmap((void *)MOCK_ADDRESS_UPDATE, WOLFBOOT_PARTITION_SIZE); + munmap((void *)MOCK_ADDRESS_BOOT, WOLFBOOT_PARTITION_SIZE); +} + +/* ------------------------------------------------------------------------- */ + +START_TEST(test_install_writes_the_whole_image) +{ + int ret; + + prepare(TEST_PAYLOAD_SZ, TEST_WINDOW_BITS); + ret = wolfBoot_inflate_update(&update_img, &boot_img, TEST_PAYLOAD_SZ, + TEST_WINDOW_BITS); + ck_assert_int_eq(ret, (int)TEST_PAYLOAD_SZ); + ck_assert_mem_eq((void *)WOLFBOOT_PARTITION_BOOT_ADDRESS, expect, + TEST_PAYLOAD_SZ); + cleanup(); +} +END_TEST + +/* A payload that is not a whole number of chunks still has its tail + * programmed, rather than being left in the write-combining buffer. */ +START_TEST(test_install_writes_a_partial_tail) +{ + uint32_t sz = TEST_PAYLOAD_SZ - 37; + int ret; + + prepare(sz, TEST_WINDOW_BITS); + ret = wolfBoot_inflate_update(&update_img, &boot_img, sz, + TEST_WINDOW_BITS); + ck_assert_int_eq(ret, (int)sz); + ck_assert_mem_eq((void *)WOLFBOOT_PARTITION_BOOT_ADDRESS, expect, sz); + cleanup(); +} +END_TEST + +/* Sectors recorded as installed by an earlier attempt are inflated again, + * to rebuild the decoder's window, but must not be written back. */ +START_TEST(test_install_resumes_without_rewriting) +{ + uint8_t *dst = (uint8_t *)WOLFBOOT_PARTITION_BOOT_ADDRESS; + const uint32_t done = 2U * WOLFBOOT_SECTOR_SIZE; + uint32_t i; + int ret; + + prepare(TEST_PAYLOAD_SZ, TEST_WINDOW_BITS); + /* Put the first two sectors in place and record them, as a completed + * run would have left them. */ + hal_flash_write((haladdr_t)dst, expect, (int)done); + for (i = 0; i < (done / WOLFBOOT_SECTOR_SIZE); i++) { + wolfBoot_set_update_sector_flag((uint16_t)i, SECT_FLAG_UPDATED); + } + erased_boot = 0; + + ret = wolfBoot_inflate_update(&update_img, &boot_img, TEST_PAYLOAD_SZ, + TEST_WINDOW_BITS); + ck_assert_int_eq(ret, (int)TEST_PAYLOAD_SZ); + /* The whole image must still be correct... */ + ck_assert_mem_eq(dst, expect, TEST_PAYLOAD_SZ); + /* ...and the sectors already in place must not have been erased again. */ + ck_assert_int_eq(erased_boot, (int)((TEST_PAYLOAD_SZ - done) / + WOLFBOOT_SECTOR_SIZE)); + cleanup(); +} +END_TEST + +/* The gzip trailer binds the inflated length; this binds it to the + * manifest, so a container whose declared size disagrees is refused. */ +START_TEST(test_install_rejects_size_mismatch) +{ + int ret; + + prepare(TEST_PAYLOAD_SZ, TEST_WINDOW_BITS); + ret = wolfBoot_inflate_update(&update_img, &boot_img, + TEST_PAYLOAD_SZ - WOLFBOOT_SECTOR_SIZE, TEST_WINDOW_BITS); + ck_assert_int_lt(ret, 0); + cleanup(); +} +END_TEST + +/* Compressed with a window larger than this build's buffer: the back- + * reference cannot be resolved and the install must fail rather than + * produce a partly correct image. */ +START_TEST(test_install_rejects_oversized_window) +{ + /* The larger window is decoded into the buffer this build sized for + * TEST_WINDOW_BITS, so the payload has to stay inside that buffer: + * one and a half windows does, and still puts the repeated run further + * back than a TEST_WINDOW_BITS decoder can reach. */ + const uint32_t sz = (3U << TEST_WINDOW_BITS) / 2U; + int ret; + + if (TEST_WINDOW_BITS >= GZIP_SINK_WINDOW_MAX_BITS) { + /* Nothing larger exists to test against in this build. */ + return; + } + prepare_ex(sz, GZIP_SINK_WINDOW_MAX_BITS, 1); + /* Same stream decodes at the window it was made with. */ + ret = wolfBoot_inflate_update(&update_img, &boot_img, sz, + GZIP_SINK_WINDOW_MAX_BITS); + ck_assert_int_eq(ret, (int)sz); + cleanup(); + + prepare_ex(sz, GZIP_SINK_WINDOW_MAX_BITS, 1); + ret = wolfBoot_inflate_update(&update_img, &boot_img, sz, + TEST_WINDOW_BITS); + ck_assert_int_lt(ret, 0); + cleanup(); +} +END_TEST + +/* A truncated container must be refused, not installed up to the cut. */ +START_TEST(test_install_rejects_truncated_container) +{ + int ret; + + prepare(TEST_PAYLOAD_SZ, TEST_WINDOW_BITS); + update_img.fw_size -= 16; + ret = wolfBoot_inflate_update(&update_img, &boot_img, TEST_PAYLOAD_SZ, + TEST_WINDOW_BITS); + ck_assert_int_lt(ret, 0); + cleanup(); +} +END_TEST + +/* ------------------------------------------------------------------------- */ + +Suite *wolfboot_suite(void) +{ + Suite *s = suite_create("wolfBoot-compressed-install"); + TCase *tc = tcase_create("install"); + + tcase_set_timeout(tc, 30); + tcase_add_test(tc, test_install_writes_the_whole_image); + tcase_add_test(tc, test_install_writes_a_partial_tail); + tcase_add_test(tc, test_install_resumes_without_rewriting); + tcase_add_test(tc, test_install_rejects_size_mismatch); + tcase_add_test(tc, test_install_rejects_oversized_window); + tcase_add_test(tc, test_install_rejects_truncated_container); + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int failed; + SRunner *sr = srunner_create(wolfboot_suite()); + srunner_run_all(sr, CK_NORMAL); + failed = srunner_ntests_failed(sr); + srunner_free(sr); + return failed ? 1 : 0; +} diff --git a/tools/unit-tests/unit-compressed-tlv.c b/tools/unit-tests/unit-compressed-tlv.c new file mode 100644 index 0000000000..6ed8dc3351 --- /dev/null +++ b/tools/unit-tests/unit-compressed-tlv.c @@ -0,0 +1,252 @@ +/* unit-compressed-tlv.c + * + * Unit tests for the compressed-image manifest reader in image.c, which the + * execute-in-place installer uses to read an image's compression tags. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +#ifndef WOLFBOOT_HASH_SHA256 + #define WOLFBOOT_HASH_SHA256 +#endif +#define IMAGE_HEADER_SIZE 256 +#define MOCK_ADDRESS_UPDATE 0xCC000000 +#define MOCK_ADDRESS_BOOT 0xCD000000 +#define MOCK_ADDRESS_SWAP 0xCE000000 + +#define NO_FORK 1 + +#include "target.h" + +static __thread unsigned char wolfboot_ram[2 * WOLFBOOT_PARTITION_SIZE + + IMAGE_HEADER_SIZE]; +#define WOLFBOOT_LOAD_ADDRESS (((uintptr_t)wolfboot_ram + IMAGE_HEADER_SIZE)) + +#include +#include +#include +#include +#include +#include +#include "user_settings.h" +#include "wolfboot/wolfboot.h" +#include "libwolfboot.c" +#include +#include "unit-mock-flash.c" + +const char *argv0; + +Suite *wolfboot_suite(void); + +int wolfBoot_staged_ok = 0; + +void do_boot(const uint32_t *address) +{ + (void)address; + wolfBoot_staged_ok++; +} + +int hal_flash_protect(haladdr_t address, int len) +{ + (void)address; + (void)len; + return 0; +} + +/* Manifest construction helpers */ + +static uint8_t test_hdr[IMAGE_HEADER_SIZE]; +static struct wolfBoot_image test_img; + +static uint32_t hdr_idx; + +static void hdr_reset(void) +{ + memset(test_hdr, 0xFF, sizeof(test_hdr)); + memcpy(test_hdr, "WOLF", 4); + /* image size field, unused by the reader under test */ + memset(test_hdr + 4, 0, 4); + hdr_idx = 8; + + memset(&test_img, 0, sizeof(test_img)); + test_img.hdr = test_hdr; + test_img.not_ext = 1; + test_img.hdr_ok = 1; + test_img.part = PART_BOOT; +} + +static void hdr_add(uint16_t tag, uint16_t len, const uint8_t *val) +{ + test_hdr[hdr_idx++] = (uint8_t)(tag & 0xFF); + test_hdr[hdr_idx++] = (uint8_t)(tag >> 8); + test_hdr[hdr_idx++] = (uint8_t)(len & 0xFF); + test_hdr[hdr_idx++] = (uint8_t)(len >> 8); + if ((val != NULL) && (len > 0)) { + memcpy(test_hdr + hdr_idx, val, len); + } + hdr_idx += len; +} + +static void hdr_add_u16(uint16_t tag, uint16_t v) +{ + uint8_t b[2]; + b[0] = (uint8_t)(v & 0xFF); + b[1] = (uint8_t)(v >> 8); + hdr_add(tag, 2, b); +} + +static void hdr_add_u32(uint16_t tag, uint32_t v) +{ + uint8_t b[4]; + b[0] = (uint8_t)(v & 0xFF); + b[1] = (uint8_t)((v >> 8) & 0xFF); + b[2] = (uint8_t)((v >> 16) & 0xFF); + b[3] = (uint8_t)((v >> 24) & 0xFF); + hdr_add(tag, 4, b); +} + +/* wolfBoot_image_compression() */ + +/* No compression TLV: an uncompressed image, which must be reported as + * "none" (0) rather than as an error, so the plain path still runs. */ +START_TEST(test_compression_absent) +{ + uint32_t sz = 0xA5A5A5A5; + + hdr_reset(); + hdr_add_u32(HDR_VERSION, 1); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz, NULL), 0); +} +END_TEST + +/* A well-formed gzip manifest. Also pins the little-endian decode of the + * uncompressed length, which is what bounds the inflate. */ +START_TEST(test_compression_gzip_ok) +{ + uint32_t sz = 0; + uint32_t bits = 0; + + hdr_reset(); + hdr_add_u16(HDR_IMG_COMPRESSION, HDR_COMP_GZIP); + hdr_add_u32(HDR_IMG_UNCOMPRESSED_SIZE, 0x00029A28); /* 170536 */ + hdr_add_u16(HDR_IMG_COMP_WINDOW, 14); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz, &bits), 1); + ck_assert_uint_eq(sz, 0x00029A28); + ck_assert_uint_eq(bits, 14); +} +END_TEST + +/* Unknown algorithm must be -1, never a fall-through to booting raw: + * that would be a jump into DEFLATE data. */ +START_TEST(test_compression_unknown_algo) +{ + uint32_t sz = 0; + + hdr_reset(); + hdr_add_u16(HDR_IMG_COMPRESSION, 0x00FE); + hdr_add_u32(HDR_IMG_UNCOMPRESSED_SIZE, 1024); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz, NULL), -1); +} +END_TEST + +/* Wrong length on the algorithm tag. */ +START_TEST(test_compression_bad_algo_len) +{ + uint32_t sz = 0; + uint8_t one = 1; + + hdr_reset(); + hdr_add(HDR_IMG_COMPRESSION, 1, &one); + hdr_add_u32(HDR_IMG_UNCOMPRESSED_SIZE, 1024); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz, NULL), -1); +} +END_TEST + +/* Compression declared but no uncompressed size: the loader would have + * nothing to bound the inflate with. */ +START_TEST(test_compression_missing_size) +{ + uint32_t sz = 0; + + hdr_reset(); + hdr_add_u16(HDR_IMG_COMPRESSION, HDR_COMP_GZIP); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz, NULL), -1); +} +END_TEST + +/* Size tag present but the wrong width. */ +START_TEST(test_compression_bad_size_len) +{ + uint32_t sz = 0; + uint8_t two[2] = { 0x00, 0x04 }; + + hdr_reset(); + hdr_add_u16(HDR_IMG_COMPRESSION, HDR_COMP_GZIP); + hdr_add(HDR_IMG_UNCOMPRESSED_SIZE, 2, two); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz, NULL), -1); +} +END_TEST + +/* Wrong length on the window tag. Rejected whether or not the caller wants + * the value, so a malformed tag can never pass as a usable manifest. */ +START_TEST(test_compression_bad_window_len) +{ + uint32_t sz = 0; + uint32_t bits = 0; + uint8_t one = 14; + + hdr_reset(); + hdr_add_u16(HDR_IMG_COMPRESSION, HDR_COMP_GZIP); + hdr_add_u32(HDR_IMG_UNCOMPRESSED_SIZE, 1024); + hdr_add(HDR_IMG_COMP_WINDOW, 1, &one); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz, &bits), -1); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz, NULL), -1); +} +END_TEST + +Suite *wolfboot_suite(void) +{ + Suite *s = suite_create("wolfBoot-compressed-tlv"); + TCase *tc = tcase_create("compressed manifest"); + + tcase_add_test(tc, test_compression_absent); + tcase_add_test(tc, test_compression_gzip_ok); + tcase_add_test(tc, test_compression_unknown_algo); + tcase_add_test(tc, test_compression_bad_algo_len); + tcase_add_test(tc, test_compression_missing_size); + tcase_add_test(tc, test_compression_bad_size_len); + tcase_add_test(tc, test_compression_bad_window_len); + suite_add_tcase(s, tc); + return s; +} + +int main(int argc, char *argv[]) +{ + int number_failed; + SRunner *sr; + + argv0 = argv[0]; + (void)argc; + + sr = srunner_create(wolfboot_suite()); + srunner_set_fork_status(sr, CK_NOFORK); + srunner_run_all(sr, CK_NORMAL); + number_failed = srunner_ntests_failed(sr); + srunner_free(sr); + return (number_failed == 0) ? 0 : 1; +} diff --git a/tools/unit-tests/unit-gzip-sink.c b/tools/unit-tests/unit-gzip-sink.c new file mode 100644 index 0000000000..d129eb85d6 --- /dev/null +++ b/tools/unit-tests/unit-gzip-sink.c @@ -0,0 +1,648 @@ +/* unit-gzip-sink.c + * + * unit tests for the sink entry point of the wolfBoot gzip inflater + * (wolfBoot_gunzip_sink in src/gzip.c), which decompresses to a callback + * instead of to a buffer so the output need not be addressable. + * + * Fixtures come from zlib so the LZ77 window can be chosen; host gzip(1) + * always uses 32KB and cannot produce the bounded-window streams this + * decoder exists for. + * + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +#include +#include +#include +#include +#include +#include + +#include "gzip.h" + +/* Pull in the implementation under test directly */ +#include "../../src/gzip.c" + +#define SINK_MAX_OUT (512U * 1024U) + +/* ------------------------------------------------------------------------- */ +/* Fixtures */ +/* ------------------------------------------------------------------------- */ + +/* Compress with an explicit window so the decoder's window bound can be + * exercised; level 0 emits stored blocks. */ +static uint8_t *gz_deflate_window(const uint8_t *in, uint32_t in_len, + int window_bits, int level, uint32_t *out_len) +{ + z_stream zs; + uint8_t *out; + uLong cap = compressBound(in_len) + 64; + + out = malloc(cap); + ck_assert_ptr_nonnull(out); + memset(&zs, 0, sizeof(zs)); + ck_assert_int_eq(deflateInit2(&zs, level, Z_DEFLATED, 16 + window_bits, 8, + Z_DEFAULT_STRATEGY), Z_OK); + zs.next_in = (Bytef *)in; + zs.avail_in = in_len; + zs.next_out = out; + zs.avail_out = (uInt)cap; + ck_assert_int_eq(deflate(&zs, Z_FINISH), Z_STREAM_END); + ck_assert_int_eq(deflateEnd(&zs), Z_OK); + *out_len = (uint32_t)zs.total_out; + return out; +} + +/* Deterministic, partly compressible. */ +static void fill_mixed(uint8_t *p, uint32_t len, unsigned seed) +{ + uint32_t i; + + for (i = 0; i < len; i++) { + seed = seed * 1103515245u + 12345u; + p[i] = (i % 97 < 60) ? (uint8_t)('a' + (i % 23)) + : (uint8_t)((seed >> 16) & 0xFF); + } +} + +/* ------------------------------------------------------------------------- */ +/* Sink that records what it was handed */ +/* ------------------------------------------------------------------------- */ + +struct capture { + uint8_t *buf; + uint32_t cap; + uint32_t next_off; /* offset the next call must start at */ + uint32_t calls; + uint32_t fail_at; /* 0 = never fail */ + int out_of_order; + int overflow; +}; + +static int capture_write(void *ctx, uint32_t off, const uint8_t *buf, + uint32_t len) +{ + struct capture *c = (struct capture *)ctx; + + c->calls++; + if ((c->fail_at != 0) && (c->calls >= c->fail_at)) { + return -1; + } + /* The contract is in order, contiguous and exactly once. */ + if (off != c->next_off) { + c->out_of_order = 1; + return -1; + } + if ((off + len) > c->cap) { + c->overflow = 1; + return -1; + } + memcpy(c->buf + off, buf, len); + c->next_off = off + len; + return 0; +} + +static void capture_init(struct capture *c, uint32_t cap) +{ + memset(c, 0, sizeof(*c)); + c->buf = malloc(cap ? cap : 1); + ck_assert_ptr_nonnull(c->buf); + c->cap = cap; +} + +static void capture_free(struct capture *c) +{ + free(c->buf); + c->buf = NULL; +} + +/* Inflate `comp` through the sink and compare against `expect`. */ +static int sink_roundtrip(const uint8_t *comp, uint32_t comp_len, + const uint8_t *expect, uint32_t expect_len, int window_bits, + uint32_t out_max) +{ + uint8_t *scratch; + struct capture c; + uint32_t out_len = 0xFFFFFFFFu; + int ret; + + scratch = malloc(GZIP_SINK_BUF_SIZE(GZIP_SINK_WINDOW_MAX_BITS)); + ck_assert_ptr_nonnull(scratch); + capture_init(&c, expect_len); + + ret = wolfBoot_gunzip_sink(comp, comp_len, NULL, NULL, scratch, + (uint32_t)window_bits, capture_write, &c, out_max, &out_len); + + if (ret == 0) { + ck_assert_int_eq(c.out_of_order, 0); + ck_assert_int_eq(c.overflow, 0); + ck_assert_uint_eq(out_len, expect_len); + ck_assert_uint_eq(c.next_off, expect_len); + if (expect_len > 0) { + ck_assert_mem_eq(c.buf, expect, expect_len); + } + } + capture_free(&c); + free(scratch); + return ret; +} + +/* ------------------------------------------------------------------------- */ +/* A source that hands the stream over a piece at a time */ +/* ------------------------------------------------------------------------- */ + +struct source { + const uint8_t *data; + uint32_t len; /* what the source will actually serve */ + uint32_t claimed; /* what the decoder is told the stream is */ + uint32_t calls; + uint32_t fail_at; /* 0 = never fail */ + uint32_t next_off; /* reads must not go backwards */ + int out_of_order; + int over_read; +}; + +static int source_read(void *ctx, uint32_t off, uint8_t *buf, uint32_t len) +{ + struct source *src = (struct source *)ctx; + + src->calls++; + if ((src->fail_at != 0) && (src->calls >= src->fail_at)) { + return -1; + } + if (off < src->next_off) { + src->out_of_order = 1; + return -1; + } + if ((off + len) > src->len) { + src->over_read = 1; + return -1; + } + memcpy(buf, src->data + off, len); + src->next_off = off; + return 0; +} + +/* Inflate through a reader and compare, as sink_roundtrip() does in place. */ +static int reader_roundtrip(const uint8_t *comp, uint32_t comp_len, + const uint8_t *expect, uint32_t expect_len, int window_bits, + uint32_t claimed, uint32_t fail_at) +{ + uint8_t *scratch; + struct capture c; + struct source src; + uint32_t out_len = 0xFFFFFFFFu; + int ret; + + scratch = malloc(GZIP_SINK_BUF_SIZE(GZIP_SINK_WINDOW_MAX_BITS)); + ck_assert_ptr_nonnull(scratch); + capture_init(&c, expect_len); + memset(&src, 0, sizeof(src)); + src.data = comp; + src.len = comp_len; + src.claimed = (claimed == 0) ? comp_len : claimed; + src.fail_at = fail_at; + + ret = wolfBoot_gunzip_sink(NULL, src.claimed, source_read, &src, + scratch, (uint32_t)window_bits, capture_write, &c, SINK_MAX_OUT, + &out_len); + + ck_assert_int_eq(src.out_of_order, 0); + ck_assert_int_eq(src.over_read, 0); + if (ret == 0) { + ck_assert_uint_eq(out_len, expect_len); + if (expect_len > 0) { + ck_assert_mem_eq(c.buf, expect, expect_len); + } + } + capture_free(&c); + free(scratch); + return ret; +} + +/* ------------------------------------------------------------------------- */ +/* Positive cases */ +/* ------------------------------------------------------------------------- */ + +/* The same stream through both entry points must produce the same bytes. */ +START_TEST(test_sink_matches_flat_decoder) +{ + static const uint32_t sizes[] = { 0, 1, 300, 5000, 70000, 400000 }; + uint8_t *raw, *comp, *flat; + uint32_t comp_len, flat_len; + unsigned si; + int bits; + + raw = malloc(400000); + flat = malloc(400000); + ck_assert_ptr_nonnull(raw); + ck_assert_ptr_nonnull(flat); + fill_mixed(raw, 400000, 1); + + for (bits = GZIP_SINK_WINDOW_MIN_BITS; bits <= GZIP_SINK_WINDOW_MAX_BITS; + bits++) { + for (si = 0; si < (sizeof(sizes) / sizeof(sizes[0])); si++) { + comp = gz_deflate_window(raw, sizes[si], bits, 9, &comp_len); + + ck_assert_int_eq(wolfBoot_gunzip(comp, comp_len, flat, 400000, + &flat_len), 0); + ck_assert_uint_eq(flat_len, sizes[si]); + + ck_assert_int_eq(sink_roundtrip(comp, comp_len, flat, flat_len, + bits, SINK_MAX_OUT), 0); + free(comp); + } + } + free(flat); + free(raw); +} +END_TEST + +/* A stored block can be 64KB, larger than the scratch buffer, so it is + * drained mid-copy rather than bounded up front. */ +START_TEST(test_sink_stored_blocks) +{ + uint8_t *raw, *comp; + uint32_t comp_len, i; + unsigned seed = 7; + + raw = malloc(300000); + ck_assert_ptr_nonnull(raw); + for (i = 0; i < 300000; i++) { + seed = seed * 1103515245u + 12345u; + raw[i] = (uint8_t)(seed >> 16); + } + comp = gz_deflate_window(raw, 300000, GZIP_SINK_WINDOW_MIN_BITS, 0, + &comp_len); + + ck_assert_int_eq(sink_roundtrip(comp, comp_len, raw, 300000, + GZIP_SINK_WINDOW_MIN_BITS, SINK_MAX_OUT), 0); + + free(comp); + free(raw); +} +END_TEST + +/* Matches reaching a full window back must still resolve once the window + * has started sliding. */ +START_TEST(test_sink_max_distance_matches) +{ + uint8_t *raw, *comp; + uint32_t comp_len, i; + + raw = malloc(300000); + ck_assert_ptr_nonnull(raw); + for (i = 0; i < 300000; i++) { + raw[i] = (uint8_t)(i % 251); + } + comp = gz_deflate_window(raw, 300000, GZIP_SINK_WINDOW_MAX_BITS, 9, + &comp_len); + + ck_assert_int_eq(sink_roundtrip(comp, comp_len, raw, 300000, + GZIP_SINK_WINDOW_MAX_BITS, SINK_MAX_OUT), 0); + + free(comp); + free(raw); +} +END_TEST + +/* ------------------------------------------------------------------------- */ +/* Negative cases */ +/* ------------------------------------------------------------------------- */ + +/* The scratch buffer holds two windows, so a stream using a larger window + * than declared could be decoded by accident. It must be refused instead, + * because the manifest's window is what sized this build's buffer. */ +START_TEST(test_sink_rejects_distance_beyond_window) +{ + uint8_t *raw, *comp; + uint32_t comp_len, i; + unsigned seed = 99; + + raw = malloc(28000); + ck_assert_ptr_nonnull(raw); + for (i = 0; i < 28000; i++) { + seed = seed * 1103515245u + 12345u; + raw[i] = (uint8_t)(seed >> 16); + } + /* Repeat an incompressible run 20000 bytes on, which can only be coded + * as a distance far beyond a 4KB window. */ + memcpy(raw + 20000, raw, 8000); + comp = gz_deflate_window(raw, 28000, GZIP_SINK_WINDOW_MAX_BITS, 9, + &comp_len); + + ck_assert_int_eq(sink_roundtrip(comp, comp_len, raw, 28000, + GZIP_SINK_WINDOW_MAX_BITS, SINK_MAX_OUT), 0); + ck_assert_int_eq(sink_roundtrip(comp, comp_len, raw, 28000, + GZIP_SINK_WINDOW_MIN_BITS, SINK_MAX_OUT), WOLFBOOT_GZIP_E_DISTANCE); + + free(comp); + free(raw); +} +END_TEST + +/* out_max is the capacity of the real destination, so it is enforced even + * though the decoder never holds the whole output. */ +START_TEST(test_sink_out_max_enforced) +{ + uint8_t *raw, *comp; + uint32_t comp_len; + + raw = malloc(100000); + ck_assert_ptr_nonnull(raw); + fill_mixed(raw, 100000, 3); + comp = gz_deflate_window(raw, 100000, 14, 9, &comp_len); + + ck_assert_int_eq(sink_roundtrip(comp, comp_len, raw, 100000, 14, 100000), + 0); + ck_assert_int_eq(sink_roundtrip(comp, comp_len, raw, 100000, 14, 99999), + WOLFBOOT_GZIP_E_OUTPUT); + + free(comp); + free(raw); +} +END_TEST + +START_TEST(test_sink_rejects_corrupt_and_truncated) +{ + uint8_t *raw, *comp; + uint32_t comp_len; + + raw = malloc(50000); + ck_assert_ptr_nonnull(raw); + fill_mixed(raw, 50000, 5); + + comp = gz_deflate_window(raw, 50000, 14, 9, &comp_len); + ck_assert_int_ne(sink_roundtrip(comp, comp_len - 9, raw, 50000, 14, + SINK_MAX_OUT), 0); + comp[comp_len / 2] ^= 0x40; + ck_assert_int_ne(sink_roundtrip(comp, comp_len, raw, 50000, 14, + SINK_MAX_OUT), 0); + + free(comp); + free(raw); +} +END_TEST + +/* A sink that cannot take the bytes must abort the inflate, not be ignored. */ +START_TEST(test_sink_write_failure_propagates) +{ + uint8_t *raw, *comp, *scratch; + struct capture c; + uint32_t comp_len, out_len; + + raw = malloc(200000); + ck_assert_ptr_nonnull(raw); + fill_mixed(raw, 200000, 11); + comp = gz_deflate_window(raw, 200000, 12, 9, &comp_len); + scratch = malloc(GZIP_SINK_BUF_SIZE(12)); + ck_assert_ptr_nonnull(scratch); + + capture_init(&c, 200000); + c.fail_at = 2; + ck_assert_int_eq(wolfBoot_gunzip_sink(comp, comp_len, NULL, NULL, scratch, + 12, capture_write, &c, SINK_MAX_OUT, &out_len), + WOLFBOOT_GZIP_E_OUTPUT); + capture_free(&c); + + free(scratch); + free(comp); + free(raw); +} +END_TEST + +START_TEST(test_sink_param_validation) +{ + uint8_t *raw, *comp, *scratch; + struct capture c; + uint32_t comp_len, out_len; + + raw = malloc(1000); + ck_assert_ptr_nonnull(raw); + fill_mixed(raw, 1000, 13); + comp = gz_deflate_window(raw, 1000, 14, 9, &comp_len); + scratch = malloc(GZIP_SINK_BUF_SIZE(GZIP_SINK_WINDOW_MAX_BITS)); + ck_assert_ptr_nonnull(scratch); + capture_init(&c, 1000); + + ck_assert_int_eq(wolfBoot_gunzip_sink(NULL, comp_len, NULL, NULL, scratch, + 14, capture_write, &c, SINK_MAX_OUT, &out_len), + WOLFBOOT_GZIP_E_PARAM); + ck_assert_int_eq(wolfBoot_gunzip_sink(comp, comp_len, NULL, NULL, NULL, + 14, capture_write, &c, SINK_MAX_OUT, &out_len), + WOLFBOOT_GZIP_E_PARAM); + ck_assert_int_eq(wolfBoot_gunzip_sink(comp, comp_len, NULL, NULL, scratch, + 14, NULL, &c, SINK_MAX_OUT, &out_len), WOLFBOOT_GZIP_E_PARAM); + ck_assert_int_eq(wolfBoot_gunzip_sink(comp, comp_len, NULL, NULL, scratch, + 14, capture_write, &c, SINK_MAX_OUT, NULL), + WOLFBOOT_GZIP_E_PARAM); + /* Outside the window range the scratch buffer was sized for. */ + ck_assert_int_eq(wolfBoot_gunzip_sink(comp, comp_len, NULL, NULL, scratch, + GZIP_SINK_WINDOW_MIN_BITS - 1, capture_write, &c, SINK_MAX_OUT, + &out_len), WOLFBOOT_GZIP_E_PARAM); + ck_assert_int_eq(wolfBoot_gunzip_sink(comp, comp_len, NULL, NULL, scratch, + GZIP_SINK_WINDOW_MAX_BITS + 1, capture_write, &c, SINK_MAX_OUT, + &out_len), WOLFBOOT_GZIP_E_PARAM); + + capture_free(&c); + free(scratch); + free(comp); + free(raw); +} +END_TEST + +/* A stream pulled in through a callback must decode to the same bytes as + * the same stream read in place. This is the net for the input refactor: + * the parser's bounds now mean "left in the stream", not "left in the + * buffer", and getting one wrong truncates instead of rejecting. */ +START_TEST(test_reader_matches_in_place) +{ + static const uint32_t sizes[] = { 0, 1, 300, 5000, 70000, 400000 }; + uint8_t *raw, *comp, *flat; + uint32_t comp_len, flat_len; + unsigned si; + int bits; + + raw = malloc(400000); + flat = malloc(400000); + ck_assert_ptr_nonnull(raw); + ck_assert_ptr_nonnull(flat); + fill_mixed(raw, 400000, 21); + + for (bits = GZIP_SINK_WINDOW_MIN_BITS; bits <= GZIP_SINK_WINDOW_MAX_BITS; + bits++) { + for (si = 0; si < (sizeof(sizes) / sizeof(sizes[0])); si++) { + comp = gz_deflate_window(raw, sizes[si], bits, 9, &comp_len); + ck_assert_int_eq(wolfBoot_gunzip(comp, comp_len, flat, 400000, + &flat_len), 0); + ck_assert_int_eq(reader_roundtrip(comp, comp_len, flat, flat_len, + bits, 0, 0), 0); + free(comp); + } + } + free(flat); + free(raw); +} +END_TEST + +/* Stored blocks are copied straight from the input, so they exercise the + * refill inside the copy rather than through the bit reader. */ +START_TEST(test_reader_stored_blocks) +{ + uint8_t *raw, *comp; + uint32_t comp_len, i; + unsigned seed = 31; + + raw = malloc(300000); + ck_assert_ptr_nonnull(raw); + for (i = 0; i < 300000; i++) { + seed = seed * 1103515245u + 12345u; + raw[i] = (uint8_t)(seed >> 16); + } + comp = gz_deflate_window(raw, 300000, GZIP_SINK_WINDOW_MIN_BITS, 0, + &comp_len); + ck_assert_int_eq(reader_roundtrip(comp, comp_len, raw, 300000, + GZIP_SINK_WINDOW_MIN_BITS, 0, 0), 0); + free(comp); + free(raw); +} +END_TEST + +/* A source that stops early, or that was described as longer than it is, + * must be rejected rather than yielding a short image. */ +START_TEST(test_reader_truncation_is_rejected) +{ + uint8_t *raw, *comp; + uint32_t comp_len; + + raw = malloc(200000); + ck_assert_ptr_nonnull(raw); + fill_mixed(raw, 200000, 23); + comp = gz_deflate_window(raw, 200000, 14, 9, &comp_len); + + /* The reader fails part way through. */ + ck_assert_int_ne(reader_roundtrip(comp, comp_len, raw, 200000, 14, 0, 3), + 0); + /* The stream is declared shorter than it is. */ + ck_assert_int_ne(reader_roundtrip(comp, comp_len, raw, 200000, 14, + comp_len - 64, 0), 0); + + free(comp); + free(raw); +} +END_TEST + +/* Alternating compressible and incompressible runs make deflate emit both + * Huffman and stored blocks, so the stored decoder and the block driver each + * read s->in_end straight after the Huffman decoder returned. That is where + * a cursor the decoder failed to write back shows up. */ +START_TEST(test_reader_mixed_block_types) +{ + uint8_t *raw, *comp; + uint32_t comp_len, i; + unsigned seed = 77; + int bits; + + raw = malloc(200000); + ck_assert_ptr_nonnull(raw); + for (i = 0; i < 200000; i++) { + if ((i / 8192) % 2 == 0) { + raw[i] = (uint8_t)('a' + (i % 3)); + } + else { + seed = seed * 1103515245u + 12345u; + raw[i] = (uint8_t)(seed >> 16); + } + } + for (bits = GZIP_SINK_WINDOW_MIN_BITS; bits <= GZIP_SINK_WINDOW_MAX_BITS; + bits++) { + comp = gz_deflate_window(raw, 200000, bits, 9, &comp_len); + ck_assert_int_eq(reader_roundtrip(comp, comp_len, raw, 200000, bits, + 0, 0), 0); + free(comp); + /* Level 0 is all stored blocks, which refill inside the copy. */ + comp = gz_deflate_window(raw, 200000, bits, 0, &comp_len); + ck_assert_int_eq(reader_roundtrip(comp, comp_len, raw, 200000, bits, + 0, 0), 0); + free(comp); + } + free(raw); +} +END_TEST + +START_TEST(test_reader_param_validation) +{ + uint8_t *scratch; + struct capture c; + struct source src; + uint32_t out_len; + uint8_t dummy[8]; + + scratch = malloc(GZIP_SINK_BUF_SIZE(GZIP_SINK_WINDOW_MAX_BITS)); + ck_assert_ptr_nonnull(scratch); + capture_init(&c, 8); + memset(&src, 0, sizeof(src)); + memset(dummy, 0, sizeof(dummy)); + + /* Neither input form, and both at once, are equally wrong. */ + ck_assert_int_eq(wolfBoot_gunzip_sink(NULL, 8, NULL, NULL, scratch, 14, + capture_write, &c, SINK_MAX_OUT, &out_len), WOLFBOOT_GZIP_E_PARAM); + ck_assert_int_eq(wolfBoot_gunzip_sink(dummy, 8, source_read, &src, + scratch, 14, capture_write, &c, SINK_MAX_OUT, &out_len), + WOLFBOOT_GZIP_E_PARAM); + + capture_free(&c); + free(scratch); +} +END_TEST + +/* ------------------------------------------------------------------------- */ + +static Suite *gzip_sink_suite(void) +{ + Suite *s = suite_create("wolfBoot-gzip-sink"); + TCase *tc = tcase_create("sink"); + + tcase_set_timeout(tc, 60); + tcase_add_test(tc, test_sink_matches_flat_decoder); + tcase_add_test(tc, test_sink_stored_blocks); + tcase_add_test(tc, test_sink_max_distance_matches); + tcase_add_test(tc, test_sink_rejects_distance_beyond_window); + tcase_add_test(tc, test_sink_out_max_enforced); + tcase_add_test(tc, test_sink_rejects_corrupt_and_truncated); + tcase_add_test(tc, test_sink_write_failure_propagates); + tcase_add_test(tc, test_sink_param_validation); + tcase_add_test(tc, test_reader_matches_in_place); + tcase_add_test(tc, test_reader_stored_blocks); + tcase_add_test(tc, test_reader_truncation_is_rejected); + tcase_add_test(tc, test_reader_mixed_block_types); + tcase_add_test(tc, test_reader_param_validation); + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int failed; + SRunner *sr = srunner_create(gzip_sink_suite()); + srunner_run_all(sr, CK_NORMAL); + failed = srunner_ntests_failed(sr); + srunner_free(sr); + return failed ? 1 : 0; +} diff --git a/tools/unit-tests/unit-sign-encrypted-output.c b/tools/unit-tests/unit-sign-encrypted-output.c index 8d8de172e8..bebc0f4d2f 100644 --- a/tools/unit-tests/unit-sign-encrypted-output.c +++ b/tools/unit-tests/unit-sign-encrypted-output.c @@ -851,7 +851,7 @@ START_TEST(test_header_required_size_covers_all_branches) static const int sign_opts[] = { NO_SIGN, SIGN_ED25519, SIGN_ECC256, SIGN_RSA2048 }; static const int hash_opts[] = { HASH_SHA256, HASH_SHA384, HASH_SHA3 }; - int s, h, policy, no_ts, is_diff, has_dts; + int s, h, policy, no_ts, is_diff, has_dts, compress; ck_assert_ptr_nonnull(mkdtemp(tempdir)); @@ -881,6 +881,7 @@ START_TEST(test_header_required_size_covers_all_branches) for (no_ts = 0; no_ts <= 1; no_ts++) { for (is_diff = 0; is_diff <= 1; is_diff++) { for (has_dts = 0; has_dts <= 1; has_dts++) { + for (compress = 0; compress <= 1; compress++) { uint32_t required; uint32_t idx; int ret; @@ -913,6 +914,12 @@ START_TEST(test_header_required_size_covers_all_branches) if (is_diff) { CMD.no_base_sha = 1; } + /* Any size will do; the writer only emits + * the tag. */ + if (compress && !is_diff) { + CMD.compress = 1; + CMD.uncompressed_sz = 0x1234; + } /* Model first, on the clean CMD we just set. */ required = header_required_size(is_diff, 0, 0); @@ -925,6 +932,7 @@ START_TEST(test_header_required_size_covers_all_branches) ck_assert_uint_le(idx, required); unlink(output_path); } + } } } }