ESPHome 2026.5.0b1
Loading...
Searching...
No Matches
ota_backend_host.cpp
Go to the documentation of this file.
1#ifdef USE_HOST
2#include "ota_backend_host.h"
3
6#include "esphome/core/log.h"
7
8#include <cerrno>
9#include <cstdint>
10#include <cstdio>
11#include <cstring>
12
13#include <fcntl.h>
14#include <sys/stat.h>
15#include <unistd.h>
16
17#ifdef __linux__
18#include <elf.h>
19#include <endian.h>
20#endif
21
22#ifdef __APPLE__
23#include <mach-o/loader.h>
24#endif
25
26namespace esphome::ota {
27
28namespace {
29
30const char *const TAG = "ota.host";
31
32constexpr size_t MAX_OTA_SIZE = 256u * 1024u * 1024u; // 256 MiB
33constexpr size_t HEADER_PEEK_SIZE = 64;
34
35ssize_t read_header_(const char *path, uint8_t *buf, size_t len) {
36 int fd = ::open(path, O_RDONLY);
37 if (fd < 0)
38 return -1;
39 ssize_t got = ::read(fd, buf, len);
40 ::close(fd);
41 return got;
42}
43
44#ifdef __linux__
45struct ElfIdent {
46 bool valid;
47 uint8_t ei_class;
48 uint8_t ei_data;
49 uint16_t e_machine;
50 uint16_t e_type;
51};
52
53ElfIdent parse_elf_(const uint8_t *buf, size_t len) {
54 ElfIdent out{};
55 if (len < EI_NIDENT + 4)
56 return out;
57 if (buf[EI_MAG0] != ELFMAG0 || buf[EI_MAG1] != ELFMAG1 || buf[EI_MAG2] != ELFMAG2 || buf[EI_MAG3] != ELFMAG3)
58 return out;
59 out.ei_class = buf[EI_CLASS];
60 out.ei_data = buf[EI_DATA];
61 // e_type @ 16, e_machine @ 18, both in EI_DATA endianness.
62 uint16_t e_type;
63 uint16_t e_machine;
64 std::memcpy(&e_type, buf + 16, sizeof(e_type));
65 std::memcpy(&e_machine, buf + 18, sizeof(e_machine));
66 if (out.ei_data == ELFDATA2LSB) {
67 out.e_type = le16toh(e_type);
68 out.e_machine = le16toh(e_machine);
69 } else if (out.ei_data == ELFDATA2MSB) {
70 out.e_type = be16toh(e_type);
71 out.e_machine = be16toh(e_machine);
72 } else {
73 return out;
74 }
75 out.valid = true;
76 return out;
77}
78
79bool validate_elf_(const char *staging_path, const std::string &exe_path) {
80 uint8_t new_buf[HEADER_PEEK_SIZE];
81 uint8_t cur_buf[HEADER_PEEK_SIZE];
82 ssize_t new_n = read_header_(staging_path, new_buf, sizeof(new_buf));
83 ssize_t cur_n = read_header_(exe_path.c_str(), cur_buf, sizeof(cur_buf));
84 if (new_n < static_cast<ssize_t>(EI_NIDENT + 4) || cur_n < static_cast<ssize_t>(EI_NIDENT + 4)) {
85 ESP_LOGE(TAG, "ELF header read failed");
86 return false;
87 }
88 ElfIdent new_id = parse_elf_(new_buf, new_n);
89 ElfIdent cur_id = parse_elf_(cur_buf, cur_n);
90 if (!new_id.valid) {
91 ESP_LOGE(TAG, "Uploaded payload is not a valid ELF");
92 return false;
93 }
94 if (!cur_id.valid) {
95 ESP_LOGE(TAG, "Could not parse running exe ELF header");
96 return false;
97 }
98 if (new_id.ei_class != cur_id.ei_class) {
99 ESP_LOGE(TAG, "ELF class mismatch (uploaded=%u, running=%u)", new_id.ei_class, cur_id.ei_class);
100 return false;
101 }
102 if (new_id.ei_data != cur_id.ei_data) {
103 ESP_LOGE(TAG, "ELF endianness mismatch");
104 return false;
105 }
106 if (new_id.e_machine != cur_id.e_machine) {
107 ESP_LOGE(TAG, "ELF e_machine mismatch (uploaded=0x%04x, running=0x%04x)", new_id.e_machine, cur_id.e_machine);
108 return false;
109 }
110 if (new_id.e_type != ET_EXEC && new_id.e_type != ET_DYN) {
111 ESP_LOGE(TAG, "ELF e_type=%u is not executable", new_id.e_type);
112 return false;
113 }
114 return true;
115}
116#endif // __linux__
117
118#ifdef __APPLE__
119struct MachOIdent {
120 bool valid;
123};
124
125MachOIdent parse_macho_(const uint8_t *buf, size_t len) {
126 MachOIdent out{};
127 // mach_header is the common prefix of mach_header and mach_header_64;
128 // cputype/cpusubtype/filetype have identical offsets in both.
129 if (len < sizeof(struct mach_header))
130 return out;
131 uint32_t magic;
132 std::memcpy(&magic, buf, sizeof(magic));
133 bool swap;
134 if (magic == MH_MAGIC || magic == MH_MAGIC_64) {
135 swap = false;
136 } else if (magic == MH_CIGAM || magic == MH_CIGAM_64) {
137 swap = true;
138 } else {
139 return out;
140 }
141 struct mach_header hdr;
142 std::memcpy(&hdr, buf, sizeof(hdr));
143 if (swap) {
144 hdr.cputype = OSSwapInt32(hdr.cputype);
145 hdr.cpusubtype = OSSwapInt32(hdr.cpusubtype);
146 hdr.filetype = OSSwapInt32(hdr.filetype);
147 }
148 if (hdr.filetype != MH_EXECUTE)
149 return out;
150 out.cputype = hdr.cputype;
151 out.cpusubtype = hdr.cpusubtype;
152 out.valid = true;
153 return out;
154}
155
156bool validate_macho_(const char *staging_path, const std::string &exe_path) {
157 uint8_t new_buf[HEADER_PEEK_SIZE];
158 uint8_t cur_buf[HEADER_PEEK_SIZE];
159 ssize_t new_n = read_header_(staging_path, new_buf, sizeof(new_buf));
160 ssize_t cur_n = read_header_(exe_path.c_str(), cur_buf, sizeof(cur_buf));
161 if (new_n < static_cast<ssize_t>(sizeof(struct mach_header)) ||
162 cur_n < static_cast<ssize_t>(sizeof(struct mach_header))) {
163 ESP_LOGE(TAG, "Mach-O header read failed");
164 return false;
165 }
166 MachOIdent new_id = parse_macho_(new_buf, new_n);
167 MachOIdent cur_id = parse_macho_(cur_buf, cur_n);
168 if (!new_id.valid) {
169 ESP_LOGE(TAG, "Uploaded payload is not a valid thin Mach-O executable");
170 return false;
171 }
172 if (!cur_id.valid) {
173 ESP_LOGE(TAG, "Could not parse running exe Mach-O header");
174 return false;
175 }
176 if (new_id.cputype != cur_id.cputype || new_id.cpusubtype != cur_id.cpusubtype) {
177 ESP_LOGE(TAG, "Mach-O arch mismatch (uploaded=0x%x/0x%x, running=0x%x/0x%x)", new_id.cputype, new_id.cpusubtype,
178 cur_id.cputype, cur_id.cpusubtype);
179 return false;
180 }
181 return true;
182}
183#endif // __APPLE__
184
185bool validate_executable_(const char *staging_path, const std::string &exe_path) {
186#ifdef __linux__
187 return validate_elf_(staging_path, exe_path);
188#elif defined(__APPLE__)
189 return validate_macho_(staging_path, exe_path);
190#else
191 (void) staging_path;
192 (void) exe_path;
193 ESP_LOGE(TAG, "Host OTA validation not implemented for this OS");
194 return false;
195#endif
196}
197
198} // namespace
199
200std::unique_ptr<HostOTABackend> make_ota_backend() { return make_unique<HostOTABackend>(); }
201
202OTAResponseTypes HostOTABackend::begin(size_t image_size, OTAType ota_type) {
203 if (ota_type != OTA_TYPE_UPDATE_APP)
205 // 0 = unknown size (web_server multipart); cap at MAX_OTA_SIZE.
206 if (image_size > MAX_OTA_SIZE) {
207 ESP_LOGE(TAG, "Refusing OTA of size %zu (exceeds %zu)", image_size, MAX_OTA_SIZE);
209 }
210
211 const std::string &exe = host::get_exe_path();
212 if (exe.empty()) {
213 ESP_LOGE(TAG, "Could not resolve running executable path; cannot stage OTA");
215 }
216 this->final_path_ = exe;
217 this->staging_path_ = exe + ".ota.new";
218
219 // Clean up any leftover from a prior aborted OTA.
220 ::unlink(this->staging_path_.c_str());
221
222 this->fd_ = ::open(this->staging_path_.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0755);
223 if (this->fd_ < 0) {
224 ESP_LOGE(TAG, "Open '%s' failed: %s", this->staging_path_.c_str(), std::strerror(errno));
226 }
227
228 this->expected_size_ = image_size;
229 this->bytes_written_ = 0;
230 this->md5_set_ = false;
231 this->md5_.init();
232
233 ESP_LOGD(TAG, "OTA begin: staging=%s, size=%zu", this->staging_path_.c_str(), image_size);
234 return OTA_RESPONSE_OK;
235}
236
237void HostOTABackend::set_update_md5(const char *md5) {
238 if (parse_hex(md5, this->expected_md5_, 16))
239 this->md5_set_ = true;
240}
241
243 if (this->fd_ < 0)
245 size_t limit = this->expected_size_ != 0 ? this->expected_size_ : MAX_OTA_SIZE;
246 if (this->bytes_written_ + len > limit) {
247 ESP_LOGE(TAG, "Write past size limit (%zu)", limit);
249 }
250
251 size_t remaining = len;
252 const uint8_t *p = data;
253 while (remaining > 0) {
254 ssize_t n = ::write(this->fd_, p, remaining);
255 if (n < 0) {
256 if (errno == EINTR)
257 continue;
258 ESP_LOGE(TAG, "Write failed: %s", std::strerror(errno));
260 }
261 p += n;
262 remaining -= n;
263 }
264 this->md5_.add(data, len);
265 this->bytes_written_ += len;
266 return OTA_RESPONSE_OK;
267}
268
270 if (this->fd_ < 0)
272
273 if (this->bytes_written_ == 0) {
274 ESP_LOGE(TAG, "OTA ended with no data written");
275 this->abort();
277 }
278 if (this->expected_size_ != 0 && this->bytes_written_ != this->expected_size_) {
279 ESP_LOGE(TAG, "Size mismatch: got %zu, expected %zu", this->bytes_written_, this->expected_size_);
280 this->abort();
282 }
283
284 if (this->md5_set_) {
285 this->md5_.calculate();
286 if (!this->md5_.equals_bytes(this->expected_md5_)) {
287 ESP_LOGE(TAG, "MD5 mismatch");
288 this->abort();
290 }
291 }
292
293 if (::fsync(this->fd_) != 0) {
294 ESP_LOGW(TAG, "fsync failed: %s", std::strerror(errno));
295 }
296 ::close(this->fd_);
297 this->fd_ = -1;
298
299 if (!validate_executable_(this->staging_path_.c_str(), this->final_path_)) {
300 ::unlink(this->staging_path_.c_str());
301 this->staging_path_.clear();
303 }
304
305 if (::chmod(this->staging_path_.c_str(), 0755) != 0) {
306 ESP_LOGW(TAG, "chmod failed: %s", std::strerror(errno));
307 }
308
309 if (::rename(this->staging_path_.c_str(), this->final_path_.c_str()) != 0) {
310 ESP_LOGE(TAG, "rename '%s' -> '%s' failed: %s", this->staging_path_.c_str(), this->final_path_.c_str(),
311 std::strerror(errno));
312 ::unlink(this->staging_path_.c_str());
313 this->staging_path_.clear();
315 }
316
317 // arch_restart() (via App::safe_reboot) will execv this path with the original argv.
319 this->staging_path_.clear();
320 ESP_LOGI(TAG, "OTA staged at %s; will re-exec on reboot", this->final_path_.c_str());
321 return OTA_RESPONSE_OK;
322}
323
325 if (this->fd_ >= 0) {
326 ::close(this->fd_);
327 this->fd_ = -1;
328 }
329 if (!this->staging_path_.empty()) {
330 ::unlink(this->staging_path_.c_str());
331 this->staging_path_.clear();
332 }
333 this->expected_size_ = 0;
334 this->bytes_written_ = 0;
335 this->md5_set_ = false;
336}
337
338} // namespace esphome::ota
339#endif
bool equals_bytes(const uint8_t *expected)
Compare the hash against a provided byte-encoded hash.
Definition hash_base.h:32
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
OTAResponseTypes write(uint8_t *data, size_t len)
OTAResponseTypes begin(size_t image_size, OTAType ota_type=OTA_TYPE_UPDATE_APP)
void set_update_md5(const char *md5)
__int64 ssize_t
Definition httplib.h:178
const std::string & get_exe_path()
Absolute path to running exe (resolved at startup); empty on failure.
Definition core.cpp:61
void arm_reexec(const std::string &path)
Arm an execv on the next arch_restart(). Pass empty to disarm.
Definition core.cpp:66
@ OTA_RESPONSE_ERROR_MD5_MISMATCH
Definition ota_backend.h:41
@ OTA_RESPONSE_ERROR_WRITING_FLASH
Definition ota_backend.h:33
@ OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE
Definition ota_backend.h:44
@ OTA_RESPONSE_ERROR_UPDATE_END
Definition ota_backend.h:34
@ OTA_RESPONSE_ERROR_UPDATE_PREPARE
Definition ota_backend.h:31
std::unique_ptr< ArduinoLibreTinyOTABackend > make_ota_backend()
const char *const TAG
Definition spi.cpp:7
std::string size_t len
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count)
Parse bytes from a hex-encoded string into a byte array.
Definition helpers.cpp:275
uint8_t ei_class
uint32_t cpusubtype
uint16_t e_machine
bool valid
uint16_t e_type
uint8_t ei_data
uint32_t cputype
static void uint32_t