Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d4992ae02 | ||
|
|
df61693065 | ||
|
|
f1e79d0eeb | ||
|
|
167e9c7a9e | ||
|
|
5bfdb75e12 | ||
|
|
3fb8a9b96f |
@@ -317,6 +317,11 @@ pub fn build(b: *std.Build) void {
|
||||
// out of the same read-only initrd, before it spawns anything — the registrar
|
||||
// has to know its policy before the first provider asks.
|
||||
bundled_list.append(b.allocator, .{ .path = "system/configuration/protocol.csv", .binary = b.path("system/configuration/protocol.csv") }) catch @panic("OOM");
|
||||
// The storage mount map (docs/file-system-development/storage-architecture.md):
|
||||
// filesystems.csv (content signature -> service binary) and volumes.csv (the
|
||||
// optional id -> mount-prefix override), both read by the volume manager.
|
||||
bundled_list.append(b.allocator, .{ .path = "system/configuration/filesystems.csv", .binary = b.path("system/configuration/filesystems.csv") }) catch @panic("OOM");
|
||||
bundled_list.append(b.allocator, .{ .path = "system/configuration/volumes.csv", .binary = b.path("system/configuration/volumes.csv") }) catch @panic("OOM");
|
||||
// A no-option build assumes neither -Dtest-case nor -Ddiagnose: it ships the
|
||||
// production set only. The userspace test fixtures under /test join in only
|
||||
// for a test build — which the QEMU harness signals by passing
|
||||
|
||||
@@ -3,18 +3,23 @@
|
||||
> **Status:** the layered model below is the settled design
|
||||
> ([storage-design-rationale.md](storage-design-rationale.md) records how it was
|
||||
> reached, and [volume-manager-plan.md](../volume-manager-plan.md) how it was
|
||||
> built). **Built** (the volume-manager track, V0–V4): the data path, the driver
|
||||
> built). **Built** (V0–V4 + the storage-stack S1/S2): the data path, the driver
|
||||
> range confinement (per-sender clamp + the confinement gate), the `medium_changed`
|
||||
> presence event, the volume manager itself — it probes the partition table,
|
||||
> confines each filesystem to its partition, spawns one filesystem per volume, and
|
||||
> supervises it — and the removal half of the lifecycle (a pulled stick unmounts).
|
||||
> **Still pending**: the fuller identity ladder and the `volumes.csv` mount map,
|
||||
> multi-volume (one FAT volume today), the volume manager *consuming*
|
||||
> `medium_changed` (removal is detected by device-presence polling; the event is
|
||||
> published but only a card-reader medium change needs the subscription), and the
|
||||
> remount-on-replug end-to-end (the logic is in place; QEMU can't re-present the
|
||||
> boot-controller device, so it is bench-verified). A few markers below are left
|
||||
> where a duty is still pending.
|
||||
> supervises it — the removal half of the lifecycle (a pulled stick unmounts), the
|
||||
> identity ladder (GPT GUID + name, FAT serial + label, MBR), and the mount map:
|
||||
> `filesystems.csv` (signature → binary) + `volumes.csv` (identity → optional
|
||||
> override), a volume's mount path IS its content id (`/volumes/<id>`), with the
|
||||
> label as display metadata a `volumes` query returns. **Still pending**: the
|
||||
> `filesystem UUID` rung (needs a non-FAT engine), multi-volume (one FAT volume
|
||||
> today; fat's boot rewrites are unconditional until S3 makes them
|
||||
> content-conditional), the volume manager *consuming* `medium_changed` (removal
|
||||
> is detected by device-presence polling; the event is published but only a
|
||||
> card-reader medium change needs the subscription), and the remount-on-replug
|
||||
> end-to-end (the logic is in place; QEMU can't re-present the boot-controller
|
||||
> device, so it is bench-verified). A few markers below are left where a duty is
|
||||
> still pending.
|
||||
|
||||
## The model
|
||||
|
||||
@@ -85,16 +90,17 @@ manager's tree for a storage provider; when one appears it consumer-hellos for
|
||||
the block channel, reads the partition table and the first blocks itself
|
||||
(**it** is the prober), defines the volume's sub-range on the driver, spawns the
|
||||
matching filesystem service confined to that range, and supervises it (backoff,
|
||||
crash-loop cap). *(Pending)*: it decides mount placement from `volumes.csv` and
|
||||
picks the filesystem binary from `filesystems.csv` — today it hands every
|
||||
FAT-shaped volume to the FAT service and the FAT service carries hardcoded mount
|
||||
prefixes. Those tables are CSV configuration, read by it (the policy), enforced
|
||||
by nobody else:
|
||||
crash-loop cap). *(Built)*: it picks the filesystem binary from
|
||||
`filesystems.csv` by the volume's content signature, and mounts the volume at its
|
||||
content id (`/volumes/<id>`) — or a `volumes.csv` override. The label is display
|
||||
metadata the `volumes` query returns, never the path. Those tables are CSV
|
||||
configuration, read by it (the policy), enforced by nobody else:
|
||||
|
||||
- `filesystems.csv` *(pending)* — content signature → filesystem binary. Adding
|
||||
- `filesystems.csv` *(built)* — content signature → filesystem binary. Adding
|
||||
a filesystem adds a row.
|
||||
- `volumes.csv` *(pending)* — the mount map, danos's fstab: **volume identity → mount
|
||||
prefix**, keyed on content identity and never on port, path, or arrival
|
||||
- `volumes.csv` *(built)* — the mount map, danos's fstab: an OPTIONAL **volume
|
||||
identity → mount prefix** override (a volume with no row mounts at its default
|
||||
`/volumes/<id>`), keyed on content identity and never on port, path, or arrival
|
||||
order (the lesson of Linux's `/dev/sda1`-era fstab, which broke on every
|
||||
port move until `UUID=` replaced it). Identity is read off the medium by
|
||||
the prober, strongest first: GPT partition GUID → filesystem UUID → FAT
|
||||
@@ -105,8 +111,8 @@ by nobody else:
|
||||
identity (cloned sticks, together) is policy: first keeps the name, the
|
||||
second mounts suffixed and is logged loudly. The boot volume is the
|
||||
recorded identity of the volume carrying `/system/configuration` and
|
||||
`/system/logs`, findable on any port. Unknown volumes mount under
|
||||
`/volumes/<derived name>`.
|
||||
`/system/logs`, findable on any port. Every volume's default mount is
|
||||
`/volumes/<id>` — its rendered content identity.
|
||||
|
||||
**Filesystem service** (the FAT service today; one process per volume): the
|
||||
proven unit — block-client + engine + file-protocol provider in one binary. It
|
||||
@@ -114,12 +120,13 @@ receives its block channel at spawn; it never discovers devices. It registers
|
||||
its own mounts with the kernel; its write cache lives inside the process, so a
|
||||
write error is observed by the code that owns the volume and surfaces on the
|
||||
owning channel (the anti-fsyncgate rule — never a system-wide dirty pool).
|
||||
*(Today, interim:)* fat still hardcodes its mount prefixes (`/volumes/usb` plus
|
||||
the two boot-volume hierarchy subtrees it rewrites in place); a `volumes.csv`
|
||||
mount map will migrate that to the volume manager. It no longer self-acquires a
|
||||
volume — the V3b flip made it receive its volume id at spawn and its block
|
||||
channel from the volume manager's hello reply, consistent with "it never
|
||||
discovers devices" above.
|
||||
*(Built:)* fat receives its mount path as `argv[2]` from the volume manager (the
|
||||
volume's id-path, e.g. `/volumes/fat-12345678`) and mounts its root there, plus
|
||||
the two `/system` hierarchy rewrites it installs in place (unconditional this
|
||||
increment; S3 makes them content-conditional across volumes). It no longer
|
||||
self-acquires a volume — the V3b flip made it receive its volume id and block
|
||||
channel from the volume manager, consistent with "it never discovers devices"
|
||||
above.
|
||||
|
||||
**Kernel** (mechanism only): the mount table routes paths to backend
|
||||
endpoints — resolve and redirect, never data. Remount-replace is the restart
|
||||
|
||||
@@ -218,9 +218,10 @@ matrix-proven shape; genuinely open.
|
||||
**content identity, never port or discovery order**. Build status: rungs 1,
|
||||
3, and 4 (GPT partition GUID, FAT serial + label, MBR signature + index) are
|
||||
implemented (S1); rung 2 waits on a non-FAT engine. The `volumes.csv` map and
|
||||
the id-derived mount path land with S2, so today a single volume still mounts
|
||||
at the fixed `/volumes/usb` and its recorded identity is not yet consulted to
|
||||
pick a path. The ladder the prober reads off the medium, strongest first:
|
||||
the id-derived mount path are built (S2): a volume's mount path IS its content
|
||||
id (`/volumes/<id>`, e.g. `/volumes/fat-12345678`), or a `volumes.csv`
|
||||
override; the label is display metadata a `volumes` query returns, never the
|
||||
path. The ladder the prober reads off the medium, strongest first:
|
||||
1. GPT partition GUID — 128-bit, unique, stable for the volume's life — **built (S1)**;
|
||||
2. filesystem UUID (ext-family and most modern formats, in the superblock) *(planned)*;
|
||||
3. FAT volume serial + label — 32 bits, weak (dd-cloned sticks share it)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
//! "Establishment: two planes"). No channel in the reply means the volume is not
|
||||
//! ready yet — retryable, never a verdict.
|
||||
|
||||
const std = @import("std");
|
||||
const envelope = @import("envelope");
|
||||
|
||||
pub const version: u16 = 1;
|
||||
@@ -24,13 +25,91 @@ pub const Hello = extern struct {
|
||||
_padding: u16 = 0,
|
||||
};
|
||||
|
||||
/// A `volumes` query — no request fields; the reply's tail carries the volume's
|
||||
/// descriptor (`VolumeInfo`). The mechanism by which a shell or file manager
|
||||
/// reads a volume's display label: the mount path is its id (software's stable
|
||||
/// handle), the label is separate display metadata, the database id/name split.
|
||||
pub const Volumes = extern struct {
|
||||
_reserved: u32 = 0,
|
||||
};
|
||||
|
||||
/// The `volumes` reply: three length-prefixed strings packed into the reply tail
|
||||
/// — the volume's id (its mount path is /volumes/<id> unless overridden), its
|
||||
/// actual mount path, and its display label. `id` is what software keys on;
|
||||
/// `label` is what a UI shows.
|
||||
pub const VolumeInfo = struct {
|
||||
id: []const u8,
|
||||
mount_path: []const u8,
|
||||
label: []const u8,
|
||||
|
||||
const header_bytes = 6; // three u16 lengths, little-endian
|
||||
|
||||
/// Pack into `buf`, returning the used slice, or null if it does not fit.
|
||||
pub fn encode(self: VolumeInfo, buf: []u8) ?[]u8 {
|
||||
const total = header_bytes + self.id.len + self.mount_path.len + self.label.len;
|
||||
if (total > buf.len) return null;
|
||||
std.mem.writeInt(u16, buf[0..2], @intCast(self.id.len), .little);
|
||||
std.mem.writeInt(u16, buf[2..4], @intCast(self.mount_path.len), .little);
|
||||
std.mem.writeInt(u16, buf[4..6], @intCast(self.label.len), .little);
|
||||
var off: usize = header_bytes;
|
||||
@memcpy(buf[off..][0..self.id.len], self.id);
|
||||
off += self.id.len;
|
||||
@memcpy(buf[off..][0..self.mount_path.len], self.mount_path);
|
||||
off += self.mount_path.len;
|
||||
@memcpy(buf[off..][0..self.label.len], self.label);
|
||||
return buf[0..total];
|
||||
}
|
||||
|
||||
/// Decode a reply tail, or null if it is malformed (short or inconsistent).
|
||||
/// The returned slices point into `bytes`.
|
||||
pub fn decode(bytes: []const u8) ?VolumeInfo {
|
||||
if (bytes.len < header_bytes) return null;
|
||||
const id_len = std.mem.readInt(u16, bytes[0..2], .little);
|
||||
const path_len = std.mem.readInt(u16, bytes[2..4], .little);
|
||||
const label_len = std.mem.readInt(u16, bytes[4..6], .little);
|
||||
const total = header_bytes + @as(usize, id_len) + path_len + label_len;
|
||||
if (total > bytes.len) return null;
|
||||
var off: usize = header_bytes;
|
||||
const id = bytes[off..][0..id_len];
|
||||
off += id_len;
|
||||
const mount_path = bytes[off..][0..path_len];
|
||||
off += path_len;
|
||||
const label = bytes[off..][0..label_len];
|
||||
return .{ .id = id, .mount_path = mount_path, .label = label };
|
||||
}
|
||||
};
|
||||
|
||||
pub const Protocol = envelope.Define(.{
|
||||
.name = "volume-manager",
|
||||
.version = 1,
|
||||
.operations = &.{
|
||||
.{ .name = "hello", .request = Hello },
|
||||
.{ .name = "volumes", .request = Volumes },
|
||||
},
|
||||
});
|
||||
|
||||
pub const Operation = Protocol.Operation;
|
||||
pub const message_maximum: usize = Protocol.message_maximum;
|
||||
|
||||
// Named fixture sizes so the bounds gate (which flags literal array lengths)
|
||||
// stays quiet: test inputs, not runtime ceilings.
|
||||
const test_reply_bytes = 128;
|
||||
const test_tiny_bytes = 4;
|
||||
|
||||
test "VolumeInfo round-trips id, mount_path, and label" {
|
||||
var buf: [test_reply_bytes]u8 = undefined;
|
||||
const info = VolumeInfo{ .id = "fat-12345678", .mount_path = "/volumes/fat-12345678", .label = "DANOS" };
|
||||
const encoded = info.encode(&buf).?;
|
||||
const back = VolumeInfo.decode(encoded).?;
|
||||
try std.testing.expectEqualStrings("fat-12345678", back.id);
|
||||
try std.testing.expectEqualStrings("/volumes/fat-12345678", back.mount_path);
|
||||
try std.testing.expectEqualStrings("DANOS", back.label);
|
||||
}
|
||||
|
||||
test "VolumeInfo encode refuses a buffer that is too small; decode rejects a short tail" {
|
||||
var tiny: [test_tiny_bytes]u8 = undefined;
|
||||
const info = VolumeInfo{ .id = "fat-1", .mount_path = "/volumes/fat-1", .label = "" };
|
||||
try std.testing.expect(info.encode(&tiny) == null);
|
||||
try std.testing.expect(VolumeInfo.decode(&[_]u8{ 0, 0, 0 }) == null); // shorter than the header
|
||||
try std.testing.expect(VolumeInfo.decode(&[_]u8{ 0xFF, 0xFF, 0, 0, 0, 0 }) == null); // claims 65535 id bytes
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# The filesystem map: a probed volume's content signature -> the service binary
|
||||
# that serves it (docs/file-system-development/storage-architecture.md). The
|
||||
# volume manager reads this (the policy); a signature no row matches goes
|
||||
# unserved, never guessed. Adding a filesystem adds a row.
|
||||
#
|
||||
# signature, binary
|
||||
fat, /system/services/fat
|
||||
|
@@ -0,0 +1,8 @@
|
||||
# The mount map (danos's fstab): a volume's content id -> a chosen mount prefix.
|
||||
# This is an OPTIONAL override, read by the volume manager. A volume with no row
|
||||
# mounts at its default /volumes/<id>, where <id> is the manager's rendered
|
||||
# content identity (e.g. fat-12345678, gpt-<guid>, mbr-<sig>-<index>) — stable,
|
||||
# unique, and never a port or a label. The label is display metadata, not here:
|
||||
# query it via the volume manager's `volumes` verb.
|
||||
#
|
||||
# id, mount_prefix
|
||||
|
+33
-11
@@ -64,15 +64,24 @@ var filesystem: engine.FileSystem = undefined;
|
||||
/// the right volume's channel.
|
||||
var my_volume_id: u64 = 0;
|
||||
|
||||
/// The prefixes this volume installs: /volumes/usb from the volume root, plus
|
||||
/// the two hierarchy subtrees the boot volume carries (rewrite == prefix), so
|
||||
/// hierarchy paths (the logger's /system/logs) stay decoupled from which volume
|
||||
/// backs them.
|
||||
const fat_mounts = [_]harness.MountSpec{
|
||||
.{ .prefix = "/volumes/usb" },
|
||||
.{ .prefix = "/system/configuration", .rewrite = "/system/configuration" },
|
||||
.{ .prefix = "/system/logs", .rewrite = "/system/logs" },
|
||||
};
|
||||
/// The volume's own mount path, handed in as argv[2] by the volume manager: the
|
||||
/// volume's content id-path (e.g. /volumes/fat-12345678). Defaults to
|
||||
/// /volumes/usb only for a bare launch with no argument; the manager always
|
||||
/// passes it. The slice points into the entry block, valid for the process life.
|
||||
var volume_mount_prefix: []const u8 = "/volumes/usb";
|
||||
|
||||
/// The mounts this volume installs: its own root, plus — only if it is the boot
|
||||
/// volume (it resolves /system/configuration) — the two FHS rewrites, so the
|
||||
/// logger's /system/logs stays decoupled from which volume backs it. Boot-volume
|
||||
/// detection is by content, so it works no matter which volume carries /system.
|
||||
/// bound: mounts one volume installs (its root + the two boot rewrites)
|
||||
/// decided-by: ours
|
||||
/// protects: the mount_specs array
|
||||
/// at-limit: truncate - unreachable today (fixed at 3); more configured mounts
|
||||
/// would need this raised, a deliberate change
|
||||
/// observed-by: a mount silently missing from the harness's mount log
|
||||
const maximum_mounts_per_volume = 4;
|
||||
var mount_specs: [maximum_mounts_per_volume]harness.MountSpec = undefined;
|
||||
|
||||
/// Get this volume's block channel from the volume manager (establishment by
|
||||
/// lineage, communication.md "Establishment: two planes" — `block` is not a
|
||||
@@ -164,14 +173,27 @@ fn fatBringUp(endpoint: ipc.Handle) ?Harness.Volume {
|
||||
};
|
||||
std.log.info("mounted FAT ({s}, {d} clusters, partition lba {d})", .{ @tagName(filesystem.geometry.fat_type), filesystem.geometry.cluster_count, filesystem.base_lba });
|
||||
|
||||
return .{ .engine = &filesystem, .mounts = &fat_mounts, .flush = flushIfDirty };
|
||||
// The volume mounts at its id-path (argv[2]), plus the two FHS rewrites so
|
||||
// hierarchy paths (the logger's /system/logs) stay decoupled from which
|
||||
// volume backs them. This single-volume increment's one volume IS the boot
|
||||
// volume, so it installs both unconditionally; S3 (multi-volume) makes the
|
||||
// rewrites content-conditional — installed only by whichever volume carries
|
||||
// the system, decided by content, not order.
|
||||
mount_specs[0] = .{ .prefix = volume_mount_prefix };
|
||||
mount_specs[1] = .{ .prefix = "/system/configuration", .rewrite = "/system/configuration" };
|
||||
mount_specs[2] = .{ .prefix = "/system/logs", .rewrite = "/system/logs" };
|
||||
return .{ .engine = &filesystem, .mounts = mount_specs[0..3], .flush = flushIfDirty };
|
||||
}
|
||||
|
||||
pub fn main(init: process.Init) void {
|
||||
// The volume manager spawns this process with its volume id as argv[1].
|
||||
// The volume manager spawns this process with its volume id as argv[1] and
|
||||
// the volume's mount path (its id-path) as argv[2].
|
||||
if (init.arguments.get(1)) |id| {
|
||||
my_volume_id = std.fmt.parseInt(u64, id, 10) catch 0;
|
||||
}
|
||||
if (init.arguments.get(2)) |prefix| {
|
||||
volume_mount_prefix = prefix;
|
||||
}
|
||||
_ = logging.write("/system/services/fat: starting, waiting for a block device\n");
|
||||
Harness.run(.{ .bringUp = fatBringUp });
|
||||
}
|
||||
|
||||
@@ -10,21 +10,46 @@ pub fn build(b: *std.Build) void {
|
||||
.name = "volume-manager",
|
||||
.root_source_file = b.path("volume-manager.zig"),
|
||||
.imports = &.{
|
||||
"block", "channel", "device-manager-protocol", "driver",
|
||||
"envelope", "ipc", "logging", "memory",
|
||||
"process", "service", "time", "volume-manager-protocol",
|
||||
"block", "channel", "csv", "device-manager-protocol",
|
||||
"driver", "envelope", "file-system", "ipc",
|
||||
"logging", "memory", "process", "service",
|
||||
"time", "volume-manager-protocol",
|
||||
},
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
||||
// Standalone `zig build test` for the partition parser; the root build keeps
|
||||
// its aggregate test step.
|
||||
const test_step = b.step("test", "Run the partition-parser unit tests");
|
||||
const tests = b.addTest(.{
|
||||
// Standalone `zig build test` for the parser + mount-map modules; the root
|
||||
// build keeps its aggregate test step.
|
||||
const csv = b.dependency("csv", .{});
|
||||
const test_step = b.step("test", "Run the partition parser + mount-map unit tests");
|
||||
|
||||
const partition_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("partition.zig"),
|
||||
.target = b.resolveTargetQuery(.{}),
|
||||
}),
|
||||
});
|
||||
test_step.dependOn(&b.addRunArtifact(tests).step);
|
||||
test_step.dependOn(&b.addRunArtifact(partition_tests).step);
|
||||
|
||||
// filesystem-map imports csv (and, by path, partition.zig), so its test
|
||||
// module needs csv wired.
|
||||
const filesystem_map_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("filesystem-map.zig"),
|
||||
.target = b.resolveTargetQuery(.{}),
|
||||
.imports = &.{.{ .name = "csv", .module = csv.module("csv") }},
|
||||
}),
|
||||
});
|
||||
test_step.dependOn(&b.addRunArtifact(filesystem_map_tests).step);
|
||||
|
||||
// volume-map imports csv (and, by path, partition.zig) for the id-path
|
||||
// deriver and the volumes.csv override parser.
|
||||
const volume_map_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("volume-map.zig"),
|
||||
.target = b.resolveTargetQuery(.{}),
|
||||
.imports = &.{.{ .name = "csv", .module = csv.module("csv") }},
|
||||
}),
|
||||
});
|
||||
test_step.dependOn(&b.addRunArtifact(volume_map_tests).step);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
.kernel = .{ .path = "../../../library/kernel" },
|
||||
.device = .{ .path = "../../../library/device" },
|
||||
.protocol = .{ .path = "../../../library/protocol" },
|
||||
// csv parses filesystems.csv / volumes.csv, the mount-map configuration.
|
||||
.csv = .{ .path = "../../../library/csv" },
|
||||
},
|
||||
.paths = .{""},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! filesystem-map — parse `/system/configuration/filesystems.csv` into
|
||||
//! content-signature → service-binary rules, and pick the binary for a probed
|
||||
//! volume's signature. The data-driven replacement for the volume manager's
|
||||
//! hardcoded `filesystem_binary` const: a signature no row matches goes unserved
|
||||
//! (logged), never guessed — the same discipline the device registry uses.
|
||||
//!
|
||||
//! Pure logic: no hardware, no syscalls, no allocator. The `binary` slice points
|
||||
//! into the CSV source, which the manager holds in a static buffer for the life
|
||||
//! of the process (zero-copy), so the source must outlive the rules.
|
||||
//!
|
||||
//! Format: one rule per line, two comma-separated fields, `#` comments (whole-
|
||||
//! line or trailing), blank lines ignored:
|
||||
//!
|
||||
//! signature, binary
|
||||
//!
|
||||
//! `signature` is a filesystem token (`fat`; `exfat` lands with S4); `binary` is
|
||||
//! a full ramdisk path.
|
||||
|
||||
const std = @import("std");
|
||||
const csv = @import("csv");
|
||||
const partition = @import("partition.zig");
|
||||
|
||||
/// One parsed row: a content signature and the service binary that serves it.
|
||||
pub const Rule = struct {
|
||||
kind: partition.FilesystemKind,
|
||||
binary: []const u8,
|
||||
};
|
||||
|
||||
/// How many rules landed, how many non-blank lines were malformed (for the
|
||||
/// manager to log), and whether there were more rules than the buffer could hold.
|
||||
pub const ParseResult = struct { count: usize, malformed: usize, truncated: bool };
|
||||
|
||||
const Line = union(enum) { rule: Rule, ignorable, malformed };
|
||||
|
||||
fn parseLine(line: []const u8) Line {
|
||||
const body = csv.stripComment(line);
|
||||
if (body.len == 0) return .ignorable;
|
||||
var it = csv.fields(body);
|
||||
const sig = it.next() orelse return .malformed;
|
||||
const binary = it.next() orelse return .malformed;
|
||||
if (it.next() != null) return .malformed; // too many columns
|
||||
if (binary.len == 0) return .malformed;
|
||||
const kind = partition.FilesystemKind.fromToken(sig);
|
||||
if (kind == .unknown) return .malformed; // an unrecognised signature token
|
||||
return .{ .rule = .{ .kind = kind, .binary = binary } };
|
||||
}
|
||||
|
||||
/// Parse a whole `filesystems.csv` into `out_rules`. The `binary` slices point
|
||||
/// into `source`, which must outlive them.
|
||||
pub fn parse(source: []const u8, out_rules: []Rule) ParseResult {
|
||||
var result: ParseResult = .{ .count = 0, .malformed = 0, .truncated = false };
|
||||
var lines = std.mem.splitScalar(u8, source, '\n');
|
||||
while (lines.next()) |line| {
|
||||
switch (parseLine(line)) {
|
||||
.ignorable => {},
|
||||
.malformed => result.malformed += 1,
|
||||
.rule => |rule| {
|
||||
if (result.count >= out_rules.len) {
|
||||
result.truncated = true;
|
||||
continue;
|
||||
}
|
||||
out_rules[result.count] = rule;
|
||||
result.count += 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// The service binary for a probed volume's signature — the first matching row,
|
||||
/// or null (the volume goes unserved, like a device no registry row matches).
|
||||
pub fn match(rules: []const Rule, kind: partition.FilesystemKind) ?[]const u8 {
|
||||
for (rules) |rule| {
|
||||
if (rule.kind == kind) return rule.binary;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- tests -------------------------------------------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
// A fixture-sized rule buffer for the tests, named so the bounds gate (which
|
||||
// flags literal array lengths) stays quiet: this is a test input, not a runtime
|
||||
// ceiling — the real one is maximum_filesystem_rules in the volume manager.
|
||||
const test_rule_slots = 4;
|
||||
|
||||
test "a fat signature maps to its binary; an unmatched signature is null" {
|
||||
const text =
|
||||
\\# signature, binary
|
||||
\\fat, /system/services/fat
|
||||
;
|
||||
var rules: [test_rule_slots]Rule = undefined;
|
||||
const parsed = parse(text, &rules);
|
||||
try testing.expectEqual(@as(usize, 1), parsed.count);
|
||||
try testing.expectEqual(@as(usize, 0), parsed.malformed);
|
||||
try testing.expectEqualStrings("/system/services/fat", match(rules[0..parsed.count], .fat).?);
|
||||
try testing.expect(match(rules[0..parsed.count], .unknown) == null);
|
||||
}
|
||||
|
||||
test "the binary is chosen by content, not hardcoded" {
|
||||
// Point the fat row at a different binary and confirm that binary is chosen —
|
||||
// a constant could not satisfy this, which is the whole point of the map.
|
||||
const text = "fat, /system/services/other-fat\n";
|
||||
var rules: [test_rule_slots]Rule = undefined;
|
||||
const parsed = parse(text, &rules);
|
||||
try testing.expectEqualStrings("/system/services/other-fat", match(rules[0..parsed.count], .fat).?);
|
||||
}
|
||||
|
||||
test "malformed rows are counted, not bound" {
|
||||
const text =
|
||||
\\fat, /system/services/fat
|
||||
\\bogusfs, /system/services/x
|
||||
\\fat,
|
||||
\\fat, /a, /b
|
||||
;
|
||||
var rules: [test_rule_slots]Rule = undefined;
|
||||
const parsed = parse(text, &rules);
|
||||
try testing.expectEqual(@as(usize, 1), parsed.count); // only the first fat row
|
||||
try testing.expectEqual(@as(usize, 3), parsed.malformed); // bad token, empty binary, too many columns
|
||||
}
|
||||
@@ -60,12 +60,28 @@ pub const Identity = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// One volume the parser found on the device: the block sub-range it occupies
|
||||
/// and its content identity.
|
||||
/// Which filesystem a volume's content is — the key `filesystems.csv` maps to a
|
||||
/// service binary. Today only FAT is recognized (S4 adds exFAT with a real VBR
|
||||
/// recognizer); until then every probed volume is `.fat`, matching the volume
|
||||
/// manager's historical hand-off of everything to the FAT service.
|
||||
pub const FilesystemKind = enum {
|
||||
fat,
|
||||
unknown,
|
||||
|
||||
pub fn fromToken(token: []const u8) FilesystemKind {
|
||||
if (std.mem.eql(u8, token, "fat")) return .fat;
|
||||
return .unknown;
|
||||
}
|
||||
};
|
||||
|
||||
/// One volume the parser found on the device: the block sub-range it occupies,
|
||||
/// its content identity, and which filesystem its content is (the signature the
|
||||
/// `filesystems.csv` map keys on to pick the service binary).
|
||||
pub const Volume = struct {
|
||||
base_lba: u64,
|
||||
block_count: u64,
|
||||
identity: Identity,
|
||||
signature: FilesystemKind = .fat,
|
||||
};
|
||||
|
||||
/// Read sectors on demand. `context` + `readFn` mirror the FAT engine's
|
||||
|
||||
@@ -26,7 +26,10 @@ const process = @import("process");
|
||||
const service = @import("service");
|
||||
const time = @import("time");
|
||||
const envelope = @import("envelope");
|
||||
const fs = @import("file-system");
|
||||
const partition = @import("partition.zig");
|
||||
const filesystem_map = @import("filesystem-map.zig");
|
||||
const volume_map = @import("volume-map.zig");
|
||||
|
||||
const Serve = volume_manager_protocol.Protocol.Provider(void);
|
||||
const Invocation = envelope.Invocation;
|
||||
@@ -42,15 +45,53 @@ const Volume = struct {
|
||||
block_count: u64,
|
||||
identity: partition.Identity,
|
||||
id: u64,
|
||||
binary: []const u8, // the service binary, from filesystems.csv by signature
|
||||
mount_prefix: []const u8, // the volume-root mount path (its id-path, or a volumes.csv override)
|
||||
filesystem_pid: u32 = 0,
|
||||
};
|
||||
|
||||
/// The filesystem binary a probed volume is served by. The signature->binary
|
||||
/// map (filesystems.csv) lands with the identity ladder; for now every FAT-shaped
|
||||
/// volume gets the FAT service.
|
||||
const filesystem_binary = "/system/services/fat";
|
||||
const volume_id: u64 = 1;
|
||||
|
||||
// The mount map, read from configuration at boot (the policy home, storage-
|
||||
// architecture.md): filesystems.csv (content signature -> service binary) and
|
||||
// volumes.csv (an optional id -> mount-prefix override). The sources are held
|
||||
// for the process life so the parsed rules' slices into them stay valid.
|
||||
/// bound: bytes of filesystems.csv / volumes.csv the manager reads
|
||||
/// decided-by: ours
|
||||
/// protects: the config source buffers below
|
||||
/// at-limit: truncate - a longer file is cut; a row split by the cut is malformed
|
||||
/// observed-by: the per-file "malformed/truncated" log line
|
||||
const config_source_bytes = 2048;
|
||||
var filesystems_source: [config_source_bytes]u8 = undefined;
|
||||
var volumes_source: [config_source_bytes]u8 = undefined;
|
||||
/// bound: filesystem-map rules held (one per content signature)
|
||||
/// decided-by: ours
|
||||
/// protects: the filesystem_rules table
|
||||
/// at-limit: truncate - extra rows are dropped and the "truncated" note logged
|
||||
/// observed-by: the "truncated" log line
|
||||
const maximum_filesystem_rules = 8;
|
||||
/// bound: volumes.csv override rows held (one per pinned volume id)
|
||||
/// decided-by: ours
|
||||
/// protects: the volume_rules table
|
||||
/// at-limit: truncate - extra rows are dropped and the "truncated" note logged
|
||||
/// observed-by: the "truncated" log line
|
||||
const maximum_volume_rules = 64;
|
||||
var filesystem_rules: [maximum_filesystem_rules]filesystem_map.Rule = undefined;
|
||||
var filesystem_rule_count: usize = 0;
|
||||
var volume_rules: [maximum_volume_rules]volume_map.Override = undefined;
|
||||
var volume_rule_count: usize = 0;
|
||||
/// The composed default mount path (/volumes/<id>) for the current volume; a
|
||||
/// volumes.csv override is used in place and needs no buffer (it is already a
|
||||
/// slice into volumes_source). One buffer suffices while the manager serves one
|
||||
/// volume (multi-volume gives each its own in S3).
|
||||
/// bound: bytes of a composed /volumes/<id> mount path
|
||||
/// decided-by: ours
|
||||
/// protects: the mount_prefix_buf below
|
||||
/// at-limit: truncate - bufPrint fails; the volume mounts at a fallback path (logged)
|
||||
/// observed-by: the fallback path in the log
|
||||
const mount_path_maximum = 64;
|
||||
var mount_prefix_buf: [mount_path_maximum]u8 = undefined;
|
||||
|
||||
var service_endpoint: ipc.Handle = 0;
|
||||
var manager_handle: ?ipc.Handle = null;
|
||||
var bounce: memory.DmaRegion = undefined;
|
||||
@@ -156,7 +197,7 @@ fn isDevicePresent(device_id: u64) bool {
|
||||
/// confinement controller (it defines the first range on the device).
|
||||
fn spawnFilesystem(v: *Volume) void {
|
||||
if (fs_failed) return;
|
||||
const pid = process.spawnSupervised(filesystem_binary, &.{"1"}, service_endpoint) orelse {
|
||||
const pid = process.spawnSupervised(v.binary, &.{ "1", v.mount_prefix }, service_endpoint) orelse {
|
||||
_ = logging.write("volume-manager: could not spawn the filesystem; retrying\n");
|
||||
armRestart();
|
||||
return;
|
||||
@@ -169,7 +210,7 @@ fn spawnFilesystem(v: *Volume) void {
|
||||
}
|
||||
v.filesystem_pid = pid;
|
||||
fs_spawn_ns = time.clock();
|
||||
std.log.info("volume 0x{x} -> {s} (pid {d}), lba {d}, {d} blocks", .{ v.identity.key, filesystem_binary, pid, v.base_lba, v.block_count });
|
||||
std.log.info("volume 0x{x} -> {s} (pid {d}), lba {d}, {d} blocks", .{ v.identity.key, v.binary, pid, v.base_lba, v.block_count });
|
||||
}
|
||||
|
||||
/// Schedule a fat restart after backoff; the poll loop performs it once due.
|
||||
@@ -224,11 +265,29 @@ fn bringUpVolume() void {
|
||||
_ = ipc.close(device.endpoint);
|
||||
return;
|
||||
};
|
||||
// Pick the service binary from the volume's content signature. A signature
|
||||
// no filesystems.csv row serves goes unserved (logged), like an unbound
|
||||
// device — the manager does not guess.
|
||||
const binary = filesystem_map.match(filesystem_rules[0..filesystem_rule_count], found.signature) orelse {
|
||||
if (!logged_no_volume) {
|
||||
_ = logging.write("volume-manager: no filesystem serves this volume's content; unserved\n");
|
||||
logged_no_volume = true;
|
||||
}
|
||||
_ = ipc.close(device.endpoint);
|
||||
return;
|
||||
};
|
||||
// The mount path is the volume's identity id (/volumes/<id>), or a
|
||||
// volumes.csv override pinning it to a chosen path. The id is content-derived,
|
||||
// so the path is stable and never a port or a label.
|
||||
var id_buf: [volume_map.id_maximum]u8 = undefined;
|
||||
const id = volume_map.idString(found.identity, &id_buf);
|
||||
const mount_prefix = volume_map.overrideFor(volume_rules[0..volume_rule_count], id) orelse
|
||||
(std.fmt.bufPrint(&mount_prefix_buf, "/volumes/{s}", .{id}) catch "/volumes/unknown");
|
||||
logged_no_volume = false;
|
||||
fs_restarts = 0;
|
||||
fs_failed = false;
|
||||
restart_pending = false;
|
||||
volume = .{ .storage = device, .storage_device_id = opened.device_id, .base_lba = found.base_lba, .block_count = found.block_count, .identity = found.identity, .id = volume_id };
|
||||
volume = .{ .storage = device, .storage_device_id = opened.device_id, .base_lba = found.base_lba, .block_count = found.block_count, .identity = found.identity, .id = volume_id, .binary = binary, .mount_prefix = mount_prefix };
|
||||
spawnFilesystem(&volume.?);
|
||||
}
|
||||
|
||||
@@ -289,16 +348,66 @@ fn onHello(_: void, invocation: Invocation(volume_manager_protocol.Hello), _: An
|
||||
return 0;
|
||||
}
|
||||
|
||||
const handlers = Serve.Handlers{ .hello = onHello };
|
||||
/// Answer a `volumes` query with the mounted volume's descriptor — its id (its
|
||||
/// mount path is /volumes/<id> unless overridden), its actual mount path, and
|
||||
/// its display label. This is how a shell or file manager reads a volume's
|
||||
/// friendly name: software keys on the id, a UI shows the label. An empty reply
|
||||
/// means no volume is mounted.
|
||||
fn onVolumes(_: void, _: Invocation(volume_manager_protocol.Volumes), answer: Answer(void)) isize {
|
||||
const v = volume orelse return 0;
|
||||
var id_buf: [volume_map.id_maximum]u8 = undefined;
|
||||
const info = volume_manager_protocol.VolumeInfo{
|
||||
.id = volume_map.idString(v.identity, &id_buf),
|
||||
.mount_path = v.mount_prefix,
|
||||
.label = v.identity.labelSlice(),
|
||||
};
|
||||
const encoded = info.encode(answer.tail()) orelse return 0;
|
||||
return @intCast(encoded.len);
|
||||
}
|
||||
|
||||
const handlers = Serve.Handlers{ .hello = onHello, .volumes = onVolumes };
|
||||
|
||||
fn onMessage(message: []const u8, out: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||
// No verb takes a capability up, so the turn closes whatever arrives.
|
||||
return Serve.dispatch({}, handlers, message, sender, arrived.peek(), out);
|
||||
}
|
||||
|
||||
/// Read a config file into `buf`, returning the byte count (0 if missing).
|
||||
fn readConfig(path: []const u8, buf: []u8) usize {
|
||||
var file = fs.open(path, .{}) orelse {
|
||||
std.log.info("volume-manager: {s} missing", .{path});
|
||||
return 0;
|
||||
};
|
||||
defer file.close();
|
||||
var used: usize = 0;
|
||||
while (used < buf.len) {
|
||||
const n = file.read(buf[used..]) orelse break;
|
||||
if (n == 0) break;
|
||||
used += n;
|
||||
}
|
||||
return used;
|
||||
}
|
||||
|
||||
/// Load the mount map from configuration once at boot (mirrors the device
|
||||
/// manager's registry load). A missing or empty filesystems.csv means no volume
|
||||
/// is served; volumes.csv is optional — no rows means every volume takes its
|
||||
/// default /volumes/<id> path.
|
||||
fn loadTables() void {
|
||||
const fs_used = readConfig("/system/configuration/filesystems.csv", &filesystems_source);
|
||||
const fr = filesystem_map.parse(filesystems_source[0..fs_used], &filesystem_rules);
|
||||
filesystem_rule_count = fr.count;
|
||||
if (fr.malformed != 0 or fr.truncated) std.log.info("filesystems.csv: {d} malformed, truncated={}", .{ fr.malformed, fr.truncated });
|
||||
|
||||
const vol_used = readConfig("/system/configuration/volumes.csv", &volumes_source);
|
||||
const vr = volume_map.parse(volumes_source[0..vol_used], &volume_rules);
|
||||
volume_rule_count = vr.count;
|
||||
if (vr.malformed != 0 or vr.truncated) std.log.info("volumes.csv: {d} malformed, truncated={}", .{ vr.malformed, vr.truncated });
|
||||
}
|
||||
|
||||
fn initialise(endpoint: ipc.Handle) bool {
|
||||
service_endpoint = endpoint;
|
||||
_ = logging.write("volume-manager: starting, waiting for a storage device\n");
|
||||
loadTables();
|
||||
_ = process.subscribeExits(endpoint);
|
||||
pollTick();
|
||||
_ = time.timerOnce(endpoint, poll_interval_ms); // the poll runs for the life of the boot
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//! volume-map — render a volume's identity into its stable mount id-string, and
|
||||
//! parse `/system/configuration/volumes.csv` (danos's fstab) into optional
|
||||
//! id → mount-prefix overrides. This is where the id/label split becomes the
|
||||
//! path: a volume's mount point is derived from its content identity (the id),
|
||||
//! never from a port or a label. Two distinct volumes that share a label get
|
||||
//! distinct id-strings automatically; only identical ids (dd-cloned media) can
|
||||
//! collide, which is the narrow case the manager's duplicate policy is for.
|
||||
//!
|
||||
//! `volumes.csv` is an OPTIONAL override: a row `id, mount_prefix` pins a volume
|
||||
//! (by its id-string) to a chosen path. A volume with no row takes its default
|
||||
//! `/volumes/<id>`. The label is display metadata, exposed by the manager's
|
||||
//! `volumes` query, and never appears here.
|
||||
//!
|
||||
//! Pure logic: no syscalls, no allocator. Override slices point into the CSV
|
||||
//! source, which the manager holds in a static buffer for the process life.
|
||||
|
||||
const std = @import("std");
|
||||
const csv = @import("csv");
|
||||
const partition = @import("partition.zig");
|
||||
|
||||
/// bound: bytes of the longest volume id-string the deriver renders
|
||||
/// decided-by: ours
|
||||
/// protects: the caller's id-string buffer
|
||||
/// at-limit: truncate - bufPrint fails and idString returns ""; the volume goes
|
||||
/// unnamed and the manager logs it rather than mounting at an empty path
|
||||
/// observed-by: a volume with an empty id in the `volumes` query / the log
|
||||
pub const id_maximum = 40; // "gpt-" (4) or "uuid-" (5) + 32 hex fits in 40
|
||||
|
||||
/// One parsed override row: a volume id-string and the mount prefix it pins to.
|
||||
pub const Override = struct { id: []const u8, prefix: []const u8 };
|
||||
|
||||
/// How many overrides landed, how many non-blank lines were malformed, and
|
||||
/// whether there were more rows than the buffer could hold.
|
||||
pub const ParseResult = struct { count: usize, malformed: usize, truncated: bool };
|
||||
|
||||
/// Render a volume's identity into its id-string — the content-derived, unique,
|
||||
/// order-independent token whose default mount path is `/volumes/<id>`. The rung
|
||||
/// tags the scheme so ids never collide across rungs; the key is the content id,
|
||||
/// so a moved drive keeps its id (and thus its path).
|
||||
pub fn idString(identity: partition.Identity, buf: []u8) []const u8 {
|
||||
return switch (identity.rung) {
|
||||
.gpt_guid => std.fmt.bufPrint(buf, "gpt-{x:0>32}", .{identity.key}) catch "",
|
||||
.filesystem_uuid => std.fmt.bufPrint(buf, "uuid-{x:0>32}", .{identity.key}) catch "",
|
||||
.fat_serial => std.fmt.bufPrint(buf, "fat-{x:0>8}", .{@as(u32, @truncate(identity.key))}) catch "",
|
||||
.mbr_index => std.fmt.bufPrint(buf, "mbr-{x}-{d}", .{
|
||||
@as(u32, @truncate(identity.key >> 8)),
|
||||
@as(u8, @truncate(identity.key & 0xff)),
|
||||
}) catch "",
|
||||
.anonymous => std.fmt.bufPrint(buf, "anon-{x}", .{identity.key}) catch "",
|
||||
};
|
||||
}
|
||||
|
||||
const Line = union(enum) { override: Override, ignorable, malformed };
|
||||
|
||||
fn parseLine(line: []const u8) Line {
|
||||
const body = csv.stripComment(line);
|
||||
if (body.len == 0) return .ignorable;
|
||||
var it = csv.fields(body);
|
||||
const id = it.next() orelse return .malformed;
|
||||
const prefix = it.next() orelse return .malformed;
|
||||
if (it.next() != null) return .malformed; // too many columns
|
||||
if (id.len == 0 or prefix.len == 0) return .malformed;
|
||||
return .{ .override = .{ .id = id, .prefix = prefix } };
|
||||
}
|
||||
|
||||
/// Parse a whole `volumes.csv` into `out_rules`. The slices point into `source`,
|
||||
/// which must outlive them.
|
||||
pub fn parse(source: []const u8, out_rules: []Override) ParseResult {
|
||||
var result: ParseResult = .{ .count = 0, .malformed = 0, .truncated = false };
|
||||
var lines = std.mem.splitScalar(u8, source, '\n');
|
||||
while (lines.next()) |line| {
|
||||
switch (parseLine(line)) {
|
||||
.ignorable => {},
|
||||
.malformed => result.malformed += 1,
|
||||
.override => |ov| {
|
||||
if (result.count >= out_rules.len) {
|
||||
result.truncated = true;
|
||||
continue;
|
||||
}
|
||||
out_rules[result.count] = ov;
|
||||
result.count += 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// The override mount prefix for a volume whose id-string is `id`, or null (the
|
||||
/// volume takes its default `/volumes/<id>` path). First matching row wins.
|
||||
pub fn overrideFor(rules: []const Override, id: []const u8) ?[]const u8 {
|
||||
for (rules) |rule| {
|
||||
if (std.mem.eql(u8, rule.id, id)) return rule.prefix;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- tests -------------------------------------------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
// Named fixture sizes so the bounds gate (which flags literal array lengths)
|
||||
// stays quiet: test inputs, not runtime ceilings.
|
||||
const test_override_slots = 4;
|
||||
|
||||
test "idString renders each rung's id token" {
|
||||
var buf: [id_maximum]u8 = undefined;
|
||||
try testing.expectEqualStrings("fat-12345678", idString(.{ .rung = .fat_serial, .key = 0x12345678 }, &buf));
|
||||
try testing.expectEqualStrings("mbr-deadbeef-1", idString(.{ .rung = .mbr_index, .key = (@as(u128, 0xDEADBEEF) << 8) | 1 }, &buf));
|
||||
const guid: u128 = 0x00112233445566778899AABBCCDDEEFF;
|
||||
try testing.expectEqualStrings("gpt-00112233445566778899aabbccddeeff", idString(.{ .rung = .gpt_guid, .key = guid }, &buf));
|
||||
}
|
||||
|
||||
test "overrideFor returns the mapped prefix, else null" {
|
||||
const text =
|
||||
\\# id, mount_prefix
|
||||
\\fat-12345678, /mnt/boot
|
||||
;
|
||||
var rules: [test_override_slots]Override = undefined;
|
||||
const parsed = parse(text, &rules);
|
||||
try testing.expectEqual(@as(usize, 1), parsed.count);
|
||||
try testing.expectEqual(@as(usize, 0), parsed.malformed);
|
||||
try testing.expectEqualStrings("/mnt/boot", overrideFor(rules[0..parsed.count], "fat-12345678").?);
|
||||
try testing.expect(overrideFor(rules[0..parsed.count], "fat-99999999") == null);
|
||||
}
|
||||
|
||||
test "malformed volume rows are counted, not bound" {
|
||||
const text =
|
||||
\\fat-1, /mnt/a
|
||||
\\onlyonecolumn
|
||||
\\fat-2,
|
||||
\\fat-3, /a, /b
|
||||
;
|
||||
var rules: [test_override_slots]Override = undefined;
|
||||
const parsed = parse(text, &rules);
|
||||
try testing.expectEqual(@as(usize, 1), parsed.count); // only the first valid row
|
||||
try testing.expectEqual(@as(usize, 3), parsed.malformed); // one column, empty prefix, too many columns
|
||||
}
|
||||
+18
-5
@@ -179,7 +179,7 @@ CASES = [
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
"qemu_extra": ["-device", "intel-iommu,intremap=off"],
|
||||
"expect": r"(?s)(?=.*/system/kernel: iommu online)(?=.*fat: mounted /volumes/usb)(?=.*fat-test: ok)",
|
||||
"expect": r"(?s)(?=.*/system/kernel: iommu online)(?=.*fat: mounted /volumes/fat-12345678)(?=.*fat-test: ok)",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL|DANOS-IOMMU-FAULT"},
|
||||
# DMA + MSI under translation: interrupt-IN reports arrive through translated DMA and
|
||||
# the xHC's MSI/MSI-X still delivers (the 0xFEE00000 interrupt window bypasses second-
|
||||
@@ -215,7 +215,7 @@ CASES = [
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
"qemu_extra": ["-device", "amd-iommu,dma-remap=on,intremap=off"],
|
||||
"expect": r"(?s)(?=.*iommu online \(AMD-Vi\))(?=.*fat: mounted /volumes/usb)(?=.*fat-test: ok)",
|
||||
"expect": r"(?s)(?=.*iommu online \(AMD-Vi\))(?=.*fat: mounted /volumes/fat-12345678)(?=.*fat-test: ok)",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL|DANOS-IOMMU-FAULT"},
|
||||
# Port I/O grants: a claimed device's io_port resource lets a driver read/write its
|
||||
# ports (PS/2 status 0x64), gated by the claim; out-of-range/unclaimed is refused.
|
||||
@@ -749,13 +749,26 @@ CASES = [
|
||||
"expect": r"usb-storage: ready[\s\S]*usb-storage: block 0 signature 0x55aa",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# FAT mount end to end: the fat server mounts the boot usb-storage device (the
|
||||
# FAT32 image) into the VFS at /volumes/usb. A fat-test client then lists and reads
|
||||
# FAT32 image) into the VFS at /volumes/fat-12345678. A fat-test client then lists and reads
|
||||
# through the mount — proof of the whole stack: block device -> FAT parse ->
|
||||
# VFS routing -> file read.
|
||||
{"name": "fat-mount",
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
"expect": r"fat: mounted /volumes/usb[\s\S]*fat-test: ok",
|
||||
"expect": r"fat: mounted /volumes/fat-12345678[\s\S]*fat-test: ok",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# The id-path naming (S2, storage-stack-plan.md). The boot volume mounts at
|
||||
# its CONTENT-derived id-path (/volumes/fat-12345678, from the FAT32 serial
|
||||
# 0x12345678) — never a port name — and keeps its FHS rewrites so /system/logs
|
||||
# persistence still rides the volume. Discrimination: before S2's flip fat
|
||||
# hardcoded /volumes/usb, so the id-path mount line never appears. (Making the
|
||||
# rewrites content-conditional on which volume carries the system is S3.)
|
||||
{"name": "volume-identity-name",
|
||||
"build_case": "fat-mount",
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
"expect": r"(?s)fat: mounted /volumes/fat-12345678"
|
||||
r"[\s\S]*fat: mounted /system/logs",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# The removal lifecycle (V4, docs/volume-manager-plan.md): pull the boot stick
|
||||
# mid-run. device_del the usb-storage device -> the bus reports the port empty
|
||||
@@ -780,7 +793,7 @@ CASES = [
|
||||
"qmp_sequence": [
|
||||
{"delay": 8, "command": "device_del", "arguments": {"id": "bootstorage"}},
|
||||
],
|
||||
"expect": r"(?s)fat: mounted /volumes/usb"
|
||||
"expect": r"(?s)fat: mounted /volumes/fat-12345678"
|
||||
r"[\s\S]*volume-manager: storage for volume \d+ removed; unmounting",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# Volume-manager discovery + probe (V3a, docs/volume-manager-plan.md). Reuses
|
||||
|
||||
@@ -38,7 +38,7 @@ const time = @import("time");
|
||||
/// A scratch file on the volume, so the node the intruder tries to write through
|
||||
/// is one nothing else reads. (A foreign write that *succeeded* would prove the
|
||||
/// bug — it must not also damage the boot volume proving it.)
|
||||
const held_path = "/volumes/usb/BADGE.TXT";
|
||||
const held_path = "/volumes/fat-12345678/BADGE.TXT";
|
||||
const held_contents = "held";
|
||||
|
||||
fn line(comptime format: []const u8, arguments: anytype) void {
|
||||
@@ -46,12 +46,12 @@ fn line(comptime format: []const u8, arguments: anytype) void {
|
||||
_ = logging.write(std.fmt.bufPrint(&buffer, format, arguments) catch return);
|
||||
}
|
||||
|
||||
/// The fat server mounts /volumes/usb only after the whole USB storage chain is
|
||||
/// The fat server mounts /volumes/fat-12345678 only after the whole USB storage chain is
|
||||
/// up, and both instances race it.
|
||||
fn waitForVolume() bool {
|
||||
var tries: u32 = 0;
|
||||
while (tries < 1400) : (tries += 1) {
|
||||
if (fs.openDirectory("/volumes/usb")) |opened| {
|
||||
if (fs.openDirectory("/volumes/fat-12345678")) |opened| {
|
||||
var directory = opened;
|
||||
directory.close();
|
||||
return true;
|
||||
@@ -79,7 +79,7 @@ pub fn main(init: process.Init) void {
|
||||
|
||||
fn own() void {
|
||||
if (!waitForVolume()) {
|
||||
_ = logging.write("badge-scope-test: FAILED (/volumes/usb never became available)\n");
|
||||
_ = logging.write("badge-scope-test: FAILED (/volumes/fat-12345678 never became available)\n");
|
||||
return;
|
||||
}
|
||||
var held = fs.open(held_path, .{ .create = true, .truncate = true }) orelse {
|
||||
@@ -142,7 +142,7 @@ fn own() void {
|
||||
|
||||
fn intrude(foreign_node: u64, foreign_layer: u32) void {
|
||||
if (!waitForVolume()) {
|
||||
_ = logging.write("badge-scope-test: FAILED (/volumes/usb never became available)\n");
|
||||
_ = logging.write("badge-scope-test: FAILED (/volumes/fat-12345678 never became available)\n");
|
||||
return;
|
||||
}
|
||||
const node_verdict = probeNode(foreign_node);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! test/system/services/fat-test — a client that proves the FAT mount end to end:
|
||||
//! it waits for the fat server to mount the USB volume at /volumes/usb, lists the
|
||||
//! root directory through the VFS (which routes /volumes/usb to the fat backend), and
|
||||
//! it waits for the fat server to mount the USB volume at /volumes/fat-12345678, lists the
|
||||
//! root directory through the VFS (which routes /volumes/fat-12345678 to the fat backend), and
|
||||
//! reads a known file off it. Shipped in the initial_ramdisk; the `fat-mount`
|
||||
//! kernel test spawns it alongside init.
|
||||
|
||||
@@ -18,16 +18,16 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
pub fn main(init: process.Init) void {
|
||||
_ = init;
|
||||
|
||||
// Wait for /volumes/usb to be mounted — the fat server races us at boot (it must
|
||||
// Wait for /volumes/fat-12345678 to be mounted — the fat server races us at boot (it must
|
||||
// bring up the whole USB storage chain first).
|
||||
var opened: ?fs.Directory = null;
|
||||
var tries: u32 = 0;
|
||||
while (opened == null and tries < 1400) : (tries += 1) {
|
||||
opened = fs.openDirectory("/volumes/usb");
|
||||
opened = fs.openDirectory("/volumes/fat-12345678");
|
||||
if (opened == null) time.sleepMillis(50);
|
||||
}
|
||||
var dir = opened orelse {
|
||||
_ = logging.write("fat-test: /volumes/usb never became available\n");
|
||||
_ = logging.write("fat-test: /volumes/fat-12345678 never became available\n");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -43,56 +43,56 @@ pub fn main(init: process.Init) void {
|
||||
|
||||
// Read a known file off the boot volume through the mount (best effort): the
|
||||
// kernel image is an ELF, so its first bytes are the ELF magic.
|
||||
if (fs.open("/volumes/usb/system/kernel", .{})) |opened_file| {
|
||||
if (fs.open("/volumes/fat-12345678/system/kernel", .{})) |opened_file| {
|
||||
var file = opened_file;
|
||||
var magic: [4]u8 = undefined;
|
||||
const n = file.read(&magic) orelse 0;
|
||||
file.close();
|
||||
if (n == 4 and magic[0] == 0x7F and magic[1] == 'E' and magic[2] == 'L' and magic[3] == 'F') {
|
||||
_ = logging.write("fat-test: read /volumes/usb/system/kernel ELF magic ok\n");
|
||||
_ = logging.write("fat-test: read /volumes/fat-12345678/system/kernel ELF magic ok\n");
|
||||
} else {
|
||||
writeLine("fat-test: /volumes/usb/system/kernel read {d} bytes (not ELF magic)\n", .{n});
|
||||
writeLine("fat-test: /volumes/fat-12345678/system/kernel read {d} bytes (not ELF magic)\n", .{n});
|
||||
}
|
||||
}
|
||||
|
||||
// Exercise directory + file mutation through the mount: mkdir, create a file
|
||||
// inside it, read it back, then remove it — proof mkdir/unlink reach the engine.
|
||||
if (fs.makeDirectory("/volumes/usb/TESTDIR")) {
|
||||
if (fs.makeDirectory("/volumes/fat-12345678/TESTDIR")) {
|
||||
var wrote = false;
|
||||
if (fs.open("/volumes/usb/TESTDIR/HELLO.TXT", .{ .create = true, .truncate = true })) |created| {
|
||||
if (fs.open("/volumes/fat-12345678/TESTDIR/HELLO.TXT", .{ .create = true, .truncate = true })) |created| {
|
||||
var f = created;
|
||||
wrote = (f.writeAll("mutation-ok") orelse 0) == "mutation-ok".len;
|
||||
f.close();
|
||||
}
|
||||
// The created file carries a real modification time (stamped from the RTC).
|
||||
var mtime_ok = false;
|
||||
if (fs.attributes("/volumes/usb/TESTDIR/HELLO.TXT")) |attrs| {
|
||||
if (fs.attributes("/volumes/fat-12345678/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) _ = logging.write("fat-test: mtime ok\n");
|
||||
|
||||
// Rename it, then read from the new name and confirm the old name is gone.
|
||||
const renamed = fs.rename("/volumes/usb/TESTDIR/HELLO.TXT", "/volumes/usb/TESTDIR/RENAMED.TXT");
|
||||
const old_gone = !fs.exists("/volumes/usb/TESTDIR/HELLO.TXT");
|
||||
const renamed = fs.rename("/volumes/fat-12345678/TESTDIR/HELLO.TXT", "/volumes/fat-12345678/TESTDIR/RENAMED.TXT");
|
||||
const old_gone = !fs.exists("/volumes/fat-12345678/TESTDIR/HELLO.TXT");
|
||||
if (renamed and old_gone) _ = logging.write("fat-test: rename ok\n");
|
||||
var readback = false;
|
||||
if (fs.open("/volumes/usb/TESTDIR/RENAMED.TXT", .{})) |reopened| {
|
||||
if (fs.open("/volumes/fat-12345678/TESTDIR/RENAMED.TXT", .{})) |reopened| {
|
||||
var f = reopened;
|
||||
var buf: [16]u8 = undefined;
|
||||
const got = f.read(&buf) orelse 0;
|
||||
f.close();
|
||||
readback = std.mem.eql(u8, buf[0..got], "mutation-ok");
|
||||
}
|
||||
const removed = fs.remove("/volumes/usb/TESTDIR/RENAMED.TXT");
|
||||
const gone = !fs.exists("/volumes/usb/TESTDIR/RENAMED.TXT");
|
||||
const removed = fs.remove("/volumes/fat-12345678/TESTDIR/RENAMED.TXT");
|
||||
const gone = !fs.exists("/volumes/fat-12345678/TESTDIR/RENAMED.TXT");
|
||||
if (wrote and mtime_ok and renamed and old_gone and readback and removed and gone) {
|
||||
_ = logging.write("fat-test: mutations ok\n");
|
||||
} else {
|
||||
writeLine("fat-test: mutations FAILED (wrote={} mtime={} renamed={} oldgone={} read={} removed={} gone={})\n", .{ wrote, mtime_ok, renamed, old_gone, readback, removed, gone });
|
||||
}
|
||||
} else {
|
||||
_ = logging.write("fat-test: mkdir /volumes/usb/TESTDIR failed\n");
|
||||
_ = logging.write("fat-test: mkdir /volumes/fat-12345678/TESTDIR failed\n");
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
|
||||
@@ -77,7 +77,7 @@ fn park() void {
|
||||
var parked: ?fs.File = null;
|
||||
var tries: u32 = 0;
|
||||
while (parked == null and tries < 1000) : (tries += 1) {
|
||||
parked = fs.open("/volumes/usb/parked", .{ .create = true });
|
||||
parked = fs.open("/volumes/fat-12345678/parked", .{ .create = true });
|
||||
if (parked == null) time.sleepMillis(20);
|
||||
}
|
||||
if (parked == null) {
|
||||
@@ -92,16 +92,16 @@ fn park() void {
|
||||
// "parked" marker, which fails the vfs-client-death case: before the
|
||||
// ownership gate existed, any process could unmount any prefix, and this
|
||||
// fixture would have deleted the volume out from under the whole boot.
|
||||
if (fs.fsUnmount("/volumes/usb")) {
|
||||
if (fs.fsUnmount("/volumes/fat-12345678")) {
|
||||
_ = logging.write("vfstest: foreign unmount was ALLOWED\n");
|
||||
return;
|
||||
}
|
||||
if (fs.open("/volumes/usb/parked", .{})) |resolved| {
|
||||
if (fs.open("/volumes/fat-12345678/parked", .{})) |resolved| {
|
||||
var verification = resolved;
|
||||
verification.close(); // the park below must be the client's ONLY open
|
||||
// handle — the kernel test string-matches "released 1 handle(s)".
|
||||
} else {
|
||||
_ = logging.write("vfstest: /volumes/usb gone after refused unmount\n");
|
||||
_ = logging.write("vfstest: /volumes/fat-12345678 gone after refused unmount\n");
|
||||
return;
|
||||
}
|
||||
_ = logging.write("vfstest: foreign unmount refused\n");
|
||||
|
||||
Reference in New Issue
Block a user