From 16365e1d04176ce0bd97dad9d041ab8bd23a9474 Mon Sep 17 00:00:00 2001 From: Yandrik Date: Wed, 29 Apr 2026 19:55:10 +0200 Subject: [PATCH] fix: align bootloader image validation limits --- docs/bootloader-ota-operator-guide.md | 4 +- lib/model/shifter_types.dart | 2 +- .../firmware_file_selection_service.dart | 11 +++++- .../firmware_file_selection_service_test.dart | 37 +++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/docs/bootloader-ota-operator-guide.md b/docs/bootloader-ota-operator-guide.md index b6027c3..9d737fc 100644 --- a/docs/bootloader-ota-operator-guide.md +++ b/docs/bootloader-ota-operator-guide.md @@ -18,7 +18,7 @@ This guide explains the Universal Shifters single-slot bootloader update flow in ## Image Requirements - File extension must be `.bin`. -- Image must be at least 8 bytes and no larger than `0x42000` bytes. +- Image must be at least 8 bytes and no larger than `0x3F000` bytes (252 KiB). - Image bytes must start at application address `0x00030000`. - Initial stack pointer must be aligned and within `0x20000000..=0x20010000`. - Reset vector must have the Thumb bit set and point inside the image after the first two vector words. @@ -40,7 +40,7 @@ This guide explains the Universal Shifters single-slot bootloader update flow in | Invalid stack pointer or reset vector | `.bin` is not a raw app image for `0x00030000` | Rebuild/export the application image from the correct linker layout. | | Could not connect to bootloader DFU mode | Phone did not find `US-DFU` after app reset | Move closer, retry, and verify the device is advertising `US-DFU`. | | Timed out waiting for bootloader DFU status | Status indication/read did not arrive | Reconnect to `US-DFU` and retry. | -| Bootloader status `bounds error` | Image length or app start rejected | Use a valid app image no larger than `0x42000` bytes. | +| Bootloader status `bounds error` | Image length or app start rejected | Use a valid app image no larger than `0x3F000` bytes (252 KiB). | | Bootloader status `CRC error` | Full-image CRC did not match flash contents | Re-export or re-download the `.bin`, then retry. | | Bootloader status `vector table error` | Bootloader rejected the written vector table | Rebuild firmware for app start `0x00030000`. | | Bootloader status `flash error` | Flash erase/write/read failed | Retry once; if repeated, service or externally reflash the device. | diff --git a/lib/model/shifter_types.dart b/lib/model/shifter_types.dart index aae5b29..a5a27c1 100644 --- a/lib/model/shifter_types.dart +++ b/lib/model/shifter_types.dart @@ -48,7 +48,7 @@ const int universalShifterAttWriteOverheadBytes = 3; const int universalShifterDfuPreferredMtu = 128; const int universalShifterDfuAppStart = 0x00030000; -const int universalShifterDfuAppSlotSizeBytes = 0x00042000; +const int universalShifterDfuAppSlotSizeBytes = 0x0003F000; const int universalShifterDfuMinimumImageLengthBytes = 8; const int universalShifterDfuRamStart = 0x20000000; const int universalShifterDfuRamEnd = 0x20010000; diff --git a/lib/service/firmware_file_selection_service.dart b/lib/service/firmware_file_selection_service.dart index f8f62a4..f59ba70 100644 --- a/lib/service/firmware_file_selection_service.dart +++ b/lib/service/firmware_file_selection_service.dart @@ -138,12 +138,14 @@ class FirmwareFileSelectionService { final vectorStackPointer = _readLeU32(selection.fileBytes, 0); final vectorReset = _readLeU32(selection.fileBytes, 4); + final sessionId = _normalizeSessionId(_sessionIdGenerator()); + final metadata = BootloaderDfuFirmwareMetadata( totalLength: selection.fileBytes.length, crc32: BootloaderDfuProtocol.crc32(selection.fileBytes), appStart: universalShifterDfuAppStart, imageVersion: 0, - sessionId: _sessionIdGenerator() & 0xFF, + sessionId: sessionId, flags: universalShifterDfuFlagNone, vectorStackPointer: vectorStackPointer, vectorReset: vectorReset, @@ -215,7 +217,12 @@ class FirmwareFileSelectionService { return ByteData.sublistView(bytes).getUint32(offset, Endian.little); } + static int _normalizeSessionId(int sessionId) { + final normalized = sessionId & 0xFF; + return normalized == 0 ? 1 : normalized; + } + static int _randomSessionId() { - return Random.secure().nextInt(256); + return Random.secure().nextInt(255) + 1; } } diff --git a/test/service/firmware_file_selection_service_test.dart b/test/service/firmware_file_selection_service_test.dart index c24e77c..9520036 100644 --- a/test/service/firmware_file_selection_service_test.dart +++ b/test/service/firmware_file_selection_service_test.dart @@ -120,6 +120,26 @@ void main() { result.failure?.reason, FirmwareSelectionFailureReason.imageTooLarge); }); + test('accepts image exactly at application slot size', () async { + final image = Uint8List(universalShifterDfuAppSlotSizeBytes); + _writeLeU32(image, 0, 0x20001000); + _writeLeU32(image, 4, 0x00030009); + final service = FirmwareFileSelectionService( + filePicker: _FakeFirmwareFilePicker( + selection: FirmwarePickerSelection( + fileName: 'firmware.bin', + fileBytes: image, + ), + ), + ); + + final result = await service.selectAndPrepareBootloaderDfu(); + + expect(result.isSuccess, isTrue); + expect(result.firmware?.metadata.totalLength, + universalShifterDfuAppSlotSizeBytes); + }); + test('rejects images with invalid vector table', () async { final image = _validBootloaderImage(); _writeLeU32(image, 0, 0x10001000); @@ -158,6 +178,23 @@ void main() { expect(second.firmware?.metadata.sessionId, 10); }); + test('normalizes generated zero session id to one', () async { + final service = FirmwareFileSelectionService( + filePicker: _FakeFirmwareFilePicker( + selection: FirmwarePickerSelection( + fileName: 'firmware.bin', + fileBytes: _validBootloaderImage(), + ), + ), + sessionIdGenerator: () => 0, + ); + + final result = await service.selectAndPrepareBootloaderDfu(); + + expect(result.isSuccess, isTrue); + expect(result.firmware?.metadata.sessionId, 1); + }); + test('maps picker read failure to explicit validation error', () async { final service = FirmwareFileSelectionService( filePicker: _FakeFirmwareFilePicker(