1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
|
//! The Espressif ROM loader protocol, enough of it to flash a chip: SLIP framing, SYNC, flash
//! attach, and uncompressed block writes. No software stub is uploaded - the ROM can do all of
//! this by itself, and for a ~1 KB image the stub's compression and 16 KB blocks buy nothing.
//!
//! Frame format (esptool loader.py:526-534, 577):
//! request: C0 | 00 op len16 chk32 | payload | C0
//! response: C0 | 01 op len16 val32 | data | C0
//! with C0 -> DB DC and DB -> DB DD inside the frame.
//!
//! The ESP32 ROM loaders (unlike the ESP8266's, and unlike the software stub) append FOUR trailing
//! bytes to every response: status, reason, and two reserved bytes (esptool loader.py:653-655,
//! 676). Reading the status at data[len - 2] therefore reads a reserved byte and turns every ROM
//! error into a success - which is exactly the bug an adversarial review of this file found, after
//! driving it over a pty with a rejected FLASH_DATA block.
const std = @import("std");
const Port = @import("serial.zig").Port;
pub const Cmd = enum(u8) {
flash_begin = 0x02,
flash_data = 0x03,
flash_end = 0x04,
sync = 0x08,
read_reg = 0x0A,
spi_set_params = 0x0B,
spi_attach = 0x0D,
change_baud = 0x0F,
spi_flash_md5 = 0x13,
get_security_info = 0x14,
};
pub const Error = error{
SyncFailed,
CommandFailed,
ShortResponse,
Timeout,
BadFrame,
};
pub const Loader = struct {
port: *Port,
/// Bytes already read from the port but not yet consumed by the frame parser. Reading a byte
/// at a time costs a poll+read syscall pair each, which turned a 1.5 KB flash into a 600 ms
/// affair; refilling in bursts brings it under 60 ms.
rx: [1024]u8 = undefined,
rx_len: usize = 0,
rx_pos: usize = 0,
/// The ROM's own block size. The stub raises this to 0x4000; we do not use the stub.
pub const block_size = 0x400;
fn nextByte(l: *Loader, deadline_ms: i64) !?u8 {
while (l.rx_pos == l.rx_len) {
const remaining = deadline_ms - l.port.nowMs();
if (remaining <= 0) return null;
const n = try l.port.readTimeout(&l.rx, @intCast(@min(remaining, 50)));
if (n == 0) continue;
l.rx_len = n;
l.rx_pos = 0;
}
defer l.rx_pos += 1;
return l.rx[l.rx_pos];
}
/// Consume input until the line has been quiet for `quiet_ms`, but never for longer than
/// twenty such windows: a board stuck in a brownout-reset loop re-prints its ROM banner
/// forever, and an unbounded version of this loop hangs the flash with no output at all.
fn drainUntilQuiet(l: *Loader, quiet_ms: i64) void {
l.rx_pos = 0;
l.rx_len = 0;
const deadline = l.port.nowMs() + 20 * quiet_ms;
while (l.port.nowMs() < deadline) {
const n = l.port.readTimeout(&l.rx, @intCast(quiet_ms)) catch return;
if (n == 0) return;
}
}
fn frame(gpa: std.mem.Allocator, cmd: Cmd, payload: []const u8, checksum: u32) ![]u8 {
var out: std.ArrayList(u8) = .empty;
errdefer out.deinit(gpa);
var head: [8]u8 = undefined;
head[0] = 0x00;
head[1] = @intFromEnum(cmd);
std.mem.writeInt(u16, head[2..4], @intCast(payload.len), .little);
std.mem.writeInt(u32, head[4..8], checksum, .little);
try out.append(gpa, 0xC0);
for (head) |b| try escape(gpa, &out, b);
for (payload) |b| try escape(gpa, &out, b);
try out.append(gpa, 0xC0);
return out.toOwnedSlice(gpa);
}
fn escape(gpa: std.mem.Allocator, out: *std.ArrayList(u8), b: u8) !void {
switch (b) {
0xC0 => try out.appendSlice(gpa, &.{ 0xDB, 0xDC }),
0xDB => try out.appendSlice(gpa, &.{ 0xDB, 0xDD }),
else => try out.append(gpa, b),
}
}
/// Read one SLIP frame, un-escaping as it goes.
fn readFrame(l: *Loader, gpa: std.mem.Allocator, timeout_ms: u32) ![]u8 {
var out: std.ArrayList(u8) = .empty;
errdefer out.deinit(gpa);
var started = false;
var escaping = false;
const deadline = l.port.nowMs() + @as(i64, timeout_ms);
while (try l.nextByte(deadline)) |b| {
if (!started) {
if (b == 0xC0) started = true;
continue;
}
if (escaping) {
try out.append(gpa, switch (b) {
0xDC => 0xC0,
0xDD => 0xDB,
else => return Error.BadFrame,
});
escaping = false;
continue;
}
switch (b) {
0xDB => escaping = true,
0xC0 => {
if (out.items.len == 0) continue; // empty frame, keep looking
return out.toOwnedSlice(gpa);
},
else => try out.append(gpa, b),
}
}
return Error.Timeout;
}
/// Send a command and wait for its matching response. Returns the response `val` field, and
/// copies any leading response data into `out` when one is given.
pub fn command(
l: *Loader,
gpa: std.mem.Allocator,
cmd: Cmd,
payload: []const u8,
checksum: u32,
timeout_ms: u32,
out: ?[]u8,
) !u32 {
const pkt = try frame(gpa, cmd, payload, checksum);
defer gpa.free(pkt);
try l.port.write(pkt);
const want_data: usize = if (out) |o| o.len else 0;
var tries: usize = 0;
while (tries < 100) : (tries += 1) {
const resp = l.readFrame(gpa, timeout_ms) catch |err| return err;
defer gpa.free(resp);
// Skip anything that is not this command's reply: stale frames from a previous
// session, or the ROM's repeated SYNC echoes.
if (resp.len < 8) continue;
if (resp[0] != 0x01 or resp[1] != @intFromEnum(cmd)) continue;
const val = std.mem.readInt(u32, resp[4..8], .little);
const data = resp[8..];
// Status sits after the expected payload. The ROM appends four bytes (status, reason,
// two reserved) where the stub appends two; esptool tolerates either, so gate on two
// and read the status at the payload end - reading it at len-2 is what made every ROM
// error look like success.
if (data.len < want_data + 2) return Error.ShortResponse;
if (data[want_data] != 0) return Error.CommandFailed;
if (out) |o| @memcpy(o, data[0..want_data]);
return val;
}
return Error.Timeout;
}
pub fn sync(l: *Loader, gpa: std.mem.Allocator) !void {
var payload: [36]u8 = undefined;
payload[0..4].* = .{ 0x07, 0x07, 0x12, 0x20 };
@memset(payload[4..], 0x55);
var attempt: usize = 0;
// Short per-attempt timeout: the first SYNC after a reset usually lands before the ROM is
// listening, and waiting 200 ms for that is most of the flash time on a small image.
while (attempt < 20) : (attempt += 1) {
// A reset that did not take is the usual reason SYNC never answers, so re-run it
// periodically rather than failing the build - esptool retries the whole connect
// seven times for the same reason (loader.py:891-899).
if (attempt > 0 and attempt % 5 == 0) l.port.resetToDownload(.{}) catch {};
if (l.command(gpa, .sync, &payload, 0, 40, null)) |_| {
// The ROM answers SYNC eight times. Swallow the echoes, but stop as soon as the
// line goes quiet instead of burning a fixed 350 ms.
l.drainUntilQuiet(15);
return;
} else |_| {}
}
return Error.SyncFailed;
}
pub fn attachFlash(l: *Loader, gpa: std.mem.Allocator) !void {
var payload: [8]u8 = @splat(0); // default SPI pins, not legacy
_ = try l.command(gpa, .spi_attach, &payload, 0, 3000, null);
}
pub fn setFlashParams(l: *Loader, gpa: std.mem.Allocator, total_size: u32) !void {
var payload: [24]u8 = undefined;
std.mem.writeInt(u32, payload[0..4], 0, .little); // fl_id, ignored by the ROM
std.mem.writeInt(u32, payload[4..8], total_size, .little);
std.mem.writeInt(u32, payload[8..12], 64 * 1024, .little); // block
std.mem.writeInt(u32, payload[12..16], 4 * 1024, .little); // sector
std.mem.writeInt(u32, payload[16..20], 256, .little); // page
std.mem.writeInt(u32, payload[20..24], 0xFFFF, .little); // status mask
_ = try l.command(gpa, .spi_set_params, &payload, 0, 3000, null);
}
/// Write `data` at `offset`. The ROM erases synchronously inside FLASH_BEGIN.
pub fn writeFlash(l: *Loader, gpa: std.mem.Allocator, offset: u32, data: []const u8) !void {
const blocks: u32 = @intCast(std.math.divCeil(usize, data.len, block_size) catch unreachable);
var begin: [20]u8 = undefined;
std.mem.writeInt(u32, begin[0..4], @intCast(data.len), .little); // erase size
std.mem.writeInt(u32, begin[4..8], blocks, .little);
std.mem.writeInt(u32, begin[8..12], block_size, .little);
std.mem.writeInt(u32, begin[12..16], offset, .little);
std.mem.writeInt(u32, begin[16..20], 0, .little); // not encrypted
const erase_timeout: u32 = @intCast(@max(@as(usize, 3000), data.len / 1024 * 30));
_ = try l.command(gpa, .flash_begin, &begin, 0, erase_timeout, null);
var seq: u32 = 0;
var sent: usize = 0;
var block_buf: [16 + block_size]u8 = undefined;
while (sent < data.len) : (seq += 1) {
const take = @min(block_size, data.len - sent);
const chunk = data[sent .. sent + take];
std.mem.writeInt(u32, block_buf[0..4], block_size, .little);
std.mem.writeInt(u32, block_buf[4..8], seq, .little);
std.mem.writeInt(u32, block_buf[8..12], 0, .little);
std.mem.writeInt(u32, block_buf[12..16], 0, .little);
@memcpy(block_buf[16 .. 16 + take], chunk);
@memset(block_buf[16 + take ..], 0xFF); // ROM writes whole blocks; pad with erased value
var checksum: u32 = 0xEF;
for (block_buf[16..]) |b| checksum ^= b;
var attempt: usize = 0;
while (true) : (attempt += 1) {
if (l.command(gpa, .flash_data, &block_buf, checksum, 3000, null)) |_| break else |err| {
if (attempt >= 2) return err;
}
}
sent += take;
}
}
/// MD5 over a flash range, as 32 ASCII hex characters from the ROM. Routed through
/// `command()` so the direction byte, the opcode and the status are all checked - this is the
/// only thing standing between a rejected block and a bricked image.
pub fn flashMd5(l: *Loader, gpa: std.mem.Allocator, offset: u32, len: u32, out: *[32]u8) !void {
var payload: [16]u8 = undefined;
std.mem.writeInt(u32, payload[0..4], offset, .little);
std.mem.writeInt(u32, payload[4..8], len, .little);
std.mem.writeInt(u32, payload[8..12], 0, .little);
std.mem.writeInt(u32, payload[12..16], 0, .little);
_ = try l.command(gpa, .spi_flash_md5, &payload, 0, @max(1000, len / 1024 * 8), out);
}
};
|