ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
ota_backend_esp_idf.cpp
Go to the documentation of this file.
1#ifdef USE_ESP32
3
7#include "esphome/core/log.h"
8
9#include <esp_ota_ops.h>
10#include <esp_task_wdt.h>
11#include <spi_flash_mmap.h>
12#ifdef USE_OTA_DOWNGRADE_PROTECTION
13#include <esp_app_desc.h>
14#endif
15
16namespace esphome::ota {
17
18static const char *const TAG = "ota.idf";
19
20std::unique_ptr<IDFOTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); }
21
23#ifdef USE_OTA_PARTITIONS
24 this->ota_type_ = ota_type;
25 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
26 // Reject any size other than ESP_PARTITION_TABLE_MAX_LEN
27 if (image_size != ESP_PARTITION_TABLE_MAX_LEN) {
28 ESP_LOGE(TAG, "Wrong partition table size: expected %u bytes, got %zu", ESP_PARTITION_TABLE_MAX_LEN, image_size);
30 }
31 memset(this->buf_, 0xFF, sizeof this->buf_);
32 this->buf_written_ = 0;
33 this->image_size_ = image_size;
34 this->md5_.init();
35 return OTA_RESPONSE_OK;
36 }
37 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER) {
38 OTAResponseTypes result = this->prepare_bootloader_update_(image_size);
39 if (result != OTA_RESPONSE_OK) {
40 return result;
41 }
42 }
43 if (!this->is_app_or_bootloader_update_()) {
45 }
46#else
47 if (ota_type != ota::OTA_TYPE_UPDATE_APP) {
49 }
50#endif
51#ifdef USE_OTA_ROLLBACK
52 // If we're starting an OTA, the current boot is good enough - mark it valid
53 // to prevent rollback and allow the OTA to proceed even if the safe mode
54 // timer hasn't expired yet.
55 esp_ota_mark_app_valid_cancel_rollback();
56#endif
57
58 this->partition_ = esp_ota_get_next_update_partition(nullptr);
59 if (this->partition_ == nullptr) {
61 }
62
63 // esp_ota_begin() erases the destination region, which blocks loopTask and
64 // scales with the erase size -- a fixed watchdog overruns on large OTA slots.
65 // An unknown size (0, e.g. web_server uploads) erases the whole partition, so
66 // budget against the bytes actually erased. ~10ms/KiB (conservative
67 // ~100 KiB/s erase) over a 15s floor; panic stays on so a stuck erase still
68 // resets rather than hanging forever.
69 size_t erase_size = image_size;
70 if (erase_size == 0 || erase_size > this->partition_->size) {
71 erase_size = this->partition_->size;
72 }
73 const uint32_t erase_budget_ms = 15000 + (erase_size >> 10) * 10;
74 watchdog::WatchdogManager watchdog(erase_budget_ms);
75 esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_);
76
77 if (err != ESP_OK) {
78 ESP_LOGE(TAG, "esp_ota_begin failed (err=0x%X)", err);
79 esp_ota_abort(this->update_handle_);
80 this->update_handle_ = 0;
81 if (err == ESP_ERR_INVALID_SIZE) {
83 } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
85 } else if (err == ESP_ERR_OTA_PARTITION_CONFLICT) {
86 // This error appears with 1 factory and 1 ota partition
88 }
90 }
91#ifdef USE_OTA_PARTITIONS
92 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER) {
94 if (result != OTA_RESPONSE_OK) {
95 return result;
96 }
97 }
98#endif
99 this->md5_.init();
100 return OTA_RESPONSE_OK;
101}
102
103void IDFOTABackend::set_update_md5(const char *expected_md5) {
104 memcpy(this->expected_bin_md5_, expected_md5, 32);
105 this->md5_set_ = true;
106}
107
109#ifdef USE_OTA_PARTITIONS
110 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
111 if (len > PARTITION_TABLE_BUFFER_SIZE - this->buf_written_) {
112 ESP_LOGE(TAG, "Wrong partition table size");
114 }
115 memcpy(this->buf_ + this->buf_written_, data, len);
116 this->buf_written_ += len;
117 this->md5_.add(data, len);
118 return OTA_RESPONSE_OK;
119 }
120 if (!this->is_app_or_bootloader_update_()) {
122 }
123#endif
124 esp_err_t err = esp_ota_write(this->update_handle_, data, len);
125 this->md5_.add(data, len);
126 if (err != ESP_OK) {
127 ESP_LOGE(TAG, "esp_ota_write failed (err=0x%X)", err);
128 if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
130 } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
132 }
134 }
135 return OTA_RESPONSE_OK;
136}
137
139 if (this->md5_set_) {
140 this->md5_.calculate();
141 if (!this->md5_.equals_hex(this->expected_bin_md5_)) {
142 this->abort();
144 }
145 }
146#ifdef USE_OTA_PARTITIONS
147 // A partition-table update carries an MD5 (checked by IDF), not a Secure Boot
148 // signature, and only re-points boot at an already-installed app -- so it is
149 // intentionally not run through the signature verifier below.
150 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
151 return this->update_partition_table();
152 }
153 if (!this->is_app_or_bootloader_update_()) {
155 }
156#endif
157 esp_err_t err = esp_ota_end(this->update_handle_);
158 this->update_handle_ = 0;
159 if (err != ESP_OK) {
160 ESP_LOGE(TAG, "esp_ota_end failed (err=0x%X)", err);
161 }
162#ifdef USE_OTA_PARTITIONS
163 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER) {
164 return this->finalize_bootloader_update_(err);
165 }
166#endif
167 if (err == ESP_OK) {
168#ifdef USE_OTA_SIGNED_VERIFICATION_MULTI_KEY
169 // IDF's built-in on-update check is disabled for this scheme (it only
170 // matches the incoming image's first signature block against the running
171 // app's first). Verify here against every key the running app trusts, so
172 // rotation and backup keys are accepted. Leaving the boot partition
173 // unchanged means a rejected image never boots.
174 if (!this->verify_signed_image_(this->partition_)) {
176 }
177#endif
178#ifdef USE_OTA_DOWNGRADE_PROTECTION
179 // The image is written and (when signing is enabled) signature-verified by
180 // esp_ota_end(), so its embedded project version can be trusted. Reject the
181 // update if it is older than the running version by leaving the boot
182 // partition unchanged -- the staged image simply never boots.
183 esp_app_desc_t incoming;
184 esp_err_t desc_err = esp_ota_get_partition_description(this->partition_, &incoming);
185 if (desc_err != ESP_OK) {
186 // Couldn't read the staged image's version, so the comparison is skipped.
187 // Warn so the bypassed check is observable rather than silent.
188 ESP_LOGW(TAG, "Downgrade protection: could not read image version (err=0x%X); allowing update", desc_err);
189 } else if (version_is_older(incoming.version, ESPHOME_PROJECT_VERSION)) {
190 ESP_LOGE(TAG, "Rejecting downgrade: image version '%s' is older than running version '%s'", incoming.version,
191 ESPHOME_PROJECT_VERSION);
193 }
194#endif
195 err = esp_ota_set_boot_partition(this->partition_);
196 if (err == ESP_OK) {
197 return OTA_RESPONSE_OK;
198 }
199 }
200 if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
201#ifdef USE_OTA_SIGNED_VERIFICATION
202 ESP_LOGE(TAG, "OTA validation failed (err=0x%X) - possible signature verification failure", err);
204#else
206#endif
207 }
208 if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
210 }
212}
213
215#ifdef USE_OTA_PARTITIONS
216 if (this->partition_table_part_ != nullptr) {
217 esp_partition_deregister_external(this->partition_table_part_);
218 this->partition_table_part_ = nullptr;
219 }
220 if (this->bootloader_part_ != nullptr) {
221 esp_partition_deregister_external(this->bootloader_part_);
222 this->bootloader_part_ = nullptr;
223 }
224#endif
225 // esp_ota_abort with handle 0 returns ESP_ERR_INVALID_ARG harmlessly, so this is safe whether
226 // or not an update is in flight.
227 esp_ota_abort(this->update_handle_);
228 this->update_handle_ = 0;
229}
230
231} // namespace esphome::ota
232#endif // USE_ESP32
bool equals_hex(const char *expected)
Compare the hash against a provided hex-encoded hash.
Definition hash_base.h:35
void calculate() override
Compute the digest, based on the provided data.
Definition md5.cpp:16
void add(const uint8_t *data, size_t len) override
Add bytes of data for the digest.
Definition md5.cpp:14
void init() override
Initialize a new MD5 digest computation.
Definition md5.cpp:9
void set_update_md5(const char *md5)
OTAResponseTypes finalize_bootloader_update_(esp_err_t ota_end_err)
OTAResponseTypes begin(size_t image_size, ota::OTAType ota_type=ota::OTA_TYPE_UPDATE_APP)
OTAResponseTypes prepare_bootloader_update_(size_t image_size)
OTAResponseTypes write(uint8_t *data, size_t len)
@ OTA_TYPE_UPDATE_BOOTLOADER
Definition ota_backend.h:80
@ OTA_TYPE_UPDATE_PARTITION_TABLE
Definition ota_backend.h:79
bool version_is_older(const char *candidate, const char *reference)
Compare two dotted-numeric version strings (such as "1.2.3").
@ OTA_RESPONSE_ERROR_MD5_MISMATCH
Definition ota_backend.h:43
@ OTA_RESPONSE_ERROR_VERSION_DOWNGRADE
Definition ota_backend.h:51
@ OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE
Definition ota_backend.h:41
@ OTA_RESPONSE_ERROR_WRITING_FLASH
Definition ota_backend.h:35
@ OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE
Definition ota_backend.h:46
@ OTA_RESPONSE_ERROR_UPDATE_END
Definition ota_backend.h:36
@ OTA_RESPONSE_ERROR_SIGNATURE_INVALID
Definition ota_backend.h:45
@ OTA_RESPONSE_ERROR_UNKNOWN
Definition ota_backend.h:52
@ OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION
Definition ota_backend.h:42
@ OTA_RESPONSE_ERROR_MAGIC
Definition ota_backend.h:32
@ OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY
Definition ota_backend.h:47
std::unique_ptr< ArduinoLibreTinyOTABackend > make_ota_backend()
const void size_t len
Definition hal.h:64
static void uint32_t