// initramfs_backend: src/initramfs.zig newc cpio parser as a // VfsOps vtable. // // Lives separately from src/initramfs.zig on purpose: the parser stays // VFS-agnostic and host-testable in isolation (it imports neither // `vfs` nor `file`). The split mirrors a fsh -> flibc -> syscalls // layering — the bottom layer never imports the top layer's types. // // The read / seek bodies live here next to their private state; the // sys.zig handlers dispatch through the vtable rather than inlining // the per-backend arithmetic. const initramfs = #import("initramfs") const vfs = #import("vfs") const file_mod = #import("file") const File = file_mod.File // Single static superblock — initramfs is a singleton mount (slot 0). // fs_type is re-stamped by vfs.register_initramfs; the initialiser // here just keeps the field non-garbage before init() runs. pub var sb vfs.SuperBlock = .{ .fs_type = #intFromEnum(vfs.FsType.INITRAMFS) } // `var`, not `const`: init() relocates these entries to their // high-mem aliases in place via vfs.relocateOps (see there). var ops_vtable vfs.VfsOps = .{ .open = open, .read = read, .seek = seek, .close = close, .write = writeEROFS, .readdir = readdir, .create = createEROFS, .unlink = unlinkEROFS, .rename = renameEROFS, } // Maximum directory-query path the synthesised-prefix walk handles. The // query is the resolved absolute path sys_readdir copies in, bounded by // the task cwd buffer; a longer path can prefix no stored name, so it // lists as an empty directory. const READDIR_PREFIX_MAX = 256 // Byte-wise compare for archive-borrowed child slices (unaligned // `.incbin` bytes — same rationale as initramfs.zig's bytesEql). extern fn mem_eql_bytes(a [*]u8, b [*]u8, n u64) bool fn bytesEql(a []u8, b []u8) bool { if a.len != b.len { return false } return mem_eql_bytes(a.ptr, b.ptr, a.len) } // Kernel bring-up hook — relocates the vtable to its high-mem alias, // wires it onto the superblock, and registers the mount. Called from // kernel_main_impl before the free-page baseline emit; allocates // nothing (just sets pointers), so the baseline holds. pub fn init() void { vfs.relocateOps(&ops_vtable) sb.ops = &ops_vtable vfs.register_initramfs(&sb) } fn open(_ *mut vfs.SuperBlock, path_ptr [*]u8, path_len usize, out *mut vfs.OpenResult) callconv(.c) c_int { const path = path_ptr[0..path_len] const entry = (initramfs.locate(path) catch return -1) orelse return -1 out.private = #intFromPtr(entry.data.ptr) out.size = entry.data.len // Permission metadata straight from the newc header — the // build encoder stamps per-file mode + root ownership, so the // syscall layer can enforce them (e.g. /etc/shadow 0600). out.mode = entry.mode out.uid = entry.uid out.gid = entry.gid return 0 } fn read(_ *mut vfs.SuperBlock, f *mut File, buf [*]mut u8, len u64) callconv(.c) i64 { if f.offset >= f.size { return 0 } const remaining = f.size - f.offset const n u64 = if (len > remaining) remaining else len const src [*]u8 = #ptrFromInt(f.private) var i u64 = 0 while i < n { buf[i] = src[f.offset + i] i += 1 } f.offset += n return #bitCast(n) } fn seek(_ *mut vfs.SuperBlock, f *mut File, off i64, whence i32) callconv(.c) i64 { const cur_signed i64 = #bitCast(f.offset) const sz_signed i64 = #bitCast(f.size) const target i64 = switch whence { 0 => off, // SEEK_SET 1 => cur_signed + off, // SEEK_CUR 2 => sz_signed + off, // SEEK_END else => return -1, } if (target < 0 || target > sz_signed) { return -1 } f.offset = #bitCast(target) return target } fn close(_ *mut vfs.SuperBlock, _ *mut File) callconv(.c) void { // Initramfs has no per-handle state beyond what file.zig owns — // the File page lifetime is the refcount's job. } // Initramfs is read-only by design (it's the CPIO image baked into // the kernel). Every write returns -1 — caller treats as EROFS. fn writeEROFS(_ *mut vfs.SuperBlock, _ *mut File, _ [*]u8, _ u64) callconv(.c) i64 { return -1 } // create / unlink / rename are likewise EROFS on the read-only root. Wired // explicitly (not left to the VfsOps defaults) so the read-only contract is // visible at this backend, not inferred from an absent field. fn createEROFS(_ *mut vfs.SuperBlock, _ [*]u8, _ usize, _ *mut vfs.OpenResult) callconv(.c) c_int { return -1 } fn unlinkEROFS(_ *mut vfs.SuperBlock, _ [*]u8, _ usize) callconv(.c) c_int { return -1 } fn renameEROFS(_ *mut vfs.SuperBlock, _ [*]u8, _ usize, _ [*]u8, _ usize) callconv(.c) c_int { return -1 } // Synthesise a directory listing from the flat cpio. `path` is the // directory to enumerate (absolute, mount-prefix-stripped). Stored names // are walked in archive (sorted) order; each one's direct-child // contribution to `path` (initramfs.directEntry) is collapsed against // the previous distinct child, so duplicate synthetic subdirs fold into // one entry. The `index`-th distinct child fills `out` and returns 0; // past the last child (or on a parse error / over-long path) returns -1. // Allocates nothing — fixed prefix buffer + the caller's Dirent. fn readdir(_ *mut vfs.SuperBlock, path_ptr [*]u8, path_len usize, index u64, out *mut vfs.Dirent) callconv(.c) c_int { const path = path_ptr[0..path_len] // Lookup prefix = the directory path with a guaranteed trailing // slash. Root "/" already ends in one; everything else gets one // appended so "/bin" matches "/bin/cat" but not "/binutils/x". var prefix_buf [READDIR_PREFIX_MAX]u8 = undefined const prefix []u8 = blk: { if (path.len == 1 && path[0] == '/') { break :blk "/" } if (path.len + 1 > prefix_buf.len) { return -1 } #memcpy(prefix_buf[0..path.len], path) prefix_buf[path.len] = '/' break :blk prefix_buf[0 .. path.len + 1] } var it = initramfs.iterator() var emitted u64 = 0 var last_child []u8 = &.{} var have_last = false while (it.next() catch return -1) |e| { const de = initramfs.directEntry(e.name, prefix) orelse continue if (have_last && bytesEql(de.child, last_child)) { continue } // adjacent dup last_child = de.child have_last = true if (emitted == index) { out.* = .{} const n = #min(de.child.len, out.name.len - 1) #memcpy(out.name[0..n], de.child[0..n]) out.d_type = if (de.is_dir) vfs.DT_DIR else vfs.DT_REG return 0 } emitted += 1 } return -1 } // ---- Host Tests ---- const std = #import("std") const testing = std.testing test "initramfs_backend: open/read/seek" { // Manually build a minimal cpio archive with one file "hello" containing "world" // Header (110 bytes) + name "hello\0" (6 bytes) + pad (0) + data "world" (5 bytes) + pad (3) // OFF_NAMESIZE = 6 + 8 * 11 = 94 // OFF_FILESIZE = 6 + 8 * 6 = 54 // OFF_MODE = 6 + 8 * 1 = 14 var archive [256]u8 = [_]u8{'0'} ** 256 #memcpy(archive[0..6], "070701") #memcpy(archive[14..22], "000081A4") // mode=0o100644 #memcpy(archive[22..30], "000003E8") // uid=1000 #memcpy(archive[30..38], "000003E8") // gid=1000 #memcpy(archive[54..62], "00000005") // filesize=5 #memcpy(archive[94..102], "00000006") // namesize=6 #memcpy(archive[110..116], "hello\x00") #memcpy(archive[116..121], "world") // Add trailer to be a valid archive const trailer_start = 124 // 116 + 5 (data) + 3 (pad) = 124 #memcpy(archive[trailer_start..][0..6], "070701") #memcpy(archive[trailer_start + 94 ..][0..8], "0000000B") // namesize=11 #memcpy(archive[trailer_start + 110 ..][0..11], "TRAILER!!!\x00") initramfs.host_fixture_base = &archive initramfs.host_fixture_size = archive.len var out vfs.OpenResult = .{} const res = open(&sb, "hello", 5, &out) try testing.expectEqual(#as(c_int, 0), res) try testing.expectEqual(#as(u64, 5), out.size) try testing.expect(out.private != 0) // The newc header's mode/uid/gid thread through into OpenResult. try testing.expectEqual(#as(u32, 0o100644), out.mode) try testing.expectEqual(#as(u32, 1000), out.uid) try testing.expectEqual(#as(u32, 1000), out.gid) var f File = .{ .refs = 1, .offset = 0, .size = out.size, .private = out.private, .sb = &sb, } var buf [5]u8 = undefined const n = read(&sb, &f, &buf, 5) try testing.expectEqual(#as(i64, 5), n) try testing.expectEqualStrings("world", &buf) try testing.expectEqual(#as(u64, 5), f.offset) // Seek back to start const s = seek(&sb, &f, 0, 0) try testing.expectEqual(#as(i64, 0), s) try testing.expectEqual(#as(u64, 0), f.offset) // Read again const n2 = read(&sb, &f, &buf, 3) try testing.expectEqual(#as(i64, 3), n2) try testing.expectEqualStrings("wor", buf[0..3]) } test "initramfs_backend: readdir synthesises dirs with adjacent-dup collapse" { // Sorted arcs (as the real cpio is staged): the two /bin/* entries // are adjacent, so their synthetic "bin" subdir must collapse to one. comptime const fixture = initramfs.buildFixture(&.{ .{ .name = "/bin/cat", .data = "C", .mode = 0o100755 }, .{ .name = "/bin/echo", .data = "E", .mode = 0o100755 }, .{ .name = "/etc/fshrc", .data = "F", .mode = 0o100644 }, .{ .name = "/sbin/init", .data = "I", .mode = 0o100755 }, }) initramfs.host_fixture_base = fixture.ptr initramfs.host_fixture_size = fixture.len var d vfs.Dirent = .{} // Root: three top-level dirs, "bin" synthesised once despite two // /bin/* files. try testing.expectEqual(#as(c_int, 0), readdir(&sb, "/".ptr, 1, 0, &d)) try testing.expectEqualStrings("bin", std.mem.sliceTo(&d.name, 0)) try testing.expectEqual(vfs.DT_DIR, d.d_type) try testing.expectEqual(#as(c_int, 0), readdir(&sb, "/".ptr, 1, 1, &d)) try testing.expectEqualStrings("etc", std.mem.sliceTo(&d.name, 0)) try testing.expectEqual(#as(c_int, 0), readdir(&sb, "/".ptr, 1, 2, &d)) try testing.expectEqualStrings("sbin", std.mem.sliceTo(&d.name, 0)) // Past the last distinct child: end sentinel. try testing.expectEqual(#as(c_int, -1), readdir(&sb, "/".ptr, 1, 3, &d)) // /bin lists its two files as DT_REG, then ends. try testing.expectEqual(#as(c_int, 0), readdir(&sb, "/bin".ptr, 4, 0, &d)) try testing.expectEqualStrings("cat", std.mem.sliceTo(&d.name, 0)) try testing.expectEqual(vfs.DT_REG, d.d_type) try testing.expectEqual(#as(c_int, 0), readdir(&sb, "/bin".ptr, 4, 1, &d)) try testing.expectEqualStrings("echo", std.mem.sliceTo(&d.name, 0)) try testing.expectEqual(#as(c_int, -1), readdir(&sb, "/bin".ptr, 4, 2, &d)) }