Phase 2d (ii): filesystem modification time (mtime)

Completes Phase 2: the FAT filesystem now stamps and reports a real modification
time, built on the Phase 2d(i) kernel wall-clock. This is the last stat field the
compiler's build cache needs to reason about (source vs cached output).

- on-disk.zig: fatToEpoch / epochToFatDateTime convert between the two 16-bit DOS
  date/time fields and Unix epoch seconds (UTC — FAT has no timezone). Host-tested
  round-trip + an absolute check (1577836800 == 2020-01-01).
- engine: a settable current_time_epoch that create/write stamp into the entry's
  write (and creation) date/time; Node/Listing gained an mtime decoded from those
  fields on read. Host test: a create stamps the mtime, read back through resolve
  and listEntry.
- vfs protocol FileStatus + runtime.fs.Attributes gained an mtime field; the fat
  server sets current_time_epoch from runtime.system.wallClock() per request and
  returns mtime from stat. The flat ramfs reports 0 (it has no timestamps).
- fat-test reads the created file's mtime through stat and checks it is a real
  current time, behind a new `fat-mtime` QEMU case.

Verified against the host: the guest stamped mtime 1783971676 while the host clock
was 1783971680 (boot+test lag) — the file's mtime is real current time. zig build,
zig build test (the epoch<->DOS conversions + the engine mtime test),
zig build check-fat-image, and a sequential QEMU sweep — fat-mount, fat-mutations,
fat-rename, fat-mtime, vfs, vfs-client-death, log-flush, orderly-shutdown,
initial-ramdisk, smoke, wall-clock, usb-storage — all green. mode/inode remain.
This commit is contained in:
Daniel Samson
2026-07-13 20:43:40 +01:00
parent 8a38540312
commit 67702fa250
8 changed files with 180 additions and 16 deletions
+14 -10
View File
@@ -165,9 +165,9 @@ What the seam needs, and what danos already provides:
| monotonic clock | `clock` syscall | none | | monotonic clock | `clock` syscall | none |
| args / argv | SysV entry stack ([sysv.md](sysv.md)), `runtime.process.Init` | none | | args / argv | SysV entry stack ([sysv.md](sysv.md)), `runtime.process.Init` | none |
| stdout / stderr | `debug_write` today | wire fd 1/2 to a console **byte** stream | | stdout / stderr | `debug_write` today | wire fd 1/2 to a console **byte** stream |
| **mkdir / unlink / rename / truncate** | — | **missing** in the VFS protocol *and* FAT engine | | mkdir / unlink / rename / truncate | done — engine + VFS + `runtime.fs` (Phase 2) | — |
| **stat fields** | `{size, kind}` only | needs **mtime + inode + mode** (cache validity) | | stat fields | `{size, kind, mtime}` | **mode / inode** still missing (cache validity) |
| **wall-clock / realtime** | monotonic only | needs an RTC/time service | | wall-clock / realtime | done — `wall_clock` syscall (CMOS RTC, Phase 2d) | — |
| **environment variables** | `Init` has no env field | missing (can start empty) | | **environment variables** | `Init` has no env field | missing (can start empty) |
| **cwd / chdir** | paths are absolute or bare | missing (no cwd anchor) | | **cwd / chdir** | paths are absolute or bare | missing (no cwd anchor) |
| **entropy / random** | — | missing (needed behind `vtable.random`) | | **entropy / random** | — | missing (needed behind `vtable.random`) |
@@ -251,15 +251,19 @@ Because `std.fs`/`std.Io` have no per-OS branches, finishing this in `runtime.os
lights up the whole file tower for the compiler at once. Environment can stay an empty lights up the whole file tower for the compiler at once. Environment can stay an empty
map until the kernel populates a non-empty `envp`. map until the kernel populates a non-empty `envp`.
**Status (Phase 2a–2c landed):** `truncate` (O_TRUNC, closing the boot-log stale-tail **Status — Phase 2 complete.** `truncate` (O_TRUNC, closing the boot-log stale-tail
bug), `mkdir`, `unlink`, and `rename` are all wired through the FAT engine, the VFS bug), `mkdir`, `unlink`, and `rename` are all wired through the FAT engine, the VFS
protocol + router, and `runtime.fs` (`makeDirectory` / `remove` / `rename`) — protocol + router, and `runtime.fs` (`makeDirectory` / `remove` / `rename`) —
host-tested and QEMU-tested (the `fat-mutations` + `fat-rename` cases make a directory, host-tested and QEMU-tested (`fat-mutations` + `fat-rename` make a directory, write+read
write+read a file in it, rename it, then remove it through the mount). `removeFile` and a file in it, rename it, then remove it through the mount). `removeFile` and `rename`
`rename` are LFN-aware; `rename` is same-directory + 8.3 (cross-directory and are LFN-aware; `rename` is same-directory + 8.3 (cross-directory and long-name-
long-name-preserving rename are noted limitations). **Remaining Phase 2:** the richer preserving rename are noted limitations). Wall-clock is now a kernel syscall
`stat` — mtime/mode — which needs **wall-clock** (a kernel RTC read behind a syscall, (`wall_clock`, a CMOS-RTC read anchored to the monotonic clock), and the FAT engine
consistent with time being a kernel concern), the natural next kernel-side step. stamps and reports **mtime** — `stat` / `runtime.fs.Attributes` carry a real
modification time (the `fat-mtime` case reads it back within seconds of the host clock).
The remaining `stat` fields, `mode`/`inode`, are deferred (not needed until the
compiler's cache layer wants them). **Everything past here is gated on Phase 0 (the
fork):** the `runtime.os` seam, `cwd`, stdio-as-fds, and the compiler bring-up.
### Phase 3 — Single-threaded, self-linked compiler bring-up ### Phase 3 — Single-threaded, self-linked compiler bring-up
+7 -2
View File
@@ -19,7 +19,12 @@ const protocol = @import("vfs-protocol");
pub const Kind = protocol.NodeKind; pub const Kind = protocol.NodeKind;
/// A node's metadata (the answer to a status request). /// A node's metadata (the answer to a status request).
pub const Attributes = struct { size: u64, kind: Kind }; pub const Attributes = struct {
size: u64,
kind: Kind,
/// Modification time — Unix epoch seconds, UTC. 0 if the filesystem has none.
mtime: u64 = 0,
};
// Map a wire `NodeKind` value to the enum, defaulting anything unrecognised to // Map a wire `NodeKind` value to the enum, defaulting anything unrecognised to
// `.regular` (the server is trusted, but a value outside the enum would be // `.regular` (the server is trusted, but a value outside the enum would be
@@ -138,7 +143,7 @@ pub const File = struct {
const r = transact(request, &.{}, &buffer) orelse return null; const r = transact(request, &.{}, &buffer) orelse return null;
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return null; if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return null;
const status = std.mem.bytesToValue(protocol.FileStatus, buffer[0..@sizeOf(protocol.FileStatus)]); const status = std.mem.bytesToValue(protocol.FileStatus, buffer[0..@sizeOf(protocol.FileStatus)]);
return .{ .size = status.size, .kind = kindFromWire(status.kind) }; return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime };
} }
/// Release the VFS's open handle for this file. /// Release the VFS's open handle for this file.
+43 -1
View File
@@ -41,6 +41,9 @@ pub const Node = struct {
first_cluster: u32, first_cluster: u32,
size: u32, size: u32,
is_directory: bool, is_directory: bool,
// Modification time (Unix epoch seconds, UTC), decoded from the directory
// entry's DOS write date/time. 0 if unset.
mtime: u64 = 0,
// The absolute sector and byte offset of this node's 8.3 directory entry, so // The absolute sector and byte offset of this node's 8.3 directory entry, so
// size/first-cluster changes can be written back. Absent for the root. // size/first-cluster changes can be written back. Absent for the root.
entry_sector: u64 = 0, entry_sector: u64 = 0,
@@ -63,6 +66,10 @@ pub const FileSystem = struct {
sector: [sector_size]u8 = undefined, sector: [sector_size]u8 = undefined,
fat_sector: [sector_size]u8 = undefined, fat_sector: [sector_size]u8 = undefined,
dir_sector: [sector_size]u8 = undefined, dir_sector: [sector_size]u8 = undefined,
// Wall-clock time (Unix epoch seconds) to stamp on create/write, set by the
// server before a mutating op. 0 leaves the on-disk timestamps untouched (host
// tests that don't care about time, and reads).
current_time_epoch: u64 = 0,
// Every filesystem-relative sector access adds the partition base. // Every filesystem-relative sector access adds the partition base.
fn blockRead(self: *FileSystem, lba: u64, buffer: []u8) bool { fn blockRead(self: *FileSystem, lba: u64, buffer: []u8) bool {
@@ -428,6 +435,7 @@ pub const FileSystem = struct {
.first_cluster = entry.firstCluster(), .first_cluster = entry.firstCluster(),
.size = entry.file_size, .size = entry.file_size,
.is_directory = entry.isDirectory(), .is_directory = entry.isDirectory(),
.mtime = on_disk.fatToEpoch(entry.write_date, entry.write_time),
.entry_sector = entry_sector, .entry_sector = entry_sector,
.entry_offset = entry_offset, .entry_offset = entry_offset,
.has_entry = true, .has_entry = true,
@@ -456,7 +464,7 @@ pub const FileSystem = struct {
/// The `cursor`th real entry of a directory (for readdir): its display name, /// The `cursor`th real entry of a directory (for readdir): its display name,
/// kind, and size. Returns null past the end. /// kind, and size. Returns null past the end.
pub const Listing = struct { name_buffer: [260]u8 = undefined, name_len: usize = 0, is_directory: bool = false, size: u32 = 0 }; pub const Listing = struct { name_buffer: [260]u8 = undefined, name_len: usize = 0, is_directory: bool = false, size: u32 = 0, mtime: u64 = 0 };
const ListContext = struct { target: u32, index: u32 = 0, out: *Listing, done: bool = false }; const ListContext = struct { target: u32, index: u32 = 0, out: *Listing, done: bool = false };
fn listVisit(context: *ListContext, entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool { fn listVisit(context: *ListContext, entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool {
@@ -468,6 +476,7 @@ pub const FileSystem = struct {
context.out.name_len = n; context.out.name_len = n;
context.out.is_directory = entry.isDirectory(); context.out.is_directory = entry.isDirectory();
context.out.size = entry.file_size; context.out.size = entry.file_size;
context.out.mtime = on_disk.fatToEpoch(entry.write_date, entry.write_time);
context.done = true; context.done = true;
return true; return true;
} }
@@ -582,6 +591,14 @@ pub const FileSystem = struct {
var entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[node.entry_offset .. node.entry_offset + @sizeOf(on_disk.DirectoryEntry)]); var entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[node.entry_offset .. node.entry_offset + @sizeOf(on_disk.DirectoryEntry)]);
entry.file_size = node.size; entry.file_size = node.size;
entry.setFirstCluster(node.first_cluster); entry.setFirstCluster(node.first_cluster);
// A write updates the modification time (leave it if no time is set, so host
// tests and reads don't zero it).
if (self.current_time_epoch != 0) {
const stamp = on_disk.epochToFatDateTime(self.current_time_epoch);
entry.write_date = stamp.date;
entry.write_time = stamp.time;
entry.last_access_date = stamp.date;
}
@memcpy(self.dir_sector[node.entry_offset .. node.entry_offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&entry)); @memcpy(self.dir_sector[node.entry_offset .. node.entry_offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&entry));
_ = self.blockWrite(node.entry_sector, &self.dir_sector); _ = self.blockWrite(node.entry_sector, &self.dir_sector);
} }
@@ -603,12 +620,19 @@ pub const FileSystem = struct {
entry.attributes = attributes; entry.attributes = attributes;
entry.file_size = size; entry.file_size = size;
entry.setFirstCluster(first_cluster); entry.setFirstCluster(first_cluster);
const stamp = on_disk.epochToFatDateTime(self.current_time_epoch);
entry.creation_date = stamp.date;
entry.creation_time = stamp.time;
entry.write_date = stamp.date;
entry.write_time = stamp.time;
entry.last_access_date = stamp.date;
@memcpy(self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&entry)); @memcpy(self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&entry));
if (!self.blockWrite(lba, &self.dir_sector)) return null; if (!self.blockWrite(lba, &self.dir_sector)) return null;
return .{ return .{
.first_cluster = first_cluster, .first_cluster = first_cluster,
.size = size, .size = size,
.is_directory = attributes & on_disk.attribute_directory != 0, .is_directory = attributes & on_disk.attribute_directory != 0,
.mtime = self.current_time_epoch,
.entry_sector = lba, .entry_sector = lba,
.entry_offset = offset, .entry_offset = offset,
.has_entry = true, .has_entry = true,
@@ -1061,3 +1085,21 @@ test "rename a file in place, keeping its contents" {
// After the refused renames, NEW.TXT is untouched. // After the refused renames, NEW.TXT is untouched.
try std.testing.expect(fs.resolve("/NEW.TXT") != null); try std.testing.expect(fs.resolve("/NEW.TXT") != null);
} }
test "a create stamps the modification time" {
const allocator = std.testing.allocator;
const bytes = try allocator.alloc(u8, 5000 * sector_size);
defer allocator.free(bytes);
formatFat16(bytes);
var disk = RamDisk{ .bytes = bytes };
var fs = FileSystem.mount(disk.device()).?;
fs.current_time_epoch = 1_700_000_000; // an even-second UTC time
var node = fs.createFile(fs.rootNode(), "STAMP.TXT").?;
_ = fs.writeFile(&node, 0, "hi");
// The persisted entry carries the stamped mtime (even seconds round-trip exactly),
// as does a fresh listing.
try std.testing.expectEqual(@as(u64, 1_700_000_000), fs.resolve("/STAMP.TXT").?.mtime);
try std.testing.expectEqual(@as(u64, 1_700_000_000), fs.listEntry(fs.rootNode(), 0).?.mtime);
}
+10 -2
View File
@@ -62,6 +62,14 @@ pub fn main(init: runtime.process.Init) void {
wrote = (f.writeAll("mutation-ok") orelse 0) == "mutation-ok".len; wrote = (f.writeAll("mutation-ok") orelse 0) == "mutation-ok".len;
f.close(); f.close();
} }
// The created file carries a real modification time (stamped from the RTC).
var mtime_ok = false;
if (fs.attributes("/mnt/usb/TESTDIR/HELLO.TXT")) |attrs| {
writeLine("fat-test: mtime {d}\n", .{attrs.mtime});
mtime_ok = attrs.mtime > 1_577_836_800; // after 2020-01-01
}
if (mtime_ok) _ = runtime.system.write("fat-test: mtime ok\n");
// Rename it, then read from the new name and confirm the old name is gone. // Rename it, then read from the new name and confirm the old name is gone.
const renamed = fs.rename("/mnt/usb/TESTDIR/HELLO.TXT", "/mnt/usb/TESTDIR/RENAMED.TXT"); const renamed = fs.rename("/mnt/usb/TESTDIR/HELLO.TXT", "/mnt/usb/TESTDIR/RENAMED.TXT");
const old_gone = !fs.exists("/mnt/usb/TESTDIR/HELLO.TXT"); const old_gone = !fs.exists("/mnt/usb/TESTDIR/HELLO.TXT");
@@ -76,10 +84,10 @@ pub fn main(init: runtime.process.Init) void {
} }
const removed = fs.remove("/mnt/usb/TESTDIR/RENAMED.TXT"); const removed = fs.remove("/mnt/usb/TESTDIR/RENAMED.TXT");
const gone = !fs.exists("/mnt/usb/TESTDIR/RENAMED.TXT"); const gone = !fs.exists("/mnt/usb/TESTDIR/RENAMED.TXT");
if (wrote and renamed and old_gone and readback and removed and gone) { if (wrote and mtime_ok and renamed and old_gone and readback and removed and gone) {
_ = runtime.system.write("fat-test: mutations ok\n"); _ = runtime.system.write("fat-test: mutations ok\n");
} else { } else {
writeLine("fat-test: mutations FAILED (wrote={} renamed={} oldgone={} read={} removed={} gone={})\n", .{ wrote, renamed, old_gone, readback, removed, gone }); writeLine("fat-test: mutations FAILED (wrote={} mtime={} renamed={} oldgone={} read={} removed={} gone={})\n", .{ wrote, mtime_ok, renamed, old_gone, readback, removed, gone });
} }
} else { } else {
_ = runtime.system.write("fat-test: mkdir /mnt/usb/TESTDIR failed\n"); _ = runtime.system.write("fat-test: mkdir /mnt/usb/TESTDIR failed\n");
+5 -1
View File
@@ -152,6 +152,10 @@ fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.i
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]); const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
const payload = message[protocol.request_size..]; const payload = message[protocol.request_size..];
// Stamp create/write with the current wall-clock time (mtime). Cheap, and it
// keeps the engine pure (it takes the time as data, not a syscall).
filesystem.current_time_epoch = runtime.system.wallClock();
switch (request.operation) { switch (request.operation) {
.open => return handleOpen(out, payload[0..@min(payload.len, request.len)], request.flags), .open => return handleOpen(out, payload[0..@min(payload.len, request.len)], request.flags),
.read => { .read => {
@@ -170,7 +174,7 @@ fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.i
.status => { .status => {
const o = openAt(request.node) orelse return fail(out); const o = openAt(request.node) orelse return fail(out);
const kind: protocol.NodeKind = if (o.node.is_directory) .directory else .regular; const kind: protocol.NodeKind = if (o.node.is_directory) .directory else .regular;
const status = protocol.FileStatus{ .size = o.node.size, .kind = @intFromEnum(kind) }; const status = protocol.FileStatus{ .size = o.node.size, .kind = @intFromEnum(kind), .mtime = o.node.mtime };
return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&status)); return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&status));
}, },
.readdir => { .readdir => {
+90
View File
@@ -202,6 +202,96 @@ pub fn geometryOf(sector: []const u8) ?Geometry {
}; };
} }
// --- DOS date/time <-> Unix epoch --------------------------------------------
//
// FAT stamps a file's modification time as two 16-bit DOS fields. There is no
// timezone, so danos treats them as UTC. `date`: year-1980(7)|month(4)|day(5);
// `time`: hour(5)|minute(6)|(second/2)(5).
fn isLeapYear(year: u32) bool {
return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0);
}
const days_in_month = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
/// Convert a FAT date+time to Unix epoch seconds (UTC). Returns 0 for an unset
/// (zero) date.
pub fn fatToEpoch(date: u16, time: u16) u64 {
if (date == 0) return 0;
const day: u32 = date & 0x1F;
const month: u32 = (date >> 5) & 0x0F;
const year: u32 = 1980 + (date >> 9);
if (month < 1 or month > 12 or day < 1) return 0;
const second: u32 = @as(u32, time & 0x1F) * 2;
const minute: u32 = (time >> 5) & 0x3F;
const hour: u32 = (time >> 11) & 0x1F;
var days: u64 = 0;
var y: u32 = 1970;
while (y < year) : (y += 1) days += if (isLeapYear(y)) 366 else 365;
var m: u32 = 1;
while (m < month) : (m += 1) {
days += days_in_month[m - 1];
if (m == 2 and isLeapYear(year)) days += 1;
}
days += day - 1;
return ((days * 24 + hour) * 60 + minute) * 60 + second;
}
pub const FatDateTime = struct { date: u16, time: u16 };
/// Convert Unix epoch seconds (UTC) to a FAT date+time. Returns {0,0} for epoch 0 or
/// any time before 1980 (which DOS cannot represent).
pub fn epochToFatDateTime(epoch: u64) FatDateTime {
if (epoch == 0) return .{ .date = 0, .time = 0 };
var remaining = epoch;
const second: u32 = @intCast(remaining % 60);
remaining /= 60;
const minute: u32 = @intCast(remaining % 60);
remaining /= 60;
const hour: u32 = @intCast(remaining % 24);
remaining /= 24;
var days: u32 = @intCast(remaining); // whole days since 1970-01-01
var year: u32 = 1970;
while (true) {
const y_days: u32 = if (isLeapYear(year)) 366 else 365;
if (days < y_days) break;
days -= y_days;
year += 1;
}
if (year < 1980) return .{ .date = 0, .time = 0 };
var month: u32 = 1;
while (true) {
var m_days: u32 = days_in_month[month - 1];
if (month == 2 and isLeapYear(year)) m_days += 1;
if (days < m_days) break;
days -= m_days;
month += 1;
}
const day = days + 1;
return .{
.date = @intCast(((year - 1980) << 9) | (month << 5) | day),
.time = @intCast((hour << 11) | (minute << 5) | (second / 2)),
};
}
test "FAT date/time <-> Unix epoch round trip" {
// Even-second UTC times (FAT stores seconds/2, so even seconds round-trip exactly).
for ([_]u64{ 1_577_836_800, 1_700_000_000, 1_262_304_000, 1_783_971_244 }) |epoch| {
const fat = epochToFatDateTime(epoch);
try std.testing.expectEqual(epoch, fatToEpoch(fat.date, fat.time));
}
// Absolute check: 1577836800 is 2020-01-01 00:00:00 UTC.
const y2020 = epochToFatDateTime(1_577_836_800);
try std.testing.expectEqual(@as(u16, 2020), 1980 + (y2020.date >> 9));
try std.testing.expectEqual(@as(u16, 1), (y2020.date >> 5) & 0x0F); // month
try std.testing.expectEqual(@as(u16, 1), y2020.date & 0x1F); // day
// 0 is "unset" both ways.
try std.testing.expectEqual(@as(u64, 0), fatToEpoch(0, 0));
try std.testing.expectEqual(@as(u16, 0), epochToFatDateTime(0).date);
}
test "on-disk struct sizes match the specification" { test "on-disk struct sizes match the specification" {
try std.testing.expectEqual(@as(usize, 36), @sizeOf(BiosParameterBlock)); try std.testing.expectEqual(@as(usize, 36), @sizeOf(BiosParameterBlock));
try std.testing.expectEqual(@as(usize, 26), @sizeOf(ExtendedBootRecord16)); try std.testing.expectEqual(@as(usize, 26), @sizeOf(ExtendedBootRecord16));
+3
View File
@@ -81,6 +81,9 @@ pub const FileStatus = extern struct {
size: u64, size: u64,
kind: u32, kind: u32,
_padding: u32 = 0, _padding: u32 = 0,
/// Modification time — Unix epoch seconds, UTC. 0 if the backend has none (the
/// flat ramfs). Filled from the FAT directory entry's write date/time.
mtime: u64 = 0,
}; };
pub const message_maximum: usize = 256; pub const message_maximum: usize = 256;
+8
View File
@@ -342,6 +342,14 @@ CASES = [
"timeout": 150, "timeout": 150,
"expect": r"fat-test: rename ok", "expect": r"fat-test: rename ok",
"fail": r"fat-test: mutations FAILED|DANOS-TEST-RESULT: FAIL"}, "fail": r"fat-test: mutations FAILED|DANOS-TEST-RESULT: FAIL"},
# Phase 2d: filesystem timestamps — a freshly-created file's mtime is a real
# current wall-clock time (stamped from the RTC), read back through stat.
{"name": "fat-mtime",
"build_case": "fat-mount",
"smp": 4,
"timeout": 150,
"expect": r"fat-test: mtime ok",
"fail": r"fat-test: mutations FAILED|DANOS-TEST-RESULT: FAIL"},
# Boot-from-USB smoke: the whole system now boots off the FAT32 image on a # Boot-from-USB smoke: the whole system now boots off the FAT32 image on a
# usb-storage device (OVMF -> \EFI\BOOT\BOOTX64.efi -> kernel), so the kernel # usb-storage device (OVMF -> \EFI\BOOT\BOOTX64.efi -> kernel), so the kernel
# reaching its PASS marker at all proves the USB boot path end to end. Reuses # reaching its PASS marker at all proves the USB boot path end to end. Reuses