From 9dff14d50919e0f5961ab72d02376c0fe09f618f Mon Sep 17 00:00:00 2001 From: Jared Baur Date: Tue, 2 Jul 2024 21:10:56 -0700 Subject: [PATCH] Big refactor of entire PID1 --- build.zig | 11 - flake.lock | 6 +- kernel-configs/generic.nix | 1 - options.nix | 5 +- pkgs/tinyboot/default.nix | 2 - src/autoboot.zig | 74 +++ src/boot.zig | 311 ------------ src/boot/bootloader.zig | 219 +++++++++ src/boot/{bls.zig => disk.zig} | 868 ++++++++++++++------------------- src/boot/xmodem.zig | 215 ++++---- src/client.zig | 671 ------------------------- src/console.zig | 664 +++++++++++++++++++++++++ src/device.zig | 690 +++----------------------- src/kobject.zig | 188 +++++++ src/log.zig | 86 +++- src/message.zig | 61 --- src/runner.zig | 1 - src/security.zig | 130 +++-- src/server.zig | 95 ---- src/system.zig | 55 +-- src/tboot-bless-boot.zig | 12 +- src/tboot-loader.zig | 398 +++++++++------ src/tboot-nixos-install.zig | 8 +- src/tboot-sign.zig | 2 +- src/test.zig | 42 +- src/tmp.zig | 34 -- src/tmpdir.zig | 34 ++ src/utils.zig | 12 + src/watch.zig | 303 ++++++++++++ src/xmodem.zig | 2 +- 30 files changed, 2479 insertions(+), 2721 deletions(-) create mode 100644 src/autoboot.zig delete mode 100644 src/boot.zig create mode 100644 src/boot/bootloader.zig rename src/boot/{bls.zig => disk.zig} (58%) delete mode 100644 src/client.zig create mode 100644 src/console.zig create mode 100644 src/kobject.zig delete mode 100644 src/message.zig delete mode 100644 src/server.zig delete mode 100644 src/tmp.zig create mode 100644 src/tmpdir.zig create mode 100644 src/utils.zig create mode 100644 src/watch.zig diff --git a/build.zig b/build.zig index 8bbcf67..d710caf 100644 --- a/build.zig +++ b/build.zig @@ -14,12 +14,6 @@ pub fn build(b: *std.Build) !void { const with_loader = b.option(bool, "loader", "With boot loader") orelse true; const with_tools = b.option(bool, "tools", "With tools") orelse true; - const loglevel = b.option( - u8, - "loglevel", - "Log level", - ) orelse @intFromEnum(std.log.Level.debug); - const clap = b.dependency("clap", .{}); const linux_headers_translated = b.addTranslateC(.{ @@ -30,9 +24,6 @@ pub fn build(b: *std.Build) !void { }); const linux_headers_module = linux_headers_translated.addModule("linux_headers"); - const tboot_loader_options = b.addOptions(); - tboot_loader_options.addOption(u8, "loglevel", loglevel); - if (with_loader) { const tboot_loader = b.addExecutable(.{ .name = "tboot-loader", @@ -41,7 +32,6 @@ pub fn build(b: *std.Build) !void { .optimize = tboot_loader_optimize, .strip = optimize != std.builtin.OptimizeMode.Debug, }); - tboot_loader.root_module.addOptions("build_options", tboot_loader_options); tboot_loader.root_module.addAnonymousImport("test_key", .{ .root_source_file = b.path("test/keys/tboot/key.der"), }); @@ -148,7 +138,6 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); - unit_tests.root_module.addOptions("build_options", tboot_loader_options); unit_tests.root_module.addImport("linux_headers", linux_headers_module); const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&b.addRunArtifact(unit_tests).step); diff --git a/flake.lock b/flake.lock index 71977c7..495b040 100644 --- a/flake.lock +++ b/flake.lock @@ -21,11 +21,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1718895438, - "narHash": "sha256-k3JqJrkdoYwE3fHE6xGDY676AYmyh4U2Zw+0Bwe5DLU=", + "lastModified": 1719848872, + "narHash": "sha256-H3+EC5cYuq+gQW8y0lSrrDZfH71LB4DAf+TDFyvwCNA=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d603719ec6e294f034936c0d0dc06f689d91b6c3", + "rev": "00d80d13810dbfea8ab4ed1009b09100cca86ba8", "type": "github" }, "original": { diff --git a/kernel-configs/generic.nix b/kernel-configs/generic.nix index 90baab2..defcebd 100644 --- a/kernel-configs/generic.nix +++ b/kernel-configs/generic.nix @@ -51,7 +51,6 @@ in IMA_DEFAULT_HASH_SHA256 = yes; IMA_KEXEC = yes; IMA_MEASURE_ASYMMETRIC_KEYS = yes; - INOTIFY_USER = yes; INPUT = yes; INPUT_KEYBOARD = yes; INPUT_MOUSE = unset; diff --git a/options.nix b/options.nix index e9b8daf..b7d6621 100644 --- a/options.nix +++ b/options.nix @@ -217,7 +217,10 @@ in linux.kconfig.CMDLINE = lib.kernel.freeform ( toString ( lib.optionals config.video [ "fbcon=logo-count:1" ] - ++ [ (if config.debug then "debug" else "quiet") ] + ++ [ + "printk.devkmsg=on" + "loglevel=${if config.debug then "8" else "5"}" + ] ++ map (c: "console=${c}") config.linux.consoles ) ); diff --git a/pkgs/tinyboot/default.nix b/pkgs/tinyboot/default.nix index 79a0eeb..deb6e8a 100644 --- a/pkgs/tinyboot/default.nix +++ b/pkgs/tinyboot/default.nix @@ -1,5 +1,4 @@ { - debug ? false, withLoader ? true, withTools ? true, @@ -51,7 +50,6 @@ stdenv.mkDerivation ( zigBuildFlags = [ "-Dtarget=${stdenv.hostPlatform.qemuArch}-${stdenv.hostPlatform.parsed.kernel.name}-${zigLibc}" "-Ddynamic-linker=${stdenv.cc.bintools.dynamicLinker}" - "-Dloglevel=${toString (if debug then 3 else 2)}" # https://github.com/ziglang/zig/blob/084c2cd90f79d5e7edf76b7ddd390adb95a27f0c/lib/std/log.zig#L78 "-Dloader=${lib.boolToString withLoader}" "-Dtools=${lib.boolToString withTools}" "--system" diff --git a/src/autoboot.zig b/src/autoboot.zig new file mode 100644 index 0000000..b4ba4c8 --- /dev/null +++ b/src/autoboot.zig @@ -0,0 +1,74 @@ +const std = @import("std"); +const posix = std.posix; + +const BootLoader = @import("./boot/bootloader.zig"); +const Console = @import("./console.zig"); + +const Autoboot = @This(); + +boot_loader: ?*BootLoader = null, + +pub fn init() Autoboot { + return .{}; +} + +pub fn run( + self: *Autoboot, + boot_loaders: *std.ArrayList(*BootLoader), + timerfd: posix.fd_t, +) !?Console.Event { + if (self.boot_loader) |boot_loader| { + defer { + self.boot_loader = null; + } + + std.log.info("autobooting {s}", .{boot_loader.device}); + + const entries = try boot_loader.probe(); + + for (entries) |entry| { + boot_loader.load(entry) catch |err| { + std.log.err( + "failed to load entry {s}: {}", + .{ entry.linux, err }, + ); + continue; + }; + return .kexec; + } + } else { + if (boot_loaders.items.len == 0) { + return error.NoBootloaders; + } + + const head = boot_loaders.orderedRemove(0); + try boot_loaders.append(head); + + // If we've already tried this boot loader, this means we've gone full + // circle back to the first bootloader, so we are done. + if (head.boot_attempted) { + return error.NoBootloaders; + } + + self.boot_loader = head; + + const timeout = try self.boot_loader.?.timeout(); + if (timeout == 0) { + return self.run(boot_loaders, timerfd); + } else { + try posix.timerfd_settime(timerfd, .{}, &.{ + // oneshot + .it_interval = .{ .tv_sec = 0, .tv_nsec = 0 }, + // consider settled after N seconds without any new events + .it_value = .{ .tv_sec = timeout, .tv_nsec = 0 }, + }, null); + + std.log.info( + "will boot in {} seconds without any user input", + .{timeout}, + ); + } + } + + return null; +} diff --git a/src/boot.zig b/src/boot.zig deleted file mode 100644 index 6b04b50..0000000 --- a/src/boot.zig +++ /dev/null @@ -1,311 +0,0 @@ -const std = @import("std"); -const posix = std.posix; -const system = std.posix.system; - -const linux_headers = @import("linux_headers"); - -const BootLoaderSpec = @import("./boot/bls.zig").BootLoaderSpec; -const Xmodem = @import("./boot/xmodem.zig").Xmodem; - -const KEXEC_LOADED = "/sys/kernel/kexec_loaded"; - -// In eventfd, zero has special meaning (notably it will block reads), so we -// ensure our enum values aren't zero. -fn enumToEventfd(enum_variant: anytype) u64 { - return @as(u64, @intFromEnum(enum_variant)) + 1; -} - -fn eventfdRead(fd: posix.fd_t) !u64 { - var ev: u64 = 0; - _ = try posix.read(fd, std.mem.asBytes(&ev)); - return ev; -} - -fn eventfdWrite(fd: posix.fd_t, val: u64) !void { - _ = try posix.write(fd, std.mem.asBytes(&val)); -} - -fn kexecIsLoaded(f: std.fs.File) bool { - f.seekTo(0) catch return false; - - var is_loaded: u8 = 0; - const bytes_read = f.read(std.mem.asBytes(&is_loaded)) catch return false; - - if (bytes_read != 1) { - return false; - } - - return is_loaded == '1'; -} - -pub const BootEntry = struct { - /// Will be passed to entryLoaded() after a successful kexec load. - context: *anyopaque, - /// Path to the linux kernel image. - linux: []const u8, - /// Optional path to the initrd. - initrd: ?[]const u8 = null, - /// Optional kernel parameters. - cmdline: ?[]const u8 = null, -}; - -pub fn kexecLoad( - allocator: std.mem.Allocator, - linux: []const u8, - initrd: ?[]const u8, - params: ?[]const u8, -) !void { - std.log.info("preparing kexec", .{}); - std.log.info("loading linux {s}", .{linux}); - std.log.info("loading initrd {s}", .{initrd orelse ""}); - std.log.info("loading params {s}", .{params orelse ""}); - - const _linux = try std.fs.cwd().openFile(linux, .{}); - defer _linux.close(); - - const linux_fd = @as(usize, @bitCast(@as(isize, _linux.handle))); - - const initrd_fd = b: { - if (initrd) |_initrd| { - const file = try std.fs.cwd().openFile(_initrd, .{}); - break :b file.handle; - } else { - break :b 0; - } - }; - - defer { - if (initrd_fd != 0) { - posix.close(initrd_fd); - } - } - - var flags: usize = 0; - if (initrd == null) { - flags |= linux_headers.KEXEC_FILE_NO_INITRAMFS; - } - - // dupeZ() returns a null-terminated slice, however the null-terminator - // is not included in the length of the slice, so we must add 1. - const cmdline = try allocator.dupeZ(u8, params orelse ""); - defer allocator.free(cmdline); - const cmdline_len = cmdline.len + 1; - - const rc = system.syscall5( - .kexec_file_load, - linux_fd, - @as(usize, @bitCast(@as(isize, initrd_fd))), - cmdline_len, - @intFromPtr(cmdline.ptr), - flags, - ); - - switch (posix.errno(rc)) { - .SUCCESS => {}, - // IMA appraisal failed - .ACCES => return error.PermissionDenied, - // Invalid kernel image (CONFIG_RELOCATABLE not enabled?) - .NOEXEC => return error.InvalidExe, - // Another image is already loaded - .BUSY => return error.FilesAlreadyRegistered, - .NOMEM => return error.SystemResources, - .BADF => return error.InvalidFileDescriptor, - else => |err| { - std.log.err("kexec load failed for unknown reason: {}", .{err}); - return posix.unexpectedErrno(err); - }, - } - - // Wait for up to a second for kernel to report for kexec to be loaded. - var i: u8 = 10; - var f = try std.fs.cwd().openFile(KEXEC_LOADED, .{}); - defer f.close(); - while (!kexecIsLoaded(f) and i > 0) : (i -= 1) { - std.time.sleep(100 * std.time.ns_per_ms); - } - - std.log.info("kexec loaded", .{}); -} - -pub const BootDevice = struct { - name: []const u8, - /// Timeout in seconds which will be used to determine when a boot entry - /// should be selected automatically by the bootloader. - timeout: u8, - /// Boot entries found on this device. The entry at the first index will - /// serve as the default entry. - entries: []const BootEntry, -}; - -pub const BootLoader = union(enum) { - bls: *BootLoaderSpec, - xmodem: *Xmodem, - - pub fn setup(self: @This()) !void { - switch (self) { - inline else => |boot_loader| try boot_loader.setup(), - } - } - - /// Caller is responsible for all memory corresponding to return value. - pub fn probe(self: @This()) ![]const BootDevice { - return switch (self) { - inline else => |boot_loader| try boot_loader.probe(), - }; - } - - /// An infallible function that provides a way to hook into the stage of - /// the boot process after a successful kexec load has been performed - /// and before the reboot occurs. - pub fn entryLoaded(self: @This(), ctx: *anyopaque) void { - switch (self) { - inline else => |boot_loader| boot_loader.entryLoaded(ctx), - } - } - - pub fn teardown(self: @This()) !void { - switch (self) { - inline else => |boot_loader| try boot_loader.teardown(), - } - } -}; - -fn autobootWrapper(self: *Autoboot) void { - const success = autoboot(self) catch |err| b: { - std.log.err("autoboot failed: {}", .{err}); - break :b false; - }; - - if (success) { - eventfdWrite(self.ready_fd, enumToEventfd(Autoboot.ReadyStatus.ready)) catch {}; - } else { - // We must write something to ready_fd to ensure reads don't block. - eventfdWrite(self.ready_fd, enumToEventfd(Autoboot.ReadyStatus.not_ready)) catch {}; - } -} - -// TODO(jared): more fine-grained return values, we can fail to kexec-load for -// many reasons. -/// Returns true if kexec has been successfully loaded. -fn autoboot(self: *Autoboot) !bool { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - - std.log.info("autoboot started", .{}); - - var bls = BootLoaderSpec.init(); - var boot_loader: BootLoader = .{ .bls = &bls }; - defer { - boot_loader.teardown() catch |err| { - std.log.err("failed to teardown bootloader: {}", .{err}); - }; - std.log.debug("autoboot stopped", .{}); - } - - try boot_loader.setup(); - if (self.needToStop()) { - return false; - } - - const boot_devices = try boot_loader.probe(); - if (self.needToStop()) { - return false; - } - - for (boot_devices) |dev| { - std.log.info("using device \"{s}\"", .{dev.name}); - var countdown = dev.timeout; - while (countdown > 0) : (countdown -= 1) { - std.log.info("booting in {} second(s)", .{countdown}); - std.time.sleep(std.time.ns_per_s); - if (self.needToStop()) { - return false; - } - } - - for (dev.entries) |entry| { - if (kexecLoad( - arena.allocator(), - entry.linux, - entry.initrd, - entry.cmdline, - )) { - boot_loader.entryLoaded(entry.context); - return true; - } else |err| { - std.log.err("failed to load boot entry: {}", .{err}); - } - } - } - - return false; -} - -pub const Autoboot = struct { - ready_fd: posix.fd_t, - thread: ?std.Thread, - mutex: std.Thread.Mutex, - need_to_stop: bool, - - pub const ReadyStatus = enum { - ready, - not_ready, - }; - - pub fn init() !@This() { - return .{ - .ready_fd = try posix.eventfd(0, 0), - .thread = null, - .mutex = std.Thread.Mutex{}, - .need_to_stop = false, - }; - } - - pub fn register(self: *@This(), epoll_fd: posix.fd_t) !void { - var ready_event = system.epoll_event{ - .data = .{ .fd = self.ready_fd }, - // we will only be ready to boot once - .events = system.EPOLL.IN | system.EPOLL.ONESHOT, - }; - try posix.epoll_ctl(epoll_fd, system.EPOLL.CTL_ADD, self.ready_fd, &ready_event); - } - - pub fn deregister(self: *@This(), epoll_fd: posix.fd_t) !void { - try posix.epoll_ctl(epoll_fd, system.EPOLL.CTL_DEL, self.ready_fd, null); - } - - pub fn start(self: *@This()) !void { - self.thread = try std.Thread.spawn(.{}, autobootWrapper, .{self}); - } - - pub fn needToStop(self: *@This()) bool { - self.mutex.lock(); - defer self.mutex.unlock(); - return self.need_to_stop; - } - - pub fn stop(self: *@This()) void { - if (self.thread) |thread| { - self.mutex.lock(); - self.need_to_stop = true; - self.mutex.unlock(); - thread.join(); - } - - self.thread = null; - } - - pub fn finish(self: *@This()) !?posix.RebootCommand { - const rc = try eventfdRead(self.ready_fd); - if (rc == enumToEventfd(ReadyStatus.ready)) { - return posix.RebootCommand.KEXEC; - } else { - return null; - } - } - - pub fn deinit(self: *@This()) void { - posix.close(self.ready_fd); - self.stop(); - } -}; diff --git a/src/boot/bootloader.zig b/src/boot/bootloader.zig new file mode 100644 index 0000000..987a427 --- /dev/null +++ b/src/boot/bootloader.zig @@ -0,0 +1,219 @@ +const std = @import("std"); +const posix = std.posix; + +const linux_headers = @import("linux_headers"); + +const Device = @import("../device.zig"); + +const BootLoader = @This(); + +pub const Entry = struct { + /// Will be passed to underlying boot loader after a successful kexec load. + context: *anyopaque, + /// Path to the linux kernel image. + linux: []const u8, + /// Optional path to the initrd. + initrd: ?[]const u8 = null, + /// Optional kernel parameters. + cmdline: ?[]const u8 = null, +}; + +probed: bool = false, +boot_attempted: bool = false, +priority: u8, +device: Device, +allocator: std.mem.Allocator, +entries: std.ArrayList(Entry), +inner: *anyopaque, +vtable: *const struct { + name: *const fn () []const u8, + probe: *const fn (*anyopaque, *std.ArrayList(Entry), Device) anyerror!void, + timeout: *const fn (*anyopaque) u8, + entryLoaded: *const fn (*anyopaque, Entry) void, + deinit: *const fn (*anyopaque, std.mem.Allocator) void, +}, + +pub fn init( + comptime T: type, + device: Device, + priority: u8, + allocator: std.mem.Allocator, +) !BootLoader { + const inner = try allocator.create(T); + + inner.* = T.init(); + + const wrapper = struct { + pub fn deinit(ctx: *anyopaque, a: std.mem.Allocator) void { + const self: *T = @ptrCast(@alignCast(ctx)); + defer a.destroy(self); + + self.deinit(); + } + + pub fn probe( + ctx: *anyopaque, + entries: *std.ArrayList(Entry), + d: Device, + ) !void { + const self: *T = @ptrCast(@alignCast(ctx)); + + try self.probe(entries, d); + } + + pub fn entryLoaded(ctx: *anyopaque, entry: Entry) void { + const self: *T = @ptrCast(@alignCast(ctx)); + + self.entryLoaded(entry.context); + } + + pub fn timeout(ctx: *anyopaque) u8 { + const self: *T = @ptrCast(@alignCast(ctx)); + + return self.timeout(); + } + }; + + return .{ + .priority = priority, + .device = device, + .allocator = allocator, + .entries = std.ArrayList(Entry).init(allocator), + .inner = inner, + .vtable = &.{ + .name = T.name, + .probe = wrapper.probe, + .timeout = wrapper.timeout, + .entryLoaded = wrapper.entryLoaded, + .deinit = wrapper.deinit, + }, + }; +} + +pub fn deinit(self: *BootLoader) void { + defer self.entries.deinit(); + + self.vtable.deinit(self.inner, self.allocator); +} + +pub fn name(self: *BootLoader) []const u8 { + return self.vtable.name(); +} + +pub fn timeout(self: *BootLoader) !u8 { + _ = try self.probe(); + + return self.vtable.timeout(self.inner); +} + +pub fn probe(self: *BootLoader) ![]const Entry { + if (!self.probed) { + std.log.debug("bootloader not yet probed", .{}); + try self.vtable.probe(self.inner, &self.entries, self.device); + self.probed = true; + std.log.debug("bootloader probed", .{}); + } + + return self.entries.items; +} + +pub fn load(self: *BootLoader, entry: Entry) !void { + self.boot_attempted = true; + + try kexecLoad(self.allocator, entry.linux, entry.initrd, entry.cmdline); + + self.vtable.entryLoaded(self.inner, entry); +} + +const KEXEC_LOADED = "/sys/kernel/kexec_loaded"; + +fn kexecIsLoaded(f: std.fs.File) bool { + f.seekTo(0) catch return false; + + var is_loaded: u8 = 0; + const bytes_read = f.read(std.mem.asBytes(&is_loaded)) catch return false; + + if (bytes_read != 1) { + return false; + } + + return is_loaded == '1'; +} + +fn kexecLoad( + allocator: std.mem.Allocator, + linux: []const u8, + initrd: ?[]const u8, + cmdline: ?[]const u8, +) !void { + std.log.info("preparing kexec", .{}); + std.log.info("loading linux {s}", .{linux}); + std.log.info("loading initrd {s}", .{initrd orelse ""}); + std.log.info("loading params {s}", .{cmdline orelse ""}); + + const _linux = try std.fs.cwd().openFile(linux, .{}); + defer _linux.close(); + + const linux_fd = @as(usize, @bitCast(@as(isize, _linux.handle))); + + const initrd_fd = b: { + if (initrd) |_initrd| { + const file = try std.fs.cwd().openFile(_initrd, .{}); + break :b file.handle; + } else { + break :b 0; + } + }; + + defer { + if (initrd_fd != 0) { + posix.close(initrd_fd); + } + } + + var flags: usize = 0; + if (initrd == null) { + flags |= linux_headers.KEXEC_FILE_NO_INITRAMFS; + } + + // dupeZ() returns a null-terminated slice, however the null-terminator + // is not included in the length of the slice, so we must add 1. + const cmdline_z = try allocator.dupeZ(u8, cmdline orelse ""); + defer allocator.free(cmdline_z); + const cmdline_len = cmdline_z.len + 1; + + const rc = posix.system.syscall5( + .kexec_file_load, + linux_fd, + @as(usize, @bitCast(@as(isize, initrd_fd))), + cmdline_len, + @intFromPtr(cmdline_z.ptr), + flags, + ); + + switch (posix.errno(rc)) { + .SUCCESS => {}, + // IMA appraisal failed + .ACCES => return error.PermissionDenied, + // Invalid kernel image (CONFIG_RELOCATABLE not enabled?) + .NOEXEC => return error.InvalidExe, + // Another image is already loaded + .BUSY => return error.FilesAlreadyRegistered, + .NOMEM => return error.SystemResources, + .BADF => return error.InvalidFileDescriptor, + else => |err| { + std.log.err("kexec load failed for unknown reason: {}", .{err}); + return posix.unexpectedErrno(err); + }, + } + + // Wait for up to a second for kernel to report for kexec to be loaded. + var i: u8 = 10; + var f = try std.fs.cwd().openFile(KEXEC_LOADED, .{}); + defer f.close(); + while (!kexecIsLoaded(f) and i > 0) : (i -= 1) { + std.time.sleep(100 * std.time.ns_per_ms); + } + + std.log.info("kexec loaded", .{}); +} diff --git a/src/boot/bls.zig b/src/boot/disk.zig similarity index 58% rename from src/boot/bls.zig rename to src/boot/disk.zig index c279fea..2133618 100644 --- a/src/boot/bls.zig +++ b/src/boot/disk.zig @@ -4,98 +4,277 @@ const system = std.posix.system; const linux_headers = @import("linux_headers"); -const BootDevice = @import("../boot.zig").BootDevice; -const BootEntry = @import("../boot.zig").BootEntry; const FsType = @import("../disk/filesystem.zig").FsType; const Gpt = @import("../disk/partition_table.zig").Gpt; const GptPartitionType = @import("../disk/partition_table.zig").GptPartitionType; const Mbr = @import("../disk/partition_table.zig").Mbr; const MbrPartitionType = @import("../disk/partition_table.zig").MbrPartitionType; -const device = @import("../device.zig"); +const TmpDir = @import("../tmpdir.zig"); -const Mount = struct { - dir: *std.fs.Dir, - disk_name: []const u8, -}; +const BootLoader = @import("./bootloader.zig"); +const Device = @import("../device.zig"); + +const DiskBootLoader = @This(); + +arena: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator), +tmpdir: ?TmpDir = null, +loader_timeout: u8 = 0, + +pub fn match(device: *const Device) ?u8 { + if (device.subsystem != .block) { + return null; + } + + var sysfs_disk_path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const sysfs_disk_path = device.nodeSysfsPath(&sysfs_disk_path_buf) catch return null; -fn diskIsRemovable(allocator: std.mem.Allocator, devname: []const u8) bool { - const removable_path = std.fs.path.join(allocator, &.{ - std.fs.path.sep_str, - "sys", - "class", - "block", - devname, - "removable", - }) catch return false; - defer allocator.free(removable_path); - - const removable_file = std.fs.openFileAbsolute(removable_path, .{}) catch return false; + var sysfs_dir = std.fs.cwd().openDir(sysfs_disk_path, .{}) catch return null; + defer sysfs_dir.close(); + + // Disk block devices have a "removable" file in their sysfs directory, + // partitions do not. + var removable_file = sysfs_dir.openFile("removable", .{}) catch return null; defer removable_file.close(); var buf: [1]u8 = undefined; - if ((removable_file.read(&buf) catch return false) != 1) { - return false; + if ((removable_file.read(&buf) catch return null) != 1) { + return null; } - return std.mem.eql(u8, &buf, "1"); + // Prioritize removable devices over non-removable. This allows for + // plugging in a USB-stick and having it "just work". + if (std.mem.eql(u8, &buf, "1")) { + return 50; + } else { + return 55; + } } -/// Caller is responsible for the returned value. -fn diskName(allocator: std.mem.Allocator, devname: []const u8) ![]const u8 { - var name = std.ArrayList(u8).init(allocator); - - const vendor = b: { - const path = std.fs.path.join(allocator, &.{ - std.fs.path.sep_str, - "sys", - "class", - "block", - devname, - "device", - "vendor", - }) catch break :b null; - defer allocator.free(path); - - const file = std.fs.openFileAbsolute(path, .{}) catch break :b null; - defer file.close(); +pub fn init() DiskBootLoader { + return .{}; +} + +pub fn name() []const u8 { + return "disk"; +} + +pub fn timeout(self: *DiskBootLoader) u8 { + return self.loader_timeout; +} + +pub fn deinit(self: *DiskBootLoader) void { + defer self.arena.deinit(); - var buf: [1024]u8 = undefined; - const bytes_read = file.readAll(&buf) catch break :b null; - break :b std.mem.trim(u8, buf[0..bytes_read], "\n "); + self.unmount() catch |err| { + std.log.err("failed to unmount: {}", .{err}); }; +} - const model = b: { - const path = std.fs.path.join(allocator, &.{ - std.fs.path.sep_str, - "sys", - "class", - "block", - devname, - "device", - "model", - }) catch break :b null; - defer allocator.free(path); - - const file = std.fs.openFileAbsolute(path, .{}) catch break :b null; - defer file.close(); +pub fn probe( + self: *DiskBootLoader, + entries: *std.ArrayList(BootLoader.Entry), + disk_device: Device, +) !void { + var disk_path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const disk_path = try disk_device.nodePath(&disk_path_buf); + + var sysfs_disk_path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const sysfs_disk_path = try disk_device.nodeSysfsPath(&sysfs_disk_path_buf); + + var sysfs_disk_dir = try std.fs.cwd().openDir(sysfs_disk_path, .{}); + defer sysfs_disk_dir.close(); + + var disk = try std.fs.cwd().openFile(disk_path, .{}); + defer disk.close(); + + var disk_source = std.io.StreamSource{ .file = disk }; + // All GPTs also have an MBR, so we can invalidate the disk + // entirely if it does not have an MBR. + var mbr = Mbr.init(&disk_source) catch |err| { + std.log.err("no MBR found on disk {s}: {}", .{ disk_device, err }); + return; + }; - var buf: [1024]u8 = undefined; - const bytes_read = file.readAll(&buf) catch break :b null; - break :b std.mem.trim(u8, buf[0..bytes_read], "\n "); + const boot_partn = b: { + for (mbr.partitions(), 1..) |part, mbr_partn| { + const part_type = MbrPartitionType.fromValue(part.partType()) orelse continue; + + if (part.isBootable() and + // BootLoaderSpec uses this partition type for MBR, see + // https://uapi-group.org/specifications/specs/boot_loader_specification/#the-partitionsl. + (part_type == .LinuxExtendedBoot or + // QEMU uses this partition type when using a FAT + // emulated drive with `-drive file=fat:rw:some/directory`. + part_type == .Fat16)) + { + break :b mbr_partn; + } + + // disk has a GPT + if (!part.isBootable() and part_type == .ProtectedMbr) { + var gpt = Gpt.init(&disk_source) catch |err| switch (err) { + Gpt.Error.MissingMagicNumber => { + std.log.debug("disk {s} does not contain a GUID partition table", .{disk_device}); + continue; + }, + Gpt.Error.HeaderCrcFail => { + std.log.err("disk {s} CRC integrity check failed", .{disk_device}); + continue; + }, + else => { + std.log.err("failed to read disk {s}: {}", .{ disk_device, err }); + continue; + }, + }; + + const partitions = try gpt.partitions(self.arena.allocator()); + for (partitions, 1..) |partition, gpt_partn| { + if (partition.partType() orelse continue == .EfiSystem) { + break :b gpt_partn; + } + } + } + } + + return; + }; + + const disk_major, const disk_minor = disk_device.type.node; + + // Construct a new Device for the partition, which will have the same major + // number and a minor number equalling the minor number of the disk plus + // the number of the partition in the disk. + const boot_partition = Device{ + .subsystem = disk_device.subsystem, + .type = .{ + .node = .{ + disk_major, disk_minor + @as(u32, @intCast(boot_partn)), + }, + }, + }; + + std.log.info("found boot partition on disk {s} partition {d}", .{ disk_device, boot_partition }); + + var partition_path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const partition_path = try boot_partition.nodePath(&partition_path_buf); + + var partition = try std.fs.cwd().openFile(partition_path, .{}); + defer partition.close(); + + var esp_file_source = std.io.StreamSource{ .file = partition }; + const fstype = try FsType.detect(&esp_file_source) orelse { + std.log.err("could not detect filesystem on boot partition {}", .{boot_partition}); + return; }; - if (vendor) |_vendor| { - try name.appendSlice(_vendor); + try self.mount(fstype, partition_path); + + try self.searchForEntries(disk_device, entries); +} + +pub fn entryLoaded(self: *DiskBootLoader, ctx: *anyopaque) void { + self._entryLoaded(ctx) catch |err| { + std.log.err( + "failed to finalize BLS boot counter for chosen entry: {}", + .{err}, + ); + }; +} + +fn _entryLoaded(self: *@This(), ctx: *anyopaque) !void { + var bls_entry_file: *BlsEntryFile = @ptrCast(@alignCast(ctx)); + + var tmpdir = self.tmpdir orelse return; + + const allocator = self.arena.allocator(); + + const original_name = try bls_entry_file.toFilename(allocator); + defer allocator.free(original_name); + + if (bls_entry_file.tries_done) |*done| done.* +|= 1; + if (bls_entry_file.tries_left) |*left| left.* -|= 1; + + if (bls_entry_file.tries_left) |tries_left| { + std.log.info( + "{} {s} remaining for entry \"{s}\"", + .{ + tries_left, + if (tries_left == 1) "try" else "tries", + bls_entry_file.name, + }, + ); } - if (model) |_model| { - if (vendor != null) { - try name.append(' '); - } - try name.appendSlice(_model); + const new_name = try bls_entry_file.toFilename(allocator); + defer allocator.free(new_name); + + if (!std.mem.eql(u8, original_name, new_name)) { + var mount_dir = try tmpdir.dir.openDir(mountpath, .{}); + defer mount_dir.close(); + + var entries_dir = try mount_dir.openDir("loader/entries", .{}); + defer entries_dir.close(); + + try entries_dir.rename(original_name, new_name); + posix.sync(); + + std.log.debug("entry renamed to {s}", .{new_name}); + } +} + +const random_bytes_count = 12; +const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count); + +const mountpath = "mount"; + +fn mount(self: *DiskBootLoader, fstype: FsType, path: []const u8) !void { + // make sure there are no current mountpoints + try self.unmount(); + + const tmpdir = try TmpDir.create(.{}); + + try tmpdir.dir.makePath(mountpath); + + var where_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const tmp_where = try tmpdir.dir.realpath(mountpath, &where_buf); + const where = try self.arena.allocator().dupeZ(u8, tmp_where); + defer self.arena.allocator().free(where); + + var what_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const tmp_what = try std.fs.cwd().realpath(path, &what_buf); + const what = try self.arena.allocator().dupeZ(u8, tmp_what); + defer self.arena.allocator().free(what); + + switch (posix.errno(system.mount( + what, + where, + switch (fstype) { + .Vfat => "vfat", + }, + system.MS.NOSUID | system.MS.NODEV | system.MS.NOEXEC, + 0, + ))) { + .SUCCESS => {}, + else => |err| { + return posix.unexpectedErrno(err); + }, } - return name.toOwnedSlice(); + self.tmpdir = tmpdir; +} + +fn unmount(self: *DiskBootLoader) !void { + if (self.tmpdir) |*tmpdir| { + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const mountpoint = try tmpdir.dir.realpathZ(mountpath, &buf); + + _ = system.umount2(@ptrCast(mountpoint.ptr), system.MNT.DETACH); + + std.log.info("unmounted disk from {s}", .{mountpoint}); + + tmpdir.cleanup(); + self.tmpdir = null; + } } fn versionInId(id: []const u8) ?std.SemanticVersion { @@ -319,469 +498,156 @@ test "boot entry sorting" { )); } -pub const BootLoaderSpec = struct { - const EntryContext = struct { - bls_entry_file: *BlsEntryFile, - parent_dir: []const u8, - }; +fn searchForEntries( + self: *DiskBootLoader, + disk_device: Device, + entries: *std.ArrayList(BootLoader.Entry), +) !void { + const allocator = self.arena.allocator(); - arena: std.heap.ArenaAllocator, + var tmpdir = self.tmpdir.?; + var mount_dir = try tmpdir.dir.openDir(mountpath, .{}); + defer mount_dir.close(); - /// Mounts to block devices that are non-removable (i.e. "internal" to the - /// system). - internal_mounts: []Mount, + var entries_dir = try mount_dir.openDir( + "loader/entries", + .{ .iterate = true }, + ); + defer entries_dir.close(); - /// Mounts to block devices that are removable (i.e. "external" to the - /// system). This includes USB mass-storage devices, SD cards, etc. - external_mounts: []Mount, + var bls_entries = std.ArrayList(BlsEntry).init(allocator); + defer bls_entries.deinit(); - pub fn init() @This() { - return .{ - .arena = std.heap.ArenaAllocator.init(std.heap.page_allocator), - .internal_mounts = &.{}, - .external_mounts = &.{}, + const loader_conf: LoaderConf = b: { + var file = mount_dir.openFile("loader/loader.conf", .{}) catch { + std.log.debug("no loader.conf found on {s}, using defaults", .{disk_device}); + break :b .{}; }; - } - - pub fn setup(self: *@This()) !void { - std.log.debug("BLS setup", .{}); - - const allocator = self.arena.allocator(); - - var internal_mounts = std.ArrayList(Mount).init(allocator); - var external_mounts = std.ArrayList(Mount).init(allocator); - - var dev_disk_alias = try std.fs.cwd().openDir( - "/dev/disk", - .{}, - ); - defer dev_disk_alias.close(); - - var mountpoint_dir = try std.fs.cwd().openDir( - "/mnt", - .{}, - ); - defer mountpoint_dir.close(); - - var sysfs_block = try std.fs.cwd().openDir( - "/sys/class/block", - .{ .iterate = true }, - ); - defer sysfs_block.close(); - var it = sysfs_block.iterate(); - - while (try it.next()) |dir_entry| { - if (dir_entry.kind != .sym_link) { - continue; - } - - const uevent_path = try std.fs.path.join(allocator, &.{ dir_entry.name, "uevent" }); - var uevent_file = try sysfs_block.openFile(uevent_path, .{}); - defer uevent_file.close(); + defer file.close(); - const max_bytes = 10 * 1024 * 1024; - const uevent_contents = try uevent_file.readToEndAlloc(allocator, max_bytes); + std.log.debug("found loader.conf on {s}", .{disk_device}); - var uevent = try device.parseUeventFileContents(allocator, uevent_contents); + const contents = try file.readToEndAlloc(allocator, 4096); - const devtype = uevent.get("DEVTYPE") orelse continue; + break :b LoaderConf.parse(contents); + }; - std.log.debug( - "inspecting block device {s} ({s})", - .{ dir_entry.name, devtype }, - ); + self.loader_timeout = loader_conf.timeout; - if (!std.mem.eql(u8, devtype, "disk")) { - continue; - } + var it = entries_dir.iterate(); + while (try it.next()) |dir_entry| { + if (dir_entry.kind != .file) { + continue; + } - const diskseq = uevent.get("DISKSEQ") orelse continue; - const devname = uevent.get("DEVNAME") orelse continue; + const bls_entry_file = BlsEntryFile.parse(dir_entry.name) catch |err| { + std.log.err("invalid entry filename for {s}: {}", .{ dir_entry.name, err }); + continue; + }; - const disk_handle = dev_disk_alias.openFile( - try std.fmt.allocPrint(allocator, "disk{s}", .{diskseq}), - .{}, - ) catch |err| { - std.log.err( - "failed to open disk alias for {s}: {}", - .{ dir_entry.name, err }, + if (bls_entry_file.tries_left) |tries_left| { + if (tries_left == 0) { + std.log.warn( + "skipping entry {s} because all tries have been exhausted", + .{dir_entry.name}, ); continue; - }; - - var disk_source = std.io.StreamSource{ .file = disk_handle }; - - // All GPTs also have an MBR, so we can invalidate the disk - // entirely if it does not have an MBR. - var mbr = Mbr.init(&disk_source) catch |err| { - std.log.err("no MBR found on disk {s}: {}", .{ devname, err }); - continue; - }; - - const boot_partn = b: { - for (mbr.partitions(), 1..) |part, mbr_partn| { - const part_type = MbrPartitionType.fromValue(part.partType()) orelse continue; - - if (part.isBootable() and - // BootLoaderSpec uses this partition type for MBR, see - // https://uapi-group.org/specifications/specs/boot_loader_specification/#the-partitionsl. - (part_type == .LinuxExtendedBoot or - // QEMU uses this partition type when using a FAT - // emulated drive with `-drive file=fat:rw:some/directory`. - part_type == .Fat16)) - { - break :b mbr_partn; - } - - // disk is GPT partitioned - if (!part.isBootable() and part_type == .ProtectedMbr) { - var gpt = Gpt.init(&disk_source) catch |err| switch (err) { - Gpt.Error.MissingMagicNumber => { - std.log.debug("disk {s} does not contain a GUID partition table", .{devname}); - continue; - }, - Gpt.Error.HeaderCrcFail => { - std.log.err("disk {s} CRC integrity check failed", .{devname}); - continue; - }, - else => { - std.log.err("failed to read disk {s}: {}", .{ devname, err }); - continue; - }, - }; - - const partitions = try gpt.partitions(allocator); - for (partitions, 1..) |partition, gpt_partn| { - if (partition.partType() orelse continue == .EfiSystem) { - break :b gpt_partn; - } - } - } - } - - continue; - }; - - const partition_filename = try std.fmt.allocPrint( - allocator, - "disk{s}_part{d}", - .{ diskseq, boot_partn }, - ); - - std.log.info("found boot partition on disk {s} partition {d}", .{ devname, boot_partn }); - - var esp_handle = try dev_disk_alias.openFile(partition_filename, .{}); - defer esp_handle.close(); - - var esp_file_source = std.io.StreamSource{ .file = esp_handle }; - const fstype = try FsType.detect(&esp_file_source) orelse { - std.log.err("could not detect filesystem on EFI system partition", .{}); - continue; - }; - - mountpoint_dir.makePath(partition_filename) catch |err| { - std.log.err("failed to create mountpoint: {}", .{err}); - continue; - }; - - const mountpoint = try mountpoint_dir.realpathAlloc(allocator, partition_filename); - - switch (posix.errno(system.mount( - try allocator.dupeZ(u8, try dev_disk_alias.realpathAlloc(allocator, partition_filename)), - try allocator.dupeZ(u8, mountpoint), - switch (fstype) { - .Vfat => "vfat", - }, - system.MS.NOSUID | system.MS.NODEV | system.MS.NOEXEC, - 0, - ))) { - .SUCCESS => {}, - else => |err| { - std.log.err("failed to mount disk {s} partition {d}: {}", .{ devname, boot_partn, err }); - continue; - }, - } - - std.log.info("mounted disk \"{s}\"", .{devname}); - - const dir = try allocator.create(std.fs.Dir); - dir.* = try mountpoint_dir.openDir(partition_filename, .{}); - - const mount = Mount{ - .disk_name = try diskName(allocator, devname), - .dir = dir, - }; - - if (diskIsRemovable(allocator, devname)) { - try external_mounts.append(mount); - } else { - try internal_mounts.append(mount); } } - self.internal_mounts = try internal_mounts.toOwnedSlice(); - self.external_mounts = try external_mounts.toOwnedSlice(); - } - - fn searchForEntries(self: *@This(), mount: Mount) !BootDevice { - const allocator = self.arena.allocator(); - - var entries = std.ArrayList(BootEntry).init(allocator); - errdefer entries.deinit(); + var entry_file = entries_dir.openFile(dir_entry.name, .{}) catch continue; + defer entry_file.close(); - var bls_entries = std.ArrayList(BlsEntry).init(allocator); + std.log.debug("inspecting BLS entry {s} on {s}", .{ dir_entry.name, disk_device }); - const loader_conf: LoaderConf = b: { - var file = mount.dir.openFile("loader/loader.conf", .{}) catch { - std.log.debug("no loader.conf found on {s}, using defaults", .{mount.disk_name}); - break :b .{}; - }; - defer file.close(); - std.log.debug("found loader.conf on \"{s}\"", .{mount.disk_name}); - const contents = try file.readToEndAlloc(allocator, 4096); - break :b LoaderConf.parse(contents); + // We should definitely not get any boot entry files larger than this. + const entry_contents = try entry_file.readToEndAlloc(allocator, 1 << 16); + var type1_entry = BlsEntry.parse(allocator, bls_entry_file, entry_contents) catch |err| { + std.log.err("failed to parse {s} as BLS type 1 entry: {}", .{ dir_entry.name, err }); + continue; }; + errdefer type1_entry.deinit(); - var entries_dir = try mount.dir.openDir( - "loader/entries", - .{ .iterate = true }, - ); - defer entries_dir.close(); + try bls_entries.append(type1_entry); + } - var it = entries_dir.iterate(); - while (try it.next()) |dir_entry| { - if (dir_entry.kind != .file) { + std.log.debug("sorting BLS entries", .{}); + std.mem.sort(BlsEntry, bls_entries.items, {}, blsEntryLessThan); + + for (bls_entries.items) |entry| { + const linux = mount_dir.realpathAlloc(allocator, entry.linux orelse { + std.log.err("missing linux kernel in entry {s}", .{entry.id}); + continue; + }) catch |err| switch (err) { + error.OutOfMemory => return err, + else => { + std.log.err("linux kernel \"{?s}\" not found on {s}", .{ entry.linux, disk_device }); continue; + }, + }; + errdefer allocator.free(linux); + + // NOTE: Multiple initrds won't work if we have IMA appraisal + // of signed initrds, so we can only load one. + // + // TODO(jared): If IMA appraisal is disabled, we can + // concatenate all the initrds together. + var initrd: ?[]const u8 = null; + if (entry.initrd) |_initrd| { + if (_initrd.len > 0) { + initrd = mount_dir.realpathAlloc(allocator, _initrd[0]) catch |err| switch (err) { + error.OutOfMemory => return err, + else => { + std.log.err("initrd \"{s}\" not found on {s}", .{ _initrd[0], disk_device }); + continue; + }, + }; } - const bls_entry_file = BlsEntryFile.parse(dir_entry.name) catch |err| { - std.log.err("invalid entry filename for {s}: {}", .{ dir_entry.name, err }); - continue; - }; - - if (bls_entry_file.tries_left) |tries_left| { - if (tries_left == 0) { - std.log.warn( - "skipping entry {s} because all tries have been exhausted", - .{dir_entry.name}, - ); - continue; - } + if (_initrd.len > 1) { + std.log.warn("cannot verify more than 1 initrd, using first initrd", .{}); } - - var entry_file = entries_dir.openFile(dir_entry.name, .{}) catch continue; - defer entry_file.close(); - - std.log.debug("inspecting BLS entry {s} on \"{s}\"", .{ dir_entry.name, mount.disk_name }); - - // We should definitely not get any boot entry files larger than this. - const entry_contents = try entry_file.readToEndAlloc(allocator, 1 << 16); - var type1_entry = BlsEntry.parse(allocator, bls_entry_file, entry_contents) catch |err| { - std.log.err("failed to parse {s} as BLS type 1 entry: {}", .{ dir_entry.name, err }); - continue; - }; - errdefer type1_entry.deinit(); - - try bls_entries.append(type1_entry); } - - std.log.debug("sorting BLS entries", .{}); - std.mem.sort(BlsEntry, bls_entries.items, {}, blsEntryLessThan); - - for (bls_entries.items) |entry| { - const linux = mount.dir.realpathAlloc(allocator, entry.linux orelse { - std.log.err("missing linux kernel in entry {s}", .{entry.id}); - continue; - }) catch |err| switch (err) { - error.OutOfMemory => return err, - else => { - std.log.err("linux kernel \"{?s}\" not found on \"{s}\"", .{ entry.linux, mount.disk_name }); - continue; - }, - }; - errdefer allocator.free(linux); - - // NOTE: Multiple initrds won't work if we have IMA appraisal - // of signed initrds, so we can only load one. - // - // TODO(jared): If IMA appraisal is disabled, we can - // concatenate all the initrds together. - var initrd: ?[]const u8 = null; - if (entry.initrd) |_initrd| { - if (_initrd.len > 0) { - initrd = mount.dir.realpathAlloc(allocator, _initrd[0]) catch |err| switch (err) { - error.OutOfMemory => return err, - else => { - std.log.err("initrd \"{s}\" not found on \"{s}\"", .{ _initrd[0], mount.disk_name }); - continue; - }, - }; - } - - if (_initrd.len > 1) { - std.log.warn("cannot verify more than 1 initrd, using first initrd", .{}); - } - } - errdefer { - if (initrd) |_initrd| { - allocator.free(_initrd); - } + errdefer { + if (initrd) |_initrd| { + allocator.free(_initrd); } - - // TODO(jared): Make it nicer to continue appending kernel - // params...perhaps keep the ArrayList so we still get to - // `.append()`. - var options_with_bls_entry: [linux_headers.COMMAND_LINE_SIZE]u8 = undefined; - const options = b: { - if (entry.options) |opts| { - const orig = try std.mem.join(allocator, " ", opts); - break :b try std.fmt.bufPrint(&options_with_bls_entry, "{s} tboot.bls-entry={s}", .{ orig, entry.id }); - } else { - break :b try std.fmt.bufPrint(&options_with_bls_entry, "tboot.bls-entry={s}", .{entry.id}); - } - }; - - const final_options = try allocator.dupe(u8, options); - errdefer allocator.free(options); - - const context = try allocator.create(EntryContext); - errdefer allocator.destroy(context); - - const bls_entry_file = try allocator.create(BlsEntryFile); - errdefer allocator.destroy(bls_entry_file); - - bls_entry_file.* = BlsEntryFile.init(entry.id, .{ - .tries_left = entry.tries_left, - .tries_done = entry.tries_done, - }); - - context.* = .{ - .parent_dir = try mount.dir.realpathAlloc(allocator, "loader/entries"), - .bls_entry_file = bls_entry_file, - }; - - try entries.append( - .{ - .context = context, - .cmdline = final_options, - .initrd = initrd, - .linux = linux, - }, - ); } - return .{ - .name = try allocator.dupe(u8, mount.disk_name), - .timeout = loader_conf.timeout, - .entries = try entries.toOwnedSlice(), - }; - } - - /// Caller is responsible for the returned slice. - pub fn probe(self: *@This()) ![]const BootDevice { - std.log.debug("BLS probe start", .{}); - var devices = std.ArrayList(BootDevice).init(self.arena.allocator()); - - // Mounts of external devices are ordered before external mounts so - // they are prioritized in the boot process. - std.log.debug("BLS probe found {} external device(s)", .{self.external_mounts.len}); - for (self.external_mounts) |mount| { - try devices.append(self.searchForEntries(mount) catch |err| { - std.log.err( - "failed to search for entries on \"{s}\": {}", - .{ mount.disk_name, err }, - ); - continue; - }); - } - - std.log.debug("BLS probe found {} internal device(s)", .{self.internal_mounts.len}); - for (self.internal_mounts) |mount| { - try devices.append(self.searchForEntries(mount) catch |err| { - std.log.err( - "failed to search for entries on \"{s}\": {}", - .{ mount.disk_name, err }, - ); - continue; - }); - } - - std.log.debug( - "BLS probe found {} device(s) with BLS entries", - .{devices.items.len}, - ); - return try devices.toOwnedSlice(); - } - - pub fn entryLoaded(self: *@This(), ctx: *anyopaque) void { - self._entryLoaded(ctx) catch |err| { - std.log.err( - "failed to finalize BLS boot counter for chosen entry: {}", - .{err}, - ); + // TODO(jared): Make it nicer to continue appending kernel + // params...perhaps keep the ArrayList so we still get to + // `.append()`. + var options_with_bls_entry: [linux_headers.COMMAND_LINE_SIZE]u8 = undefined; + const options = b: { + if (entry.options) |opts| { + const orig = try std.mem.join(allocator, " ", opts); + break :b try std.fmt.bufPrint(&options_with_bls_entry, "{s} tboot.bls-entry={s}", .{ orig, entry.id }); + } else { + break :b try std.fmt.bufPrint(&options_with_bls_entry, "tboot.bls-entry={s}", .{entry.id}); + } }; - } - - fn _entryLoaded(self: *@This(), ctx: *anyopaque) !void { - const context: *EntryContext = @ptrCast(@alignCast(ctx)); - - const original_name = try context.bls_entry_file.toFilename(self.arena.allocator()); - - var bls_entry_file = context.bls_entry_file; - - if (bls_entry_file.tries_done) |*done| done.* +|= 1; - if (bls_entry_file.tries_left) |*left| left.* -|= 1; - if (bls_entry_file.tries_left) |tries_left| { - std.log.info( - "{} {s} remaining for entry \"{s}\"", - .{ - tries_left, - if (tries_left == 1) "try" else "tries", - bls_entry_file.name, - }, - ); - } - - const new_name = try bls_entry_file.toFilename(self.arena.allocator()); + const final_options = try allocator.dupe(u8, options); + errdefer allocator.free(options); - if (!std.mem.eql(u8, original_name, new_name)) { - var dir = try std.fs.cwd().openDir(context.parent_dir, .{}); - defer dir.close(); + const context = try allocator.create(BlsEntryFile); + errdefer allocator.destroy(context); - try dir.rename(original_name, new_name); - posix.sync(); - - std.log.debug("entry renamed to {s}", .{new_name}); - } - } - - pub fn teardown(self: *@This()) !void { - std.log.debug("BLS teardown", .{}); - - defer self.arena.deinit(); - - var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - - for (self.external_mounts) |mount| { - std.log.info("unmounted disk \"{s}\"", .{mount.disk_name}); - _ = system.umount2( - try self.arena.allocator().dupeZ(u8, try mount.dir.realpath(".", &buf)), - system.MNT.DETACH, - ); - mount.dir.close(); - } + context.* = BlsEntryFile.init(entry.id, .{ + .tries_left = entry.tries_left, + .tries_done = entry.tries_done, + }); - for (self.internal_mounts) |mount| { - std.log.info("unmounted disk \"{s}\"", .{mount.disk_name}); - _ = system.umount2( - try self.arena.allocator().dupeZ(u8, try mount.dir.realpath(".", &buf)), - system.MNT.DETACH, - ); - mount.dir.close(); - } + try entries.append( + .{ + .context = context, + .cmdline = final_options, + .initrd = initrd, + .linux = linux, + }, + ); } -}; +} pub const BlsEntryFile = struct { name: []const u8, @@ -817,12 +683,12 @@ pub const BlsEntryFile = struct { return filename.toOwnedSlice(); } - pub fn init(name: []const u8, opts: struct { + pub fn init(entry_name: []const u8, opts: struct { tries_left: ?u8 = null, tries_done: ?u8 = null, }) @This() { return .{ - .name = name, + .name = entry_name, .tries_left = opts.tries_left, .tries_done = opts.tries_done, }; @@ -837,7 +703,7 @@ pub const BlsEntryFile = struct { var plus_split = std.mem.splitSequence(u8, stem, "+"); - const name = plus_split.next().?; + const entry_name = plus_split.next().?; if (plus_split.next()) |counter_info| { var minus_split = std.mem.splitSequence(u8, counter_info, "-"); @@ -851,17 +717,17 @@ pub const BlsEntryFile = struct { const tries_done = std.fmt.parseInt(u8, minus_info, 10) catch { return Error.InvalidTriesSyntax; }; - return @This().init(name, .{ + return @This().init(entry_name, .{ .tries_left = tries_left, .tries_done = tries_done, }); } else { - return @This().init(name, .{ + return @This().init(entry_name, .{ .tries_left = tries_left, }); } } else { - return @This().init(name, .{}); + return @This().init(entry_name, .{}); } } }; diff --git a/src/boot/xmodem.zig b/src/boot/xmodem.zig index f337e9e..0f2732c 100644 --- a/src/boot/xmodem.zig +++ b/src/boot/xmodem.zig @@ -1,121 +1,120 @@ const std = @import("std"); const posix = std.posix; -const BootEntry = @import("../boot.zig").BootEntry; -const BootDevice = @import("../boot.zig").BootDevice; -const tmp = @import("../tmp.zig"); -const Tty = @import("../system.zig").Tty; -const xmodemRecv = @import("../xmodem.zig").xmodemRecv; -const setupTty = @import("../system.zig").setupTty; const linux_headers = @import("linux_headers"); -pub const Xmodem = struct { - allocator: std.mem.Allocator, - tmp_dir: tmp.TmpDir, - original_serial: ?Tty = null, - serial_fd: posix.fd_t, - serial_name: []const u8, - skip_initrd: bool = false, - - pub fn init(allocator: std.mem.Allocator, opts: struct { - /// A human-friendly name of the serial console - serial_name: []const u8, - /// The file descriptor associated with this serial console - serial_fd: posix.fd_t, - /// Indicates whether the initrd should be fetched in addition to the - /// kernel and cmdline parameters. - skip_initrd: bool, - }) !@This() { - return .{ - .serial_name = opts.serial_name, - .serial_fd = opts.serial_fd, - .skip_initrd = opts.skip_initrd, - .allocator = allocator, - .tmp_dir = try tmp.tmpDir(.{}), - }; - } +const BootLoader = @import("./bootloader.zig"); +const Device = @import("../device.zig"); +const TmpDir = @import("../tmpdir.zig"); +const system = @import("../system.zig"); +const xmodemRecv = @import("../xmodem.zig").xmodemRecv; + +const XmodemBootLoader = @This(); - pub fn setup(self: *@This()) !void { - self.original_serial = try setupTty(self.serial_fd, .file_transfer_recv); +arena: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator), +tmpdir: ?TmpDir = null, + +pub fn match(device: *const Device) ?u8 { + if (device.subsystem != .tty) { + return null; } - pub fn probe(self: *@This()) ![]const BootDevice { - errdefer { - if (self.original_serial) |tty| { - tty.reset(); + switch (device.type) { + .node => |node| { + // https://www.kernel.org/doc/Documentation/admin-guide/devices.txt + const major, const minor = node; + if (major == 4 and minor >= 64) { + return 100; + } else { + return null; } - self.original_serial = null; - } - - var linux = try self.tmp_dir.dir.createFile("linux", .{}); - defer linux.close(); - - try xmodemRecv(self.serial_fd, linux.writer()); - - if (!self.skip_initrd) { - var initrd = try self.tmp_dir.dir.createFile("initrd", .{}); - defer initrd.close(); - - try xmodemRecv(self.serial_fd, initrd.writer()); - } - - var params_file = try self.tmp_dir.dir.createFile("params", .{ - .read = true, - }); - defer params_file.close(); - - try xmodemRecv( - self.serial_fd, - params_file.writer(), - ); - - if (self.original_serial) |tty| { - tty.reset(); - } - self.original_serial = null; - - try params_file.seekTo(0); - const kernel_params_bytes = try params_file.readToEndAlloc( - self.allocator, - linux_headers.COMMAND_LINE_SIZE, - ); - - // trim out whitespace characters - const kernel_params = std.mem.trim(u8, kernel_params_bytes, " \t\n"); - - var devices = std.ArrayList(BootDevice).init(self.allocator); - var entries = std.ArrayList(BootEntry).init(self.allocator); - - try entries.append(.{ - .context = try self.allocator.create(struct {}), - .cmdline = if (kernel_params.len > 0) kernel_params else null, - .initrd = if (!self.skip_initrd) try self.tmp_dir.dir.realpathAlloc( - self.allocator, - "initrd", - ) else null, - .linux = try self.tmp_dir.dir.realpathAlloc( - self.allocator, - "linux", - ), - }); - - try devices.append(.{ - .timeout = 0, - .entries = try entries.toOwnedSlice(), - .name = self.serial_name, - }); - return devices.toOwnedSlice(); + }, + else => return null, } +} - pub fn entryLoaded(self: *@This(), ctx: *anyopaque) void { - _ = self; - _ = ctx; - } +pub fn init() XmodemBootLoader { + return .{}; +} + +pub fn name() []const u8 { + return "xmodem"; +} + +pub fn timeout(self: *XmodemBootLoader) u8 { + _ = self; + return 0; +} + +pub fn probe(self: *XmodemBootLoader, entries: *std.ArrayList(BootLoader.Entry), device: Device) !void { + const allocator = self.arena.allocator(); + + var serial_path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const serial_path = try device.nodePath(&serial_path_buf); + + var serial = try std.fs.cwd().openFile(serial_path, .{ .mode = .read_write }); + defer serial.close(); + + var original_serial = try system.setupTty(serial.handle, .file_transfer_recv); + defer original_serial.reset(); + + self.tmpdir = try TmpDir.create(.{}); + + var tmpdir = self.tmpdir.?; + + var linux = try tmpdir.dir.createFile("linux", .{}); + defer linux.close(); + + try xmodemRecv(serial.handle, linux.writer()); + + var initrd = try tmpdir.dir.createFile("initrd", .{}); + defer initrd.close(); + + try xmodemRecv(serial.handle, initrd.writer()); + + var params_file = try tmpdir.dir.createFile("params", .{ + .read = true, + }); + defer params_file.close(); + + try xmodemRecv( + serial.handle, + params_file.writer(), + ); + + try params_file.seekTo(0); + const kernel_params_bytes = try params_file.readToEndAlloc( + allocator, + linux_headers.COMMAND_LINE_SIZE, + ); + + // trim out whitespace characters + const kernel_params = std.mem.trim(u8, kernel_params_bytes, " \t\n"); + + try entries.append(.{ + .context = try allocator.create(struct {}), + .cmdline = if (kernel_params.len > 0) kernel_params else null, + .initrd = try tmpdir.dir.realpathAlloc( + allocator, + "initrd", + ), + .linux = try tmpdir.dir.realpathAlloc( + allocator, + "linux", + ), + }); +} + +pub fn entryLoaded(self: *XmodemBootLoader, ctx: *anyopaque) void { + _ = self; + _ = ctx; +} + +pub fn deinit(self: *XmodemBootLoader) void { + self.arena.deinit(); - pub fn teardown(self: *@This()) !void { - self.tmp_dir.cleanup(); - if (self.original_serial) |tty| { - tty.reset(); - } + if (self.tmpdir) |*tmpdir| { + tmpdir.cleanup(); + self.tmpdir = null; } -}; +} diff --git a/src/client.zig b/src/client.zig deleted file mode 100644 index 1663aa6..0000000 --- a/src/client.zig +++ /dev/null @@ -1,671 +0,0 @@ -const std = @import("std"); -const posix = std.posix; -const process = std.process; -const system = std.posix.system; - -const linux_headers = @import("linux_headers"); - -const BootEntry = @import("./boot.zig").BootEntry; -const ClientMsg = @import("./message.zig").ClientMsg; -const ServerMsg = @import("./message.zig").ServerMsg; -const Xmodem = @import("./boot/xmodem.zig").Xmodem; -const BootLoader = @import("./boot.zig").BootLoader; -const kernelLogs = @import("./system.zig").kernelLogs; -const kexecLoad = @import("./boot.zig").kexecLoad; -const readMessage = @import("./message.zig").readMessage; -const writeMessage = @import("./message.zig").writeMessage; - -pub const Client = struct { - waiting_for_response: bool = false, - has_prompt: bool = false, - watching_logs: bool = true, - input_cursor: u16 = 0, - input_end: u16 = 0, - stream: std.net.Stream, - input_buffer: [buffer_size]u8 = undefined, - log_file: std.fs.File, - writer: BufferedWriter, - - /// Used for dynamically allocating memory when running commands. This is - /// reset after every command run. - arena: std.heap.ArenaAllocator, - - const buffer_size = 4096; - const BufferedWriter = std.io.BufferedWriter(buffer_size, std.fs.File.Writer); - - pub fn init() !@This() { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - errdefer arena.deinit(); - - return @This(){ - .stream = try std.net.connectUnixSocket("/run/bus"), - .arena = arena, - .writer = std.io.bufferedWriter(std.io.getStdOut().writer()), - .log_file = try std.fs.cwd().openFile("/run/log", .{}), - }; - } - - pub fn deinit(self: *@This()) void { - self.log_file.close(); - self.stream.close(); - self.arena.deinit(); - } - - fn flush(self: *@This()) !void { - try self.writer.flush(); - } - - fn writeAll(self: *@This(), bytes: []const u8) !void { - var index: usize = 0; - while (index < bytes.len) { - index += try self.writer.write(bytes[index..]); - } - } - - fn writeAllAndFlush(self: *@This(), bytes: []const u8) !void { - try self.writeAll(bytes); - return self.flush(); - } - - fn prompt(self: *@This()) !void { - try self.writeAllAndFlush(&.{ 0xc2, 0xbb, 0x20 }); - } - - pub fn run(self: *@This()) !void { - try self.writeAllAndFlush("\npress to interrupt\n\n"); - - try self.printLogs(.{}); // print all logs we've received up to now - - const epoll_fd = try posix.epoll_create1(linux_headers.EPOLL_CLOEXEC); - defer posix.close(epoll_fd); - - var stdin_event = system.epoll_event{ - .data = .{ .fd = posix.STDIN_FILENO }, - .events = system.EPOLL.IN, - }; - try posix.epoll_ctl(epoll_fd, system.EPOLL.CTL_ADD, posix.STDIN_FILENO, &stdin_event); - - var server_event = system.epoll_event{ - .data = .{ .fd = self.stream.handle }, - .events = system.EPOLL.IN, - }; - try posix.epoll_ctl(epoll_fd, system.EPOLL.CTL_ADD, self.stream.handle, &server_event); - - const inotify_fd = try posix.inotify_init1(0); - const logs_watch_fd = try posix.inotify_add_watch(inotify_fd, "/run/log", system.IN.MODIFY); - defer posix.close(inotify_fd); - var inotify_event = system.epoll_event{ - .data = .{ .fd = inotify_fd }, - .events = system.EPOLL.IN, - }; - try posix.epoll_ctl(epoll_fd, system.EPOLL.CTL_ADD, inotify_fd, &inotify_event); - - // main event loop - while (true) { - const max_events = 8; // arbitrary - var events = [_]system.epoll_event{undefined} ** max_events; - - const n_events = posix.epoll_wait(epoll_fd, &events, -1); - - var i_event: usize = 0; - while (i_event < n_events) : (i_event += 1) { - const event = events[i_event]; - - // If we got an event that wasn't on the inotify fd, it means - // the client will no longer need to passively watch logs, so - // we remove the inotify watcher. - if (event.data.fd != inotify_fd and self.watching_logs) { - try posix.epoll_ctl(epoll_fd, system.EPOLL.CTL_DEL, inotify_fd, null); - posix.inotify_rm_watch(inotify_fd, logs_watch_fd); - self.watching_logs = false; - } - - if (event.data.fd == self.stream.handle) { - const should_quit = try self.handleMsg(); - if (should_quit) { - self.writeAllAndFlush("\ngoodbye!\n\n") catch {}; - return; - } - - if (self.waiting_for_response) { - self.waiting_for_response = false; - try self.prompt(); - } - } else if (event.data.fd == posix.STDIN_FILENO) { - try self.handleStdin(); - } else if (event.data.fd == inotify_fd and self.watching_logs) { - // Consume the event on the inotify fd. We don't - // actually use the data since we only have one file - // registered. If we don't do this, we will continue to - // get epoll notifications for this fd. - var buf: [@sizeOf(system.inotify_event)]u8 = undefined; - _ = posix.read(inotify_fd, &buf) catch {}; - try self.printLogs(.{ .from_start = false }); - } - } - } - } - - fn notifyUserPresence(self: *@This()) !void { - writeMessage(ClientMsg{ .data = .Empty }, self.stream.writer()) catch { - std.log.err("failed to notify user presence", .{}); - }; - } - - /// Returns true if the remote side shutdown, indicating we are done. - fn handleMsg(self: *@This()) !bool { - const msg = readMessage( - ServerMsg, - self.arena.allocator(), - self.stream.reader(), - ) catch |err| { - if (err == error.EOF) { - return true; - } - return err; - }; - defer msg.deinit(); - - switch (msg.value.data) { - .ForceShell => { - if (!self.has_prompt) { - std.log.debug("shell forced from server", .{}); - try self.prompt(); - self.has_prompt = true; - } - }, - } - - return false; - } - - pub fn printLogs(self: *@This(), opts: struct { - from_start: bool = true, - }) !void { - if (opts.from_start) { - try self.log_file.seekTo(0); - } - - while (true) { - var buf: [4096]u8 = undefined; - const n_bytes = try self.log_file.reader().readAll(&buf); - try self.writeAll(buf[0..n_bytes]); - if (n_bytes < buf.len) { - try self.flush(); - break; - } - } - } - - /// Caller required to flush - fn cursorLeft(self: *@This(), n: u16) void { - if (n > 0) { - var buf: [5]u8 = undefined; - const out = std.fmt.bufPrint(&buf, "{d:0>5}", .{n}) catch return; - self.writeAll(&.{ 0x1b, '[', out[0], out[1], out[2], out[3], out[4], 'D' }) catch {}; - } - } - - /// Caller required to flush - fn cursorRight(self: *@This(), n: u16) void { - if (n > 0) { - var buf: [5]u8 = undefined; - const out = std.fmt.bufPrint(&buf, "{d:0>5}", .{n}) catch return; - self.writeAll(&.{ 0x1b, '[', out[0], out[1], out[2], out[3], out[4], 'C' }) catch {}; - } - } - - /// Caller required to flush - fn eraseToEndOfLine(self: *@This()) void { - self.writeAll(&.{ 0x1b, '[', 'K' }) catch {}; - } - - /// Empties the display and moves the cursor to absolute position 0, 0. - fn clearScreen(self: *@This()) void { - // empties the display - self.writeAll(&.{ 0x1b, '[', '2', 'J' }) catch {}; - // moves the cursor to 0, 0 - self.writeAll(&.{ 0x1b, '[', '0', ';', '0', 'H' }) catch {}; - } - - fn handleStdin(self: *@This()) !void { - // We may already have a prompt from a boot timeout, so don't print - // a prompt if we already have one. - if (!self.has_prompt) { - try self.notifyUserPresence(); - try self.prompt(); - self.has_prompt = true; - } - - // We should only ever get 1 byte of data from stdin since we put the - // terminal in raw mode. - var buf = [_]u8{0}; - if (try posix.read(posix.STDIN_FILENO, &buf) != 1) { - return; - } - - const char = buf[0]; - - var done = false; - - const needs_flush = switch (char) { - // C-k - 0x0b => b: { - self.eraseToEndOfLine(); - self.input_end = self.input_cursor; - break :b true; - }, - // C-a - 0x01 => b: { - if (self.input_cursor > 0) { - self.cursorLeft(self.input_cursor); - self.input_cursor = 0; - break :b true; - } - - break :b false; - }, - // C-b - 0x02 => b: { - if (self.input_cursor > 0) { - self.cursorLeft(1); - self.input_cursor -= 1; - break :b true; - } - - break :b false; - }, - // C-c - 0x03 => b: { - try self.writeAll("\n"); - self.input_cursor = 0; - self.input_end = 0; - try self.prompt(); - break :b false; - }, - // C-d - 0x04 => b: { - if (self.input_cursor < self.input_end) { - std.mem.copyForwards( - u8, - self.input_buffer[self.input_cursor .. self.input_end - 1], - self.input_buffer[self.input_cursor + 1 .. self.input_end], - ); - self.input_end -= 1; - try self.writeAll(self.input_buffer[self.input_cursor..self.input_end]); - self.eraseToEndOfLine(); - self.cursorLeft(self.input_end - self.input_cursor); - break :b true; - } - - break :b false; - }, - // C-e - 0x05 => b: { - if (self.input_cursor < self.input_end) { - self.cursorRight(self.input_end - self.input_cursor); - self.input_cursor = self.input_end; - break :b true; - } - - break :b false; - }, - // C-f - 0x06 => b: { - if (self.input_cursor < self.input_end) { - self.cursorRight(1); - self.input_cursor += 1; - break :b true; - } - - break :b false; - }, - // C-h, Backspace - 0x08, 0x7f => b: { - if (self.input_cursor > 0) { - std.mem.copyForwards( - u8, - self.input_buffer[self.input_cursor - 1 .. self.input_end - 1], - self.input_buffer[self.input_cursor..self.input_end], - ); - self.input_cursor -= 1; - self.input_end -= 1; - self.cursorLeft(1); - try self.writeAll(self.input_buffer[self.input_cursor..self.input_end]); - self.eraseToEndOfLine(); - self.cursorLeft(self.input_end - self.input_cursor); - break :b true; - } - - break :b false; - }, - // C-l - 0x0c => b: { - self.clearScreen(); - try self.prompt(); - try self.writeAll(self.input_buffer[0..self.input_end]); - self.cursorLeft(self.input_end - self.input_cursor); - break :b true; - }, - // \r, \n; \n is also known as C-j - 0x0d, 0x0a => b: { - try self.writeAll("\n"); - if (self.input_cursor == 0) { - try self.prompt(); - } else { - done = true; - } - break :b true; - }, - // C-n - 0x0e => false, - // C-p - 0x10 => false, - // C-r - 0x12 => false, - // C-t - 0x14 => b: { - if (0 < self.input_cursor and self.input_cursor < self.input_end) { - std.mem.swap( - u8, - &self.input_buffer[self.input_cursor - 1], - &self.input_buffer[self.input_cursor], - ); - self.cursorLeft(1); - self.input_cursor += 1; - try self.writeAll(self.input_buffer[self.input_cursor - 2 .. self.input_cursor]); - break :b true; - } - - break :b false; - }, - // C-u - 0x15 => b: { - if (self.input_cursor > 0) { - self.cursorLeft(self.input_cursor); - try self.writeAll(self.input_buffer[self.input_cursor..self.input_end]); - self.eraseToEndOfLine(); - self.input_end = self.input_end - self.input_cursor; - self.input_cursor = 0; - self.cursorLeft(self.input_end); - break :b true; - } - - break :b false; - }, - // C-w - 0x17 => false, - // Space...~ - 0x20...0x7e => b: { - // make sure we have room for another character - if (self.input_end + 1 < self.input_buffer.len) { - std.mem.copyBackwards( - u8, - self.input_buffer[self.input_cursor + 1 .. self.input_end + 1], - self.input_buffer[self.input_cursor..self.input_end], - ); - self.input_buffer[self.input_cursor] = char; - self.input_end += 1; - try self.writeAll(self.input_buffer[self.input_cursor..self.input_end]); - self.input_cursor += 1; - self.cursorLeft(self.input_end - self.input_cursor); - break :b true; - } - - break :b false; - }, - else => false, - }; - - if (needs_flush) { - try self.writer.flush(); - } - - if (done and self.input_end > 0) { - _ = self.arena.reset(.retain_capacity); - - const end = self.input_end; - self.input_cursor = 0; - self.input_end = 0; - - var cmd = Command{ - .allocator = self.arena.allocator(), - .user_input = self.input_buffer[0..end], - .shell_instance = self, - }; - - const maybe_msg = cmd.run() catch |err| { - std.debug.print("\nerror running command: {any}\n", .{err}); - try self.prompt(); - return; - }; - - if (maybe_msg) |msg| { - if (writeMessage(msg, self.stream.writer())) { - self.waiting_for_response = true; - } else |err| { - std.log.err("failed to send message to server: {}", .{err}); - } - } else { - // Write the next prompt - try self.prompt(); - } - } - } -}; - -pub const Command = struct { - allocator: std.mem.Allocator, - user_input: []const u8, - shell_instance: *Client, - - const ArgsIterator = process.ArgIteratorGeneral(.{}); - - const argv0 = enum { - help, // NOTE: keep "help" at the top - boot_xmodem, - clear, - dmesg, - logs, - poweroff, - reboot, - }; - - pub fn run(self: *@This()) !?ClientMsg { - var args = try ArgsIterator.init(self.allocator, self.user_input); - defer args.deinit(); - - if (args.next()) |cmd| { - var found = false; - - inline for (std.meta.fields(argv0)) |field| { - if (std.mem.eql(u8, field.name, cmd)) { - found = true; - return @field(@This(), field.name).run(self, &args); - } - } - - if (!found) { - std.debug.print("unknown command \"{s}\"\n", .{cmd}); - } - } - - return null; - } - - const help = struct { - const short_help = "get help"; - const long_help = - \\Print all available commands or print specific command usage. - \\ - \\Usage: - \\help [command] - ; - - fn run(c: *Command, args: *ArgsIterator) !?ClientMsg { - _ = c; - - if (args.next()) |next| { - var found = false; - inline for (std.meta.fields(argv0)) |field| { - if (std.mem.eql(u8, field.name, next)) { - found = true; - const cmd_long_help = comptime @field(Command, field.name).long_help; - std.debug.print("\n{s}\n", .{cmd_long_help}); - } - } - - if (!found) { - std.debug.print("unknown command \"{s}\"\n", .{next}); - } - } else { - std.debug.print("\n", .{}); - - inline for (std.meta.fields(argv0)) |field| { - const cmd_short_help = comptime @field(Command, field.name).short_help; - const space = 20 - comptime field.name.len; - std.debug.print("{s}{s}{s}\n", .{ field.name, " " ** space, cmd_short_help }); - } - } - - return null; - } - }; - - const poweroff = struct { - const short_help = "poweroff the machine"; - const long_help = - \\Immediately poweroff the machine. - \\ - \\Usage: - \\poweroff - ; - - fn run(c: *Command, args: *ArgsIterator) !?ClientMsg { - _ = args; - _ = c; - - return .{ .data = .Poweroff }; - } - }; - - const reboot = struct { - const short_help = "reboot the machine"; - const long_help = - \\Immediately reboot the machine. - \\ - \\Usage: - \\reboot - ; - - fn run(c: *Command, args: *ArgsIterator) !?ClientMsg { - _ = c; - _ = args; - - return .{ .data = .Reboot }; - } - }; - - const logs = struct { - const short_help = "view tinyboot logs"; - const long_help = - \\View tinyboot logs. - \\ - \\Usage: - \\logs - ; - - fn run(c: *Command, args: *ArgsIterator) !?ClientMsg { - _ = args; - - try c.shell_instance.printLogs(.{}); - return null; - } - }; - - const dmesg = struct { - const short_help = "view kernel logs"; - const long_help = - \\View kernel logs. - \\ - \\Usage: - \\dmesg [filter log level] Default filter is log level 6 - \\ - \\Example: - \\dmesg 7 - ; - - fn run(c: *Command, args: *ArgsIterator) !?ClientMsg { - const filter = if (args.next()) |filter_str| try std.fmt.parseInt(u8, filter_str, 10) else 6; - const kernel_logs = try kernelLogs(c.allocator, filter); - defer c.allocator.free(kernel_logs); - try c.shell_instance.writeAllAndFlush(kernel_logs); - - return null; - } - }; - - const clear = struct { - const short_help = "clear the screen"; - const long_help = - \\Clear the screen. - \\ - \\Usage: - \\clear - ; - - fn run(c: *Command, args: *ArgsIterator) !?ClientMsg { - _ = args; - - c.shell_instance.clearScreen(); - - return null; - } - }; - - const boot_xmodem = struct { - const short_help = "boot over xmodem"; - const long_help = - \\Boot via kernel and initrd obtained over the xmodem protocol. The - \\serial console will fetch the following content over xmodem in - \\succession: - \\ - kernel - \\ - initrd (optional) - \\ - kernel params - \\ - \\Usage: - \\boot_xmodem [options] - \\ - \\Options: - \\ -n No initrd - ; - - fn run(c: *Command, args: *ArgsIterator) !?ClientMsg { - var xmodem = try Xmodem.init(c.allocator, .{ - .serial_name = "client-stdin", - .serial_fd = posix.STDIN_FILENO, - .skip_initrd = if (args.next()) |next| - std.mem.eql(u8, next, "-n") - else - false, - }); - var boot_loader = BootLoader{ .xmodem = &xmodem }; - defer boot_loader.teardown() catch {}; - - try boot_loader.setup(); - const devices = try boot_loader.probe(); - for (devices) |device| { - for (device.entries) |entry| { - if (kexecLoad(c.allocator, entry.linux, entry.initrd, entry.cmdline)) { - boot_loader.entryLoaded(entry.context); - return .{ .data = .Kexec }; - } else |err| { - std.log.err("failed to load kernel: {}", .{err}); - } - } - } - - return null; - } - }; -}; diff --git a/src/console.zig b/src/console.zig new file mode 100644 index 0000000..6d0f70b --- /dev/null +++ b/src/console.zig @@ -0,0 +1,664 @@ +const std = @import("std"); +const posix = std.posix; +const process = std.process; + +const linux_headers = @import("linux_headers"); + +const BootLoader = @import("./boot/bootloader.zig"); +const Device = @import("./device.zig"); +const Xmodem = @import("./boot/xmodem.zig").Xmodem; +const system = @import("./system.zig"); +const utils = @import("./utils.zig"); + +const esc = std.ascii.control_code.esc; + +const ArgsIterator = process.ArgIteratorGeneral(.{}); + +pub const Event = enum { + /// Reboot initiated from console. + reboot, + + /// Poweroff initiated from console. + poweroff, + + /// Kexec initiated from console. + kexec, +}; + +const Console = @This(); + +const CONSOLE = "/dev/char/5:1"; + +const IO_BUFFER_SIZE = 4096; + +pub const IN = posix.STDIN_FILENO; + +var out = std.io.bufferedWriter(std.io.getStdOut().writer()); + +arena: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator), +input_cursor: u16 = 0, +input_end: u16 = 0, +input_buffer: [IO_BUFFER_SIZE]u8 = undefined, +context: ?*BootLoader = null, +tty: ?system.Tty = null, + +pub fn init() !Console { + // Turn off local echo, making the ENTER key the only thing that shows a + // sign of user input. + { + _ = try system.setupTty(IN, .no_echo); + writeAllAndFlush("\npress to interrupt\n\n"); + } + + return .{}; +} + +fn flush() void { + out.flush() catch {}; +} + +/// Flushes occur transparently. Do not use if control over when flushes occur +/// is needed. +fn print(comptime format: []const u8, args: anytype) void { + out.writer().print(format, args) catch {}; +} + +fn writeAll(bytes: []const u8) void { + out.writer().writeAll(bytes) catch {}; +} + +fn writeAllAndFlush(bytes: []const u8) void { + writeAll(bytes); + flush(); +} + +fn prompt(self: *Console) !void { + if (self.context) |ctx| { + try out.writer().writeAll(ctx.name()); + } + writeAllAndFlush("> "); +} + +/// Caller required to flush +fn cursorLeft(n: u16) void { + if (n > 0) { + out.writer().print(.{esc} ++ "[{d:0>5}D", .{n}) catch {}; + } +} + +/// Caller required to flush +fn cursorRight(n: u16) void { + if (n > 0) { + out.writer().print(.{esc} ++ "[{d}C", .{n}) catch {}; + } +} + +/// Caller required to flush +fn eraseToEndOfLine() void { + out.writer().writeAll(.{esc} ++ "[K") catch {}; +} + +/// Empties the display and moves the cursor to absolute position 0, 0. +fn clearScreen() void { + // empties the display + out.writer().writeAll(.{esc} ++ "[2J") catch {}; + // moves the cursor to 0, 0 + out.writer().writeAll(.{esc} ++ "[0;0H") catch {}; +} + +pub fn deinit(self: *Console) void { + defer self.arena.deinit(); + + if (self.tty) |*tty| { + tty.reset(); + } +} + +pub fn handleStdin(self: *Console, boot_loaders: []*BootLoader) !?Event { + // We may already have a prompt from a boot timeout, so don't print + // a prompt if we already have one. + if (self.tty == null) { + self.tty = try system.setupTty(IN, .user_input); + std.log.debug("user presence detected", .{}); + try self.prompt(); + } + + // We should only ever get 1 byte of data from stdin since we put the + // terminal in raw mode. + var buf = [_]u8{0}; + if (try std.io.getStdIn().read(&buf) != 1) { + return null; + } + + const char = buf[0]; + + var done = false; + + const needs_flush = switch (char) { + // C-k + 0x0b => b: { + eraseToEndOfLine(); + self.input_end = self.input_cursor; + break :b true; + }, + // C-a + 0x01 => b: { + if (self.input_cursor > 0) { + cursorLeft(self.input_cursor); + self.input_cursor = 0; + break :b true; + } + + break :b false; + }, + // C-b + 0x02 => b: { + if (self.input_cursor > 0) { + cursorLeft(1); + self.input_cursor -= 1; + break :b true; + } + + break :b false; + }, + // C-c + 0x03 => b: { + writeAll("\n"); + self.input_cursor = 0; + self.input_end = 0; + try self.prompt(); + break :b false; + }, + // C-d + 0x04 => b: { + if (self.input_cursor < self.input_end) { + std.mem.copyForwards( + u8, + self.input_buffer[self.input_cursor .. self.input_end - 1], + self.input_buffer[self.input_cursor + 1 .. self.input_end], + ); + self.input_end -= 1; + writeAll(self.input_buffer[self.input_cursor..self.input_end]); + eraseToEndOfLine(); + cursorLeft(self.input_end - self.input_cursor); + break :b true; + } + + break :b false; + }, + // C-e + 0x05 => b: { + if (self.input_cursor < self.input_end) { + cursorRight(self.input_end - self.input_cursor); + self.input_cursor = self.input_end; + break :b true; + } + + break :b false; + }, + // C-f + 0x06 => b: { + if (self.input_cursor < self.input_end) { + cursorRight(1); + self.input_cursor += 1; + break :b true; + } + + break :b false; + }, + // C-h, Backspace + 0x08, 0x7f => b: { + if (self.input_cursor > 0) { + std.mem.copyForwards( + u8, + self.input_buffer[self.input_cursor - 1 .. self.input_end - 1], + self.input_buffer[self.input_cursor..self.input_end], + ); + self.input_cursor -= 1; + self.input_end -= 1; + cursorLeft(1); + writeAll(self.input_buffer[self.input_cursor..self.input_end]); + eraseToEndOfLine(); + cursorLeft(self.input_end - self.input_cursor); + break :b true; + } + + break :b false; + }, + // C-l + 0x0c => b: { + clearScreen(); + try self.prompt(); + writeAll(self.input_buffer[0..self.input_end]); + cursorLeft(self.input_end - self.input_cursor); + break :b true; + }, + // \r, \n; \n is also known as C-j + 0x0d, 0x0a => b: { + writeAll("\n"); + if (self.input_cursor == 0) { + try self.prompt(); + } else { + done = true; + } + break :b true; + }, + // C-n + 0x0e => false, + // C-p + 0x10 => false, + // C-r + 0x12 => false, + // C-t + 0x14 => b: { + if (0 < self.input_cursor and self.input_cursor < self.input_end) { + std.mem.swap( + u8, + &self.input_buffer[self.input_cursor - 1], + &self.input_buffer[self.input_cursor], + ); + cursorLeft(1); + self.input_cursor += 1; + writeAll(self.input_buffer[self.input_cursor - 2 .. self.input_cursor]); + break :b true; + } + + break :b false; + }, + // C-u + 0x15 => b: { + if (self.input_cursor > 0) { + cursorLeft(self.input_cursor); + writeAll(self.input_buffer[self.input_cursor..self.input_end]); + eraseToEndOfLine(); + self.input_end = self.input_end - self.input_cursor; + self.input_cursor = 0; + cursorLeft(self.input_end); + break :b true; + } + + break :b false; + }, + // C-w + 0x17 => false, + // Space...~ + 0x20...0x7e => b: { + // make sure we have room for another character + if (self.input_end + 1 < self.input_buffer.len) { + std.mem.copyBackwards( + u8, + self.input_buffer[self.input_cursor + 1 .. self.input_end + 1], + self.input_buffer[self.input_cursor..self.input_end], + ); + self.input_buffer[self.input_cursor] = char; + self.input_end += 1; + writeAll(self.input_buffer[self.input_cursor..self.input_end]); + self.input_cursor += 1; + cursorLeft(self.input_end - self.input_cursor); + break :b true; + } + + break :b false; + }, + else => false, + }; + + if (needs_flush) { + flush(); + } + + if (done and self.input_end > 0) { + defer { + if (self.context == null) { + _ = self.arena.reset(.retain_capacity); + } + } + + const end = self.input_end; + self.input_cursor = 0; + self.input_end = 0; + + const user_input = self.input_buffer[0..end]; + var args = try ArgsIterator.init(self.arena.allocator(), user_input); + defer args.deinit(); + + const maybe_notification = self.runCommand(&args, boot_loaders); + + const event = maybe_notification catch |err| { + print("\nerror running command: {}\n", .{err}); + try self.prompt(); + return null; + } orelse { + try self.prompt(); + return null; + }; + + return event; + } + + return null; +} + +fn runCommand( + self: *Console, + args: *ArgsIterator, + boot_loaders: []*BootLoader, +) !?Event { + if (args.next()) |cmd| { + if (std.mem.eql(u8, cmd, "help")) { + return @field(Command, "help").run(self, args, boot_loaders); + } + + if (self.context) |ctx| { + inline for (std.meta.fields(Command.Context)) |field| { + if (std.mem.eql(u8, field.name, cmd)) { + return @field(Command, field.name).run( + self, + args, + ctx, + ); + } + } + } else { + inline for (std.meta.fields(Command.NoContext)) |field| { + if (std.mem.eql(u8, field.name, cmd)) { + return @field(Command, field.name).run( + self, + args, + boot_loaders, + ); + } + } + } + + print("unknown command \"{s}\"\n", .{cmd}); + } + + return null; +} + +pub const Command = struct { + const NoContext = enum { + clear, + logs, + poweroff, + reboot, + list, + select, + }; + + const Context = enum { + exit, + probe, + boot, + }; + + const help = struct { + const short_help = "get help"; + const long_help = + \\Print all available commands or print specific command usage. + \\ + \\Usage: + \\help [command] + ; + + /// Prints a help message for all commands. + fn helpAll(t: anytype) void { + print("\n", .{}); + + inline for (std.meta.fields(t)) |field| { + const cmd_short_help = comptime @field(Command, field.name).short_help; + const space = 20 - comptime field.name.len; + print("{s}{s}{s}\n", .{ field.name, " " ** space, cmd_short_help }); + } + } + + /// Prints a help message for a single command. + fn helpOne(t: anytype, cmd: []const u8) void { + if (std.mem.eql(u8, cmd, "help")) { + const cmd_long_help = comptime @field(Command, "help").long_help; + print("\n{s}\n", .{cmd_long_help}); + return; + } + + inline for (std.meta.fields(t)) |field| { + if (std.mem.eql(u8, field.name, cmd)) { + const cmd_long_help = comptime @field(Command, field.name).long_help; + print("\n{s}\n", .{cmd_long_help}); + return; + } + } + + print("unknown command \"{s}\"\n", .{cmd}); + } + + fn run(console: *Console, args: *ArgsIterator, _: []*const BootLoader) !?Event { + if (args.next()) |cmd| { + if (console.context == null) { + helpOne(NoContext, cmd); + } else { + helpOne(Context, cmd); + } + } else { + if (console.context == null) { + helpAll(NoContext); + } else { + helpAll(Context); + } + } + + return null; + } + }; + + const poweroff = struct { + const short_help = "poweroff the machine"; + const long_help = + \\Immediately poweroff the machine. + \\ + \\Usage: + \\poweroff + ; + + fn run(_: *Console, _: *ArgsIterator, _: []*const BootLoader) !?Event { + return .poweroff; + } + }; + + const reboot = struct { + const short_help = "reboot the machine"; + const long_help = + \\Immediately reboot the machine. + \\ + \\Usage: + \\reboot + ; + + fn run(_: *Console, _: *ArgsIterator, _: []*const BootLoader) !?Event { + return .reboot; + } + }; + + const logs = struct { + const short_help = "view kernel logs"; + const long_help = + \\View kernel logs. All logs at or below the specified filter will + \\be shown. + \\ + \\Usage: + \\logs [log level filter] Default filter is log level 6 + \\ + \\Example: + \\logs 7 + ; + + fn run(console: *Console, args: *ArgsIterator, _: []*const BootLoader) !?Event { + const filter = if (args.next()) |filter_str| + try std.fmt.parseInt(u3, filter_str, 10) + else + 6; + + try system.printKernelLogs( + console.arena.allocator(), + filter, + out.writer().any(), + ); + + return null; + } + }; + + const clear = struct { + const short_help = "clear the screen"; + const long_help = + \\Clear the screen. + \\ + \\Usage: + \\clear + ; + + fn run(_: *Console, _: *ArgsIterator, _: []*const BootLoader) !?Event { + clearScreen(); + + return null; + } + }; + + const select = struct { + const short_help = "select boot loader"; + const long_help = + \\Select a boot loader. + \\ + \\Usage: + \\select + \\ + \\Example: + \\select 2 + ; + + fn run(console: *Console, args: *ArgsIterator, boot_loaders: []*BootLoader) !?Event { + const want_index = try std.fmt.parseInt( + usize, + args.next() orelse return error.InvalidArgument, + 10, + ); + + for (boot_loaders, 0..) |boot_loader, index| { + if (want_index == index) { + console.context = boot_loader; + print( + "selected boot loader: {s} ({})\n", + .{ boot_loader.name(), boot_loader.device }, + ); + return null; + } + } + + return error.NotFound; + } + }; + + const list = struct { + const short_help = "list boot loaders"; + const long_help = + \\List all active boot loaders. + \\ + \\Usage: + \\list + ; + + fn run(_: *Console, _: *ArgsIterator, boot_loaders: []*BootLoader) !?Event { + for (boot_loaders, 0..) |boot_loader, index| { + print( + "{d}\t{s} ({})\n", + .{ index, boot_loader.name(), boot_loader.device }, + ); + } + + return null; + } + }; + + const exit = struct { + const short_help = "exit context"; + const long_help = + \\Exit bootloader context. + \\ + \\Usage: + \\exit + ; + + fn run(console: *Console, _: *ArgsIterator, _: *BootLoader) !?Event { + defer console.context = null; + + return null; + } + }; + + const probe = struct { + const short_help = "probe for boot entries"; + const long_help = + \\Probe and show all boot entries on a device. + \\ + \\Usage: + \\probe + ; + + fn run(_: *Console, _: *ArgsIterator, boot_loader: *BootLoader) !?Event { + const entries = boot_loader.probe() catch |err| { + print("failed to probe: {}\n", .{err}); + return null; + }; + + for (entries, 0..) |entry, index| { + print("{d}\t{s}\n", .{ index, entry.linux }); + } + + return null; + } + }; + + const boot = struct { + const short_help = "boot an entry"; + const long_help = + \\Boot an entry. + \\ + \\Usage: + \\boot Default is to boot the first entry + \\ + \\Example + \\boot 7 + ; + + fn run(_: *Console, args: *ArgsIterator, boot_loader: *BootLoader) !?Event { + const want_index = try std.fmt.parseInt( + usize, + args.next() orelse "0", + 10, + ); + + const entries = boot_loader.probe() catch |err| { + print("failed to probe: {}\n", .{err}); + return null; + }; + + for (entries, 0..) |entry, index| { + if (want_index == index) { + if (boot_loader.load(entry)) { + print( + "selected entry: {s}\n", + .{entry.linux}, + ); + + return Event.kexec; + } else |err| { + print("failed to load entry: {}", .{err}); + return null; + } + } + } + + return error.NotFound; + } + }; +}; diff --git a/src/device.zig b/src/device.zig index 9ab231b..8c350ba 100644 --- a/src/device.zig +++ b/src/device.zig @@ -1,636 +1,104 @@ const std = @import("std"); -const posix = std.posix; -const path = std.fs.path; -const system = posix.system; -const linux_headers = @import("linux_headers"); +const utils = @import("./utils.zig"); -const Uevent = std.StringHashMap([]const u8); +const Device = @This(); -const DeviceError = error{ - CreateFailed, - IncompleteDevice, -}; - -pub fn parseUeventFileContents(allocator: std.mem.Allocator, contents: []const u8) !Uevent { - var uevent = Uevent.init(allocator); - - var iter = std.mem.splitSequence(u8, contents, "\n"); - - while (iter.next()) |line| { - var split = std.mem.splitSequence(u8, line, "="); - const key = split.next() orelse continue; - const value = split.next() orelse continue; - try uevent.put(key, value); - } - - return uevent; -} - -const Action = enum { - add, - remove, - bind, - - fn parse(action: []const u8) ?@This() { - if (std.mem.eql(u8, action, "add")) { - return .add; - } else if (std.mem.eql(u8, action, "remove")) { - return .remove; - } else if (std.mem.eql(u8, action, "bind")) { - return .bind; - } - - return null; - } -}; - -const Kobject = struct { - action: Action, - device_path: []const u8, - uevent: Uevent, - - pub fn deinit(self: *@This()) void { - self.uevent.deinit(); - } -}; - -fn parseUeventKobjectContents(allocator: std.mem.Allocator, contents: []const u8) !?Kobject { - var iter = std.mem.splitSequence(u8, contents, &.{0}); - - const first_line = iter.next().?; - var first_line_split = std.mem.splitSequence(u8, first_line, "@"); - if (Action.parse(first_line_split.next().?)) |action| { - var uevent = Uevent.init(allocator); - - const device_path = first_line_split.next().?; - - while (iter.next()) |line| { - var split = std.mem.splitSequence(u8, line, "="); - const key = split.next() orelse continue; - const value = split.next() orelse continue; - try uevent.put(key, value); - } - - return .{ - .action = action, - .device_path = device_path, - .uevent = uevent, - }; - } - - return null; -} - -fn makedev(major: u32, minor: u32) u32 { - return std.math.shl(u32, major & 0xfffff000, 32) | - std.math.shl(u32, major & 0x00000fff, 8) | - std.math.shl(u32, minor & 0xffffff00, 12) | - std.math.shl(u32, minor & 0x000000ff, 0); -} - -fn special(devtype: ?[]const u8) u32 { - if (devtype) |dtype| { - if (std.mem.eql(u8, dtype, "disk") or std.mem.eql(u8, dtype, "partition")) { - return system.S.IFBLK; - } - } - - return system.S.IFCHR; -} - -/// Caller owns return value -fn diskAliasFilename(allocator: std.mem.Allocator, uevent: Uevent) ![]const u8 { - const diskseq = uevent.get("DISKSEQ") orelse return DeviceError.IncompleteDevice; - - if (uevent.get("PARTN")) |partn| { - return try std.fmt.allocPrint(allocator, "disk{s}_part{s}", .{ diskseq, partn }); - } else { - return try std.fmt.allocPrint(allocator, "disk{s}", .{diskseq}); - } -} - -pub const DeviceWatcher = struct { - // busybox uses these buffer sizes - const USER_RCVBUF = 3 * 1024; - const KERN_RCVBUF = 128 * 1024 * 1024; - - arena: std.heap.ArenaAllocator, - - disk_dir: std.fs.Dir, - block_dir: std.fs.Dir, - char_dir: std.fs.Dir, - - /// Netlink socket fd for subscribing to new device events. - nl_fd: posix.fd_t, - - /// Timer fd for determining when new events have "settled". - settle_fd: posix.fd_t, - - pub fn init() !@This() { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - errdefer arena.deinit(); - - try std.fs.cwd().makePath("/dev/disk"); - try std.fs.cwd().makePath("/dev/block"); - try std.fs.cwd().makePath("/dev/char"); - - var self = @This(){ - .arena = arena, - .disk_dir = try std.fs.cwd().openDir("/dev/disk", .{}), - .block_dir = try std.fs.cwd().openDir("/dev/block", .{}), - .char_dir = try std.fs.cwd().openDir("/dev/char", .{}), - .nl_fd = try posix.socket( - system.AF.NETLINK, - system.SOCK.DGRAM, - system.NETLINK.KOBJECT_UEVENT, - ), - .settle_fd = try posix.timerfd_create(posix.CLOCK.REALTIME, .{}), - }; - errdefer self.deinit(); - - try self.scanAndCreateDevices(); - _ = self.arena.reset(.retain_capacity); - - try posix.setsockopt(self.nl_fd, posix.SOL.SOCKET, posix.SO.RCVBUF, &std.mem.toBytes(@as(c_int, KERN_RCVBUF))); - try posix.setsockopt(self.nl_fd, posix.SOL.SOCKET, posix.SO.RCVBUFFORCE, &std.mem.toBytes(@as(c_int, KERN_RCVBUF))); - - const nls = posix.sockaddr.nl{ - .groups = 1, // KOBJECT_UEVENT groups bitmask must be 1 - .pid = @bitCast(system.getpid()), - }; - try posix.bind(self.nl_fd, @ptrCast(&nls), @sizeOf(posix.sockaddr.nl)); - - return self; - } - - pub fn register(self: *@This(), epoll_fd: posix.fd_t) !void { - var device_event = system.epoll_event{ - .data = .{ .fd = self.nl_fd }, - .events = system.EPOLL.IN, - }; - try posix.epoll_ctl(epoll_fd, system.EPOLL.CTL_ADD, self.nl_fd, &device_event); - - var timer_event = system.epoll_event{ - .data = .{ .fd = self.settle_fd }, - .events = system.EPOLL.IN | system.EPOLL.ONESHOT, - }; - try posix.epoll_ctl(epoll_fd, system.EPOLL.CTL_ADD, self.settle_fd, &timer_event); - } - - pub fn startSettleTimer(self: *@This()) !void { - const timerspec = system.itimerspec{ - // oneshot - .it_interval = .{ .tv_sec = 0, .tv_nsec = 0 }, - // consider settled after 2 seconds without any new events - .it_value = .{ .tv_sec = 2, .tv_nsec = 0 }, - }; - try posix.timerfd_settime(self.settle_fd, .{}, &timerspec, null); - } - - pub fn handleNewEvent(self: *@This()) !void { - defer _ = self.arena.reset(.retain_capacity); - - // reset the timer - try self.startSettleTimer(); - - var recv_bytes: [USER_RCVBUF]u8 = undefined; - - const bytes_read = try posix.read(self.nl_fd, &recv_bytes); - - const kobject = try parseUeventKobjectContents( - self.arena.allocator(), - recv_bytes[0..bytes_read], - ) orelse return; - - switch (kobject.action) { - .add => try self.createDevice(kobject.uevent), - .remove => try self.removeDevice(kobject.uevent), - else => {}, - } - } - - pub fn deinit(self: *@This()) void { - self.arena.deinit(); - self.disk_dir.close(); - self.block_dir.close(); - self.char_dir.close(); - posix.close(self.nl_fd); - posix.close(self.settle_fd); - } - - fn scanAndCreateDevices(self: *@This()) !void { - const allocator = self.arena.allocator(); - - { - var sys_class_block = try std.fs.cwd().openDir( - "/sys/class/block", - .{ .iterate = true }, - ); - defer sys_class_block.close(); - - var it = sys_class_block.iterate(); - while (try it.next()) |entry| { - if (entry.kind != .sym_link) { - continue; - } - - const uevent_path = try path.join(allocator, &.{ - entry.name, - "uevent", - }); - - var uevent_file = try sys_class_block.openFile(uevent_path, .{}); - defer uevent_file.close(); - - const max_bytes = 10 * 1024 * 1024; - const uevent_contents = try uevent_file.readToEndAlloc( - allocator, - max_bytes, - ); - - const uevent = try parseUeventFileContents(allocator, uevent_contents); - - self.createDevice(uevent) catch |err| { - std.log.err("failed to create device: {any}", .{err}); - continue; - }; - } - } - - { - var sys_class_tty = try std.fs.cwd().openDir( - "/sys/class/tty", - .{ .iterate = true }, - ); - defer sys_class_tty.close(); - - var it = sys_class_tty.iterate(); - while (try it.next()) |entry| { - if (entry.kind != .sym_link) { - continue; - } +subsystem: Subsystem, +type: union(enum) { + ifindex: u32, + node: struct { u32, u32 }, +}, - // skip known non-serial devices - if (std.mem.eql(u8, entry.name, "tty") or - std.mem.eql(u8, entry.name, "console") or - std.mem.eql(u8, entry.name, "ptmx") or - std.mem.eql(u8, entry.name, "ttynull")) - { - continue; - } +pub fn nodePath(device: *const Device, buf: []u8) ![]u8 { + std.debug.assert(device.type == .node); - const tty_uevent_path = try path.join(allocator, &.{ - entry.name, - "uevent", - }); + const major, const minor = device.type.node; - var uevent_file = try sys_class_tty.openFile(tty_uevent_path, .{}); - defer uevent_file.close(); - - const max_bytes = 10 * 1024 * 1024; - const uevent_contents = try uevent_file.readToEndAlloc(allocator, max_bytes); - - const uevent = try parseUeventFileContents(allocator, uevent_contents); - - self.createDevice(uevent) catch |err| { - std.log.err("failed to create device: {any}", .{err}); - continue; - }; - } - } - } - - fn createDevice(self: *@This(), uevent: Uevent) !void { - // Nothing to do if we don't have major or minor. - const major_str = uevent.get("MAJOR") orelse return; - const minor_str = uevent.get("MINOR") orelse return; - - const major = try std.fmt.parseInt(u32, major_str, 10); - const minor = try std.fmt.parseInt(u32, minor_str, 10); - - const mode = special(uevent.get("DEVTYPE")); - - const devname = uevent.get("DEVNAME") orelse return DeviceError.IncompleteDevice; - - const dev_path = path.join(self.arena.allocator(), &.{ path.sep_str, "dev", devname }) catch return; - - if (path.dirname(dev_path)) |parent| { - try std.fs.cwd().makePath(parent); - } - - const dev_path_cstr = try self.arena.allocator().dupeZ(u8, dev_path); - - const rc = system.mknod(dev_path_cstr, mode, makedev(major, minor)); - switch (posix.errno(rc)) { - .SUCCESS => std.log.debug("created device {s}", .{dev_path}), - .EXIST => {}, // device already exists - else => return DeviceError.CreateFailed, - } - - switch (mode) { - system.S.IFBLK => try self.createBlkAlias(dev_path, major, minor, uevent), - system.S.IFCHR => try self.createCharAlias(dev_path, major, minor), - else => {}, - } - } - - fn createBlkAlias( - self: *@This(), - dev_path: []const u8, - major: u32, - minor: u32, - uevent: Uevent, - ) !void { - const alias_filename = try diskAliasFilename(self.arena.allocator(), uevent); - - self.disk_dir.symLink(dev_path, alias_filename, .{}) catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => return err, - }; - - const major_minor_filename = try std.fmt.allocPrint( - self.arena.allocator(), - "{d}:{d}", - .{ major, minor }, - ); - - self.block_dir.symLink(dev_path, major_minor_filename, .{}) catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => return err, - }; - - std.log.info("created block device alias for block device {s}", .{dev_path}); - } - - fn createCharAlias( - self: *@This(), - dev_path: []const u8, - major: u32, - minor: u32, - ) !void { - const filename = try std.fmt.allocPrint( - self.arena.allocator(), - "{d}:{d}", - .{ major, minor }, - ); - - self.char_dir.symLink(dev_path, filename, .{}) catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => return err, - }; - } - - fn removeDevice(self: *@This(), uevent: Uevent) !void { - // Nothing to do if we don't have major or minor. - const major_str = uevent.get("MAJOR") orelse return; - const minor_str = uevent.get("MINOR") orelse return; - const major = try std.fmt.parseInt(u32, major_str, 10); - const minor = try std.fmt.parseInt(u32, minor_str, 10); - const mode = special(uevent.get("DEVTYPE")); - - const devname = uevent.get("DEVNAME") orelse return DeviceError.IncompleteDevice; - const dev_path = path.join(self.arena.allocator(), &.{ path.sep_str, "dev", devname }) catch return; - - std.fs.cwd().deleteFile(dev_path) catch |err| switch (err) { - error.FileNotFound => {}, - else => return err, - }; - - switch (mode) { - system.S.IFBLK => try self.removeBlkAlias(dev_path, major, minor, uevent), - system.S.IFCHR => try self.removeCharAlias(major, minor), - else => {}, - } - } - - fn removeBlkAlias( - self: *@This(), - dev_path: []const u8, - major: u32, - minor: u32, - uevent: Uevent, - ) !void { - const alias_filename = try diskAliasFilename(self.arena.allocator(), uevent); - - self.disk_dir.deleteFile(alias_filename) catch |err| switch (err) { - error.FileNotFound => {}, - else => return err, - }; - - const major_minor_filename = try std.fmt.allocPrint(self.arena.allocator(), "{d}:{d}", .{ major, minor }); - - self.block_dir.deleteFile(major_minor_filename) catch |err| switch (err) { - error.FileNotFound => {}, - else => return err, - }; - - std.log.info("removed block device aliases for block device {s}", .{dev_path}); - } - - fn removeCharAlias( - self: *@This(), - major: u32, - minor: u32, - ) !void { - var buf: [32]u8 = undefined; - const filename = try std.fmt.bufPrint(&buf, "{d}:{d}", .{ major, minor }); - - const alias_path = try path.join(self.arena.allocator(), &.{ path.sep_str, "dev", "char", filename }); - - std.fs.cwd().deleteFile(alias_path) catch |err| switch (err) { - error.FileNotFound => {}, - else => return err, - }; - } -}; - -test "device mode" { - try std.testing.expectEqual(@as(u32, system.S.IFCHR), special(null)); - try std.testing.expectEqual(@as(u32, system.S.IFCHR), special("foo")); - try std.testing.expectEqual(@as(u32, system.S.IFBLK), special("disk")); - try std.testing.expectEqual(@as(u32, system.S.IFBLK), special("partition")); + return try std.fmt.bufPrint(buf, "/dev/{s}/{d}:{d}", .{ + switch (device.subsystem) { + .block => "block", + else => "char", + }, + major, + minor, + }); } -test "uevent file content parsing" { - const test_partition = - \\MAJOR=259 - \\MINOR=1 - \\DEVNAME=nvme0n1p1 - \\DEVTYPE=partition - \\DISKSEQ=1 - \\PARTN=1 - ; - - var partition_uevent = try parseUeventFileContents(std.testing.allocator, test_partition); - defer partition_uevent.deinit(); - - try std.testing.expectEqualStrings("259", partition_uevent.get("MAJOR").?); - try std.testing.expectEqualStrings("partition", partition_uevent.get("DEVTYPE").?); - try std.testing.expectEqualStrings("1", partition_uevent.get("DISKSEQ").?); - try std.testing.expectEqualStrings("1", partition_uevent.get("PARTN").?); - - const test_disk = - \\MAJOR=259 - \\MINOR=0 - \\DEVNAME=nvme0n1 - \\DEVTYPE=disk - \\DISKSEQ=1 - ; +pub fn nodePathZ(device: *const Device, buf: []u8) ![:0]const u8 { + std.debug.assert(device.type == .node); - var disk_uevent = try parseUeventFileContents(std.testing.allocator, test_disk); - defer disk_uevent.deinit(); + const major, const minor = device.type.node; - try std.testing.expectEqualStrings("259", disk_uevent.get("MAJOR").?); - try std.testing.expectEqualStrings("disk", disk_uevent.get("DEVTYPE").?); - try std.testing.expectEqualStrings("1", disk_uevent.get("DISKSEQ").?); - - const test_tpm = - \\MAJOR=10 - \\MINOR=224 - \\DEVNAME=tpm0 - ; - - var tpm_uevent = try parseUeventFileContents(std.testing.allocator, test_tpm); - defer tpm_uevent.deinit(); - - try std.testing.expectEqualStrings("10", tpm_uevent.get("MAJOR").?); - try std.testing.expectEqualStrings("224", tpm_uevent.get("MINOR").?); - try std.testing.expectEqualStrings("tpm0", tpm_uevent.get("DEVNAME").?); -} - -test "uevent kobject add chardev parsing" { - const content = try std.mem.join(std.testing.allocator, &.{0}, &.{ - "add@/devices/platform/serial8250/tty/ttyS6", - "ACTION=add", - "DEVPATH=/devices/platform/serial8250/tty/ttyS6", - "SUBSYSTEM=tty", - "SYNTH_UUID=0", - "MAJOR=4", - "MINOR=70", - "DEVNAME=ttyS6", - "SEQNUM=3469", + return try std.fmt.bufPrintZ(buf, "/dev/{s}/{d}:{d}", .{ + switch (device.subsystem) { + .block => "block", + else => "char", + }, + major, + minor, }); - defer std.testing.allocator.free(content); - - var kobject = try parseUeventKobjectContents(std.testing.allocator, content) orelse unreachable; - defer kobject.deinit(); - - try std.testing.expectEqual(Action.add, kobject.action); - try std.testing.expectEqualStrings("/devices/platform/serial8250/tty/ttyS6", kobject.device_path); - try std.testing.expectEqualStrings("0", kobject.uevent.get("SYNTH_UUID").?); } -test "uevent kobject remove chardev parsing" { - const content = try std.mem.join(std.testing.allocator, &.{0}, &.{ - "remove@/devices/platform/serial8250/tty/ttyS6", - "ACTION=remove", - "DEVPATH=/devices/platform/serial8250/tty/ttyS6", - "SUBSYSTEM=tty", - "SYNTH_UUID=0", - "MAJOR=4", - "MINOR=70", - "DEVNAME=ttyS6", - "SEQNUM=3471", - }); - defer std.testing.allocator.free(content); +pub fn nodeSysfsPath(device: *const Device, buf: []u8) ![]u8 { + std.debug.assert(device.type == .node); - var kobject = try parseUeventKobjectContents(std.testing.allocator, content) orelse unreachable; - defer kobject.deinit(); + const major, const minor = device.type.node; - try std.testing.expectEqual(Action.remove, kobject.action); - try std.testing.expectEqualStrings("/devices/platform/serial8250/tty/ttyS6", kobject.device_path); - try std.testing.expectEqualStrings("3471", kobject.uevent.get("SEQNUM").?); -} - -// Determine if we can use a virtual terminal. Uses the termios ws_ypixel field to detect if , which is updated by the kernel at -// https://github.com/torvalds/linux/blob/83814698cf48ce3aadc5d88a3f577f04482ff92a/drivers/tty/vt/vt.c#L1267 -fn vtIsUsable(fd: posix.fd_t) bool { - var winsize: posix.winsize = undefined; - const rc = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); - return switch (posix.errno(rc)) { - .SUCCESS => winsize.ws_col > 0 and winsize.ws_row > 0, - else => false, - }; + return try std.fmt.bufPrint(buf, "/sys/dev/{s}/{d}:{d}", .{ + switch (device.subsystem) { + .block => "block", + else => "char", + }, + major, + minor, + }); } -fn serialDeviceIsConnected(fd: posix.fd_t) bool { - var serial: c_int = 0; - - if (system.ioctl(fd, linux_headers.TIOCMGET, @intFromPtr(&serial)) != 0) { - return false; +pub fn format( + self: Device, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, +) !void { + _ = fmt; + _ = options; + + switch (self.type) { + .ifindex => |ifindex| try writer.print("{s} {}", .{ "ifindex", ifindex }), + .node => |node| { + const major, const minor = node; + try writer.print("node {}:{}", .{ major, minor }); + }, } - - return serial & linux_headers.TIOCM_DTR == linux_headers.TIOCM_DTR; } -/// Find all active serial and virtual terminals where we can spawn a console. -/// Caller is responsible for the returned list. -pub fn findActiveConsoles(allocator: std.mem.Allocator) ![]posix.fd_t { - var devs = std.ArrayList(posix.fd_t).init(allocator); - errdefer devs.deinit(); - - // Don't assume a monitor is connected - if (posix.open("/dev/tty0", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0)) |fd| { - errdefer posix.close(fd); - - if (vtIsUsable(fd)) { - try devs.append(fd); - } else { - posix.close(fd); - } - } else |err| { - err catch {}; +// ls -1 /sys/class +// +/// Subsystems we care about when acting as a bootloader. +pub const Subsystem = enum { + block, + mem, + mtd, + net, + rtc, + tty, + watchdog, + + pub fn fromStr(value: []const u8) !@This() { + return utils.enumFromStr(@This(), value); } +}; - var char_devices_dir = try std.fs.cwd().openDir( - "/dev/char", - .{ .iterate = true }, - ); - defer char_devices_dir.close(); - - var walker = try char_devices_dir.walk(allocator); - defer walker.deinit(); - - while (try walker.next()) |entry| { - if (entry.kind != .sym_link) { - continue; - } - - var split = std.mem.splitScalar(u8, entry.basename, ':'); - const major_str = split.next() orelse continue; - const minor_str = split.next() orelse continue; - if (split.next() != null) { - continue; - } - - const major = std.fmt.parseInt(u32, major_str, 10) catch continue; - const minor = std.fmt.parseInt(u32, minor_str, 10) catch continue; - - // TODO(jared): handle major number 204 - switch (major) { - linux_headers.TTY_MAJOR => { - // First serial device is at minor number 64. See - // https://github.com/torvalds/linux/blob/841c35169323cd833294798e58b9bf63fa4fa1de/Documentation/admin-guide/devices.txt#L137 - if (minor < 64) { - continue; - } - - var fullpath_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - const fullpath = char_devices_dir.realpath(entry.path, &fullpath_buf) catch continue; - - const fd = try posix.open(fullpath, .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0); +// grep --no-filename DEVTYPE /sys/class/*/*/uevent | cut -d'=' -f2 | sort | uniq +// +/// Device types we care about when acting as a bootloader. +pub const DevType = enum { + disk, + mtd, + partition, - if (serialDeviceIsConnected(fd)) { - std.log.info("found active serial device at {s}", .{fullpath}); - try devs.append(fd); - } else { - posix.close(fd); - } - }, - else => {}, - } + pub fn fromStr(value: []const u8) !@This() { + return utils.enumFromStr(@This(), value); } - - return devs.toOwnedSlice(); -} +}; diff --git a/src/kobject.zig b/src/kobject.zig new file mode 100644 index 0000000..b687f74 --- /dev/null +++ b/src/kobject.zig @@ -0,0 +1,188 @@ +const std = @import("std"); +const posix = std.posix; + +const Device = @import("./device.zig"); +const DeviceWatcher = @import("./watch.zig"); +const utils = @import("./utils.zig"); + +pub fn parseUeventFileContents( + subsystem: Device.Subsystem, + contents: []const u8, +) ?Device { + var iter = std.mem.splitSequence(u8, contents, "\n"); + + var major: ?u32 = null; + var minor: ?u32 = null; + var ifindex: ?u32 = null; + + while (iter.next()) |line| { + var split = std.mem.splitSequence(u8, line, "="); + const key = split.next() orelse continue; + const value = split.next() orelse continue; + + if (std.mem.eql(u8, key, "IFINDEX")) { + ifindex = std.fmt.parseInt(u32, value, 10) catch return null; + } else if (std.mem.eql(u8, key, "MAJOR")) { + major = std.fmt.parseInt(u32, value, 10) catch return null; + } else if (std.mem.eql(u8, key, "MINOR")) { + minor = std.fmt.parseInt(u32, value, 10) catch return null; + } + } + + return .{ + .subsystem = subsystem, + .type = if (ifindex) |ifidx| + .{ .ifindex = ifidx } + else + .{ .node = .{ + major orelse return null, + minor orelse return null, + } }, + }; +} + +pub fn parseUeventKobjectContents(contents: []const u8) ?DeviceWatcher.Event { + var iter = std.mem.splitSequence(u8, contents, &.{0}); + + const first_line = iter.next().?; + var first_line_split = std.mem.splitSequence(u8, first_line, "@"); + const action = Action.fromStr(first_line_split.next().?) catch return null; + + var subsystem: ?Device.Subsystem = null; + var major: ?u32 = null; + var minor: ?u32 = null; + var ifindex: ?u32 = null; + + while (iter.next()) |line| { + var split = std.mem.splitSequence(u8, line, "="); + const key = split.next() orelse continue; + const value = split.next() orelse continue; + + // TODO(jared): The net subsystem (possibly) doesn't set DEVNAME, but + // rather INTERFACE. + if (std.mem.eql(u8, key, "SUBSYSTEM")) { + subsystem = Device.Subsystem.fromStr(value) catch return null; + } else if (std.mem.eql(u8, key, "IFINDEX")) { + ifindex = std.fmt.parseInt(u32, value, 10) catch return null; + } else if (std.mem.eql(u8, key, "MAJOR")) { + major = std.fmt.parseInt(u32, value, 10) catch return null; + } else if (std.mem.eql(u8, key, "MINOR")) { + minor = std.fmt.parseInt(u32, value, 10) catch return null; + } + } + + return .{ + .action = action, + .device = .{ + .subsystem = subsystem orelse return null, + .type = if (ifindex) |ifidx| + .{ .ifindex = ifidx } + else + .{ .node = .{ + major orelse return null, + minor orelse return null, + } }, + }, + }; +} + +// test "uevent file content parsing" { +// const test_partition = +// \\MAJOR=259 +// \\MINOR=1 +// \\DEVNAME=nvme0n1p1 +// \\DEVTYPE=partition +// \\DISKSEQ=1 +// \\PARTN=1 +// ; +// +// var partition_uevent = try parseUeventFileContents(std.testing.allocator, test_partition); +// defer partition_uevent.deinit(); +// +// try std.testing.expectEqualStrings("259", partition_uevent.get("MAJOR").?); +// try std.testing.expectEqualStrings("partition", partition_uevent.get("DEVTYPE").?); +// try std.testing.expectEqualStrings("1", partition_uevent.get("DISKSEQ").?); +// try std.testing.expectEqualStrings("1", partition_uevent.get("PARTN").?); +// +// const test_disk = +// \\MAJOR=259 +// \\MINOR=0 +// \\DEVNAME=nvme0n1 +// \\DEVTYPE=disk +// \\DISKSEQ=1 +// ; +// +// var disk_uevent = try parseUeventFileContents(std.testing.allocator, test_disk); +// defer disk_uevent.deinit(); +// +// try std.testing.expectEqualStrings("259", disk_uevent.get("MAJOR").?); +// try std.testing.expectEqualStrings("disk", disk_uevent.get("DEVTYPE").?); +// try std.testing.expectEqualStrings("1", disk_uevent.get("DISKSEQ").?); +// +// const test_tpm = +// \\MAJOR=10 +// \\MINOR=224 +// \\DEVNAME=tpm0 +// ; +// +// var tpm_uevent = try parseUeventFileContents(std.testing.allocator, test_tpm); +// defer tpm_uevent.deinit(); +// +// try std.testing.expectEqualStrings("10", tpm_uevent.get("MAJOR").?); +// try std.testing.expectEqualStrings("224", tpm_uevent.get("MINOR").?); +// try std.testing.expectEqualStrings("tpm0", tpm_uevent.get("DEVNAME").?); +// } + +// test "uevent kobject add chardev parsing" { +// const content = try std.mem.join(std.testing.allocator, &.{0}, &.{ +// "add@/devices/platform/serial8250/tty/ttyS6", +// "ACTION=add", +// "DEVPATH=/devices/platform/serial8250/tty/ttyS6", +// "SUBSYSTEM=tty", +// "SYNTH_UUID=0", +// "MAJOR=4", +// "MINOR=70", +// "DEVNAME=ttyS6", +// "SEQNUM=3469", +// }); +// defer std.testing.allocator.free(content); +// +// var kobject = try parseUeventKobjectContents(std.testing.allocator, content) orelse unreachable; +// defer kobject.deinit(); +// +// try std.testing.expectEqual(Action.add, kobject.action); +// try std.testing.expectEqualStrings("/devices/platform/serial8250/tty/ttyS6", kobject.device_path); +// try std.testing.expectEqualStrings("0", kobject.uevent.get("SYNTH_UUID").?); +// } + +// test "uevent kobject remove chardev parsing" { +// const content = try std.mem.join(std.testing.allocator, &.{0}, &.{ +// "remove@/devices/platform/serial8250/tty/ttyS6", +// "ACTION=remove", +// "DEVPATH=/devices/platform/serial8250/tty/ttyS6", +// "SUBSYSTEM=tty", +// "SYNTH_UUID=0", +// "MAJOR=4", +// "MINOR=70", +// "DEVNAME=ttyS6", +// "SEQNUM=3471", +// }); +// defer std.testing.allocator.free(content); +// +// var kobject = try parseUeventKobjectContents(std.testing.allocator, content) orelse unreachable; +// defer kobject.deinit(); +// +// try std.testing.expectEqual(Action.remove, kobject.action); +// try std.testing.expectEqualStrings("/devices/platform/serial8250/tty/ttyS6", kobject.device_path); +// try std.testing.expectEqualStrings("3471", kobject.uevent.get("SEQNUM").?); +// } + +// https://github.com/torvalds/linux/blob/afcd48134c58d6af45fb3fdb648f1260b20f2326/lib/kobject_uevent.c#L50 +pub const Action = union(enum) { + add, + remove, + + fn fromStr(value: []const u8) !@This() { + return utils.enumFromStr(@This(), value); + } +}; diff --git a/src/log.zig b/src/log.zig index 5a91bba..d8b5b37 100644 --- a/src/log.zig +++ b/src/log.zig @@ -1,23 +1,24 @@ const std = @import("std"); -var log_file: ?std.fs.File = null; - -pub fn initLogger(t: enum { - Server, - Client, -}) !void { - switch (t) { - .Server => { - log_file = try std.fs.cwd().createFile("/run/log", .{ - .truncate = true, - }); - }, - .Client => {}, - } +const LOG_PREFIX = "boot"; + +const KMSG = "/dev/char/1:11"; + +const SYSLOG_FACILITY_USER = 1; + +// https://github.com/torvalds/linux/blob/55027e689933ba2e64f3d245fb1ff185b3e7fc81/kernel/printk/internal.h#L38C9-L38C28 +// https://github.com/torvalds/linux/blob/55027e689933ba2e64f3d245fb1ff185b3e7fc81/kernel/printk/printk.c#L735 +const PRINTKRB_RECORD_MAX = 1024; + +var mutex = std.Thread.Mutex{}; +var kmsg: ?std.fs.File = null; + +pub fn init() !void { + kmsg = try std.fs.cwd().openFile(KMSG, .{ .mode = .write_only }); } -pub fn deinitLogger() void { - if (log_file) |file| { +pub fn deinit() void { + if (kmsg) |file| { file.close(); } } @@ -28,14 +29,47 @@ pub fn logFn( comptime format: []const u8, args: anytype, ) void { - const prefix1 = comptime level.asText(); - const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; - - if (log_file) |file| { - // Write the message to the log file, silently ignoring any errors - file.writer().print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch {}; - } else { - // Print the message to stderr, silently ignoring any errors - std.debug.print(prefix1 ++ prefix2 ++ format ++ "\n", args); - } + _ = scope; + + const syslog_prefix = comptime b: { + var buf: [2]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + + // 0 KERN_EMERG + // 1 KERN_ALERT + // 2 KERN_CRIT + // 3 KERN_ERR + // 4 KERN_WARNING + // 5 KERN_NOTICE + // 6 KERN_INFO + // 7 KERN_DEBUG + + // https://github.com/torvalds/linux/blob/f2661062f16b2de5d7b6a5c42a9a5c96326b8454/Documentation/ABI/testing/dev-kmsg#L1 + const syslog_level = ((SYSLOG_FACILITY_USER << 3) | switch (level) { + .err => 3, + .warn => 4, + .info => 6, + .debug => 7, + }); + + std.fmt.formatIntValue(syslog_level, "", .{}, fbs.writer()) catch return; + break :b fbs.getWritten(); + }; + + const file = kmsg orelse return; + + mutex.lock(); + defer mutex.unlock(); + + // The Zig string formatter can make many individual writes to our + // writer depending on the format string, so we do all the formatting + // ahead of time here so we can perform the write all at once when the + // log line goes to the kernel. + var buf: [PRINTKRB_RECORD_MAX]u8 = undefined; + var stream = std.io.fixedBufferStream(&buf); + stream.writer().print( + "<" ++ syslog_prefix ++ ">" ++ LOG_PREFIX ++ ": " ++ format, + args, + ) catch {}; + _ = file.write(buf[0..stream.pos]) catch {}; } diff --git a/src/message.zig b/src/message.zig deleted file mode 100644 index 925d895..0000000 --- a/src/message.zig +++ /dev/null @@ -1,61 +0,0 @@ -const std = @import("std"); -const json = std.json; -const BootEntry = @import("./boot.zig").BootEntry; - -fn message(comptime T: type) type { - return struct { - data: T, - }; -} - -/// Message that can be sent to the server -pub const ClientMsg = message(union(enum) { - /// Request to the server that the system should be powered off. - Poweroff, - - /// Request to the server that the system should be rebooted. - Reboot, - - /// Request to the server that the system should be kexec'ed. - Kexec, - - /// Empty message used to indicate user presence. - Empty, -}); - -/// Message that can be sent to a client -pub const ServerMsg = message(union(enum) { - /// Spawn a shell prompt, even if the user is not present - ForceShell, -}); - -// This number is arbitrary, we may need to increase it at some point. -const MAX_BUF_SIZE = 1 << 12; - -/// Caller is responsible for return value's memory. -pub fn readMessage(comptime T: type, allocator: std.mem.Allocator, r: std.net.Stream.Reader) !json.Parsed(T) { - var buf: [MAX_BUF_SIZE]u8 = undefined; - - const n_bytes = try r.read(&buf); - - // If we end up here, this means our connection was dropped on the other - // side. This should only happen if the server has completed successfully - // or if I wrote a bug :). - if (n_bytes == 0) { - return error.EOF; - } - - return try json.parseFromSlice(T, allocator, buf[0..n_bytes], .{}); -} - -pub fn writeMessage(value: anytype, w: std.net.Stream.Writer) !void { - // Write to fixed buffer first prior to doing write to socket, since the - // json writer will perform many writes for each character needed to write - // valid json. - var buf: [MAX_BUF_SIZE]u8 = undefined; - var wbuf = std.io.fixedBufferStream(&buf); - - try json.stringify(value, .{}, wbuf.writer()); - - try w.writeAll(wbuf.buffer[0..(try wbuf.getPos())]); -} diff --git a/src/runner.zig b/src/runner.zig index 5714674..543ca50 100644 --- a/src/runner.zig +++ b/src/runner.zig @@ -46,7 +46,6 @@ pub fn main() !void { // TODO(jared): "-drive", "if=virtio,file=TODO.raw,format=raw,media=disk" try qemu_args.appendSlice(&.{ - "-no-reboot", "-display", "none", "-serial", diff --git a/src/security.zig b/src/security.zig index 542875c..dcc06d1 100644 --- a/src/security.zig +++ b/src/security.zig @@ -4,62 +4,84 @@ const system = std.posix.system; const linux_headers = @import("linux_headers"); -pub const IMA_POLICY_PATH = "/sys/kernel/security/ima/policy"; +const MEASURE_POLICY = + PROC_SUPER_MAGIC ++ + SYSFS_MAGIC ++ + DEBUGFS_MAGIC ++ + TMPFS_MAGIC ++ + DEVPTS_SUPER_MAGIC ++ + BINFMTFS_MAGIC ++ + SECURITYFS_MAGIC ++ + SELINUX_MAGIC ++ + SMACK_MAGIC ++ + CGROUP_SUPER_MAGIC ++ + CGROUP2_SUPER_MAGIC ++ + NSFS_MAGIC ++ + KEY_CHECK ++ + POLICY_CHECK ++ + KEXEC_KERNEL_CHECK ++ + KEXEC_INITRAMFS_CHECK ++ + KEXEC_CMDLINE; + +const APPRAISE_POLICY = KEXEC_KERNEL_CHECK_APPRAISE ++ KEXEC_INITRAMFS_CHECK_APPRAISE; + +const MEASURE_AND_APPRAISE_POLICY = MEASURE_POLICY ++ APPRAISE_POLICY; + +const IMA_POLICY_PATH = "/sys/kernel/security/ima/policy"; + +// Individual IMA policy lines below // PROC_SUPER_MAGIC = 0x9fa0 -pub const PROC_SUPER_MAGIC = withNewline("dont_measure fsmagic=0x9fa0"); +const PROC_SUPER_MAGIC = withNewline("dont_measure fsmagic=0x9fa0"); // SYSFS_MAGIC = 0x62656572 -pub const SYSFS_MAGIC = withNewline("dont_measure fsmagic=0x62656572"); +const SYSFS_MAGIC = withNewline("dont_measure fsmagic=0x62656572"); // DEBUGFS_MAGIC = 0x64626720 -pub const DEBUGFS_MAGIC = withNewline("dont_measure fsmagic=0x64626720"); +const DEBUGFS_MAGIC = withNewline("dont_measure fsmagic=0x64626720"); // TMPFS_MAGIC = 0x01021994 -pub const TMPFS_MAGIC = withNewline("dont_measure fsmagic=0x1021994"); +const TMPFS_MAGIC = withNewline("dont_measure fsmagic=0x1021994"); // DEVPTS_SUPER_MAGIC=0x1cd1 -pub const DEVPTS_SUPER_MAGIC = withNewline("dont_measure fsmagic=0x1cd1"); +const DEVPTS_SUPER_MAGIC = withNewline("dont_measure fsmagic=0x1cd1"); // BINFMTFS_MAGIC=0x42494e4d -pub const BINFMTFS_MAGIC = withNewline("dont_measure fsmagic=0x42494e4d"); +const BINFMTFS_MAGIC = withNewline("dont_measure fsmagic=0x42494e4d"); // SECURITYFS_MAGIC=0x73636673 -pub const SECURITYFS_MAGIC = withNewline("dont_measure fsmagic=0x73636673"); +const SECURITYFS_MAGIC = withNewline("dont_measure fsmagic=0x73636673"); // SELINUX_MAGIC=0xf97cff8c -pub const SELINUX_MAGIC = withNewline("dont_measure fsmagic=0xf97cff8c"); +const SELINUX_MAGIC = withNewline("dont_measure fsmagic=0xf97cff8c"); // SMACK_MAGIC=0x43415d53 -pub const SMACK_MAGIC = withNewline("dont_measure fsmagic=0x43415d53"); +const SMACK_MAGIC = withNewline("dont_measure fsmagic=0x43415d53"); // CGROUP_SUPER_MAGIC=0x27e0eb -pub const CGROUP_SUPER_MAGIC = withNewline("dont_measure fsmagic=0x27e0eb"); +const CGROUP_SUPER_MAGIC = withNewline("dont_measure fsmagic=0x27e0eb"); // CGROUP2_SUPER_MAGIC=0x63677270 -pub const CGROUP2_SUPER_MAGIC = withNewline("dont_measure fsmagic=0x63677270"); +const CGROUP2_SUPER_MAGIC = withNewline("dont_measure fsmagic=0x63677270"); // NSFS_MAGIC=0x6e736673 -pub const NSFS_MAGIC = withNewline("dont_measure fsmagic=0x6e736673"); +const NSFS_MAGIC = withNewline("dont_measure fsmagic=0x6e736673"); -pub const KEY_CHECK = withNewline("measure func=KEY_CHECK pcr=7"); +const KEY_CHECK = withNewline("measure func=KEY_CHECK pcr=7"); -pub const POLICY_CHECK = withNewline("measure func=POLICY_CHECK pcr=7"); +const POLICY_CHECK = withNewline("measure func=POLICY_CHECK pcr=7"); -pub const KEXEC_KERNEL_CHECK = withNewline("measure func=KEXEC_KERNEL_CHECK pcr=8"); +const KEXEC_KERNEL_CHECK = withNewline("measure func=KEXEC_KERNEL_CHECK pcr=8"); -pub const KEXEC_INITRAMFS_CHECK = withNewline("measure func=KEXEC_INITRAMFS_CHECK pcr=9"); +const KEXEC_INITRAMFS_CHECK = withNewline("measure func=KEXEC_INITRAMFS_CHECK pcr=9"); -pub const KEXEC_CMDLINE = withNewline("measure func=KEXEC_CMDLINE pcr=12"); +const KEXEC_CMDLINE = withNewline("measure func=KEXEC_CMDLINE pcr=12"); -pub const KEXEC_KERNEL_CHECK_APPRAISE = withNewline("appraise func=KEXEC_KERNEL_CHECK appraise_type=imasig|modsig"); +const KEXEC_KERNEL_CHECK_APPRAISE = withNewline("appraise func=KEXEC_KERNEL_CHECK appraise_type=imasig|modsig"); -pub const KEXEC_INITRAMFS_CHECK_APPRAISE = withNewline("appraise func=KEXEC_INITRAMFS_CHECK appraise_type=imasig|modsig"); - -fn installImaPolicy(allocator: std.mem.Allocator, policy_entries: []const []const u8) !void { - const policy = try std.mem.join(allocator, "", policy_entries); - defer allocator.free(policy); +const KEXEC_INITRAMFS_CHECK_APPRAISE = withNewline("appraise func=KEXEC_INITRAMFS_CHECK appraise_type=imasig|modsig"); +fn installImaPolicy(policy: []const u8) !void { var policy_file = try std.fs.openFileAbsolute(IMA_POLICY_PATH, .{ .mode = .write_only }); defer policy_file.close(); @@ -71,7 +93,7 @@ fn installImaPolicy(allocator: std.mem.Allocator, policy_entries: []const []cons const TEST_KEY = @embedFile("test_key"); // https://github.com/torvalds/linux/blob/3b517966c5616ac011081153482a5ba0e91b17ff/security/integrity/digsig.c#L193 -fn loadVerificationKey(allocator: std.mem.Allocator) !void { +fn loadVerificationKey() !void { const keyfile: std.fs.File = b: { inline for (.{ VPD_KEY, FW_CFG_KEY }) |keypath| { if (std.fs.cwd().openFile(keypath, .{})) |file| { @@ -90,13 +112,14 @@ fn loadVerificationKey(allocator: std.mem.Allocator) !void { defer keyfile.close(); const keyring_id = try addKeyring(IMA_KEYRING_NAME, KeySerial.User); - std.log.info("added ima keyring (id 0x{x})", .{keyring_id}); + std.log.info("added ima keyring (id=0x{x})", .{keyring_id}); - const keyfile_contents = try keyfile.readToEndAlloc(allocator, 8192); - defer allocator.free(keyfile_contents); + var buf: [8192]u8 = undefined; + const n_read = try keyfile.readAll(&buf); + const keyfile_contents = buf[0..n_read]; const key_id = try addKey(keyring_id, keyfile_contents); - std.log.info("added verification key (id 0x{x})", .{key_id}); + std.log.info("added verification key (id=0x{x})", .{key_id}); if (std.mem.eql(u8, keyfile_contents, TEST_KEY)) { std.log.warn("test key in use!", .{}); @@ -108,49 +131,14 @@ fn loadVerificationKey(allocator: std.mem.Allocator) !void { // with IMA since we basically get it for free; measurements are held in memory // and persisted across kexecs, and the measurements are extended to the // system's TPM if one is available. -pub fn initializeSecurity(allocator: std.mem.Allocator) !void { - var ima_policy = std.ArrayList([]const u8).init(allocator); - defer ima_policy.deinit(); - - try ima_policy.appendSlice(&.{ - PROC_SUPER_MAGIC, - SYSFS_MAGIC, - DEBUGFS_MAGIC, - TMPFS_MAGIC, - DEVPTS_SUPER_MAGIC, - BINFMTFS_MAGIC, - SECURITYFS_MAGIC, - SELINUX_MAGIC, - SMACK_MAGIC, - CGROUP_SUPER_MAGIC, - CGROUP2_SUPER_MAGIC, - NSFS_MAGIC, - KEY_CHECK, - POLICY_CHECK, - KEXEC_KERNEL_CHECK, - KEXEC_INITRAMFS_CHECK, - KEXEC_CMDLINE, - }); - - var do_verified_boot = true; - loadVerificationKey(allocator) catch |err| { +pub fn initializeSecurity() !void { + if (loadVerificationKey()) { + try installImaPolicy(MEASURE_AND_APPRAISE_POLICY); + std.log.info("boot measurement and verification is enabled", .{}); + } else |err| { std.log.warn("failed to load verification key, cannot perform boot verification: {}", .{err}); - do_verified_boot = false; - }; - - if (do_verified_boot) { - try ima_policy.appendSlice(&.{ - KEXEC_KERNEL_CHECK_APPRAISE, - KEXEC_INITRAMFS_CHECK_APPRAISE, - }); - } - - try installImaPolicy(allocator, ima_policy.items); - - std.log.info("boot measurement is enabled", .{}); - - if (do_verified_boot) { - std.log.info("boot verification is enabled", .{}); + try installImaPolicy(MEASURE_POLICY); + std.log.info("boot measurement is enabled", .{}); } } diff --git a/src/server.zig b/src/server.zig deleted file mode 100644 index bcc1768..0000000 --- a/src/server.zig +++ /dev/null @@ -1,95 +0,0 @@ -const std = @import("std"); -const posix = std.posix; -const system = std.posix.system; - -const ClientMsg = @import("./message.zig").ClientMsg; -const ServerMsg = @import("./message.zig").ServerMsg; -const readMessage = @import("./message.zig").readMessage; -const writeMessage = @import("./message.zig").writeMessage; - -pub const Server = struct { - allocator: std.mem.Allocator, - - /// The underlying server - inner: std.net.Server, - - /// Clients connected to the server - clients: std.ArrayList(std.net.Stream), - - pub fn init(allocator: std.mem.Allocator) !@This() { - const socket_addr = try std.net.Address.initUnix("/run/bus"); - - return @This(){ - .allocator = allocator, - .inner = try socket_addr.listen(.{}), - .clients = std.ArrayList(std.net.Stream).init(allocator), - }; - } - - pub fn registerSelf(self: *@This(), epoll_fd: posix.fd_t) !void { - try posix.epoll_ctl( - epoll_fd, - system.EPOLL.CTL_ADD, - self.inner.stream.handle, - @constCast(&.{ - .events = system.EPOLL.IN, - .data = .{ .fd = self.inner.stream.handle }, - }), - ); - } - - pub fn registerClient(self: *@This(), epoll_fd: posix.fd_t, client_stream: std.net.Stream) !void { - try self.clients.append(client_stream); - - try posix.epoll_ctl( - epoll_fd, - system.EPOLL.CTL_ADD, - client_stream.handle, - @constCast(&.{ - .events = system.EPOLL.IN, - .data = .{ .fd = client_stream.handle }, - }), - ); - } - - pub fn forceShell(self: *@This()) void { - for (self.clients.items) |client| { - writeMessage(ServerMsg{ .data = .ForceShell }, client.writer()) catch {}; - } - } - - pub fn handleNewEvent(self: *@This(), event: system.epoll_event) !?posix.RebootCommand { - const client = b: { - for (self.clients.items) |client| { - if (event.data.fd == client.handle) { - break :b client; - } - } - - return null; - }; - - const msg = readMessage(ClientMsg, self.allocator, client.reader()) catch |err| switch (err) { - error.EOF => return null, // Handle client disconnects - else => return err, - }; - defer msg.deinit(); - - switch (msg.value.data) { - .Empty => return null, - .Reboot => return posix.RebootCommand.RESTART, - .Poweroff => return posix.RebootCommand.POWER_OFF, - .Kexec => return posix.RebootCommand.KEXEC, - } - } - - pub fn deinit(self: *@This()) void { - for (self.clients.items) |client| { - client.close(); - } - self.clients.deinit(); - - self.inner.stream.close(); - std.fs.cwd().deleteFile("/run/bus") catch {}; - } -}; diff --git a/src/system.zig b/src/system.zig index c98d467..e5e62c3 100644 --- a/src/system.zig +++ b/src/system.zig @@ -1,49 +1,39 @@ const std = @import("std"); -const fs = std.fs; const posix = std.posix; const system = std.posix.system; const linux_headers = @import("linux_headers"); -const MountError = error{ - Todo, -}; - fn mountPseudoFs( path: [*:0]const u8, fstype: [*:0]const u8, flags: u32, -) MountError!void { +) !void { const rc = system.mount("", path, fstype, flags, 0); switch (posix.errno(rc)) { .SUCCESS => {}, - // TODO(jared): parse errno - else => return MountError.Todo, + else => |err| return posix.unexpectedErrno(err), } } -/// Does initial system setup and mounts basic psuedo-filesystems. -pub fn setupSystem() !void { - try fs.makeDirAbsolute("/proc"); +/// Mounts basic psuedo-filesystems (/dev, /proc, /sys, etc.). +pub fn mountPseudoFilesystems() !void { + try std.fs.cwd().makePath("/proc"); try mountPseudoFs("/proc", "proc", system.MS.NOSUID | system.MS.NODEV | system.MS.NOEXEC); - try fs.makeDirAbsolute("/sys"); + try std.fs.cwd().makePath("/sys"); try mountPseudoFs("/sys", "sysfs", system.MS.NOSUID | system.MS.NODEV | system.MS.NOEXEC | system.MS.RELATIME); try mountPseudoFs("/sys/kernel/security", "securityfs", system.MS.NOSUID | system.MS.NODEV | system.MS.NOEXEC | system.MS.RELATIME); try mountPseudoFs("/sys/kernel/debug", "debugfs", system.MS.NOSUID | system.MS.NODEV | system.MS.NOEXEC | system.MS.RELATIME); - // we use CONFIG_DEVTMPFS, so we don't need to create /dev + try std.fs.cwd().makePath("/dev"); try mountPseudoFs("/dev", "devtmpfs", system.MS.SILENT | system.MS.NOSUID | system.MS.NOEXEC); - try fs.makeDirAbsolute("/run"); + try std.fs.cwd().makePath("/run"); try mountPseudoFs("/run", "tmpfs", system.MS.NOSUID | system.MS.NODEV); - try fs.makeDirAbsolute("/mnt"); - - try fs.symLinkAbsolute("/proc/self/fd/0", "/dev/stdin", .{}); - try fs.symLinkAbsolute("/proc/self/fd/1", "/dev/stdout", .{}); - try fs.symLinkAbsolute("/proc/self/fd/2", "/dev/stderr", .{}); + try std.fs.cwd().makePath("/mnt"); } const TCFLSH = linux_headers.TCFLSH; @@ -88,6 +78,7 @@ fn cfmakeraw(t: *posix.termios) void { } pub const TtyMode = enum { + no_echo, user_input, file_transfer_recv, file_transfer_send, @@ -97,7 +88,7 @@ pub const Tty = struct { fd: posix.fd_t, original: posix.termios, - pub fn reset(self: *const @This()) void { + pub fn reset(self: *@This()) void { // wait until everything is sent _ = system.tcdrain(self.fd); @@ -120,6 +111,9 @@ pub fn setupTty(fd: posix.fd_t, mode: TtyMode) !Tty { var termios = orig.original; switch (mode) { + .no_echo => { + termios.lflag.ECHO = false; + }, .user_input => { termios.cc[VINTR] = 3; // C-c termios.cc[VQUIT] = 28; // C-\ @@ -203,7 +197,11 @@ const SYSLOG_ACTION_UNREAD = 9; /// Read kernel logs (AKA syslog/dmesg). Caller is responsible for returned /// slice. -pub fn kernelLogs(allocator: std.mem.Allocator, filter: u8) ![]const u8 { +pub fn printKernelLogs( + allocator: std.mem.Allocator, + filter: u3, + writer: std.io.AnyWriter, +) !void { const bytes_available = system.syscall3(system.SYS.syslog, SYSLOG_ACTION_UNREAD, 0, 0); const buf = try allocator.alloc(u8, bytes_available); defer allocator.free(buf); @@ -221,19 +219,18 @@ pub fn kernelLogs(allocator: std.mem.Allocator, filter: u8) ![]const u8 { else => |err| return posix.unexpectedErrno(err), } - var logs = std.ArrayList(u8).init(allocator); var split = std.mem.splitScalar(u8, buf, '\n'); while (split.next()) |line| { - if (line.len <= 2) { + if (line.len <= 2 or line[0] != '<') { break; } - const log_level = try std.fmt.parseInt(u8, line[1..2], 10); - if (log_level <= filter) { - try logs.appendSlice(line[3..]); - try logs.append('\n'); + if (std.mem.indexOf(u8, line[0..5], ">")) |right_chevron_index| { + const syslog_prefix = try std.fmt.parseInt(u32, line[1..right_chevron_index], 10); + const log_level = 0x7 & syslog_prefix; // lower 3 bits + if (log_level <= filter) { + try writer.print("{s}\n", .{line[right_chevron_index + 1 ..]}); + } } } - - return logs.toOwnedSlice(); } diff --git a/src/tboot-bless-boot.zig b/src/tboot-bless-boot.zig index 93153b5..78e1588 100644 --- a/src/tboot-bless-boot.zig +++ b/src/tboot-bless-boot.zig @@ -2,7 +2,9 @@ const std = @import("std"); const clap = @import("clap"); -const bls = @import("./boot/bls.zig"); +const DiskBootLoader = @import("./boot/disk.zig"); + +const BlsEntryFile = DiskBootLoader.BlsEntryFile; const Error = error{ InvalidAction, @@ -31,7 +33,7 @@ fn markAsGood( allocator: std.mem.Allocator, parent_dir: std.fs.Dir, original_entry_filename: []const u8, - bls_entry_file: bls.BlsEntryFile, + bls_entry_file: BlsEntryFile, ) !void { if (bls_entry_file.tries_left) |tries_left| { _ = tries_left; @@ -50,7 +52,7 @@ fn markAsBad( allocator: std.mem.Allocator, parent_dir: std.fs.Dir, original_entry_filename: []const u8, - bls_entry_file: bls.BlsEntryFile, + bls_entry_file: BlsEntryFile, ) !void { const new_filename = b: { if (bls_entry_file.tries_done) |tries_done| { @@ -73,7 +75,7 @@ fn markAsBad( fn printStatus( original_entry_filename: []const u8, - bls_entry_file: bls.BlsEntryFile, + bls_entry_file: BlsEntryFile, ) !void { var stdout = std.io.getStdOut().writer(); @@ -119,7 +121,7 @@ fn findEntry( continue; } - const bls_entry = bls.BlsEntryFile.parse(dir_entry.name) catch |err| { + const bls_entry = BlsEntryFile.parse(dir_entry.name) catch |err| { std.log.debug( "failed to parse boot entry {s}: {}", .{ dir_entry.name, err }, diff --git a/src/tboot-loader.zig b/src/tboot-loader.zig index 917b4e3..8c98e2a 100644 --- a/src/tboot-loader.zig +++ b/src/tboot-loader.zig @@ -1,210 +1,294 @@ const std = @import("std"); const builtin = @import("builtin"); const posix = std.posix; -const system = std.posix.system; const linux_headers = @import("linux_headers"); -const log_level: std.log.Level = @enumFromInt(@import("build_options").loglevel); -const Autoboot = @import("./boot.zig").Autoboot; -const Client = @import("./client.zig").Client; -const Server = @import("./server.zig").Server; -const device = @import("./device.zig"); -const log = @import("./log.zig"); +const Autoboot = @import("./autoboot.zig"); +const BootLoader = @import("./boot/bootloader.zig"); +const Console = @import("./console.zig"); +const Device = @import("./device.zig"); +const DeviceWatcher = @import("./watch.zig"); +const DiskBootLoader = @import("./boot/disk.zig"); +const Log = @import("./log.zig"); +const XmodemBootLoader = @import("./boot/xmodem.zig"); const security = @import("./security.zig"); -const setupSystem = @import("./system.zig").setupSystem; -const setupTty = @import("./system.zig").setupTty; +const system = @import("./system.zig"); +const utils = @import("./utils.zig"); pub const std_options = .{ - .logFn = log.logFn, - .log_level = log_level, + .logFn = Log.logFn, + .log_level = .debug, // let the kernel do our filtering for us }; -const State = struct { - /// Master epoll file descriptor for driving the event loop. - epoll_fd: posix.fd_t, +const TbootLoader = @This(); - /// All children processes managed by us. - children: std.ArrayList(posix.pid_t), +var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); +var boot_loaders = std.ArrayList(*BootLoader).init(arena.allocator()); - pub fn init(allocator: std.mem.Allocator) !@This() { - return @This(){ - .epoll_fd = try posix.epoll_create1(linux_headers.EPOLL_CLOEXEC), - .children = std.ArrayList(posix.fd_t).init(allocator), - }; +/// Master epoll file descriptor for driving the event loop. +epoll: posix.fd_t, + +/// An eventfd file descriptor used to indicate to other threads the +/// program is done. +done: posix.fd_t, + +/// DeviceWatcher instance used to handle events of devices being added or +/// removed from the system. +device_watcher: DeviceWatcher, + +/// Console instance used to handle user input. +console: Console, + +autoboot: Autoboot = Autoboot.init(), + +/// A timerfd file descriptor that is re-used for a few different purposes (of +/// which these purposes do not have overlapping time windows): +/// 1. Indication that some period of time has elapsed and we have not seen any +/// new devices show up. +/// 2. A bootloader-configured timeout, usually to allow the user to interfere +/// with the boot process. +timer: posix.fd_t, + +/// General state of the program +state: enum { init, autobooting, user_input } = .init, + +pub fn init() !TbootLoader { + var self = TbootLoader{ + .epoll = try posix.epoll_create1(linux_headers.EPOLL_CLOEXEC), + .timer = try posix.timerfd_create(posix.CLOCK.MONOTONIC, .{}), + .device_watcher = try DeviceWatcher.init(), + .done = try posix.eventfd(0, 0), + .console = try Console.init(), + }; + + try posix.epoll_ctl( + self.epoll, + posix.system.EPOLL.CTL_ADD, + self.timer, + @constCast(&.{ + .data = .{ .fd = self.timer }, + .events = posix.system.EPOLL.IN, + }), + ); + + try posix.epoll_ctl( + self.epoll, + posix.system.EPOLL.CTL_ADD, + self.device_watcher.event, + @constCast(&.{ + .data = .{ .fd = self.device_watcher.event }, + .events = posix.system.EPOLL.IN, + }), + ); + + try posix.epoll_ctl( + self.epoll, + posix.system.EPOLL.CTL_ADD, + Console.IN, + @constCast(&.{ + .data = .{ .fd = Console.IN }, + .events = posix.system.EPOLL.IN, + }), + ); + + try self.newDeviceArmTimer(); + + return self; +} + +pub fn handleConsole(self: *TbootLoader) !?posix.RebootCommand { + if (self.state != .user_input) { + // disarm the timer to prevent autoboot from taking over + try posix.epoll_ctl(self.epoll, posix.system.EPOLL.CTL_DEL, self.timer, null); + + self.state = .user_input; } - pub fn deinit(self: *@This()) void { - defer self.children.deinit(); + const outcome = try self.console.handleStdin(boot_loaders.items) orelse return null; - for (self.children.items) |child| { - _ = posix.waitpid(child, 0); + switch (outcome) { + .reboot => return posix.RebootCommand.RESTART, + .poweroff => return posix.RebootCommand.POWER_OFF, + .kexec => return posix.RebootCommand.KEXEC, + } +} + +fn newDeviceArmTimer(self: *TbootLoader) !void { + try posix.timerfd_settime(self.timer, .{}, &.{ + // oneshot + .it_interval = .{ .tv_sec = 0, .tv_nsec = 0 }, + // consider settled after 2 seconds without any new events + .it_value = .{ .tv_sec = 2, .tv_nsec = 0 }, + }, null); +} + +const all_bootloaders = .{ DiskBootLoader, XmodemBootLoader }; +pub fn handleDevice(self: *TbootLoader) !void { + // consume eventfd value + { + var uevent_val: u64 = undefined; + _ = try posix.read(self.device_watcher.event, std.mem.asBytes(&uevent_val)); + } + + o: while (self.device_watcher.nextEvent()) |event| { + const device = event.device; + + switch (event.action) { + .add => { + std.log.debug( + "new {s} device added", + .{@tagName(event.device.subsystem)}, + ); + + inline for (all_bootloaders) |bootloader_type| { + // If match() returns null, the device is not a match for + // that specific boot loader. If match() returns a non-null + // value, the device is a match with that values priority, + // where a lower number is a higher priority. + const priority: ?u8 = bootloader_type.match(&device); + + if (priority) |new_priority| { + const new_bootloader = try arena.allocator().create(BootLoader); + new_bootloader.* = try BootLoader.init( + bootloader_type, + device, + new_priority, + arena.allocator(), + ); + + for (boot_loaders.items, 0..) |boot_loader, index| { + if (new_bootloader.priority < boot_loader.priority) { + try boot_loaders.insert(index, new_bootloader); + continue :o; + } + } + + // Append to the end if we did not find an appropriate + // place to insert the bootloader prior to the end. + try boot_loaders.append(new_bootloader); + } + } + }, + .remove => { + for (boot_loaders.items, 0..) |boot_loader, index| { + if (std.meta.eql(boot_loader.device, event.device)) { + var removed_boot_loader = boot_loaders.orderedRemove(index); + removed_boot_loader.deinit(); + } + } + }, } + } - posix.close(self.epoll_fd); + if (self.state == .init) { + try self.newDeviceArmTimer(); } -}; +} -fn runEventLoop(allocator: std.mem.Allocator) !posix.RebootCommand { - var state = try State.init(allocator); - defer state.deinit(); - - var device_watcher = try device.DeviceWatcher.init(); - try device_watcher.register(state.epoll_fd); - defer device_watcher.deinit(); - - var server = try Server.init(allocator); - try server.registerSelf(state.epoll_fd); - // NOTE: This must be _after_ state.deinit() so that we are ensured that - // the server is deinitialized _before_ state deinit is called, since state - // deinit waits for all children to exit, which will only succeed after the - // server's connections to each client has been closed. - // - // TODO(jared): Just put the server - // on the state instance so we can encode this ordering properly in a - // single function. - defer server.deinit(); - - const active_consoles = try device.findActiveConsoles(allocator); - defer allocator.free(active_consoles); - - // Spawn off clients - { - const argv_buf = try allocator.allocSentinel(?[*:0]const u8, 1, null); - defer allocator.free(argv_buf); - const argv0 = try allocator.dupeZ(u8, "/proc/self/exe"); - defer allocator.free(argv0); - argv_buf[0] = argv0.ptr; - const envp_buf = try allocator.allocSentinel(?[*:0]u8, 0, null); - defer allocator.free(envp_buf); - - std.log.debug("using {} console(s)", .{active_consoles.len}); - for (active_consoles) |fd| { - const pid = try posix.fork(); - if (pid == 0) { - try posix.dup2(fd, posix.STDIN_FILENO); - try posix.dup2(fd, posix.STDOUT_FILENO); - try posix.dup2(fd, posix.STDERR_FILENO); - - const err = posix.execveZ(argv_buf.ptr[0].?, argv_buf.ptr, envp_buf); - std.log.err("failed to spawn console process: {}", .{err}); - } else { - try state.children.append(pid); +fn handleTimer(self: *TbootLoader) ?posix.RebootCommand { + if (self.state == .init) { + std.log.info("devices settled", .{}); + + self.state = .autobooting; + } else { + std.debug.assert(self.state == .autobooting); + + std.log.debug("autoboot timeout", .{}); + } + + if (self.autoboot.run(&boot_loaders, self.timer)) |maybe_event| { + if (maybe_event) |outcome| { + switch (outcome) { + .reboot => return posix.RebootCommand.RESTART, + .poweroff => return posix.RebootCommand.POWER_OFF, + .kexec => return posix.RebootCommand.KEXEC, } } + } else |err| { + std.log.err("failed to run autoboot: {}", .{err}); } - var autoboot = try Autoboot.init(); - try autoboot.register(state.epoll_fd); - defer autoboot.deinit(); + return null; +} + +pub fn deinit(self: *TbootLoader) void { + // Notify all threads that we are done. + _ = posix.write(self.done, std.mem.asBytes(&@as(u64, 1))) catch {}; + + self.console.deinit(); - try device_watcher.startSettleTimer(); + self.device_watcher.deinit(); - var user_presence = false; + posix.close(self.timer); + posix.close(self.done); + posix.close(self.epoll); +} - // main event loop +fn run(self: *TbootLoader) !posix.RebootCommand { while (true) { - const max_events = 8; - var events = [_]system.epoll_event{undefined} ** max_events; + var events = [_]posix.system.epoll_event{undefined} ** (2 << 4); - const n_events = posix.epoll_wait(state.epoll_fd, &events, -1); + const n_events = posix.epoll_wait(self.epoll, &events, -1); var i_event: usize = 0; while (i_event < n_events) : (i_event += 1) { const event = events[i_event]; - if (event.data.fd == device_watcher.settle_fd) { - std.log.info("devices settled", .{}); - if (!user_presence) { - try autoboot.start(); - } - } else if (event.data.fd == autoboot.ready_fd) { - try autoboot.deregister(state.epoll_fd); - if (try autoboot.finish()) |outcome| { + if (event.data.fd == Console.IN) { + if (try self.handleConsole()) |outcome| { return outcome; - } else { - std.log.info("nothing to boot", .{}); - server.forceShell(); - } - } else if (event.data.fd == device_watcher.nl_fd) { - device_watcher.handleNewEvent() catch |err| { - std.log.err("failed to handle new device: {}", .{err}); - }; - } else if (event.data.fd == server.inner.stream.handle) { - const conn = try server.inner.accept(); - std.log.debug("new client connected", .{}); - try server.registerClient(state.epoll_fd, conn.stream); - } else { - if (!user_presence) { - autoboot.stop(); - user_presence = true; - std.log.info("user presence detected", .{}); } - - if (try server.handleNewEvent(event)) |outcome| { - std.log.debug("got outcome {}", .{outcome}); + } else if (event.data.fd == self.device_watcher.event) { + try self.handleDevice(); + } else if (event.data.fd == self.timer) { + if (self.handleTimer()) |outcome| { return outcome; } + } else { + std.debug.panic("unknown event: {}", .{event}); } } } } -fn consoleClient() !void { - var tty = try setupTty(posix.STDIN_FILENO, .user_input); - defer tty.reset(); - - try log.initLogger(.Client); - defer log.deinitLogger(); - - var client = try Client.init(); - defer client.deinit(); - - try client.run(); -} - -fn pid1() !void { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - const allocator = arena.allocator(); +pub fn main() !void { + defer { + for (boot_loaders.items) |boot_loader| { + boot_loader.deinit(); + } + arena.deinit(); + } - try setupSystem(); + { + try system.mountPseudoFilesystems(); - try log.initLogger(.Server); - defer log.deinitLogger(); + var tboot_loader = try TbootLoader.init(); + defer tboot_loader.deinit(); - const cmdline = cmdline: { - var cmdline_file = try std.fs.cwd().openFile("/proc/self/cmdline", .{ .mode = .read_only }); - defer cmdline_file.close(); - const cmdline_raw = try cmdline_file.readToEndAlloc(allocator, 2048); - defer allocator.free(cmdline_raw); - const buf = try allocator.dupe(u8, cmdline_raw); - _ = std.mem.replace(u8, cmdline_raw, &.{0}, " ", buf); - break :cmdline buf; - }; - defer allocator.free(cmdline); - std.log.info("{s}", .{cmdline}); + // We should be able to log right after we've initialized the device + // watcher. + try Log.init(); + defer Log.deinit(); - std.log.info("tinyboot started", .{}); + var device_watch_thread = try std.Thread.spawn(.{}, DeviceWatcher.watch, .{ + &tboot_loader.device_watcher, + tboot_loader.done, + }); + defer device_watch_thread.join(); - security.initializeSecurity(allocator) catch |err| { - std.log.warn("failed to initialize secure boot: {}", .{err}); - }; + std.log.info("tinyboot started", .{}); - const reboot_cmd = try runEventLoop(allocator); + try security.initializeSecurity(); - try posix.reboot(reboot_cmd); -} + const reboot_cmd = try tboot_loader.run(); -pub fn main() !void { - switch (system.getpid()) { - 1 => { - pid1() catch |err| { - std.log.err("failed to boot: {any}\n", .{err}); - }; - - std.debug.panic("epic failure :/", .{}); - }, - else => try consoleClient(), + try posix.reboot(reboot_cmd); } + + // Sleep forever without hammering the CPU, waiting for the kernel to + // reboot. + var futex = std.atomic.Value(u32).init(0); + while (true) std.Thread.Futex.wait(&futex, 0); + unreachable; } diff --git a/src/tboot-nixos-install.zig b/src/tboot-nixos-install.zig index b1800b2..daa8c5d 100644 --- a/src/tboot-nixos-install.zig +++ b/src/tboot-nixos-install.zig @@ -3,11 +3,13 @@ const path = std.fs.path; const clap = @import("clap"); -const bls = @import("./boot/bls.zig"); +const DiskBootLoader = @import("./boot/disk.zig"); const signFile = @import("./tboot-sign.zig").signFile; const BootSpecV1 = @import("./bootspec.zig").BootSpecV1; const BootJson = @import("./bootspec.zig").BootJson; +const BlsEntryFile = DiskBootLoader.BlsEntryFile; + fn ensureFilesystemState( esp: std.fs.Dir, args: *const Args, @@ -180,7 +182,7 @@ fn installGeneration( continue; } - const existing_entry = bls.BlsEntryFile.parse(dir_entry.name) catch continue; + const existing_entry = BlsEntryFile.parse(dir_entry.name) catch continue; if (std.mem.eql(u8, existing_entry.name, entry_name)) { std.log.debug("entry {s} already installed", .{entry_name}); @@ -288,7 +290,7 @@ pub fn main() !void { } if (res.positionals.len != 1 or res.args.@"private-key" == null or res.args.@"public-key" == null) { - try diag.report(stderr, error.InvalidArgs); + try diag.report(stderr, error.InvalidArgument); try clap.usage(stderr, clap.Help, ¶ms); return; } diff --git a/src/tboot-sign.zig b/src/tboot-sign.zig index 92c8e4f..da992fc 100644 --- a/src/tboot-sign.zig +++ b/src/tboot-sign.zig @@ -305,7 +305,7 @@ pub fn main() !void { } if (res.positionals.len != 2 or res.args.@"private-key" == null or res.args.@"public-key" == null) { - try diag.report(stderr, error.InvalidArgs); + try diag.report(stderr, error.InvalidArgument); try clap.usage(stderr, clap.Help, ¶ms); return; } diff --git a/src/test.zig b/src/test.zig index d7664d9..45a6ba5 100644 --- a/src/test.zig +++ b/src/test.zig @@ -1,19 +1,27 @@ -// all non-entrypoint files, populates @This() for below -pub const bls = @import("./boot/bls.zig"); -pub const boot = @import("./boot.zig"); -pub const bootspec = @import("./bootspec.zig"); -pub const client = @import("./client.zig"); -pub const device = @import("./device.zig"); -pub const filesystem = @import("./disk/filesystem.zig"); -pub const log = @import("./log.zig"); -pub const message = @import("./message.zig"); -pub const partition_table = @import("./disk/partition_table.zig"); -pub const server = @import("./server.zig"); -pub const system = @import("./system.zig"); -pub const tmp = @import("./tmp.zig"); -pub const xmodem = @import("./boot/xmodem.zig"); - test { - // recursively test all imported files - @import("std").testing.refAllDeclsRecursive(@This()); + _ = @import("./autoboot.zig"); + _ = @import("./boot/bootloader.zig"); + _ = @import("./boot/disk.zig"); + _ = @import("./boot/xmodem.zig"); + _ = @import("./bootspec.zig"); + _ = @import("./console.zig"); + _ = @import("./cpio/main.zig"); + _ = @import("./device.zig"); + _ = @import("./disk/filesystem.zig"); + _ = @import("./disk/partition_table.zig"); + _ = @import("./kobject.zig"); + _ = @import("./log.zig"); + _ = @import("./runner.zig"); + _ = @import("./security.zig"); + _ = @import("./system.zig"); + _ = @import("./tboot-bless-boot-generator.zig"); + _ = @import("./tboot-bless-boot.zig"); + _ = @import("./tboot-loader.zig"); + _ = @import("./tboot-nixos-install.zig"); + _ = @import("./tboot-sign.zig"); + _ = @import("./test.zig"); + _ = @import("./tmpdir.zig"); + _ = @import("./utils.zig"); + _ = @import("./watch.zig"); + _ = @import("./xmodem.zig"); } diff --git a/src/tmp.zig b/src/tmp.zig deleted file mode 100644 index ee03f0c..0000000 --- a/src/tmp.zig +++ /dev/null @@ -1,34 +0,0 @@ -const std = @import("std"); - -pub const TmpDir = struct { - dir: std.fs.Dir, - parent_dir: std.fs.Dir, - sub_path: [sub_path_len]u8, - - const random_bytes_count = 12; - const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count); - - pub fn cleanup(self: *TmpDir) void { - self.dir.close(); - self.parent_dir.deleteTree(&self.sub_path) catch {}; - self.parent_dir.close(); - self.* = undefined; - } -}; - -pub fn tmpDir(opts: std.fs.Dir.OpenDirOptions) !TmpDir { - var random_bytes: [TmpDir.random_bytes_count]u8 = undefined; - std.crypto.random.bytes(&random_bytes); - var sub_path: [TmpDir.sub_path_len]u8 = undefined; - _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); - - const parent_dir = try std.fs.cwd().makeOpenPath("/run", .{}); - - const dir = try parent_dir.makeOpenPath(&sub_path, opts); - - return .{ - .dir = dir, - .parent_dir = parent_dir, - .sub_path = sub_path, - }; -} diff --git a/src/tmpdir.zig b/src/tmpdir.zig new file mode 100644 index 0000000..e876cfc --- /dev/null +++ b/src/tmpdir.zig @@ -0,0 +1,34 @@ +const std = @import("std"); + +const random_bytes_count = 12; +const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count); + +pub const TmpDir = @This(); + +dir: std.fs.Dir, +parent_dir: std.fs.Dir, +sub_path: [sub_path_len]u8, + +pub fn cleanup(self: *TmpDir) void { + self.dir.close(); + self.parent_dir.deleteTree(&self.sub_path) catch {}; + self.parent_dir.close(); + self.* = undefined; +} + +pub fn create(opts: std.fs.Dir.OpenDirOptions) !TmpDir { + var random_bytes: [TmpDir.random_bytes_count]u8 = undefined; + std.crypto.random.bytes(&random_bytes); + var sub_path: [sub_path_len]u8 = undefined; + _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); + + const parent_dir = try std.fs.cwd().makeOpenPath("/run", .{}); + + const dir = try parent_dir.makeOpenPath(&sub_path, opts); + + return .{ + .dir = dir, + .parent_dir = parent_dir, + .sub_path = sub_path, + }; +} diff --git a/src/utils.zig b/src/utils.zig new file mode 100644 index 0000000..6eb6bfe --- /dev/null +++ b/src/utils.zig @@ -0,0 +1,12 @@ +const std = @import("std"); +const posix = std.posix; + +pub fn enumFromStr(T: anytype, value: []const u8) !T { + inline for (std.meta.fields(T)) |field| { + if (std.mem.eql(u8, field.name, value)) { + return @field(T, field.name); + } + } + + return error.NotFound; +} diff --git a/src/watch.zig b/src/watch.zig new file mode 100644 index 0000000..52dc79c --- /dev/null +++ b/src/watch.zig @@ -0,0 +1,303 @@ +const std = @import("std"); +const posix = std.posix; +const path = std.fs.path; + +const Device = @import("./device.zig"); +const kobject = @import("./kobject.zig"); +const utils = @import("./utils.zig"); + +const linux_headers = @import("linux_headers"); + +const DeviceWatcher = @This(); + +pub const Event = struct { + action: kobject.Action, + device: Device, +}; + +const Queue = std.DoublyLinkedList(Event); + +fn makedev(major: u32, minor: u32) u32 { + return std.math.shl(u32, major & 0xfffff000, 32) | + std.math.shl(u32, major & 0x00000fff, 8) | + std.math.shl(u32, minor & 0xffffff00, 12) | + std.math.shl(u32, minor & 0x000000ff, 0); +} + +const NodeType = enum { block, char }; + +// busybox uses these buffer sizes +const USER_RCVBUF = 3 * 1024; +const KERN_RCVBUF = 128 * 1024 * 1024; + +arena: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator), + +block_dir: std.fs.Dir, +char_dir: std.fs.Dir, + +/// Netlink socket fd for subscribing to new device events. +nl_fd: posix.fd_t, + +/// An eventfd file descriptor used to indicate when new device events are +/// available on the device queue. +event: posix.fd_t, + +mutex: std.Thread.Mutex = .{}, +queue: Queue = .{}, + +pub fn init() !DeviceWatcher { + var self = DeviceWatcher{ + .event = try posix.eventfd(0, 0), + .block_dir = try std.fs.cwd().makeOpenPath("/dev/block", .{}), + .char_dir = try std.fs.cwd().makeOpenPath("/dev/char", .{}), + .nl_fd = try posix.socket( + posix.system.AF.NETLINK, + posix.system.SOCK.DGRAM, + posix.system.NETLINK.KOBJECT_UEVENT, + ), + }; + + try self.scanAndCreateExistingDevices(); + + return self; +} + +pub fn watch(self: *DeviceWatcher, done: posix.fd_t) !void { + defer self.deinit(); + + try posix.setsockopt( + self.nl_fd, + posix.SOL.SOCKET, + posix.SO.RCVBUF, + &std.mem.toBytes(@as(c_int, KERN_RCVBUF)), + ); + + try posix.setsockopt( + self.nl_fd, + posix.SOL.SOCKET, + posix.SO.RCVBUFFORCE, + &std.mem.toBytes(@as(c_int, KERN_RCVBUF)), + ); + + const nls = posix.sockaddr.nl{ + .groups = 1, // KOBJECT_UEVENT groups bitmask must be 1 + .pid = @bitCast(posix.system.getpid()), + }; + try posix.bind(self.nl_fd, @ptrCast(&nls), @sizeOf(posix.sockaddr.nl)); + + const epoll_fd = try posix.epoll_create1(linux_headers.EPOLL_CLOEXEC); + defer posix.close(epoll_fd); + + try posix.epoll_ctl( + epoll_fd, + posix.system.EPOLL.CTL_ADD, + self.nl_fd, + @constCast(&.{ + .data = .{ .fd = self.nl_fd }, + .events = posix.system.EPOLL.IN, + }), + ); + + try posix.epoll_ctl( + epoll_fd, + posix.system.EPOLL.CTL_ADD, + done, + @constCast(&.{ + .data = .{ .fd = done }, + .events = posix.system.EPOLL.IN, + }), + ); + + while (true) { + var events = [_]posix.system.epoll_event{undefined} ** (2 << 4); + + const n_events = posix.epoll_wait(epoll_fd, &events, -1); + + var i_event: usize = 0; + while (i_event < n_events) : (i_event += 1) { + const event = events[i_event]; + + if (event.data.fd == done) { + std.log.debug("done watching devices", .{}); + return; + } else if (event.data.fd == self.nl_fd) { + self.handleNewEvent() catch |err| { + std.log.err("failed to handle new device: {}", .{err}); + }; + } else { + std.debug.panic("unknown event: {}", .{event}); + } + } + } +} + +fn handleNewEvent(self: *DeviceWatcher) !void { + var recv_bytes: [USER_RCVBUF]u8 = undefined; + + const bytes_read = try posix.read(self.nl_fd, &recv_bytes); + + const event = kobject.parseUeventKobjectContents( + recv_bytes[0..bytes_read], + ) orelse return; + + switch (event.action) { + .add => try self.addDevice(event), + .remove => try self.removeDevice(event), + } +} + +pub fn nextEvent(self: *DeviceWatcher) ?Event { + self.mutex.lock(); + defer self.mutex.unlock(); + + const node = self.queue.pop() orelse return null; + defer self.arena.allocator().destroy(node); + + return node.data; +} + +pub fn deinit(self: *DeviceWatcher) void { + defer self.arena.deinit(); + + self.block_dir.close(); + self.char_dir.close(); + + posix.close(self.nl_fd); +} + +fn mknod(self: *DeviceWatcher, node_type: NodeType, major: u32, minor: u32) !void { + var buf: [10]u8 = undefined; + const device = try std.fmt.bufPrintZ(&buf, "{}:{}", .{ major, minor }); + + const rc = posix.system.mknodat( + switch (node_type) { + .block => self.block_dir.fd, + .char => self.char_dir.fd, + }, + device, + switch (node_type) { + .block => posix.system.S.IFBLK, + .char => posix.system.S.IFCHR, + }, + makedev(major, minor), + ); + + switch (posix.errno(rc)) { + .SUCCESS => {}, + .EXIST => {}, + else => |err| return posix.unexpectedErrno(err), + } +} + +// stat() on any uevent file always returns 4096 +const UEVENT_FILE_SIZE = 4096; + +/// Scan sysfs and create all nodes of interest that currently exist on the +/// system. +pub fn scanAndCreateExistingDevices(self: *DeviceWatcher) !void { + inline for (std.meta.fields(Device.Subsystem)) |field| { + try self.scanAndCreateExistingDevicesForSubsystem(field.name); + } +} + +pub fn scanAndCreateExistingDevicesForSubsystem( + self: *DeviceWatcher, + comptime subsystem: []const u8, +) !void { + var subsystem_dir = std.fs.cwd().openDir( + "/sys/class/" ++ subsystem, + .{ .iterate = true }, + ) catch return; // don't hard fail if the subsystem does not exist + defer subsystem_dir.close(); + + var iter = subsystem_dir.iterate(); + + while (try iter.next()) |entry| { + // TODO(jared): Do we have any reason to believe all the files + // won't be symlinks? + if (entry.kind != .sym_link) { + continue; + } + + var device_dir = subsystem_dir.openDir(entry.name, .{}) catch continue; + defer device_dir.close(); + + var device_uevent = device_dir.openFile("uevent", .{}) catch continue; + defer device_uevent.close(); + + var buf: [UEVENT_FILE_SIZE]u8 = undefined; + const n_read = device_uevent.readAll(&buf) catch continue; + + const device = kobject.parseUeventFileContents( + @field(Device.Subsystem, subsystem), + buf[0..n_read], + ) orelse continue; + + try self.addDevice(.{ .action = .add, .device = device }); + } +} + +fn addDevice(self: *DeviceWatcher, event: Event) !void { + switch (event.device.type) { + .node => |node| { + const major, const minor = node; + try self.mknod(switch (event.device.subsystem) { + .block => .block, + else => .char, + }, major, minor); + }, + else => {}, + } + + { + self.mutex.lock(); + defer self.mutex.unlock(); + + const node = try self.arena.allocator().create(Queue.Node); + node.* = .{ .data = event }; + + self.queue.append(node); + + _ = try posix.write(self.event, std.mem.asBytes(&@as(u64, 1))); + } +} + +fn removeDevice(self: *DeviceWatcher, event: Event) !void { + switch (event.device.type) { + .node => |node| { + const major, const minor = node; + try self.removeNode(switch (event.device.subsystem) { + .block => .block, + else => .char, + }, major, minor); + }, + else => {}, + } + + { + self.mutex.lock(); + defer self.mutex.unlock(); + + const node = try self.arena.allocator().create(Queue.Node); + + node.* = .{ .data = event }; + self.queue.append(node); + + _ = try posix.write(self.event, std.mem.asBytes(&@as(u64, 1))); + } +} + +fn removeNode(self: *DeviceWatcher, node_type: NodeType, major: u32, minor: u32) !void { + var buf: [10]u8 = undefined; + const device = try std.fmt.bufPrint(&buf, "{}:{}", .{ major, minor }); + + var dir = switch (node_type) { + .block => self.block_dir, + .char => self.char_dir, + }; + + dir.deleteFile(device) catch |err| switch (err) { + error.FileNotFound => {}, + else => return err, + }; +} diff --git a/src/xmodem.zig b/src/xmodem.zig index a48765c..3bc4696 100644 --- a/src/xmodem.zig +++ b/src/xmodem.zig @@ -364,7 +364,7 @@ pub fn main() !void { } if (res.positionals.len != 1 or res.args.file == null or res.args.tty == null) { - try diag.report(stderr, error.InvalidArgs); + try diag.report(stderr, error.InvalidArgument); try clap.usage(std.io.getStdErr().writer(), clap.Help, ¶ms); return; }