Skip to content

Commit 83cb9d7

Browse files
committed
Merge branch 'feature/add_test_to_verify_chip_revision_while_performing_ota' into 'master'
feat: updated check for chip revision and added testcase Closes IDF-12587 See merge request espressif/esp-idf!37546
2 parents 4bfb897 + 54eb749 commit 83cb9d7

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)