summaryrefslogtreecommitdiff
path: root/tools/image_test.zig
blob: 9f2cbc07bd1b1e24683ee19b65de67e13c0f780e (plain) (blame)
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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
//! Host tests for the image builder. Every case here encodes a rule the ESP32-P4 ROM bootloader
//! actually enforces, each of which was learned by flashing a deliberately broken image at the
//! board and reading the error off the serial port (see 04-report/evidence/).

const std = @import("std");
const image = @import("image.zig");

const testing = std.testing;

/// Build a 32-bit little-endian ELF with the given PT_LOAD segments, in memory.
const Load = struct { addr: u32, len: usize };

fn synthElf(gpa: std.mem.Allocator, entry: u32, loads: []const Load) ![]u8 {
    const ehsize = 52;
    const phentsize = 32;
    var out: std.ArrayList(u8) = .empty;
    errdefer out.deinit(gpa);

    const phoff = ehsize;
    var data_off = phoff + phentsize * loads.len;

    try out.appendSlice(gpa, &.{ 0x7F, 'E', 'L', 'F', 1, 1, 1, 0 }); // magic, 32-bit, LE, v1
    try out.appendNTimes(gpa, 0, 8); // padding
    try out.appendSlice(gpa, &std.mem.toBytes(@as(u16, 2))); // ET_EXEC
    try out.appendSlice(gpa, &std.mem.toBytes(@as(u16, 243))); // EM_RISCV
    try out.appendSlice(gpa, &std.mem.toBytes(@as(u32, 1))); // version
    try out.appendSlice(gpa, &std.mem.toBytes(entry));
    try out.appendSlice(gpa, &std.mem.toBytes(@as(u32, phoff)));
    try out.appendSlice(gpa, &std.mem.toBytes(@as(u32, 0))); // shoff
    try out.appendSlice(gpa, &std.mem.toBytes(@as(u32, 0))); // flags
    try out.appendSlice(gpa, &std.mem.toBytes(@as(u16, ehsize)));
    try out.appendSlice(gpa, &std.mem.toBytes(@as(u16, phentsize)));
    try out.appendSlice(gpa, &std.mem.toBytes(@as(u16, @intCast(loads.len))));
    try out.appendSlice(gpa, &std.mem.toBytes(@as(u16, 0))); // shentsize
    try out.appendSlice(gpa, &std.mem.toBytes(@as(u16, 0))); // shnum
    try out.appendSlice(gpa, &std.mem.toBytes(@as(u16, 0))); // shstrndx
    std.debug.assert(out.items.len == ehsize);

    for (loads) |l| {
        try out.appendSlice(gpa, &std.mem.toBytes(@as(u32, 1))); // PT_LOAD
        try out.appendSlice(gpa, &std.mem.toBytes(@as(u32, @intCast(data_off))));
        try out.appendSlice(gpa, &std.mem.toBytes(l.addr)); // vaddr
        try out.appendSlice(gpa, &std.mem.toBytes(l.addr)); // paddr
        try out.appendSlice(gpa, &std.mem.toBytes(@as(u32, @intCast(l.len)))); // filesz
        try out.appendSlice(gpa, &std.mem.toBytes(@as(u32, @intCast(l.len)))); // memsz
        try out.appendSlice(gpa, &std.mem.toBytes(@as(u32, 4))); // flags
        try out.appendSlice(gpa, &std.mem.toBytes(@as(u32, 0x1000))); // align
        data_off += l.len;
    }
    for (loads, 0..) |l, i| {
        try out.appendNTimes(gpa, @intCast('A' + i), l.len);
    }
    return out.toOwnedSlice(gpa);
}

fn segmentHeaders(bytes: []const u8) []const u8 {
    return bytes[24..];
}

test "two mapped segments in one MMU page produce a valid, tiny image" {
    const gpa = testing.allocator;
    // rodata at 0x40000020 (600 B) then text at 0x40000280: 0x20+600+8 == 0x280, congruent
    const elf = try synthElf(gpa, 0x40000280, &.{
        .{ .addr = 0x40000020, .len = 600 },
        .{ .addr = 0x40000280, .len = 760 },
    });
    defer gpa.free(elf);

    var layout = try image.fromElf(gpa, elf, .{});
    defer layout.deinit(gpa);

    try layout.validate(.{});
    try testing.expectEqual(@as(usize, 2), layout.segments.len); // no pad segment needed
    try testing.expectEqual(@as(u32, 0x40000280), layout.entry);
    // 24 B header + 2*(8 B segment header) + payload + checksum pad + 32 B digest
    try testing.expectEqual(@as(usize, 1440), layout.bytes.len);
    try testing.expectEqual(@as(u8, 0xE9), layout.bytes[0]);
    try testing.expectEqual(@as(u8, 2), layout.bytes[1]);
    try testing.expectEqual(@as(u8, 1), layout.bytes[0x17]); // hash_appended
}

