Skip to content

Instantly share code, notes, and snippets.

@jdlcdl
Last active August 14, 2023 11:12
Show Gist options
  • Save jdlcdl/5d5202d5b010ad3cbf3bab1b209e1992 to your computer and use it in GitHub Desktop.
Save jdlcdl/5d5202d5b010ad3cbf3bab1b209e1992 to your computer and use it in GitHub Desktop.

Avoiding SPI-Flash overflows in Krux

Something caught my eye recently in krux's src/krux/firmware.py here.

While this module guards against exceeding the MAX_FIRMWARE_SIZE, w/ this current setting, I believe that writing firmware to slot1 would overflow into slot2 with a firmware growth of less than 200k... at least for Maixpy Amigo TFT.

Testing/changes that I'm working with are here

Output of pytest tests/test_firmware2.py -vs

========================================== test session starts ===========================================
platform linux -- Python 3.10.12, pytest-6.2.5, py-1.11.0, pluggy-1.2.0 -- ~/.envs/krux/bin/python
cachedir: .pytest_cache
rootdir: ~/krux
plugins: cov-3.0.0, mock-3.11.1
collected 3 items                                                                                        

tests/test_firmware2.py::test_calculate_equisized_firmware_constants 
Calculating equisized firmware constants given:
upper_limit 0x0d00000 or 13631488 bytes
 chunk_size 0x010000 or 00065536 bytes
 first_slot 0x080000 or 00524288 bytes

 for 1 equisized slots:
 max_fw_gap 0xc80000 or 13107200 bytes, 200 chunks
     max_fw 0xc7ffdb or 13107163 bytes
     unused 0x000000 or 00000000 bytes

 for 2 equisized slots:
 max_fw_gap 0x640000 or 06553600 bytes, 100 chunks
     max_fw 0x63ffdb or 06553563 bytes
     unused 0x000000 or 00000000 bytes

 for 3 equisized slots:
 max_fw_gap 0x42aaaa or 04369066 bytes, 67 chunks
     max_fw 0x42ffdb or 04390875 bytes
     unused 0x000002 or 00000002 bytes

 for 4 equisized slots:
 max_fw_gap 0x320000 or 03276800 bytes, 50 chunks
     max_fw 0x31ffdb or 03276763 bytes
     unused 0x000000 or 00000000 bytes

 for 5 equisized slots:
 max_fw_gap 0x280000 or 02621440 bytes, 40 chunks
     max_fw 0x27ffdb or 02621403 bytes
     unused 0x000000 or 00000000 bytes

 for 6 equisized slots:
 max_fw_gap 0x215555 or 02184533 bytes, 34 chunks
     max_fw 0x21ffdb or 02228187 bytes
     unused 0x000002 or 00000002 bytes

 for 7 equisized slots:
 max_fw_gap 0x1c9249 or 01872457 bytes, 29 chunks
     max_fw 0x1cffdb or 01900507 bytes
     unused 0x000001 or 00000001 bytes

 for 8 equisized slots:
 max_fw_gap 0x190000 or 01638400 bytes, 25 chunks
     max_fw 0x18ffdb or 01638363 bytes
     unused 0x000000 or 00000000 bytes
PASSED
tests/test_firmware2.py::test_firmware_constants_guard_against_overflow PASSED
tests/test_firmware2.py::test_firmware_constants_consistent_w4096_aligned_sectors PASSED

=========================================== 3 passed in 0.66s ============================================

Writing to SPI Flash

Care should be taken when writing directly to SPI-Flash, because overflows can render the device seemingly a "brick". In my experience, it's always recoverable via ktool, as long as it can be turned 'off' and back 'on' again, so that it becomes visible in lsusb. In some cases, it is necessary for the battery to fully discharge first, else I imagine that the battery could be disconnected as well (I wish it were as easy as removing a jumper with the enclosure fully assembled).

While kboot/ktool flash software is strict about writing to SPI Flash in 4096 aligned sectors, it is possible to both read-from and write-to SPI Flash at the byte level, as is done here (w/ care to respect the 4096 aligned 'chunk' sizes expected by ktool).

From what I've seen, writes to flash are done in chunks of sizes:

  • 4096 bytes: for stages 0 and 1 of Kboot at 0x0 and 0x1000, and for the main and backup configurations at 0x4000 and 0x5000,
  • 65536 bytes: for firmware at 0x80000,
  • SPI-FFS??? I don't know.

Concerning firmware.bin, much like stages 0 and 1 of Kboot, firmware.bin is enveloped in a 5-byte header (0x00 aes byte + 4-byte little-endian size) and 32 byte sha256 suffix, therefore it's place in SPI-Flash will always be 37 bytes larger than the size of firmware.bin, rounded-up to the nearest 65536 bytes, since writes are done in chunks; padding is done with 0x00 bytes.

Some Math

Since kboot/ktool installs firmware at 0x80000 (and krux respects this), this is a lower limit for firmware.

Since SPI-FFS starts at 0xd00000, defined per-device ie: here, this is an upper limit, but it currently is not defined anywhere so that a firmware upgrade can respect this boundary. In the test_firmware branch linked above, I propose a few extra firmware.py constants so that they may be checked by the testing framework.

