From 492bced3e3eb33fc76e0bacb588e5f3a4122d304 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:13:59 -0500 Subject: [PATCH 1/3] fix(uf2): reject an app-only image aimed below the SoftDevice end --- changelog.md | 1 + src/usb/uf2/ghostfat.c | 10 ++++++++++ src/usb/uf2/uf2.h | 1 + 3 files changed, 12 insertions(+) diff --git a/changelog.md b/changelog.md index 520294b0..5eb5fdd9 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ it and is upstream Adafruit history, kept for provenance. - A DFU session started after a SoftDevice update no longer ends before it begins. - Build change, no behavior change: the bootloader is now compiled with `-flto`, which shrinks every image by about 3.3 KB of flash. Section addresses and SoftDevice call sites are unchanged. - The lazy-erase packet buffer holds 16 packets instead of 8. Costs about 2 KB of `.bss`. +- A UF2 whose application blocks start below the installed SoftDevice's end and carry no SoftDevice (no block at 0x1000) is now rejected as WRONG FILE and aborts the update. An app-only image built for a different SoftDevice would have overwritten the last page of the installed one. SoftDevice+app images, app-only images at the application start, and `CURRENT.UF2` restores are unaffected; the check assumes the host writes the file front to back. ## OTAFIX 2.5 diff --git a/src/usb/uf2/ghostfat.c b/src/usb/uf2/ghostfat.c index b09c9f7e..fc968617 100644 --- a/src/usb/uf2/ghostfat.c +++ b/src/usb/uf2/ghostfat.c @@ -497,6 +497,16 @@ int write_block (uint32_t block_no, uint8_t *data, WriteState *state) */ if ( in_app_space(bl->targetAddr) ) { + // An image carrying a SoftDevice starts at its first page. An app-only image aimed + // below the app start was built for another SoftDevice and would overwrite its last + // page. Assumes the host writes the file front to back. + if ( bl->targetAddr == USER_FLASH_START ) state->sd_start_seen = true; + if ( !state->sd_start_seen && bl->targetAddr < DFU_BANK_0_REGION_START ) + { + state->aborted = true; + goto reject; + } + screen_set_title(NULL); // a good block after a rejected one clears WRONG FILE PRINTF("Write addr = 0x%08lX, block = %ld (%ld of %ld)\r\n", bl->targetAddr, bl->blockNo, state->numWritten, bl->numBlocks); flash_nrf5x_write(bl->targetAddr, bl->data, bl->payloadSize, true); diff --git a/src/usb/uf2/uf2.h b/src/usb/uf2/uf2.h index 6fe0e9e1..3e3e17b1 100644 --- a/src/usb/uf2/uf2.h +++ b/src/usb/uf2/uf2.h @@ -59,6 +59,7 @@ typedef struct { bool has_uicr; // if containing uicr data bool boot_id_matches; // if bootloader id in cf2 config matches our VID/PID bool factory_erase; // if a CFG_UF2_MESHTASTIC_ERASE_ID block was received + bool sd_start_seen; // if the SoftDevice's first page (USER_FLASH_START) was received uint8_t writtenMask[MAX_BLOCKS / 8 + 1]; } WriteState; From 73d1322462b01245cb8ae0098aef9119999aad1c Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:40:36 -0500 Subject: [PATCH 2/3] fix(uf2): reject the whole wrong-SoftDevice file instead of aborting mid-copy --- changelog.md | 2 +- src/usb/uf2/ghostfat.c | 16 +++++++++------- src/usb/uf2/uf2.h | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/changelog.md b/changelog.md index 5eb5fdd9..3eb80f1c 100644 --- a/changelog.md +++ b/changelog.md @@ -12,7 +12,7 @@ it and is upstream Adafruit history, kept for provenance. - A DFU session started after a SoftDevice update no longer ends before it begins. - Build change, no behavior change: the bootloader is now compiled with `-flto`, which shrinks every image by about 3.3 KB of flash. Section addresses and SoftDevice call sites are unchanged. - The lazy-erase packet buffer holds 16 packets instead of 8. Costs about 2 KB of `.bss`. -- A UF2 whose application blocks start below the installed SoftDevice's end and carry no SoftDevice (no block at 0x1000) is now rejected as WRONG FILE and aborts the update. An app-only image built for a different SoftDevice would have overwritten the last page of the installed one. SoftDevice+app images, app-only images at the application start, and `CURRENT.UF2` restores are unaffected; the check assumes the host writes the file front to back. +- A UF2 whose application blocks start below the installed SoftDevice's end and carry no SoftDevice (no block at 0x1000) is now rejected as WRONG FILE: every block of the file is rejected and nothing is written. An app-only image built for a different SoftDevice would have overwritten the last page of the installed one. SoftDevice+app images, app-only images at the application start, and `CURRENT.UF2` restores are unaffected; the check assumes the host writes the file front to back. ## OTAFIX 2.5 diff --git a/src/usb/uf2/ghostfat.c b/src/usb/uf2/ghostfat.c index fc968617..e48a8e2f 100644 --- a/src/usb/uf2/ghostfat.c +++ b/src/usb/uf2/ghostfat.c @@ -497,15 +497,17 @@ int write_block (uint32_t block_no, uint8_t *data, WriteState *state) */ if ( in_app_space(bl->targetAddr) ) { - // An image carrying a SoftDevice starts at its first page. An app-only image aimed - // below the app start was built for another SoftDevice and would overwrite its last - // page. Assumes the host writes the file front to back. - if ( bl->targetAddr == USER_FLASH_START ) state->sd_start_seen = true; - if ( !state->sd_start_seen && bl->targetAddr < DFU_BANK_0_REGION_START ) + // An image carrying a SoftDevice starts at its first page. An app-only image aimed below + // the app start was built for another SoftDevice, so every block of that file is rejected + // and nothing is written. Assumes the host writes the file front to back. + if ( bl->blockNo == 0 ) { - state->aborted = true; - goto reject; + state->sd_start_seen = false; + state->wrong_layout = false; } + if ( bl->targetAddr == USER_FLASH_START ) state->sd_start_seen = true; + if ( !state->sd_start_seen && bl->targetAddr < DFU_BANK_0_REGION_START ) state->wrong_layout = true; + if ( state->wrong_layout ) goto reject; screen_set_title(NULL); // a good block after a rejected one clears WRONG FILE PRINTF("Write addr = 0x%08lX, block = %ld (%ld of %ld)\r\n", bl->targetAddr, bl->blockNo, state->numWritten, bl->numBlocks); diff --git a/src/usb/uf2/uf2.h b/src/usb/uf2/uf2.h index 3e3e17b1..b2114679 100644 --- a/src/usb/uf2/uf2.h +++ b/src/usb/uf2/uf2.h @@ -60,6 +60,7 @@ typedef struct { bool boot_id_matches; // if bootloader id in cf2 config matches our VID/PID bool factory_erase; // if a CFG_UF2_MESHTASTIC_ERASE_ID block was received bool sd_start_seen; // if the SoftDevice's first page (USER_FLASH_START) was received + bool wrong_layout; // if this file was built for another SoftDevice layout uint8_t writtenMask[MAX_BLOCKS / 8 + 1]; } WriteState; From f9f6fd70be1836807bdeb1344ad725ed0b9c8d93 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:19:48 -0500 Subject: [PATCH 3/3] fix(uf2): reject an app-only image that starts above the app start, and reset the layout check on every block 0 --- src/usb/uf2/ghostfat.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/usb/uf2/ghostfat.c b/src/usb/uf2/ghostfat.c index e48a8e2f..7fd1c943 100644 --- a/src/usb/uf2/ghostfat.c +++ b/src/usb/uf2/ghostfat.c @@ -495,16 +495,17 @@ int write_block (uint32_t block_no, uint8_t *data, WriteState *state) * | MBR | | MBR | * ------------- ------------- */ + // An image carrying a SoftDevice starts at or below its first page; an app-only image must + // start exactly at the app start. One built for another SoftDevice starts above or below it, + // so every block of that file is rejected and nothing is written. Block 0 opens a new file + // whatever its address (an MBR block at 0x0 included). Assumes the host writes front to back. + if ( bl->blockNo == 0 ) + { + state->sd_start_seen = false; + state->wrong_layout = (bl->targetAddr > DFU_BANK_0_REGION_START); + } if ( in_app_space(bl->targetAddr) ) { - // An image carrying a SoftDevice starts at its first page. An app-only image aimed below - // the app start was built for another SoftDevice, so every block of that file is rejected - // and nothing is written. Assumes the host writes the file front to back. - if ( bl->blockNo == 0 ) - { - state->sd_start_seen = false; - state->wrong_layout = false; - } if ( bl->targetAddr == USER_FLASH_START ) state->sd_start_seen = true; if ( !state->sd_start_seen && bl->targetAddr < DFU_BANK_0_REGION_START ) state->wrong_layout = true; if ( state->wrong_layout ) goto reject;