test "the previous segment grows when the next mapped segment is not congruent" {
    const gpa = testing.allocator;
    const elf = try synthElf(gpa, 0x40001000, &.{
        .{ .addr = 0x40000020, .len = 100 },
        .{ .addr = 0x40001000, .len = 64 }, // far away: needs padding to line up
    });
    defer gpa.free(elf);

    var layout = try image.fromElf(gpa, elf, .{});
    defer layout.deinit(gpa);

    try layout.validate(.{});
    // no extra segment: the first one carries filler instead of a pad segment paying a header
    try testing.expectEqual(@as(usize, 2), layout.segments.len);
    try testing.expect(layout.segments[0].filler > 0);
    try testing.expectEqual(@as(u32, 100), layout.payload - layout.segments[1].len);
}

test "segment lengths are padded to a multiple of four" {
    const gpa = testing.allocator;
    const elf = try synthElf(gpa, 0x40000040, &.{
        .{ .addr = 0x40000020, .len = 5 }, // 5 bytes: the loader would reject this as-is
        .{ .addr = 0x40000040, .len = 7 },
    });
    defer gpa.free(elf);

    var layout = try image.fromElf(gpa, elf, .{});
    defer layout.deinit(gpa);

    for (layout.segments) |s| try testing.expectEqual(@as(u32, 0), s.len % 4);
    try testing.expect(layout.segments[0].len >= 8); // 5 bytes rounded up, plus congruence filler
}

test "an image with one mapped segment is rejected before it can brick a board" {
    const gpa = testing.allocator;
    const elf = try synthElf(gpa, 0x40000020, &.{.{ .addr = 0x40000020, .len = 64 }});
    defer gpa.free(elf);

    var layout = try image.fromElf(gpa, elf, .{});
    defer layout.deinit(gpa);

    try testing.expectError(error.NotTwoMappedSegments, layout.validate(.{}));
}

test "RAM-loaded segments do not count as mapped" {
    const gpa = testing.allocator;
    const elf = try synthElf(gpa, 0x40000020, &.{
        .{ .addr = 0x40000020, .len = 32 },
        .{ .addr = 0x40000060, .len = 32 },
        .{ .addr = 0x4FF00000, .len = 16 }, // L2MEM: loaded, not mapped
    });
    defer gpa.free(elf);

    var layout = try image.fromElf(gpa, elf, .{});
    defer layout.deinit(gpa);

    try layout.validate(.{});
    var mapped: usize = 0;
    var loaded: usize = 0;
    for (layout.segments) |s| switch (s.kind) {
        .mapped => mapped += 1,
        .loaded => loaded += 1,
        .pad => {},
    };
    try testing.expectEqual(@as(usize, 2), mapped);
    try testing.expectEqual(@as(usize, 1), loaded);
}

test "the checksum byte lands on a 16-byte boundary and the digest covers everything before it" {
    const gpa = testing.allocator;
    const elf = try synthElf(gpa, 0x40000280, &.{
        .{ .addr = 0x40000020, .len = 600 },
        .{ .addr = 0x40000280, .len = 760 },
    });
    defer gpa.free(elf);

    var layout = try image.fromElf(gpa, elf, .{});
    defer layout.deinit(gpa);

    const checksum_off = layout.bytes.len - 33;
    try testing.expectEqual(@as(usize, 15), checksum_off % 16);

    var expect: [32]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash(layout.bytes[0 .. layout.bytes.len - 32], &expect, .{});
    try testing.expectEqualSlices(u8, &expect, layout.bytes[layout.bytes.len - 32 ..]);

    // and the checksum itself is the XOR of every segment byte, seeded 0xEF (filler is zero,
    // so including it or not gives the same answer)
    var xor: u8 = 0xEF;
    var off: usize = 24;
    for (layout.segments) |s| {
        for (layout.bytes[off + 8 .. off + 8 + s.len]) |b| xor ^= b;
        off += 8 + s.len;
    }
    try testing.expectEqual(xor, layout.bytes[checksum_off]);
}

test "the header carries the revision window that keeps a pre-v3 die bootable" {
    const gpa = testing.allocator;
    const elf = try synthElf(gpa, 0x40000040, &.{
        .{ .addr = 0x40000020, .len = 16 },
        .{ .addr = 0x40000040, .len = 16 },
    });
    defer gpa.free(elf);

    var layout = try image.fromElf(gpa, elf, .{ .min_rev_full = 100, .max_rev_full = 199 });
    defer layout.deinit(gpa);

    try testing.expectEqual(@as(u16, 0x0012), std.mem.readInt(u16, layout.bytes[0x0C..0x0E], .little));
    try testing.expectEqual(@as(u16, 100), std.mem.readInt(u16, layout.bytes[0x0F..0x11], .little));
    try testing.expectEqual(@as(u16, 199), std.mem.readInt(u16, layout.bytes[0x11..0x13], .little));
}