0xd00000 - 0x80000 = 0xc80000 or 13107200 bytes for firmware is plenty for 2 slots (plenty for more as well).

MAX_FIRMWARE_SIZE might best always be defined as a multiple of the chunk-size (65536) -37 bytes, and any two slots must not be closer to each other than MAX_FIRMWARE_SIZE + 37 and also not collide with SPI-FFS space.


Bricking the Amigo

I changed firmware.py w/

FIRMWARE_SLOT_2 = 0x00e2e000 # firmware.bin > 1908736 will exceed 16MB corrupting low sectors, brick.

and rebuilt krux.

Confirmed that the unittest failed: tests/test_firmware2.py::test_firmware_constants_guard_against_overflow

Used ktool to erase the flash entirely.

Used ktool to flash the above build via kboot.kfpkg... which will write to slot 1 at 0x80000.

~/krux$ du -b build/firmware.bin build/kboot.kfpkg
1913856	build/firmware.bin
936660	build/kboot.kfpkg

~/krux$ sha256sum build/firmware.bin build/kboot.kfpkg
ef19108ebf85124b8a08ca1cb8c4bd1fa7da67fab8e81abc6933dec50fcd1044  build/firmware.bin
7f649f9046544977ea5edda6bf54d54c5c5a55e2f36cae3e5b68d956c2356fd8  build/kboot.kfpkg

~/krux$ unzip -l ./build/kboot.kfpkg
Archive:  ./build/kboot.kfpkg
  Length      Date    Time    Name
---------  ---------- -----   ----
      647  2023-08-04 13:58   flash-list.json
      608  2023-08-04 13:58   bootloader_lo.bin
     8112  2023-08-04 13:58   bootloader_hi.bin
     4096  2023-06-13 10:01   config.bin
  1913856  2023-08-04 13:58   firmware.bin
---------                     -------
  1927319                     5 files

~/krux$ unzip -p ./build/kboot.kfpkg bootloader_lo.bin | sha256sum
2e050a92efdcb172cb5c6f7cb0b669ba654d2d20219f810fd9fc543f7ef05c3e  -

~/krux$ unzip -p ./build/kboot.kfpkg bootloader_hi.bin | sha256sum
f005f7c8b13aa2719cad58492a8432f14b68b2f845b58e5b5e81ed6309be6172  -

~/krux$ unzip -p ./build/kboot.kfpkg config.bin | sha256sum
c9b9c4adcabe9c353a907f44c101c3b29756b30762e4b282d87d2a92400e2198  -

~/krux$ unzip -p ./build/kboot.kfpkg firmware.bin | sha256sum
ef19108ebf85124b8a08ca1cb8c4bd1fa7da67fab8e81abc6933dec50fcd1044  -

I then used sdcard to upgrade with the same firmware, intending it to choose slot 2 near the limit of 16MB at 0x00e2e000 to provoke the overflow into Kboot0, Kboot1, boot-config, config-backup, reserved and partially into slot 1.

This indeed made a brick. The upgrade completed, got the "shutting down" message, and after that no life.

The amigo stayed warmer than ambient temperature for about 2h. Then it cooled off. After 6h w/ usb connected to charge, the white LED flashed in response to pwr button.

Used ktool to get a flash_dump.

Used ktool to erase entire flash.

Used ktool to flash krux again w/o problems.

analysing the brick flash_dump

Before this routine, ktool was used to erase the entire flash, so all 16MB were 0xff bytes. Then ktool was used to flash boot.kfpkg, so there would have been one firmware slot at 0x80000 that extended 5 + 1913856 + 32 bytes. Then krux performed a firmware upgrade from microsd writing the same 1913893 bytes into slot 2 at 0xe2e000, close enough to overflow 16MB limit and continuing its write from 0x0.

The math: 2**24 - 0xe2e000 = 1908736 is how many bytes were available for firmware, 1913893 - 1908736 = 5157 is how many bytes we flowed past the 16MB limit, entirely thru Kboot0 and into Kboot1 5157 - 4096 = 1061 is how many bytes into Kboot1 we flowed.

We should find most of our firmware between 0xe2e005 (minus its header) and the end of our flash_dump. We should find the rest of our firmware between 0x0 and 5125 (minus its sha256 suffix) at the front of our flash_dump.

~/krux$ tail -c 1908731 k210.flash_dump >/tmp/firmware.bin
~/krux$ head -c 5125 k210.flash_dump >>/tmp/firmware.bin

~/krux$ sha256sum /tmp/firmware.bin build/firmware.bin
ef19108ebf85124b8a08ca1cb8c4bd1fa7da67fab8e81abc6933dec50fcd1044  /tmp/firmware.bin
ef19108ebf85124b8a08ca1cb8c4bd1fa7da67fab8e81abc6933dec50fcd1044  build/firmware.bin

We could continue to analyze but this is enough to convince me that the overflow past 16MB corrupted Kboot0 and Kboot1. for more on the k210 flash see https://gist.github.com/jdlcdl/a01dbf21771516581b4ccfda49622293

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment