Skip to content

Commit dbfeaae

Browse files
committed
ci: add interrupted-OTA swap-move canary
Two tardigrade sweeps on nrf52840dk/nrf52840 with Zephyr pinned to v3.7.0: power loss across the v1->v2 swap-move upgrade, and power loss across the revert of a completed-but-unconfirmed upgrade, the failure class of issue #1966 (PR #2100) and PR #2199. The tardigrade action is pinned by full commit SHA; that revision pins Renode to a versioned release archive verified by SHA-256 before extraction, runs with strict profile validation, and resolves profile assets only inside the caller workspace. The platform description is fetched from the same pinned revision. Signed-off-by: Neil Berkman <neil@xuku.com>
1 parent 74f4d64 commit dbfeaae

1 file changed

Lines changed: 266 additions & 0 deletions

File tree

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
name: OTA resilience canary
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- .github/workflows/ota-resilience-canary.yml
9+
- boot/**
10+
- scripts/**
11+
- zephyr/**
12+
- root-rsa-2048.pem
13+
schedule:
14+
- cron: '0 8 * * 1'
15+
workflow_dispatch:
16+
inputs:
17+
zephyr_ref:
18+
description: 'Which Zephyr ref should be used?'
19+
required: true
20+
default: 'main'
21+
22+
permissions:
23+
contents: read
24+
25+
concurrency:
26+
group: ${{ github.workflow }}-${{ github.ref }}
27+
cancel-in-progress: true
28+
29+
env:
30+
ZEPHYR_VERSION: main
31+
# Tardigrade revision used both for the action pin below and for
32+
# fetching the platform description; keep the two in sync.
33+
TARDIGRADE_REF: 64840d247635c1e02588c0b12a42e289aede19f5
34+
35+
jobs:
36+
swap-move-canary:
37+
runs-on: ubuntu-24.04
38+
container:
39+
image: zephyrprojectrtos/ci-base:v0.29.0
40+
options: '--entrypoint /bin/bash'
41+
timeout-minutes: 90
42+
defaults:
43+
run:
44+
shell: bash
45+
46+
steps:
47+
- name: Set Zephyr version for manual runs
48+
if: github.event_name == 'workflow_dispatch'
49+
run: |
50+
echo "ZEPHYR_VERSION=${{ github.event.inputs.zephyr_ref }}" >> $GITHUB_ENV
51+
52+
- name: Checkout Zephyr
53+
uses: actions/checkout@v4
54+
with:
55+
repository: zephyrproject-rtos/zephyr
56+
ref: ${{ env.ZEPHYR_VERSION }}
57+
path: repos/zephyr
58+
59+
- name: Install Zephyr Python requirements
60+
working-directory: repos/zephyr
61+
run: pip install -r scripts/requirements-actions.txt --require-hashes
62+
63+
- name: Setup Zephyr SDK and workspace
64+
uses: zephyrproject-rtos/action-zephyr-setup@360ff9b36e58499d9eb28015cdcde7ca03a5b04d
65+
with:
66+
base-path: repos/zephyr
67+
toolchains: arm-zephyr-eabi
68+
sdk-version: 1.0.0
69+
west-project-filter: -.*,+cmsis,+cmsis_6,+hal_nordic,+mbedtls,+mcuboot,+tinycrypt,+zcbor,+tf-psa-crypto
70+
ccache-max-size: 256MB
71+
72+
- name: Checkout MCUboot
73+
uses: actions/checkout@v4
74+
with:
75+
fetch-depth: 0
76+
path: repos/bootloader/mcuboot
77+
78+
- name: Build MCUboot bootloader (swap-move, RSA-2048)
79+
working-directory: repos
80+
run: |
81+
export ZEPHYR_BASE=$(pwd)/zephyr
82+
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
83+
west build -b nrf52840dk/nrf52840 bootloader/mcuboot/boot/zephyr \
84+
-d build_boot \
85+
-p always \
86+
-- \
87+
-DCONFIG_BOOT_SWAP_USING_MOVE=y \
88+
-DCONFIG_BOOT_SIGNATURE_TYPE_RSA=y \
89+
-DCONFIG_BOOT_SIGNATURE_TYPE_RSA_LEN=2048 \
90+
-DCONFIG_BOOT_VALIDATE_SLOT0=n
91+
92+
- name: Build test application images
93+
working-directory: repos
94+
run: |
95+
set -euo pipefail
96+
export ZEPHYR_BASE=$(pwd)/zephyr
97+
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
98+
pip3 install --quiet click intelhex cbor2 cryptography
99+
100+
# Build hello_world as the test application.
101+
west build -b nrf52840dk/nrf52840 zephyr/samples/hello_world \
102+
-d build_app \
103+
-p always \
104+
-- \
105+
-DCONFIG_BOOTLOADER_MCUBOOT=y
106+
107+
IMGTOOL="python3 bootloader/mcuboot/scripts/imgtool.py"
108+
KEY="bootloader/mcuboot/root-rsa-2048.pem"
109+
110+
# Upgrade scenario images.
111+
# v1 (exec slot): confirmed so MCUboot boots it directly.
112+
${IMGTOOL} sign --key ${KEY} \
113+
--header-size 0x200 --align 4 --slot-size 0x76000 \
114+
--version 1.0.0 --pad --confirm \
115+
build_app/zephyr/zephyr.bin slot0.bin
116+
# v2 (staging slot): pending upgrade, trailer magic from --pad.
117+
${IMGTOOL} sign --key ${KEY} \
118+
--header-size 0x200 --align 4 --slot-size 0x76000 \
119+
--version 2.0.0 --pad \
120+
build_app/zephyr/zephyr.bin slot1.bin
121+
122+
# Revert scenario images: no --pad/--confirm, so neither slot
123+
# carries trailer state; the profile's update_trigger writes the
124+
# post-upgrade-unconfirmed trailer that starts a revert.
125+
${IMGTOOL} sign --key ${KEY} \
126+
--header-size 0x200 --align 4 --slot-size 0x76000 \
127+
--version 1.0.0 \
128+
build_app/zephyr/zephyr.bin slot0_plain.bin
129+
${IMGTOOL} sign --key ${KEY} \
130+
--header-size 0x200 --align 4 --slot-size 0x76000 \
131+
--version 2.0.0 \
132+
build_app/zephyr/zephyr.bin slot1_plain.bin
133+
134+
ls -la slot0.bin slot1.bin slot0_plain.bin slot1_plain.bin
135+
136+
- name: Fetch pinned platform description
137+
working-directory: repos
138+
run: |
139+
set -euo pipefail
140+
mkdir -p platforms
141+
python3 -c "import urllib.request; urllib.request.urlretrieve(
142+
'https://raw.githubusercontent.com/neilberkman/tardigrade/${TARDIGRADE_REF}/platforms/cortex_m4_flash_fast.repl',
143+
'platforms/cortex_m4_flash_fast.repl')"
144+
head -3 platforms/cortex_m4_flash_fast.repl
145+
146+
- name: Write canary profiles
147+
working-directory: repos
148+
run: |
149+
cat > ota_upgrade_profile.yaml << 'YAML'
150+
schema_version: 1
151+
name: mcuboot_swap_move_upgrade_canary
152+
description: >
153+
Post-merge OTA resilience canary for MCUboot swap-using-move on
154+
nrf52840dk. Upgrade scenario: exec has confirmed v1, staging has
155+
pending v2. After the swap, exec should contain the v2 image at
156+
every power-loss fault point.
157+
158+
platform: platforms/cortex_m4_flash_fast.repl
159+
flash_backend: faultFlash
160+
bootloader:
161+
elf: build_boot/zephyr/zephyr.elf
162+
entry: 0x00000000
163+
memory:
164+
sram: { start: 0x20000000, end: 0x20040000 }
165+
write_granularity: 4
166+
slots:
167+
exec: { base: 0x0000C000, size: 0x76000 }
168+
staging: { base: 0x00082000, size: 0x76000 }
169+
images:
170+
exec: slot0.bin
171+
staging: slot1.bin
172+
success_criteria:
173+
vtor_in_slot: exec
174+
image_hash: true
175+
expected_image: staging
176+
fault_sweep:
177+
mode: runtime
178+
evaluation_mode: execute
179+
max_writes: auto
180+
max_writes_cap: 200000
181+
run_duration: "5.0"
182+
max_step_limit: 50000000
183+
sweep_hash_bypass_symbols: ["bootutil_img_validate"]
184+
expect:
185+
should_find_issues: false
186+
YAML
187+
188+
cat > ota_revert_profile.yaml << 'YAML'
189+
schema_version: 1
190+
name: mcuboot_swap_move_revert_canary
191+
description: >
192+
Post-merge OTA resilience canary for MCUboot swap-using-move on
193+
nrf52840dk. Revert scenario: an upgrade to v2 completed but was
194+
never confirmed, so the next boot reverts to v1. Power loss at
195+
any point of the revert, including inside the trailer fixup,
196+
must still end with v1 restored in exec. Covers the failure
197+
class of issue #1966 (PR #2100).
198+
199+
platform: platforms/cortex_m4_flash_fast.repl
200+
flash_backend: faultFlash
201+
bootloader:
202+
elf: build_boot/zephyr/zephyr.elf
203+
entry: 0x00000000
204+
memory:
205+
sram: { start: 0x20000000, end: 0x20040000 }
206+
write_granularity: 4
207+
slots:
208+
exec: { base: 0x0000C000, size: 0x76000 }
209+
staging: { base: 0x00082000, size: 0x76000 }
210+
images:
211+
exec: slot1_plain.bin
212+
staging: slot0_plain.bin
213+
update_trigger:
214+
type: mcuboot_trailer_magic
215+
slot: exec
216+
copy_done: 1
217+
success_criteria:
218+
vtor_in_slot: exec
219+
image_hash: true
220+
expected_image: staging
221+
fault_sweep:
222+
mode: runtime
223+
evaluation_mode: execute
224+
max_writes: auto
225+
max_writes_cap: 200000
226+
run_duration: "20.0"
227+
phase1_time_slice: "0.10"
228+
max_step_limit: 20000000
229+
sweep_hash_bypass_symbols: ["bootutil_img_validate"]
230+
expect:
231+
should_find_issues: false
232+
YAML
233+
234+
sed -i 's/^ //' ota_upgrade_profile.yaml ota_revert_profile.yaml
235+
cat ota_upgrade_profile.yaml ota_revert_profile.yaml
236+
237+
- name: Run upgrade resilience sweep
238+
id: ota_upgrade
239+
uses: neilberkman/tardigrade@64840d247635c1e02588c0b12a42e289aede19f5
240+
with:
241+
profile: repos/ota_upgrade_profile.yaml
242+
asset-root: repos
243+
quick: "false"
244+
workers: "2"
245+
246+
- name: Run revert resilience sweep
247+
id: ota_revert
248+
if: always() && contains(fromJSON('["success", "failure"]'), steps.ota_upgrade.conclusion)
249+
uses: neilberkman/tardigrade@64840d247635c1e02588c0b12a42e289aede19f5
250+
with:
251+
profile: repos/ota_revert_profile.yaml
252+
asset-root: repos
253+
quick: "false"
254+
workers: "2"
255+
256+
- name: Upload artifacts
257+
if: always()
258+
uses: actions/upload-artifact@v4
259+
with:
260+
name: ota-resilience-swap-move
261+
if-no-files-found: ignore
262+
path: |
263+
repos/ota_upgrade_profile.yaml
264+
repos/ota_revert_profile.yaml
265+
${{ steps.ota_upgrade.outputs.report-path }}
266+
${{ steps.ota_revert.outputs.report-path }}

0 commit comments

Comments
 (0)