ESPHome 2026.10.0-dev
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 <sdkconfig.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";
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 // Both lazy-erase paths below replace esp_ota_begin()'s blocking full erase.
64 // Size check replaces the one that erase performed (0 = unknown size,
65 // e.g. web_server uploads).
66 if (image_size != 0 && image_size > this->partition_->size) {
68 }
69 this->written_ = 0;
70 esp_err_t err;
71#ifdef USE_OTA_BLOCK_ERASE_AHEAD
72 this->erased_end_ = 0;
73 // Unlike esp_ota_begin(), esp_ota_resume() does not reject a running app in
74 // ESP_OTA_IMG_PENDING_VERIFY; that state is unreachable here because the app
75 // was marked valid at boot (esp32/hal.cpp) or just above under USE_OTA_ROLLBACK.
76 // erase_size 0 (!= OTA_WITH_SEQUENTIAL_WRITES) means no erase; erase_ahead_() handles it
77 err = esp_ota_resume(this->partition_, 0, 0, &this->update_handle_);
78#if defined(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
79 // esp_ota_begin() does this on IDF 5.5+; esp_ota_resume() does not. Prevents
80 // booting a half-written slot after a crash mid-OTA. Not available on the
81 // 5.3.3/5.4.2 backports, whose esp_ota_begin() did not invalidate either.
82 if (err == ESP_OK) {
83 esp_ota_invalidate_inactive_ota_data_slot();
84 }
85#endif
86#else
87 err = esp_ota_begin(this->partition_, OTA_WITH_SEQUENTIAL_WRITES, &this->update_handle_);
88#endif
89
90 if (err != ESP_OK) {
91 ESP_LOGE(TAG, "OTA begin failed (err=0x%X)", err);
92 esp_ota_abort(this->update_handle_);
93 this->update_handle_ = 0;
94 if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
96 } else if (err == ESP_ERR_OTA_PARTITION_CONFLICT) {
97 // This error appears with 1 factory and 1 ota partition
99 }
101 }
102#ifdef USE_OTA_PARTITIONS
103 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER) {
105 if (result != OTA_RESPONSE_OK) {
106 return result;
107 }
108 }
109#endif
110 this->md5_.init();
111 return OTA_RESPONSE_OK;
112}
113
114void IDFOTABackend::set_update_md5(const char *expected_md5) {
115 memcpy(this->expected_bin_md5_, expected_md5, 32);
116 this->md5_set_ = true;
117}
118
120#ifdef USE_OTA_PARTITIONS
121 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
122 if (len > PARTITION_TABLE_BUFFER_SIZE - this->buf_written_) {
123 ESP_LOGE(TAG, "Wrong partition table size");
125 }
126 memcpy(this->buf_ + this->buf_written_, data, len);
127 this->buf_written_ += len;
128 this->md5_.add(data, len);
129 return OTA_RESPONSE_OK;
130 }
131 if (!this->is_app_or_bootloader_update_()) {
133 }
134#endif
135 // Overflow can only happen on unknown-size uploads (web_server); known
136 // sizes were rejected in begin().
137 if (this->written_ + len > this->partition_->size) {
139 }
140#ifdef USE_OTA_BLOCK_ERASE_AHEAD
141 OTAResponseTypes erase_result = this->erase_ahead_(len);
142 if (erase_result != OTA_RESPONSE_OK) {
143 return erase_result;
144 }
145#endif
146 esp_err_t err = esp_ota_write(this->update_handle_, data, len);
147 this->md5_.add(data, len);
148 if (err != ESP_OK) {
149 ESP_LOGE(TAG, "esp_ota_write failed (err=0x%X)", err);
150 if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
152 } else if (err == ESP_ERR_INVALID_SIZE) {
153 // Sequential-writes fallback: IDF's lazy erase reports overflow here
155 } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
157 }
159 }
160 this->written_ += len;
161 return OTA_RESPONSE_OK;
162}
163
164#ifdef USE_OTA_BLOCK_ERASE_AHEAD
165OTAResponseTypes IDFOTABackend::erase_ahead_(size_t len) {
166 const size_t end = this->written_ + len;
167 if (this->erased_end_ >= end) {
168 return OTA_RESPONSE_OK;
169 }
170 // Round up to a block boundary, clamped to the partition end; IDF splits the
171 // range into 64 KiB block erases where aligned, sector erases elsewhere.
172 const size_t erase_to = next_erase_end(end, this->partition_->size);
173 // A block erase is one uninterruptible flash op (typically ~150 ms, seconds
174 // on aged flash) and the transfer loop may not have fed the WDT for ~1s.
175 watchdog::WatchdogManager watchdog(15000);
176 esp_err_t err = esp_partition_erase_range(this->partition_, this->erased_end_, erase_to - this->erased_end_);
177 if (err != ESP_OK) {
178 ESP_LOGE(TAG, "esp_partition_erase_range failed (err=0x%X)", err);
180 }
181 this->erased_end_ = erase_to;
182 return OTA_RESPONSE_OK;
183}
184#endif
185
187 if (this->md5_set_) {
188 this->md5_.calculate();
189 if (!this->md5_.equals_hex(this->expected_bin_md5_)) {
190 this->abort();
192 }
193 }
194#ifdef USE_OTA_PARTITIONS
195 // A partition-table update carries an MD5 (checked by IDF), not a Secure Boot
196 // signature, and only re-points boot at an already-installed app -- so it is
197 // intentionally not run through the signature verifier below.
198 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
199 return this->update_partition_table();
200 }
201 if (!this->is_app_or_bootloader_update_()) {
203 }
204#endif
205 esp_err_t err = esp_ota_end(this->update_handle_);
206 this->update_handle_ = 0;
207 if (err != ESP_OK) {
208 ESP_LOGE(TAG, "esp_ota_end failed (err=0x%X)", err);
209 }
210#ifdef USE_OTA_PARTITIONS
211 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER) {
212 return this->finalize_bootloader_update_(err);
213 }
214#endif
215 if (err == ESP_OK) {
216#ifdef USE_OTA_SIGNED_VERIFICATION_MULTI_KEY
217 // IDF's built-in on-update check is disabled for this scheme (it only
218 // matches the incoming image's first signature block against the running
219 // app's first). Verify here against every key the running app trusts, so
220 // rotation and backup keys are accepted. Leaving the boot partition
221 // unchanged means a rejected image never boots.
222 if (!this->verify_signed_image_(this->partition_)) {
224 }
225#endif
226#ifdef USE_OTA_DOWNGRADE_PROTECTION
227 // The image is written and (when signing is enabled) signature-verified by
228 // esp_ota_end(), so its embedded project version can be trusted. Reject the
229 // update if it is older than the running version by leaving the boot
230 // partition unchanged -- the staged image simply never boots.
231 esp_app_desc_t incoming;
232 esp_err_t desc_err = esp_ota_get_partition_description(this->partition_, &incoming);
233 if (desc_err != ESP_OK) {
234 // Couldn't read the staged image's version, so the comparison is skipped.
235 // Warn so the bypassed check is observable rather than silent.
236 ESP_LOGW(TAG, "Downgrade protection: could not read image version (err=0x%X); allowing update", desc_err);
237 } else if (version_is_older(incoming.version, ESPHOME_PROJECT_VERSION)) {
238 ESP_LOGE(TAG, "Rejecting downgrade: image version '%s' is older than running version '%s'", incoming.version,
239 ESPHOME_PROJECT_VERSION);
241 }
242#endif
243 err = esp_ota_set_boot_partition(this->partition_);
244 if (err == ESP_OK) {
245 return OTA_RESPONSE_OK;
246 }
247 }
248 if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
249#ifdef USE_OTA_SIGNED_VERIFICATION
250 ESP_LOGE(TAG, "OTA validation failed (err=0x%X) - possible signature verification failure", err);
252#else
254#endif
255 }
256 if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
258 }
260}
261
263#ifdef USE_OTA_PARTITIONS
264 if (this->partition_table_part_ != nullptr) {
265 esp_partition_deregister_external(this->partition_table_part_);
266 this->partition_table_part_ = nullptr;
267 }
268 if (this->bootloader_part_ != nullptr) {
269 esp_partition_deregister_external(this->bootloader_part_);
270 this->bootloader_part_ = nullptr;
271 }
272#endif
273 // esp_ota_abort with handle 0 returns ESP_ERR_INVALID_ARG harmlessly, so this is safe whether
274 // or not an update is in flight.
275 esp_ota_abort(this->update_handle_);
276 this->update_handle_ = 0;
277 this->written_ = 0;
278#ifdef USE_OTA_BLOCK_ERASE_AHEAD
279 this->erased_end_ = 0;
280#endif
281}
282
283} // namespace esphome::ota
284#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:94
@ OTA_TYPE_UPDATE_PARTITION_TABLE
Definition ota_backend.h:93
bool version_is_older(const char *candidate, const char *reference)
Compare two dotted-numeric version strings (such as "1.2.3").
constexpr size_t next_erase_end(size_t write_end, size_t partition_size)
Target erased watermark for lazy block erase-ahead.
Definition ota_backend.h:78
@ 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:53
@ 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