fat/harness: filesystems coexist without the shared vfs name; two-volume proof (S3)
A second usb-storage device (a generated data volume, serial da7a0001, an empty FAT with no /system) plugged in beside the boot volume: the volume manager adopts both devices and spawns a confined fat per volume, each mounted at its own content id-path. The test surfaced a real coexistence bug. Every filesystem bound the single "vfs" contract name under /protocol; the second volume's fat lost the race, service.run refused-and-exited on the held name, and that volume never mounted. Clients don't reach filesystems by that name — fs_resolve routes a path to its backing endpoint through the kernel mount table by prefix — and nothing consumes "vfs", so the fix is to bind no shared name: the harness's service_name now defaults to null. This is the "this fades" the harness comment anticipated for the volume-manager era; a filesystem's endpoint still serves as its mount backend without a name. fat logs "is a data volume" for the non-system branch so the test can positively assert content-based detection. make-fat-image gains --serial/--label (default unchanged) so a second image gets a distinct id-path; the data image is generated per run, never committed. The case fails against the pre-fix harness (the data volume's fat exits on the refused bind) — toggle-demonstrated. Full suite 129/129 (128 + two-volumes); the single-volume path is unaffected by dropping the vestigial name bind.
This commit is contained in:
@@ -61,10 +61,14 @@ pub fn Server(comptime Engine: type) type {
|
|||||||
/// caller does the filesystem-specific bring-up (find the block
|
/// caller does the filesystem-specific bring-up (find the block
|
||||||
/// device, set up DMA, mount the engine) and returns a `Volume`.
|
/// device, set up DMA, mount the engine) and returns a `Volume`.
|
||||||
bringUp: *const fn (endpoint: ipc.Handle) ?Volume,
|
bringUp: *const fn (endpoint: ipc.Handle) ?Volume,
|
||||||
/// The vfs contract name to bind. A filesystem serving one volume
|
/// A contract name to bind under /protocol, or null to bind none. In
|
||||||
/// binds "vfs" today; the volume-manager era hands each per-volume
|
/// the volume-manager era every filesystem is a per-volume process and
|
||||||
/// process its own establishment and this fades.
|
/// clients reach it through the kernel mount table — fs_resolve routes
|
||||||
service_name: ?[]const u8 = "vfs",
|
/// a path to its backing endpoint by prefix — so no filesystem binds a
|
||||||
|
/// shared name. Two volumes would collide on one: the second's bind is
|
||||||
|
/// refused and service.run would exit, so its volume never mounts. The
|
||||||
|
/// endpoint still serves as the mount backend without a name.
|
||||||
|
service_name: ?[]const u8 = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- the harness's own state, one set per instantiation ---------------
|
// --- the harness's own state, one set per instantiation ---------------
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ fn fatBringUp(endpoint: ipc.Handle) ?Harness.Volume {
|
|||||||
mount_specs[1] = .{ .prefix = "/system/configuration", .rewrite = "/system/configuration" };
|
mount_specs[1] = .{ .prefix = "/system/configuration", .rewrite = "/system/configuration" };
|
||||||
mount_specs[2] = .{ .prefix = "/system/logs", .rewrite = "/system/logs" };
|
mount_specs[2] = .{ .prefix = "/system/logs", .rewrite = "/system/logs" };
|
||||||
mount_count = 3;
|
mount_count = 3;
|
||||||
|
} else {
|
||||||
|
std.log.info("volume {d} is a data volume; mounted at {s}", .{ my_volume_id, volume_mount_prefix });
|
||||||
}
|
}
|
||||||
return .{ .engine = &filesystem, .mounts = mount_specs[0..mount_count], .flush = flushIfDirty };
|
return .{ .engine = &filesystem, .mounts = mount_specs[0..mount_count], .flush = flushIfDirty };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -816,6 +816,27 @@ CASES = [
|
|||||||
"expect": r"volume-manager: volume 0x0*12345678 -> \S+ \(pid \d+\), lba \d+, \d+ blocks"
|
"expect": r"volume-manager: volume 0x0*12345678 -> \S+ \(pid \d+\), lba \d+, \d+ blocks"
|
||||||
r"[\s\S]*volume-manager: handed volume \d+ to pid \d+",
|
r"[\s\S]*volume-manager: handed volume \d+ to pid \d+",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# S3 multi-volume: a SECOND usb-storage device (a generated data volume, serial
|
||||||
|
# da7a0001, an empty FAT with no /system) plugged in beside the boot volume.
|
||||||
|
# Proves the volume manager adopts BOTH devices and spawns a confined fat per
|
||||||
|
# volume, each mounted at its own CONTENT id-path (/volumes/fat-<serial>); and
|
||||||
|
# that boot-volume detection is by content — only the volume that carries
|
||||||
|
# /system backs /system/configuration, while the data volume mounts at its
|
||||||
|
# id-path alone. Against the pre-S3 one-device/one-volume manager the data
|
||||||
|
# volume never mounts, so the da7a0001 lookaheads fail (toggle-demonstrated by
|
||||||
|
# checking out the step-2 volume-manager.zig).
|
||||||
|
{"name": "two-volumes",
|
||||||
|
"build_case": "fat-mount",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"data_volume": {"serial": "DA7A0001", "label": "DATAVOL", "size_mib": 64},
|
||||||
|
"expect": r"(?s)(?=.*volume-manager: volume 0x0*12345678 -> )"
|
||||||
|
r"(?=.*volume-manager: volume 0x0*da7a0001 -> )"
|
||||||
|
r"(?=.*fat: mounted /volumes/fat-12345678)"
|
||||||
|
r"(?=.*fat: mounted /volumes/fat-da7a0001)"
|
||||||
|
r"(?=.*carries the system tree)"
|
||||||
|
r"(?=.*data volume; mounted at /volumes/fat-da7a0001)",
|
||||||
|
"fail": r"data volume; mounted at /volumes/fat-12345678|DANOS-TEST-RESULT: FAIL"},
|
||||||
# Phase 2b: mkdir/unlink through the mount. Reuses the fat-mount build — the
|
# Phase 2b: mkdir/unlink through the mount. Reuses the fat-mount build — the
|
||||||
# fat-test client, after listing, makes a directory, writes+reads a file inside
|
# fat-test client, after listing, makes a directory, writes+reads a file inside
|
||||||
# it, then removes the file, exercising the whole VFS -> fat mutation path.
|
# it, then removes the file, exercising the whole VFS -> fat mutation path.
|
||||||
@@ -1422,6 +1443,23 @@ def run_case(arch, case):
|
|||||||
cmd[cmd.index("-m") + 1] = case["mem"]
|
cmd[cmd.index("-m") + 1] = case["mem"]
|
||||||
if case.get("qemu_extra"): # extra qemu args, e.g. -device intel-iommu for the IOMMU case
|
if case.get("qemu_extra"): # extra qemu args, e.g. -device intel-iommu for the IOMMU case
|
||||||
cmd += case["qemu_extra"]
|
cmd += case["qemu_extra"]
|
||||||
|
# A multi-volume case attaches a second usb-storage device backed by a freshly
|
||||||
|
# GENERATED data volume: a distinct-serial FAT32 with no /system tree, so the
|
||||||
|
# volume manager mounts it at its own id-path and the fat process marks it a
|
||||||
|
# data volume (never a system volume). Regenerated per run — no image is
|
||||||
|
# committed to the tree (the user keeps the boot files copyable, not baked in).
|
||||||
|
if case.get("data_volume"):
|
||||||
|
dv = case["data_volume"]
|
||||||
|
data_img = os.path.join(WORK, "data-volume.img")
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, os.path.join(REPO, "tools", "make-fat-image.py"),
|
||||||
|
"--serial", dv["serial"], "--label", dv.get("label", "DATAVOL"),
|
||||||
|
data_img, str(dv.get("size_mib", 64))],
|
||||||
|
check=True, stdout=subprocess.DEVNULL)
|
||||||
|
cmd += [
|
||||||
|
"-drive", f"if=none,id=datausb,format=raw,file={data_img}",
|
||||||
|
"-device", "usb-storage,bus=xhci.0,port=4,drive=datausb,removable=on,id=datastorage",
|
||||||
|
]
|
||||||
# A QMP control socket, always present (additive): how a case's `qmp_after`
|
# A QMP control socket, always present (additive): how a case's `qmp_after`
|
||||||
# hook injects host-side events into the guest mid-run. Kept under a short temp
|
# hook injects host-side events into the guest mid-run. Kept under a short temp
|
||||||
# dir, not WORK: a unix socket path is capped at ~104 bytes (sun_path), and a
|
# dir, not WORK: a unix socket path is capped at ~104 bytes (sun_path), and a
|
||||||
|
|||||||
+31
-7
@@ -47,8 +47,14 @@ def fat32_geometry(total_sectors):
|
|||||||
|
|
||||||
|
|
||||||
class Fat32Image:
|
class Fat32Image:
|
||||||
def __init__(self, total_sectors):
|
def __init__(self, total_sectors, volume_id=0x12345678, label="DANOS"):
|
||||||
self.total_sectors = total_sectors
|
self.total_sectors = total_sectors
|
||||||
|
# The FAT volume serial (its content identity — the /volumes/fat-<id>
|
||||||
|
# mount path danos derives from it) and the display label. A second image
|
||||||
|
# needs a distinct serial so its id-path does not collide with the boot
|
||||||
|
# volume's.
|
||||||
|
self.volume_id = volume_id & 0xFFFFFFFF
|
||||||
|
self.label = label
|
||||||
self.fat_size, self.cluster_count = fat32_geometry(total_sectors)
|
self.fat_size, self.cluster_count = fat32_geometry(total_sectors)
|
||||||
if self.cluster_count < 65525:
|
if self.cluster_count < 65525:
|
||||||
sys.exit(f"error: image too small for FAT32 ({self.cluster_count} clusters "
|
sys.exit(f"error: image too small for FAT32 ({self.cluster_count} clusters "
|
||||||
@@ -146,8 +152,8 @@ class Fat32Image:
|
|||||||
0x80, # drive number
|
0x80, # drive number
|
||||||
0, # reserved
|
0, # reserved
|
||||||
0x29, # extended boot signature
|
0x29, # extended boot signature
|
||||||
0x12345678, # volume id
|
self.volume_id, # volume id
|
||||||
b"DANOS ", # volume label
|
self.label.encode("ascii", "replace")[:11].ljust(11, b" "), # volume label
|
||||||
b"FAT32 ", # filesystem type
|
b"FAT32 ", # filesystem type
|
||||||
)
|
)
|
||||||
sector[510] = 0x55
|
sector[510] = 0x55
|
||||||
@@ -276,9 +282,9 @@ def build_tree(pairs):
|
|||||||
return root
|
return root
|
||||||
|
|
||||||
|
|
||||||
def build(out_path, size_mib, pairs):
|
def build(out_path, size_mib, pairs, volume_id=0x12345678, label="DANOS"):
|
||||||
total_sectors = size_mib * 1024 * 1024 // SECTOR
|
total_sectors = size_mib * 1024 * 1024 // SECTOR
|
||||||
image = Fat32Image(total_sectors)
|
image = Fat32Image(total_sectors, volume_id, label)
|
||||||
tree = build_tree(pairs)
|
tree = build_tree(pairs)
|
||||||
write_directory(image, 2, tree, 0, True)
|
write_directory(image, 2, tree, 0, True)
|
||||||
with open(out_path, "wb") as handle:
|
with open(out_path, "wb") as handle:
|
||||||
@@ -350,14 +356,32 @@ def main(argv):
|
|||||||
if len(argv) == 3 and argv[1] == "--verify":
|
if len(argv) == 3 and argv[1] == "--verify":
|
||||||
verify(argv[2])
|
verify(argv[2])
|
||||||
return 0
|
return 0
|
||||||
|
# Optional flags ahead of the positionals: --serial <hex> sets the FAT volume
|
||||||
|
# id (the /volumes/fat-<id> content identity), --label <name> its display
|
||||||
|
# label. A second FAT image passes a distinct --serial so its id-path cannot
|
||||||
|
# collide with the boot volume's.
|
||||||
|
argv = list(argv)
|
||||||
|
volume_id = 0x12345678
|
||||||
|
label = "DANOS"
|
||||||
|
i = 1
|
||||||
|
while i < len(argv):
|
||||||
|
if argv[i] == "--serial" and i + 1 < len(argv):
|
||||||
|
volume_id = int(argv[i + 1], 16)
|
||||||
|
del argv[i:i + 2]
|
||||||
|
elif argv[i] == "--label" and i + 1 < len(argv):
|
||||||
|
label = argv[i + 1]
|
||||||
|
del argv[i:i + 2]
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
if len(argv) < 3 or (len(argv) - 3) % 2 != 0:
|
if len(argv) < 3 or (len(argv) - 3) % 2 != 0:
|
||||||
sys.exit("usage: make-fat-image.py <out.img> <size-MiB> [<dest> <host>]...\n"
|
sys.exit("usage: make-fat-image.py [--serial <hex>] [--label <name>] "
|
||||||
|
"<out.img> <size-MiB> [<dest> <host>]...\n"
|
||||||
" make-fat-image.py --verify <out.img>")
|
" make-fat-image.py --verify <out.img>")
|
||||||
out_path = argv[1]
|
out_path = argv[1]
|
||||||
size_mib = int(argv[2])
|
size_mib = int(argv[2])
|
||||||
rest = argv[3:]
|
rest = argv[3:]
|
||||||
pairs = [(rest[i], rest[i + 1]) for i in range(0, len(rest), 2)]
|
pairs = [(rest[i], rest[i + 1]) for i in range(0, len(rest), 2)]
|
||||||
build(out_path, size_mib, pairs)
|
build(out_path, size_mib, pairs, volume_id, label)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user