Skip to content

Commit 54eb749

Browse files
committed
feat: updated check for chip revision and respective testcases
This commit have updated check for max chip revision along with min chip revision. Also added qemu based pytest to verify chip revision while performing OTA.
1 parent 4f3e643 commit 54eb749

File tree

6 files changed

+239
-35
lines changed

6 files changed

+239
-35
lines changed

components/bootloader_support/include/bootloader_common.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: 2018-2024 Espressif Systems (Shanghai) CO LTD
2+
* SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
33
*
44
* SPDX-License-Identifier: Apache-2.0
55
*/
@@ -24,6 +24,19 @@ typedef enum {
2424
ESP_IMAGE_APPLICATION
2525
} esp_image_type;
2626

27+
/**
28+
* @brief Check if the chip revision meets the image requirements.
29+
*
30+
* This function verifies whether the actual chip revision satisfies the minimum
31+
* and optionally the maximum chip revision requirements specified in the image.
32+
*
33+
* @param image_header Pointer to the image header containing revision details.
34+
* @param check_max_revision If true, also checks the maximum chip revision requirements.
35+
*
36+
* @return true if the chip revision meets the requirements, false otherwise.
37+
*/
38+
bool bootloader_common_check_chip_revision_validity(const esp_image_header_t *image_header, bool check_max_revision);
39+
2740
/**
2841
* @brief Read ota_info partition and fill array from two otadata structures.
2942
*

components/bootloader_support/src/bootloader_common_loader.c

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD
2+
* SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD
33
*
44
* SPDX-License-Identifier: Apache-2.0
55
*/
@@ -31,6 +31,37 @@
3131

3232
static const char* TAG = "boot_comm";
3333

