Skip to content

Commit

Permalink
feat(write_flash): Prevent flashing incompatible images
Browse files Browse the repository at this point in the history
  • Loading branch information
radimkarnis committed Sep 7, 2022
1 parent be28ea3 commit 395fcb0
Show file tree
Hide file tree
Showing 14 changed files with 77 additions and 27 deletions.
8 changes: 8 additions & 0 deletions docs/en/esptool/basic-commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ Use the ``-e/--erase-all`` option to erase all flash sectors (not just the write

This behavior can be overridden with the ``--force`` option. **Use this only at your own risk and only if you know what you are doing!**

Flashing an Incompatible Image
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``esptool.py`` checks every binary before flashing. If a valid firmware image is detected, the ``Chip ID`` and ``Minimum chip revision`` fields in its :ref:`header <image-format>` are compared against the actually connected chip.
If the image turns out to be incompatible with the chip in use or requires a newer chip revision, flashing is stopped.

This behavior can be overridden with the ``--force`` option.

Read Flash Contents: read_flash
--------------------------------

Expand Down
2 changes: 1 addition & 1 deletion esptool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ def add_spi_flash_subparsers(parent, allow_keep, auto_detect):
)
parser_write_flash.add_argument(
"--force",
help="Force write even if Secure Boot is enabled. Use with caution!",
help="Force write, skip security and compatibility checks. Use with caution!",
action="store_true",
)

Expand Down
12 changes: 9 additions & 3 deletions esptool/bin_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def align_file_position(f, size):
f.seek(align, 1)


def LoadFirmwareImage(chip, filename):
def LoadFirmwareImage(chip, image_file):
"""
Load a firmware image. Can be for any supported SoC.
Expand All @@ -43,8 +43,9 @@ def LoadFirmwareImage(chip, filename):
Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1)
or ESP8266V2FirmwareImage (v2).
"""
chip = re.sub(r"[-()]", "", chip.lower())
with open(filename, "rb") as f:

