ESPHome 2026.11.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
18ESPHOME_LOG_TAG(TAG, "ota");
19
20std::unique_ptr<IDFOTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); }
21
23 if (!this->lock_.try_acquire())
25#ifdef USE_OTA_PARTITIONS
26 this->ota_type_ = ota_type;
27 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
28 // Reject any size other than ESP_PARTITION_TABLE_MAX_LEN
29 if (image_size != ESP_PARTITION_TABLE_MAX_LEN) {
30 ESP_LOGE(TAG, "Wrong partition table size: expected %u bytes, got %zu", ESP_PARTITION_TABLE_MAX_LEN, image_size);
32 }
33 memset(this->buf_, 0xFF, sizeof this->buf_);
34 this->buf_written_ = 0;
35 this->image_size_ = image_size;
36 this->md5_.init();
37 return OTA_RESPONSE_OK;
38 }
39 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER) {
40 OTAResponseTypes result = this->prepare_bootloader_update_(image_size);
41 if (result != OTA_RESPONSE_OK) {
42 return result;
43 }
44 }
45 if (!this->is_app_or_bootloader_update_()) {
47 }
48#else
49 if (ota_type != ota::OTA_TYPE_UPDATE_APP) {
51 }
52#endif
53#ifdef USE_OTA_ROLLBACK
54 // If we're starting an OTA, the current boot is good enough - mark it valid
55 // to prevent rollback and allow the OTA to proceed even if the safe mode
56 // timer hasn't expired yet.
57 esp_ota_mark_app_valid_cancel_rollback();
58#endif
59
60 this->partition_ = esp_ota_get_next_update_partition(nullptr);
61 if (this->partition_ == nullptr) {
63 }
64
65 // Both lazy-erase paths below replace esp_ota_begin()'s blocking full erase.
66 // Size check replaces the one that erase performed (0 = unknown size,
67 // e.g. web_server uploads).
68 if (image_size != 0 && image_size > this->partition_->size) {
70 }
71 this->written_ = 0;
72 esp_err_t err;
73#ifdef USE_OTA_BLOCK_ERASE_AHEAD
74 this->erased_end_ = 0;
75 // Unlike esp_ota_begin(), esp_ota_resume() does not reject a running app in
76 // ESP_OTA_IMG_PENDING_VERIFY; that state is unreachable here because the app
77 // was marked valid at boot (esp32/hal.cpp) or just above under USE_OTA_ROLLBACK.
78 // erase_size 0 (!= OTA_WITH_SEQUENTIAL_WRITES) means no erase; erase_ahead_() handles it
79 err = esp_ota_resume(this->partition_, 0, 0, &this->update_handle_);
80#if defined(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
81 // esp_ota_begin() does this on IDF 5.5+; esp_ota_resume() does not. Prevents
82 // booting a half-written slot after a crash mid-OTA. Not available on the
83 // 5.3.3/5.4.2 backports, whose esp_ota_begin() did not invalidate either.
84 if (err == ESP_OK) {
85 esp_ota_invalidate_inactive_ota_data_slot();
86 }
87#endif
88#else
89 err = esp_ota_begin(this->partition_, OTA_WITH_SEQUENTIAL_WRITES, &this->update_handle_);
90#endif
91
92 if (err != ESP_OK) {
93 ESP_LOGE(TAG, "OTA begin failed (err=0x%X)", err);
94 esp_ota_abort(this->update_handle_);
95 this->update_handle_ = 0;
96 if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
98 } else if (err == ESP_ERR_OTA_PARTITION_CONFLICT) {
99 // This error appears with 1 factory and 1 ota partition
101 }
103 }
104#ifdef USE_OTA_PARTITIONS
105 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER) {
107 if (result != OTA_RESPONSE_OK) {
108 return result;
109 }
110 }
111#endif
112 this->md5_.init();
113 return OTA_RESPONSE_OK;
114}
115
116void IDFOTABackend::set_update_md5(const char *expected_md5) {
117 memcpy(this->expected_bin_md5_, expected_md5, 32);
118 this->md5_set_ = true;
119}
120
122#ifdef USE_OTA_PARTITIONS
123 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
124 if (len > PARTITION_TABLE_BUFFER_SIZE - this->buf_written_) {
125 ESP_LOGE(TAG, "Wrong partition table size");
127 }
128 memcpy(this->buf_ + this->buf_written_, data, len);
129 this->buf_written_ += len;
130 this->md5_.add(data, len);
131 return OTA_RESPONSE_OK;
132 }
133 if (!this->is_app_or_bootloader_update_()) {
135 }
136#endif
137 // Overflow can only happen on unknown-size uploads (web_server); known
138 // sizes were rejected in begin().
139 if (this->written_ + len > this->partition_->size) {
141 }
142#ifdef USE_OTA_BLOCK_ERASE_AHEAD
143 OTAResponseTypes erase_result = this->erase_ahead_(len);
144 if (erase_result != OTA_RESPONSE_OK) {
145 return erase_result;
146 }
147#endif
148 esp_err_t err = esp_ota_write(this->update_handle_, data, len);
149 this->md5_.add(data, len);
150 if (err != ESP_OK) {
151 ESP_LOGE(TAG, "esp_ota_write failed (err=0x%X)", err);
152 if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
154 } else if (err == ESP_ERR_INVALID_SIZE) {
155 // Sequential-writes fallback: IDF's lazy erase reports overflow here
157 } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
159 }
161 }
162 this->written_ += len;
163 return OTA_RESPONSE_OK;
164}
165
166#ifdef USE_OTA_BLOCK_ERASE_AHEAD
167OTAResponseTypes IDFOTABackend::erase_ahead_(size_t len) {
168 const size_t end = this->written_ + len;
169 if (this->erased_end_ >= end) {
170 return OTA_RESPONSE_OK;
171 }
172 // Round up to a block boundary, clamped to the partition end; IDF splits the
173 // range into 64 KiB block erases where aligned, sector erases elsewhere.
174 const size_t erase_to = next_erase_end(end, this->partition_->size);
175 // A block erase is one uninterruptible flash op (typically ~150 ms, seconds
176 // on aged flash) and the transfer loop may not have fed the WDT for ~1s.
177 watchdog::WatchdogManager watchdog(15000);
178 esp_err_t err = esp_partition_erase_range(this->partition_, this->erased_end_, erase_to - this->erased_end_);
179 if (err != ESP_OK) {
180 ESP_LOGE(TAG, "esp_partition_erase_range failed (err=0x%X)", err);
182 }
183 this->erased_end_ = erase_to;
184 return OTA_RESPONSE_OK;
185}
186#endif
187
189 if (this->md5_set_) {
190 this->md5_.calculate();
191 if (!this->md5_.equals_hex(this->expected_bin_md5_)) {
192 this->abort();
194 }
195 }
196#ifdef USE_OTA_PARTITIONS
197 // A partition-table update carries an MD5 (checked by IDF), not a Secure Boot
198 // signature, and only re-points boot at an already-installed app -- so it is
199 // intentionally not run through the signature verifier below.
200 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
201 return this->hold_lock_if_ok_(this->update_partition_table_());
202 }
203 if (!this->is_app_or_bootloader_update_()) {
205 }
206#endif
207 esp_err_t err = esp_ota_end(this->update_handle_);
208 this->update_handle_ = 0;
209 if (err != ESP_OK) {
210 ESP_LOGE(TAG, "esp_ota_end failed (err=0x%X)", err);
211 }
212#ifdef USE_OTA_PARTITIONS
213 if (this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER) {
214 return this->hold_lock_if_ok_(this->finalize_bootloader_update_(err));
215 }
216#endif
217 if (err == ESP_OK) {
218#ifdef USE_OTA_SIGNED_VERIFICATION_MULTI_KEY
219 // IDF's built-in on-update check is disabled for this scheme (it only
220 // matches the incoming image's first signature block against the running
221 // app's first). Verify here against every key the running app trusts, so
222 // rotation and backup keys are accepted. Leaving the boot partition
223 // unchanged means a rejected image never boots.
224 if (!this->verify_signed_image_(this->partition_)) {
226 }
227#endif
228#ifdef USE_OTA_DOWNGRADE_PROTECTION
229 // The image is written and (when signing is enabled) signature-verified by
230 // esp_ota_end(), so its embedded project version can be trusted. Reject the
231 // update if it is older than the running version by leaving the boot
232 // partition unchanged -- the staged image simply never boots.
233 esp_app_desc_t incoming;
234 esp_err_t desc_err = esp_ota_get_partition_description(this->partition_, &incoming);
235 if (desc_err != ESP_OK) {
236 // Couldn't read the staged image's version, so the comparison is skipped.
237 // Warn so the bypassed check is observable rather than silent.
238 ESP_LOGW(TAG, "Downgrade protection: could not read image version (err=0x%X); allowing update", desc_err);
239 } else if (version_is_older(incoming.version, ESPHOME_PROJECT_VERSION)) {
240 ESP_LOGE(TAG, "Rejecting downgrade: image version '%s' is older than running version '%s'", incoming.version,
241 ESPHOME_PROJECT_VERSION);
243 }
244#endif
245 err = esp_ota_set_boot_partition(this->partition_);
246 if (err == ESP_OK) {
247 return this->hold_lock_if_ok_(OTA_RESPONSE_OK);
248 }
249 }
250 if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
251#ifdef USE_OTA_SIGNED_VERIFICATION
252 ESP_LOGE(TAG, "OTA validation failed (err=0x%X) - possible signature verification failure", err);
254#else
256#endif
257 }
258 if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
260 }
262}
263
265 if (!this->lock_.is_held()) // refused in begin()
266 return;
267#ifdef USE_OTA_PARTITIONS
268 if (this->partition_table_part_ != nullptr) {
269 esp_partition_deregister_external(this->partition_table_part_);
270 this->partition_table_part_ = nullptr;
271 }
272 if (this->bootloader_part_ != nullptr) {
273 esp_partition_deregister_external(this->bootloader_part_);
274 this->bootloader_part_ = nullptr;
275 }
276#endif
277 // esp_ota_abort with handle 0 returns ESP_ERR_INVALID_ARG harmlessly, so this is safe whether
278 // or not an update is in flight.
279 esp_ota_abort(this->update_handle_);
280 this->update_handle_ = 0;
281 this->written_ = 0;
282#ifdef USE_OTA_BLOCK_ERASE_AHEAD
283 this->erased_end_ = 0;
284#endif
285}
286
287} // namespace esphome::ota
288#endif // USE_ESP32
bool equals_hex(const char *expected)
Compare the hash against a provided hex-encoded hash of get_size() * 2 characters,...
Definition hash_base.h:36
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)
OTAResponseTypes hold_lock_if_ok_(OTAResponseTypes result)
bool try_acquire()
Returns false if held elsewhere.
@ OTA_TYPE_UPDATE_BOOTLOADER
Definition ota_backend.h:96
@ OTA_TYPE_UPDATE_PARTITION_TABLE
Definition ota_backend.h:95
ESPHOME_LOG_TAG(TAG, "ota")
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:80
@ OTA_RESPONSE_ERROR_MD5_MISMATCH
Definition ota_backend.h:44
@ OTA_RESPONSE_ERROR_VERSION_DOWNGRADE
Definition ota_backend.h:52
@ OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE
Definition ota_backend.h:42
@ OTA_RESPONSE_ERROR_WRITING_FLASH
Definition ota_backend.h:36
@ OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE
Definition ota_backend.h:47
@ OTA_RESPONSE_ERROR_UPDATE_END
Definition ota_backend.h:37
@ OTA_RESPONSE_ERROR_SIGNATURE_INVALID
Definition ota_backend.h:46
@ OTA_RESPONSE_ERROR_UPDATE_BUSY
Definition ota_backend.h:54
@ OTA_RESPONSE_ERROR_UNKNOWN
Definition ota_backend.h:55
@ OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION
Definition ota_backend.h:43
@ OTA_RESPONSE_ERROR_MAGIC
Definition ota_backend.h:33
@ OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY
Definition ota_backend.h:48
std::unique_ptr< ArduinoLibreTinyOTABackend > make_ota_backend()
const void size_t len
Definition hal.h:68