test "a RAM segment never has filler copied into memory" {
    const gpa = testing.allocator;
    // On a P4 memory map the RAM window sorts after the flash window, so a RAM segment can never
    // sit between two mapped ones - but if one ever did, growing it would copy filler into L2MEM
    // past the real data. Assert the property directly rather than the mechanism.
    const elf = try synthElf(gpa, 0x40001000, &.{
        .{ .addr = 0x40000020, .len = 64 },
        .{ .addr = 0x40001000, .len = 64 },
        .{ .addr = 0x4FF00000, .len = 40 },
    });
    defer gpa.free(elf);

    var layout = try image.fromElf(gpa, elf, .{});
    defer layout.deinit(gpa);

    try layout.validate(.{});
    for (layout.segments) |s| {
        if (s.kind == .loaded) try testing.expectEqual(@as(u32, 0), s.filler);
    }
}

test "sweep: every rodata length either builds a device-correct image or is refused" {
    // The test the second review round asked for. For a range of rodata lengths and text
    // placements, either the builder refuses, or the produced BYTES satisfy an independent
    // re-derivation of the device's rules - including the MMU invariant that the original solver
    // violated silently. This is the only test that looks at the output rather than at an error
    // name, and it is what would catch a regression in the solver, the linker-script hole, or the
    // checksum layout.
    const gpa = testing.allocator;
    var built: usize = 0;
    var refused: usize = 0;

    var len: usize = 1;
    while (len <= 300) : (len += 7) {
        var text: u32 = 0x40;
        while (text <= 0x400) : (text += 0x20) {
            const elf = try synthElf(gpa, 0x40000000 + text, &.{
                .{ .addr = 0x40000020, .len = len },
                .{ .addr = 0x40000000 + text, .len = 64 },
            });
            defer gpa.free(elf);

            var layout = image.fromElf(gpa, elf, .{}) catch {
                refused += 1;
                continue;
            };
            defer layout.deinit(gpa);
            try layout.validate(.{});
            try checkBytes(layout.bytes, 0x10000);
            built += 1;
        }
    }
    try testing.expect(built > 100);
    try testing.expect(refused > 0); // the impossible layouts really are refused
}

/// Re-derive the device's rules from a finished image, sharing no code with the builder.
fn checkBytes(bytes: []const u8, flash_offset: u32) !void {
    try testing.expectEqual(@as(u8, 0xE9), bytes[0]);
    const count = bytes[1];

    var mapped: usize = 0;
    var deltas: [16]i64 = undefined;
    var pages: [16]u64 = undefined;
    var off: usize = 24;
    var xor: u8 = 0xEF;

    for (0..count) |_| {
        const addr = std.mem.readInt(u32, bytes[off..][0..4], .little);
        const len = std.mem.readInt(u32, bytes[off + 4 ..][0..4], .little);
        try testing.expectEqual(@as(u32, 0), len % 4); // esp_image_format.c:857
        const data = bytes[off + 8 ..][0..len];
        for (data) |b| xor ^= b;

        if (addr >= 0x40000000 and addr < 0x44000000) {
            const flash = @as(i64, flash_offset) + @as(i64, @intCast(off + 8));
            try testing.expectEqual(@mod(@as(i64, addr), 0x10000), @mod(flash, 0x10000));
            deltas[mapped] = flash - @as(i64, addr);
            pages[mapped] = addr / 0x10000;
            mapped += 1;
        }
        off += 8 + len;
    }
    try testing.expectEqual(@as(usize, 2), mapped); // bootloader_utility.c:842

    // Two mapped segments sharing a vaddr page must share the flash page: one MMU entry each.
    if (pages[0] == pages[1]) try testing.expectEqual(deltas[0], deltas[1]);

    const checksum_off = bytes.len - 33;
    try testing.expectEqual(@as(usize, 15), checksum_off % 16);
    try testing.expectEqual(xor, bytes[checksum_off]);
    for (bytes[off..checksum_off]) |b| try testing.expectEqual(@as(u8, 0), b);

    var digest: [32]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash(bytes[0 .. bytes.len - 32], &digest, .{});
    try testing.expectEqualSlices(u8, &digest, bytes[bytes.len - 32 ..]);
}