def select_image_class(f, chip):
chip = re.sub(r"[-()]", "", chip.lower())
if chip != "esp8266":
return {
"esp32": ESP32FirmwareImage,
Expand All @@ -67,6 +68,11 @@ def LoadFirmwareImage(chip, filename):
else:
raise FatalError("Invalid image magic number: %d" % magic)

if isinstance(image_file, str):
with open(image_file, "rb") as f:
return select_image_class(f, chip)
return select_image_class(image_file, chip)


class ImageSegment(object):
"""Wrapper class for a segment in an ESP image
Expand Down
39 changes: 27 additions & 12 deletions esptool/cmds.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,19 +315,34 @@ def write_flash(esp, args):
if args.compress is None and not args.no_compress:
args.compress = not args.no_stub

if (
not args.force
and esp.CHIP_NAME != "ESP8266"
and not esp.secure_download_mode
and esp.get_secure_boot_enabled()
):
for address, _ in args.addr_filename:
if address < 0x8000:
if not args.force and esp.CHIP_NAME != "ESP8266" and not esp.secure_download_mode:
# Check if secure boot is active
if esp.get_secure_boot_enabled():
for address, _ in args.addr_filename:
if address < 0x8000:
raise FatalError(
"Secure Boot detected, writing to flash regions < 0x8000 "
"is disabled to protect the bootloader. "
"Use --force to override, "
"please use with caution, otherwise it may brick your device!"
)
# Check if chip_id and min_rev in image are valid for the target in use
for _, argfile in args.addr_filename:
try:
image = LoadFirmwareImage(esp.CHIP_NAME, argfile)
except (FatalError, struct.error, RuntimeError):
continue
if image.chip_id != esp.IMAGE_CHIP_ID:
raise FatalError(
f"{argfile.name} is not an {esp.CHIP_NAME} image. "
"Use --force to flash anyway."
)
rev = esp.get_chip_revision()
if rev < image.min_rev:
raise FatalError(
"Secure Boot detected, writing to flash regions < 0x8000 "
"is disabled to protect the bootloader. "
"Use --force to override, "
"please use with caution, otherwise it may brick your device!"
f"{argfile.name} requires chip revision "
f"{image.min_rev} or higher (this chip is revision {rev}). "
"Use --force to flash anyway."
)

# In case we have encrypted files to write,
Expand Down
Binary file modified test/images/bootloader_esp32s2.bin
Binary file not shown.
File renamed without changes.
Binary file added test/images/esp32s3_header.bin
Binary file not shown.
Binary file removed test/images/esp8266_sdk/4096_user1.bin
Binary file not shown.
1 change: 0 additions & 1 deletion test/images/esp8266_sdk/blank.bin

This file was deleted.

Binary file removed test/images/esp8266_sdk/esp_init_data_default.bin
Binary file not shown.
Binary file removed test/images/helloworld-esp32_edit.bin
Binary file not shown.
Binary file removed test/images/unaligned.bin
Binary file not shown.
38 changes: 29 additions & 9 deletions test/test_esptool.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
NODEMCU_FILE = "nodemcu-master-7-modules-2017-01-19-11-10-03-integer.bin"

BL_IMAGES = {
"esp8266": "images/esp8266_sdk/boot_v1.4(b1).bin",
"esp8266": "images/bootloader_esp8266.bin",
"esp32": "images/bootloader_esp32.bin",
"esp32s2": "images/bootloader_esp32s2.bin",
"esp32s3beta2": "images/bootloader_esp32s3beta2.bin",
Expand Down Expand Up @@ -439,22 +439,21 @@ def _test_partition_table_then_bootloader(self, args):
self.verify_readback(0x4000, 96, "images/partitions_singleapp.bin")

def test_partition_table_then_bootloader(self):
self._test_partition_table_then_bootloader("write_flash")
self._test_partition_table_then_bootloader("write_flash --force")

def test_partition_table_then_bootloader_no_compression(self):
self._test_partition_table_then_bootloader("write_flash -u")
self._test_partition_table_then_bootloader("write_flash --force -u")

def test_partition_table_then_bootloader_nostub(self):
self._test_partition_table_then_bootloader("--no-stub write_flash")
self._test_partition_table_then_bootloader("--no-stub write_flash --force")

# note: there is no "partition table then bootloader" test that
# uses --no-stub and -z, as the ESP32 ROM over-erases and can't
# flash this set of files in this order. we do
# test_compressed_nostub_flash() instead.

def test_length_not_aligned_4bytes(self):
nodemcu = "nodemcu-master-7-modules-2017-01-19-11-10-03-integer.bin"
self.run_esptool("write_flash 0x0 images/%s" % nodemcu)
self.run_esptool("write_flash 0x0 images/%s" % NODEMCU_FILE)

def test_length_not_aligned_4bytes_no_compression(self):
self.run_esptool("write_flash -u 0x0 images/%s" % NODEMCU_FILE)
Expand All @@ -481,7 +480,7 @@ def test_write_sector_overlap(self):

def test_write_no_overlap(self):
output = self.run_esptool(
"write_flash 0x0 images/bootloader_esp32.bin 0x2000 images/one_kb.bin"
"write_flash 0x0 images/one_kb.bin 0x2000 images/one_kb.bin"
)
self.assertNotIn("Detected overlap at address", output)

Expand Down Expand Up @@ -515,16 +514,37 @@ def test_single_byte(self):

def test_erase_range_messages(self):
output = self.run_esptool(
"write_flash 0x1000 images/bootloader_esp32.bin 0x0FC00 images/one_kb.bin"
"write_flash 0x1000 images/sector.bin 0x0FC00 images/one_kb.bin"
)
self.assertIn("Flash will be erased from 0x00001000 to 0x00002fff...", output)
self.assertIn("Flash will be erased from 0x00001000 to 0x00001fff...", output)
self.assertIn(
"WARNING: Flash address 0x0000fc00 is not aligned to a 0x1000 "
"byte flash sector. 0xc00 bytes before this address will be erased.",
output,
)
self.assertIn("Flash will be erased from 0x0000f000 to 0x0000ffff...", output)

@unittest.skipIf(chip == "esp8266", "chip_id field exist in ESP32 and later images")
@unittest.skipIf(chip == "esp32s3", "This is a valid ESP32-S3 image, would pass")
def test_write_image_for_another_target(self):
output = self.run_esptool_error(
"write_flash 0x0 images/esp32s3_header.bin 0x1000 images/one_kb.bin"
)
self.assertIn("Unexpected chip id in image.", output)
self.assertIn("value was 9. Is this image for a different chip model?", output)
self.assertIn("images/esp32s3_header.bin is not an ", output)
self.assertIn("image. Use --force to flash anyway.", output)

@unittest.skipIf(chip == "esp8266", "min_rev field exist in ESP32 and later images")
@unittest.skipUnless(chip == "esp32s3", "This check happens only on a valid image")
def test_write_image_for_another_revision(self):
output = self.run_esptool_error(
"write_flash 0x0 images/one_kb.bin 0x1000 images/esp32s3_header.bin"
)
self.assertIn("images/esp32s3_header.bin requires chip revision 10", output)
self.assertIn("or higher (this chip is revision", output)
self.assertIn("Use --force to flash anyway.", output)


class TestFlashSizes(EsptoolTestCase):
def test_high_offset(self):
Expand Down
4 changes: 3 additions & 1 deletion test/test_image_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ def test_image_type_detection(self):
# ESP32, with and without detection
out = self.run_image_info("auto", "bootloader_esp32.bin", "2")
self.assertTrue("Detected image type: ESP32" in out)
out = self.run_image_info("auto", "helloworld-esp32_edit.bin", "2")
out = self.run_image_info(
"auto", "ram_helloworld/helloworld-esp32_edit.bin", "2"
)
self.assertTrue("Detected image type: ESP32" in out)
out = self.run_image_info("esp32", "bootloader_esp32.bin", "2")
self.assertFalse("Detected image type: ESP32" in out)
Expand Down

0 comments on commit 395fcb0

Please sign in to comment.