Skip to content

Commit 0e3bbb8

Browse files
committed
Check firmware CRC before writing
1 parent 8450e35 commit 0e3bbb8

File tree

2 files changed

+80
-4
lines changed

2 files changed

+80
-4
lines changed

tests/test_tools_flash.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import pytest
2+
13
import random
24
import zigpy_znp.types as t
35
import zigpy_znp.commands as c
46

57
from zigpy_znp.tools.flash_read import main as flash_read
6-
from zigpy_znp.tools.flash_write import main as flash_write
8+
from zigpy_znp.tools.flash_write import get_firmware_crcs, main as flash_write
79

810
from test_api import pytest_mark_asyncio_timeout # noqa: F401
911
from test_application import znp_server # noqa: F401
@@ -12,7 +14,12 @@
1214

1315
random.seed(12345)
1416
FAKE_IMAGE_SIZE = 2 ** 10
15-
FAKE_FLASH = random.getrandbits(FAKE_IMAGE_SIZE * 8).to_bytes(FAKE_IMAGE_SIZE, "little")
17+
FAKE_FLASH = bytearray(
18+
random.getrandbits(FAKE_IMAGE_SIZE * 8).to_bytes(FAKE_IMAGE_SIZE, "little")
19+
)
20+
FAKE_FLASH[c.ubl.IMAGE_CRC_OFFSET : c.ubl.IMAGE_CRC_OFFSET + 2] = get_firmware_crcs(
21+
FAKE_FLASH
22+
)[1].to_bytes(2, "little")
1623
random.seed()
1724

1825

@@ -89,3 +96,26 @@ def write_flash(req):
8996

9097
# They should be identical
9198
assert backup_file.read_bytes() == FAKE_FLASH
99+
100+
101+
@pytest_mark_asyncio_timeout(seconds=5)
102+
async def test_flash_write_bad_crc(
103+
openable_serial_znp_server, tmp_path, mocker # noqa: F811
104+
):
105+
# It takes too long otherwise
106+
mocker.patch("zigpy_znp.commands.ubl.IMAGE_SIZE", FAKE_IMAGE_SIZE)
107+
108+
# Flip the bits in one byte, the CRC should fail
109+
BAD_FIRMWARE = bytearray(len(FAKE_FLASH))
110+
BAD_FIRMWARE[FAKE_IMAGE_SIZE - 1] = BAD_FIRMWARE[FAKE_IMAGE_SIZE - 1] ^ 0xFF
111+
112+
# No communication will happen because the CRC will be invalid
113+
firmware_file = tmp_path / "bad-firmware.bin"
114+
firmware_file.write_bytes(BAD_FIRMWARE)
115+
116+
with pytest.raises(ValueError) as e:
117+
await flash_write(
118+
[openable_serial_znp_server._port_path, "-i", str(firmware_file)]
119+
)
120+
121+
assert "Firmware CRC is incorrect" in str(e)

zigpy_znp/tools/flash_write.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
import typing
23
import asyncio
34
import logging
45
import argparse
@@ -17,13 +18,59 @@
1718
LOGGER = logging.getLogger(__name__)
1819

1920

21+
def compute_crc16(data: bytes) -> int:
22+
poly = 0x1021
23+
crc = 0x0000
24+
25+
for byte in data:
26+
for _ in range(8):
27+
msb = 1 if (crc & 0x8000) else 0
28+
29+
crc <<= 1
30+
crc &= 0xFFFF
31+
32+
if byte & 0x80:
33+
crc |= 0x0001
34+
35+
if msb:
36+
crc ^= poly
37+
38+
byte <<= 1
39+
40+
return crc
41+
42+
43+
def get_firmware_crcs(firmware: bytes) -> typing.Tuple[int, int]:
44+
# There is room for *two* CRCs in the firmware file: the expected and the computed
45+
firmware_wihout_crcs = (
46+
firmware[: c.ubl.IMAGE_CRC_OFFSET]
47+
+ firmware[c.ubl.IMAGE_CRC_OFFSET + 4 :]
48+
+ b"\x00\x00"
49+
)
50+
51+
# We only use the first one. The second one is written by the bootloader into flash.
52+
real_crc = int.from_bytes(
53+
firmware[c.ubl.IMAGE_CRC_OFFSET : c.ubl.IMAGE_CRC_OFFSET + 2], "little"
54+
)
55+
56+
return real_crc, compute_crc16(firmware_wihout_crcs)
57+
58+
2059
async def write_firmware(firmware: bytes, radio_path: str):
2160
if len(firmware) != c.ubl.IMAGE_SIZE:
2261
raise ValueError(
2362
f"Firmware is the wrong size."
2463
f" Expected {c.ubl.IMAGE_SIZE}, got {len(firmware)}"
2564
)
2665

66+
expected_crc, computed_crc = get_firmware_crcs(firmware)
67+
68+
if expected_crc != computed_crc:
69+
raise ValueError(
70+
f"Firmware CRC is incorrect. "
71+
f"Expected 0x{expected_crc:04X}, got 0x{computed_crc:04X}"
72+
)
73+
2774
znp = ZNP(CONFIG_SCHEMA({"device": {"path": radio_path}}))
2875

2976
# The bootloader handshake must be the very first command
@@ -63,7 +110,6 @@ async def write_firmware(firmware: bytes, radio_path: str):
63110
assert write_rsp.Status == c.ubl.BootloaderStatus.SUCCESS
64111

65112
# Now we have to read it all back
66-
# TODO: figure out how the CRC is computed!
67113
for offset in range(0, c.ubl.IMAGE_SIZE, buffer_size):
68114
address = offset // c.ubl.FLASH_WORD_SIZE
69115
LOGGER.info(
@@ -79,7 +125,7 @@ async def write_firmware(firmware: bytes, radio_path: str):
79125
assert read_rsp.FlashWordAddr == address
80126
assert read_rsp.Data == firmware[offset : offset + buffer_size]
81127

82-
# This seems to cause the firmware to compute and verify the CRC
128+
# This seems to cause the bootloader to compute and verify the CRC
83129
enable_rsp = await znp.request_callback_rsp(
84130
request=c.UBL.EnableReq.Req(), callback=c.UBL.EnableRsp.Callback(partial=True),
85131
)

0 commit comments

Comments
 (0)