test "parse round-trips what fromElf produced" {
    // Nothing tested image.parse, and the flash and size steps both depend on it.
    const gpa = testing.allocator;
    const elf = try synthElf(gpa, 0x400002c0, &.{
        .{ .addr = 0x40000020, .len = 600 },
        .{ .addr = 0x400002c0, .len = 380 },
    });
    defer gpa.free(elf);

    var built = try image.fromElf(gpa, elf, .{});
    defer built.deinit(gpa);
    var read_back = try image.parse(gpa, built.bytes, .{});
    defer read_back.deinit(gpa);

    try testing.expectEqual(built.entry, read_back.entry);
    try testing.expectEqual(built.segments.len, read_back.segments.len);
    for (built.segments, read_back.segments) |a, b| {
        try testing.expectEqual(a.addr, b.addr);
        try testing.expectEqual(a.len, b.len);
        try testing.expectEqual(a.kind, b.kind);
    }
    try testing.expectEqualSlices(u8, built.bytes, read_back.bytes);
}

test "a gap that would push a mapped segment into the next flash page is refused, not padded" {
    const gpa = testing.allocator;
    // The old solver shifted a whole MMU page forward to satisfy congruence modulo the page. That
    // kept both segments in one *vaddr* page while putting their data in two different *flash*
    // pages, so the bootloader's second MMU write replaced the first and every rodata read
    // resolved to filler zeros. Found by adversarial review, reproduced by -Ddescriptor=full.
    const elf = try synthElf(gpa, 0x40000140, &.{
        .{ .addr = 0x40000020, .len = 268 }, // ends at 0x12C; text at 0x140 needs data@0x140,
        .{ .addr = 0x40000140, .len = 236 }, // but the next data offset is 0x134: gap 12, fine
    });
    defer gpa.free(elf);
    var ok_layout = try image.fromElf(gpa, elf, .{});
    defer ok_layout.deinit(gpa);
    try ok_layout.validate(.{});
    try testing.expect(ok_layout.bytes.len < 1024); // no 64 KiB page jump

    // Now the pathological direction: the second mapped segment sits *before* where the first one
    // already reaches, so no amount of filler can line it up.
    const bad = try synthElf(gpa, 0x40000030, &.{
        .{ .addr = 0x40000020, .len = 512 },
        .{ .addr = 0x40000030, .len = 16 },
    });
    defer gpa.free(bad);
    try testing.expectError(error.MappedSegmentsTooClose, image.fromElf(gpa, bad, .{}));
}

test "validate rejects two mapped segments that would fight over one MMU entry" {
    // Hand-built because the solver now refuses to produce this: both segments are congruent and
    // both live in vaddr page 0x4000, but their data sits in two different flash pages, so the
    // bootloader's second MMU write would replace the first. This is the shape that boots with
    // every constant reading as zero.
    var segs = [_]image.Segment{
        .{ .addr = 0x40000020, .len = 0x10008, .filler = 0, .kind = .mapped },
        .{ .addr = 0x40000030, .len = 16, .filler = 0, .kind = .mapped },
    };
    const layout: image.Layout = .{
        .bytes = &.{},
        .segments = &segs,
        .entry = 0x40000030,
        .payload = 0,
        .filler = 0,
        .overhead = 0,
    };
    // segment 1's data lands at flash 0x10000 + (24 + 8 + 0x10008) + 8 = 0x20030: congruent
    // (0x30 == 0x40000030 % 64K) but one page further along than segment 0's 0x10020.
    try testing.expectError(error.MmuEntryConflict, layout.validate(.{}));
}

test "a partition that is not MMU-page aligned is refused" {
    const gpa = testing.allocator;
    const elf = try synthElf(gpa, 0x40000040, &.{
        .{ .addr = 0x40000020, .len = 16 },
        .{ .addr = 0x40000040, .len = 16 },
    });
    defer gpa.free(elf);
    // The device checks congruence against the absolute flash address, so an image built for
    // 0x11000 needs different padding from one built for 0x10000 - and the anchor cannot be a
    // whole number of pages, which means no layout satisfies the rule.
    try testing.expectError(error.PartitionNotPageAligned, image.fromElf(gpa, elf, .{ .flash_offset = 0x11000 }));
}

test "more than sixteen segments is refused, because the loader stops there" {
    const gpa = testing.allocator;
    var loads: [20]Load = undefined;
    for (&loads, 0..) |*l, i| l.* = .{ .addr = @intCast(0x4FF00000 + i * 0x100), .len = 16 };
    loads[0] = .{ .addr = 0x40000020, .len = 16 };
    loads[1] = .{ .addr = 0x40000040, .len = 16 };
    const elf = try synthElf(gpa, 0x40000040, &loads);
    defer gpa.free(elf);
    try testing.expectError(error.TooManySegments, image.fromElf(gpa, elf, .{}));
}