34+
bool bootloader_common_check_chip_revision_validity(const esp_image_header_t *img_hdr, bool check_max_revision)
35+
{
36+
if (!img_hdr) {
37+
return false;
38+
}
39+
40+
unsigned revision = efuse_hal_chip_revision();
41+
unsigned min_rev = img_hdr->min_chip_rev_full;
42+
43+
bool is_min_rev_invalid = !ESP_CHIP_REV_ABOVE(revision, min_rev);
44+
if (is_min_rev_invalid) {
45+
ESP_LOGE(TAG, "chip revision check failed. Required >= v%d.%d, found v%d.%d.",
46+
min_rev / 100, min_rev % 100,
47+
revision / 100, revision % 100);
48+
return false;
49+
}
50+
51+
if (check_max_revision) {
52+
unsigned int max_rev = img_hdr->max_chip_rev_full;
53+
bool is_max_rev_invalid = IS_FIELD_SET(max_rev) && revision > max_rev && !efuse_hal_get_disable_wafer_version_major();
54+
if (is_max_rev_invalid) {
55+
ESP_LOGE(TAG, "chip revision check failed. Required <= v%d.%d, found v%d.%d.",
56+
max_rev / 100, max_rev % 100,
57+
revision / 100, revision % 100);
58+
return false;
59+
}
60+
}
61+
62+
return true;
63+
}
64+
3465
uint32_t bootloader_common_ota_select_crc(const esp_ota_select_entry_t *s)
3566
{
3667
return esp_rom_crc32_le(UINT32_MAX, (uint8_t*)&s->ota_seq, 4);
@@ -91,24 +122,15 @@ esp_err_t bootloader_common_check_chip_validity(const esp_image_header_t* img_hd
91122
err = ESP_FAIL;
92123
} else {
93124
#ifndef CONFIG_IDF_ENV_FPGA
94-
unsigned revision = efuse_hal_chip_revision();
95-
unsigned int major_rev = revision / 100;
96-
unsigned int minor_rev = revision % 100;
97-
unsigned min_rev = img_hdr->min_chip_rev_full;
98-
if (type == ESP_IMAGE_BOOTLOADER || type == ESP_IMAGE_APPLICATION) {
99-
if (!ESP_CHIP_REV_ABOVE(revision, min_rev)) {
100-
ESP_LOGE(TAG, "Image requires chip rev >= v%d.%d, but chip is v%d.%d",
101-
min_rev / 100, min_rev % 100,
102-
major_rev, minor_rev);
125+
if (type == ESP_IMAGE_APPLICATION) {
126+
if (!bootloader_common_check_chip_revision_validity(img_hdr, true)) {
103127
err = ESP_FAIL;
104128
}
105129
}
106-
if (type == ESP_IMAGE_APPLICATION) {
107-
unsigned max_rev = img_hdr->max_chip_rev_full;
108-
if ((IS_FIELD_SET(max_rev) && (revision > max_rev) && !efuse_hal_get_disable_wafer_version_major())) {
109-
ESP_LOGE(TAG, "Image requires chip rev <= v%d.%d, but chip is v%d.%d",
110-
max_rev / 100, max_rev % 100,
111-
major_rev, minor_rev);
130+
131+
// Maximum revision check is skipped for bootloader images
132+
if (type == ESP_IMAGE_BOOTLOADER) {
133+
if (!bootloader_common_check_chip_revision_validity(img_hdr, false)) {
112134
err = ESP_FAIL;
113135
}
114136
}

components/esp_https_ota/src/esp_https_ota.c

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -631,10 +631,7 @@ static esp_err_t esp_ota_verify_chip_revision(const void *arg)
631631
esp_image_header_t *data = (esp_image_header_t *)(arg);
632632
esp_https_ota_dispatch_event(ESP_HTTPS_OTA_VERIFY_CHIP_REVISION, (void *)(&data->min_chip_rev_full), sizeof(uint16_t));
633633

634-
uint16_t ota_img_revision = data->min_chip_rev_full;
635-
uint32_t chip_revision = efuse_hal_chip_revision();
636-
if (ota_img_revision > chip_revision) {
637-
ESP_LOGE(TAG, "Image requires chip rev >= v%d.%d, but chip is v%d.%d", ota_img_revision / 100, ota_img_revision % 100, chip_revision / 100, chip_revision % 100);
634+
if (!bootloader_common_check_chip_revision_validity(data, true)) {
638635
return ESP_ERR_INVALID_VERSION;
639636
}
640637
return ESP_OK;
1 KB
Binary file not shown.

examples/system/ota/advanced_https_ota/pytest_advanced_ota.py

Lines changed: 174 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import subprocess
1111
import time
1212
from typing import Callable
13+
from typing import Optional
1314

1415
import pexpect
1516
import pytest
@@ -79,17 +80,19 @@ def start_https_server(ota_image_dir: str, server_ip: str, server_port: int) ->
7980

8081
def start_chunked_server(ota_image_dir: str, server_port: int) -> subprocess.Popen:
8182
os.chdir(ota_image_dir)
82-
chunked_server = subprocess.Popen([
83-
'openssl',
84-
's_server',
85-
'-WWW',
86-
'-key',
87-
key_file,
88-
'-cert',
89-
server_file,
90-
'-port',
91-
str(server_port),
92-
])
83+
chunked_server = subprocess.Popen(
84+
[
85+
'openssl',
86+
's_server',
87+
'-WWW',
88+
'-key',
89+
key_file,
90+
'-cert',
91+
server_file,
92+
'-port',
93+
str(server_port),
94+
]
95+
)
9396
return chunked_server
9497

9598

@@ -129,6 +132,48 @@ def start_redirect_server(ota_image_dir: str, server_ip: str, server_port: int,
129132
httpd.serve_forever()
130133

131134

135+
# Function to modify chip revisions in the app header
136+
def modify_chip_revision(
137+
app_path: str, min_rev: Optional[int] = None, max_rev: Optional[int] = None, increment_min: bool = False
138+
) -> None:
139+
"""
140+
Modify min_chip_rev_full and max_chip_rev_full in the app header.
141+
142+
:param app_path: Path to the app binary.
143+
:param min_rev: Value to set min_chip_rev_full (if provided).
144+
:param max_rev: Value to set max_chip_rev_full (if provided).
145+
:param increment_min: If True, increments min_chip_rev_full.
146+
"""
147+
148+
HEADER_SIZE = 512
149+
TARGET_OFFSET_MIN_REV = 0x0F
150+
TARGET_OFFSET_MAX_REV = 0x11
151+
152+
if not os.path.exists(app_path):
153+
raise FileNotFoundError(f"App binary file '{app_path}' not found")
154+
155+
try:
156+
with open(app_path, 'rb') as f:
157+
header = bytearray(f.read(HEADER_SIZE))
158+
159+
# Increment or set min revision value
160+
if increment_min:
161+
header[TARGET_OFFSET_MIN_REV] = (header[TARGET_OFFSET_MIN_REV] + 1) & 0xFF
162+
elif min_rev is not None:
163+
header[TARGET_OFFSET_MIN_REV] = min_rev & 0xFF
164+
165+
# Set max revision value
166+
if max_rev is not None:
167+
header[TARGET_OFFSET_MAX_REV] = max_rev & 0xFF
168+
169+
# Write back the modified header to the binary file
170+
with open(app_path, 'r+b') as f:
171+
f.write(header)
172+
173+
except IOError as e:
174+
raise RuntimeError(f'Failed to modify app header: {e}')
175+
176+
132177
@pytest.mark.ethernet_ota
133178
@idf_parametrize('target', ['esp32'], indirect=['target'])
134179
def test_examples_protocol_advanced_https_ota_example(dut: Dut) -> None:
@@ -253,7 +298,8 @@ def test_examples_protocol_advanced_https_ota_example_truncated_bin(dut: Dut) ->
253298
bin_name = 'advanced_https_ota.bin'
254299
# Truncated binary file to be generated from original binary file
255300
truncated_bin_name = 'truncated.bin'
256-
# Size of truncated file to be grnerated. This value can range from 288 bytes (Image header size) to size of original binary file
301+
# Size of truncated file to be grnerated.
302+
# This value can range from 288 bytes (Image header size) to size of original binary file
257303
# truncated_bin_size is set to 64000 to reduce consumed by the test case
258304
truncated_bin_size = 64000
259305
binary_file = os.path.join(dut.app.binary_path, bin_name)
@@ -757,7 +803,8 @@ def test_examples_protocol_advanced_https_ota_example_ota_resumption_partial_dow
757803
@idf_parametrize('target', ['esp32', 'esp32c3', 'esp32s3'], indirect=['target'])
758804
def test_examples_protocol_advanced_https_ota_example_nimble_gatts(dut: Dut) -> None:
759805
"""
760-
Run an OTA image update while a BLE GATT Server is running in background. This GATT server will be using NimBLE Host stack.
806+
Run an OTA image update while a BLE GATT Server is running in background.
807+
This GATT server will be using NimBLE Host stack.
761808
steps: |
762809
1. join AP/Ethernet
763810
2. Run BLE advertise and then GATT server.
@@ -812,7 +859,8 @@ def test_examples_protocol_advanced_https_ota_example_nimble_gatts(dut: Dut) ->
812859
@idf_parametrize('target', ['esp32', 'esp32c3', 'esp32s3'], indirect=['target'])
813860
def test_examples_protocol_advanced_https_ota_example_bluedroid_gatts(dut: Dut) -> None:
814861
"""
815-
Run an OTA image update while a BLE GATT Server is running in background. This GATT server will be using Bluedroid Host stack.
862+
Run an OTA image update while a BLE GATT Server is running in background.
863+
This GATT server will be using Bluedroid Host stack.
816864
steps: |
817865
1. join AP/Ethernet
818866
2. Run BLE advertise and then GATT server.
@@ -907,3 +955,115 @@ def test_examples_protocol_advanced_https_ota_example_openssl_aligned_bin(dut: D
907955
pass
908956
finally:
909957
chunked_server.kill()
958+
959+
960+
@pytest.mark.qemu
961+
@pytest.mark.nightly_run
962+
@pytest.mark.host_test
963+
@pytest.mark.parametrize(
964+
'qemu_extra_args',
965+
[
966+
f'-drive file={os.path.join(os.path.dirname(__file__), "efuse_esp32c3.bin")},if=none,format=raw,id=efuse '
967+
'-global driver=nvram.esp32c3.efuse,property=drive,value=efuse '
968+
'-global driver=timer.esp32c3.timg,property=wdt_disable,value=true',
969+
],
970+
indirect=True,
971+
)
972+
@idf_parametrize('target', ['esp32c3'], indirect=['target'])
973+
@pytest.mark.parametrize('config', ['verify_revision'], indirect=True)
974+
def test_examples_protocol_advanced_https_ota_example_verify_min_chip_revision(dut: Dut) -> None:
975+
"""
976+
This is a QEMU test case that verifies the chip revision value in the application header.
977+
steps: |
978+
1. join AP/Ethernet
979+
2. Fetch OTA image over HTTPS
980+
3. Reboot with the new OTA image
981+
"""
982+
983+
# Update the min full revision field in the app header
984+
app_path = os.path.join(dut.app.binary_path, 'advanced_https_ota.bin')
985+
# Increment min_chip_rev_full
986+
modify_chip_revision(app_path, increment_min=True)
987+
988+
server_port = 8001
989+
bin_name = 'advanced_https_ota.bin'
990+
# Start server
991+
thread1 = multiprocessing.Process(target=start_https_server, args=(dut.app.binary_path, '0.0.0.0', server_port))
992+
thread1.daemon = True
993+
thread1.start()
994+
try:
995+
# start test
996+
dut.expect('Loaded app from partition at offset', timeout=30)
997+
998+
try:
999+
ip_address = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)[1].decode()
1000+
print('Connected to AP/Ethernet with IP: {}'.format(ip_address))
1001+
except pexpect.exceptions.TIMEOUT:
1002+
raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP/Ethernet')
1003+
1004+
dut.expect('Starting Advanced OTA example', timeout=30)
1005+
host_ip = get_host_ip4_by_dest_ip(ip_address)
1006+
1007+
print('writing to device: {}'.format('https://' + host_ip + ':' + str(server_port) + '/' + bin_name))
1008+
dut.write('https://' + host_ip + ':' + str(server_port) + '/' + bin_name)
1009+
dut.expect('Starting OTA...', timeout=60)
1010+
dut.expect('chip revision check failed.', timeout=150)
1011+
1012+
finally:
1013+
thread1.terminate()
1014+
1015+
1016+
@pytest.mark.qemu
1017+
@pytest.mark.nightly_run
1018+
@pytest.mark.host_test
1019+
@pytest.mark.parametrize(
1020+
'qemu_extra_args',
1021+
[
1022+
f'-drive file={os.path.join(os.path.dirname(__file__), "efuse_esp32c3.bin")},if=none,format=raw,id=efuse '
1023+
'-global driver=nvram.esp32c3.efuse,property=drive,value=efuse '
1024+
'-global driver=timer.esp32c3.timg,property=wdt_disable,value=true',
1025+
],
1026+
indirect=True,
1027+
)
1028+
@idf_parametrize('target', ['esp32c3'], indirect=['target'])
1029+
@pytest.mark.parametrize('config', ['verify_revision'], indirect=True)
1030+
def test_examples_protocol_advanced_https_ota_example_verify_max_chip_revision(dut: Dut) -> None:
1031+
"""
1032+
This is a QEMU test case that verifies the chip revision value in the application header.
1033+
steps: |
1034+
1. join AP/Ethernet
1035+
2. Fetch OTA image over HTTPS
1036+
3. Reboot with the new OTA image
1037+
"""
1038+
1039+
# Update the min full revision field in the app header
1040+
app_path = os.path.join(dut.app.binary_path, 'advanced_https_ota.bin')
1041+
# Set min_chip_rev_full to 0.0 and max_chip_rev_full to 0.2
1042+
modify_chip_revision(app_path, min_rev=0x00, max_rev=0x02)
1043+
1044+
server_port = 8001
1045+
bin_name = 'advanced_https_ota.bin'
1046+
# Start server
1047+
thread1 = multiprocessing.Process(target=start_https_server, args=(dut.app.binary_path, '0.0.0.0', server_port))
1048+
thread1.daemon = True
1049+
thread1.start()
1050+
try:
1051+
# start test
1052+
dut.expect('Loaded app from partition at offset', timeout=30)
1053+
1054+
try:
1055+
ip_address = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)[1].decode()
1056+
print('Connected to AP/Ethernet with IP: {}'.format(ip_address))
1057+
except pexpect.exceptions.TIMEOUT:
1058+
raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP/Ethernet')
1059+
1060+
dut.expect('Starting Advanced OTA example', timeout=30)
1061+
host_ip = get_host_ip4_by_dest_ip(ip_address)
1062+
1063+
print('writing to device: {}'.format('https://' + host_ip + ':' + str(server_port) + '/' + bin_name))
1064+
dut.write('https://' + host_ip + ':' + str(server_port) + '/' + bin_name)
1065+
dut.expect('Starting OTA...', timeout=60)
1066+
dut.expect('chip revision check failed.', timeout=150)
1067+
1068+
finally:
1069+
thread1.terminate()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CONFIG_IDF_TARGET="esp32c3"
2+
3+
CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL="FROM_STDIN"
4+
CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECK=y
5+
CONFIG_EXAMPLE_SKIP_VERSION_CHECK=y
6+
CONFIG_EXAMPLE_OTA_RECV_TIMEOUT=3000
7+
8+
# QEMU-Related configurations
9+
CONFIG_EXAMPLE_CONNECT_ETHERNET=y
10+
CONFIG_EXAMPLE_USE_OPENETH=y
11+
CONFIG_EXAMPLE_CONNECT_WIFI=n
12+
CONFIG_ETH_USE_SPI_ETHERNET=n

0 commit comments

Comments
 (0)