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 ============================================
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.
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.
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.
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