├── .git-blame-ignore-revs ├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── build.zig ├── build.zig.zon ├── deps ├── mbedtls │ ├── build.zig │ └── build.zig.zon └── zstd │ ├── build.zig │ └── build.zig.zon ├── examples ├── chromebook-asurada │ └── default.nix ├── chromebook-kukui │ └── default.nix ├── chromebook-trogdor │ └── default.nix ├── chromebook-x86_64 │ └── default.nix ├── qemu-arm │ └── default.nix └── x86_64-efi │ └── default.nix ├── flake.lock ├── flake.nix ├── modules ├── nixos │ └── default.nix └── standalone │ ├── compress-firmware.nix │ ├── default.nix │ ├── kernel-configs │ ├── aarch64.nix │ ├── arm.nix │ ├── chromebook.nix │ ├── debug.nix │ ├── default.nix │ ├── efi.nix │ ├── generic.nix │ ├── ima.nix │ ├── network.nix │ ├── platform.nix │ └── x86_64.nix │ └── linux │ ├── default.nix │ └── tpm-probe.patch ├── package.nix ├── src ├── autoboot.zig ├── boot │ ├── bootloader.zig │ ├── disk.zig │ └── ymodem.zig ├── bootspec.zig ├── console.zig ├── cpio.zig ├── device.zig ├── disk │ ├── filesystem.zig │ ├── gpt.zig │ └── mbr.zig ├── fdt.zig ├── kexec │ ├── arm.zig │ └── kexec.zig ├── kobject.zig ├── log.zig ├── mbedtls.zig ├── pkcs7.zig ├── runner.zig ├── security.zig ├── system.zig ├── tboot-bless-boot-generator.zig ├── tboot-bless-boot.zig ├── tboot-efi-stub.zig ├── tboot-initrd.zig ├── tboot-keygen.zig ├── tboot-loader.zig ├── tboot-nixos-install.zig ├── tboot-sign.zig ├── test.zig ├── tmpdir.zig ├── utils.zig ├── vpd.zig ├── watch.zig ├── ymodem.zig └── zstd.zig └── tests ├── disk └── default.nix ├── module.nix └── ymodem └── default.nix /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | 9ab22da9b3bb71dcba6df82e8a09523c5c93f7f0 2 | 2efde1b2275e3b286176d76b70bf91d0583a0b85 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.der 2 | *.iso 3 | *.pem 4 | *.qcow2 5 | .nixos-test-history 6 | .zig-cache 7 | /.direnv 8 | result* 9 | zig-out 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jared Baur 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tinyboot 2 | 3 | tinyboot is a kexec-based bootloader 4 | 5 | ## Hacking 6 | 7 | Make a directory (e.g. `/tmp/tboot`) and fill it with [Boot Loader Spec](https://uapi-group.org/specifications/specs/boot_loader_specification/#the-boot-loader-specification) compatible files. 8 | 9 | ```bash 10 | nix develop 11 | zig build run -- -drive if=virtio,format=raw,file=fat:rw:/tmp/tboot 12 | ``` 13 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - documentation 2 | - make recovery firmware allow booting non-signed kernels 3 | - network booting 4 | - use non-volatile storage for configuration of boot order, etc. 5 | - use CONFIG_MFD_CROS_EC_DEV & CONFIG_CROS_EC_VBC in linux 6 | - fuzz testing (outside of normal build and test procedure) 7 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .tinyboot, 3 | .fingerprint = 0xf283d479be749c48, 4 | .version = "0.1.0", 5 | .minimum_zig_version = "0.14.0", 6 | .paths = .{ "build.zig", "build.zig.zon", "src" }, 7 | .dependencies = .{ 8 | .clap = .{ 9 | .url = "https://github.com/Hejsil/zig-clap/archive/e47028deaefc2fb396d3d9e9f7bd776ae0b2a43a.tar.gz", 10 | .hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0", 11 | }, 12 | .mbedtls = .{ .path = "./deps/mbedtls/" }, 13 | .zstd = .{ .path = "./deps/zstd/" }, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /deps/mbedtls/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) !void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const upstream = b.dependency("mbedtls", .{}); 8 | const lib = b.addStaticLibrary(.{ 9 | .name = "mbedtls", 10 | .target = target, 11 | .optimize = optimize, 12 | }); 13 | 14 | lib.installHeadersDirectory(upstream.path("include"), "", .{}); 15 | 16 | lib.linkLibC(); 17 | 18 | if (lib.rootModuleTarget().isMinGW()) { 19 | lib.linkSystemLibrary("ws2_32"); // inet_pton and friends 20 | lib.linkSystemLibrary("bcrypt"); // BCryptGenRandom 21 | } 22 | 23 | lib.addIncludePath(upstream.path("include")); 24 | lib.addCSourceFiles(.{ 25 | .root = upstream.path(""), 26 | .files = &.{ 27 | "library/aes.c", 28 | "library/aesce.c", 29 | "library/aesni.c", 30 | "library/aria.c", 31 | "library/asn1parse.c", 32 | "library/asn1write.c", 33 | "library/base64.c", 34 | "library/bignum.c", 35 | "library/bignum_core.c", 36 | "library/bignum_mod.c", 37 | "library/bignum_mod_raw.c", 38 | "library/block_cipher.c", 39 | "library/camellia.c", 40 | "library/ccm.c", 41 | "library/chacha20.c", 42 | "library/chachapoly.c", 43 | "library/cipher.c", 44 | "library/cipher_wrap.c", 45 | "library/cmac.c", 46 | "library/constant_time.c", 47 | "library/ctr_drbg.c", 48 | "library/debug.c", 49 | "library/des.c", 50 | "library/dhm.c", 51 | "library/ecdh.c", 52 | "library/ecdsa.c", 53 | "library/ecjpake.c", 54 | "library/ecp.c", 55 | "library/ecp_curves.c", 56 | "library/ecp_curves_new.c", 57 | "library/entropy.c", 58 | "library/entropy_poll.c", 59 | "library/error.c", 60 | "library/gcm.c", 61 | "library/hkdf.c", 62 | "library/hmac_drbg.c", 63 | "library/lmots.c", 64 | "library/lms.c", 65 | "library/md.c", 66 | "library/md5.c", 67 | "library/memory_buffer_alloc.c", 68 | "library/mps_reader.c", 69 | "library/mps_trace.c", 70 | "library/net_sockets.c", 71 | "library/nist_kw.c", 72 | "library/oid.c", 73 | "library/padlock.c", 74 | "library/pem.c", 75 | "library/pk.c", 76 | "library/pk_ecc.c", 77 | "library/pk_wrap.c", 78 | "library/pkcs12.c", 79 | "library/pkcs5.c", 80 | "library/pkcs7.c", 81 | "library/pkparse.c", 82 | "library/pkwrite.c", 83 | "library/platform.c", 84 | "library/platform_util.c", 85 | "library/poly1305.c", 86 | "library/psa_crypto.c", 87 | "library/psa_crypto_aead.c", 88 | "library/psa_crypto_cipher.c", 89 | "library/psa_crypto_client.c", 90 | "library/psa_crypto_driver_wrappers_no_static.c", 91 | "library/psa_crypto_ecp.c", 92 | "library/psa_crypto_ffdh.c", 93 | "library/psa_crypto_hash.c", 94 | "library/psa_crypto_mac.c", 95 | "library/psa_crypto_pake.c", 96 | "library/psa_crypto_rsa.c", 97 | "library/psa_crypto_se.c", 98 | "library/psa_crypto_slot_management.c", 99 | "library/psa_crypto_storage.c", 100 | "library/psa_its_file.c", 101 | "library/psa_util.c", 102 | "library/ripemd160.c", 103 | "library/rsa.c", 104 | "library/rsa_alt_helpers.c", 105 | "library/sha1.c", 106 | "library/sha256.c", 107 | "library/sha3.c", 108 | "library/sha512.c", 109 | "library/ssl_cache.c", 110 | "library/ssl_ciphersuites.c", 111 | "library/ssl_client.c", 112 | "library/ssl_cookie.c", 113 | "library/ssl_debug_helpers_generated.c", 114 | "library/ssl_msg.c", 115 | "library/ssl_ticket.c", 116 | "library/ssl_tls.c", 117 | "library/ssl_tls12_client.c", 118 | "library/ssl_tls12_server.c", 119 | "library/ssl_tls13_client.c", 120 | "library/ssl_tls13_generic.c", 121 | "library/ssl_tls13_keys.c", 122 | "library/ssl_tls13_server.c", 123 | "library/threading.c", 124 | "library/timing.c", 125 | "library/version.c", 126 | "library/version_features.c", 127 | "library/x509.c", 128 | "library/x509_create.c", 129 | "library/x509_crl.c", 130 | "library/x509_crt.c", 131 | "library/x509_csr.c", 132 | "library/x509write.c", 133 | "library/x509write_crt.c", 134 | "library/x509write_csr.c", 135 | }, 136 | .flags = &.{}, 137 | }); 138 | 139 | b.installArtifact(lib); 140 | } 141 | -------------------------------------------------------------------------------- /deps/mbedtls/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .mbedtls, 3 | .version = "3.6.3", 4 | .paths = .{"build.zig"}, 5 | .dependencies = .{ 6 | .mbedtls = .{ 7 | .url = "https://github.com/MBED-TLS/mbedtls/archive/v3.6.3.1.tar.gz", 8 | .hash = "N-V-__8AAMrlvQKMzsBG9BReeFEEQ2phyoN7lE8U1xNUEvgP", 9 | }, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /deps/zstd/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) !void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const upstream = b.dependency("zstd", .{}); 8 | const lib = b.addStaticLibrary(.{ 9 | .name = "zstd", 10 | .target = target, 11 | .optimize = optimize, 12 | }); 13 | 14 | lib.installHeadersDirectory(upstream.path("lib"), "", .{}); 15 | 16 | lib.linkLibC(); 17 | 18 | switch (optimize) { 19 | .Debug, .ReleaseSafe => lib.bundle_compiler_rt = true, 20 | else => lib.root_module.strip = true, 21 | } 22 | 23 | lib.addCSourceFiles(.{ 24 | .root = upstream.path(""), 25 | .files = &.{ 26 | "lib/common/debug.c", 27 | "lib/common/entropy_common.c", 28 | "lib/common/error_private.c", 29 | "lib/common/fse_decompress.c", 30 | "lib/common/pool.c", 31 | "lib/common/threading.c", 32 | "lib/common/xxhash.c", 33 | "lib/common/zstd_common.c", 34 | "lib/compress/fse_compress.c", 35 | "lib/compress/hist.c", 36 | "lib/compress/huf_compress.c", 37 | "lib/compress/zstd_compress.c", 38 | "lib/compress/zstd_compress_literals.c", 39 | "lib/compress/zstd_compress_sequences.c", 40 | "lib/compress/zstd_compress_superblock.c", 41 | "lib/compress/zstd_double_fast.c", 42 | "lib/compress/zstd_fast.c", 43 | "lib/compress/zstd_lazy.c", 44 | "lib/compress/zstd_ldm.c", 45 | "lib/compress/zstd_opt.c", 46 | "lib/compress/zstd_preSplit.c", 47 | "lib/compress/zstdmt_compress.c", 48 | "lib/dictBuilder/cover.c", 49 | "lib/dictBuilder/divsufsort.c", 50 | "lib/dictBuilder/fastcover.c", 51 | "lib/dictBuilder/zdict.c", 52 | }, 53 | .flags = &.{"-DZSTD_MULTITHREAD"}, 54 | }); 55 | 56 | b.installArtifact(lib); 57 | } 58 | -------------------------------------------------------------------------------- /deps/zstd/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zstd, 3 | .version = "1.5.7", 4 | .paths = .{"build.zig"}, 5 | .dependencies = .{ 6 | .zstd = .{ 7 | .url = "https://github.com/facebook/zstd/archive/v1.5.7.tar.gz", 8 | .hash = "N-V-__8AAPZ7fwBg4JoCzM_0o2A8wxH2hsUUeiU1iuZv53L5", 9 | }, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /examples/chromebook-asurada/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | hostPlatform = "aarch64-linux"; 3 | platform.mediatek = true; 4 | chromebook = true; 5 | } 6 | -------------------------------------------------------------------------------- /examples/chromebook-kukui/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | hostPlatform = "aarch64-linux"; 3 | platform.mediatek = true; 4 | chromebook = true; 5 | } 6 | -------------------------------------------------------------------------------- /examples/chromebook-trogdor/default.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | { 3 | hostPlatform = "aarch64-linux"; 4 | platform.qualcomm = true; 5 | chromebook = true; 6 | linux.kconfig = with lib.kernel; { 7 | HID_GOOGLE_HAMMER = yes; 8 | I2C_CROS_EC_TUNNEL = yes; 9 | I2C_HID_OF = yes; 10 | KEYBOARD_CROS_EC = yes; 11 | LEDS_CLASS = yes; 12 | NEW_LEDS = yes; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /examples/chromebook-x86_64/default.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | { 3 | hostPlatform = "x86_64-linux"; 4 | chromebook = true; 5 | linux.consoles = [ "ttyS0,115200n8" ]; 6 | linux.kconfig = with lib.kernel; { 7 | PINCTRL_ALDERLAKE = yes; 8 | PINCTRL_TIGERLAKE = yes; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /examples/qemu-arm/default.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | { 3 | hostPlatform = "armv7l-linux"; 4 | platform.qemu = true; 5 | network = true; 6 | debug = true; 7 | linux.kconfig = with lib.kernel; { 8 | ARCH_VIRT = yes; 9 | SERIAL_AMBA_PL011 = yes; 10 | SERIAL_AMBA_PL011_CONSOLE = yes; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /examples/x86_64-efi/default.nix: -------------------------------------------------------------------------------- 1 | # zig build && \ 2 | # ukify build --stub ./zig-out/efi/tboot-efi-stub.efi --kernel ./result/bzImage --initrd ./result/tboot-loader.cpio.zst --output uki.efi && \ 3 | # uefi-run --boot uki.efi -- -m 2G -display none -serial mon:stdio 4 | { 5 | hostPlatform = "x86_64-linux"; 6 | platform.qemu = true; 7 | debug = true; 8 | efi = true; 9 | linux.consoles = [ "ttyS0,115200n8" ]; 10 | } 11 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1748370509, 6 | "narHash": "sha256-QlL8slIgc16W5UaI3w7xHQEP+Qmv/6vSNTpoZrrSlbk=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "4faa5f5321320e49a78ae7848582f684d64783e9", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A kexec-based bootloader"; 3 | 4 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 5 | 6 | outputs = 7 | inputs: 8 | let 9 | inherit (inputs.nixpkgs.lib) 10 | evalModules 11 | genAttrs 12 | mapAttrs 13 | recursiveUpdate 14 | ; 15 | in 16 | { 17 | nixosModules.default = { 18 | imports = [ ./modules/nixos ]; 19 | nixpkgs.overlays = [ inputs.self.overlays.default ]; 20 | }; 21 | 22 | overlays.default = final: prev: ({ 23 | tinyboot = final.callPackage ./package.nix { }; 24 | kernelPatches = prev.kernelPatches // { 25 | ima_tpm_early_init = { 26 | name = "ima_tpm_early_init"; 27 | patch = ./modules/standalone/linux/tpm-probe.patch; 28 | }; 29 | }; 30 | }); 31 | 32 | legacyPackages = genAttrs [ "armv7l-linux" "aarch64-linux" "x86_64-linux" ] ( 33 | system: 34 | import inputs.nixpkgs { 35 | inherit system; 36 | overlays = [ inputs.self.overlays.default ]; 37 | } 38 | ); 39 | 40 | packages = 41 | genAttrs 42 | [ 43 | "aarch64-linux" 44 | "x86_64-linux" 45 | ] 46 | ( 47 | system: 48 | mapAttrs ( 49 | name: _: 50 | let 51 | eval = evalModules { 52 | modules = [ 53 | ./modules/standalone 54 | ./examples/${name} 55 | ( 56 | { config, lib, ... }: 57 | let 58 | localSystem = lib.systems.elaborate system; 59 | crossSystem = lib.systems.elaborate config.hostPlatform; 60 | in 61 | { 62 | _module.args.pkgs = import inputs.nixpkgs ( 63 | { 64 | inherit localSystem; 65 | overlays = [ inputs.self.overlays.default ]; 66 | } 67 | // lib.optionalAttrs (!(lib.systems.equals localSystem crossSystem)) { 68 | crossSystem = config.hostPlatform; 69 | } 70 | ); 71 | } 72 | ) 73 | ]; 74 | }; 75 | in 76 | eval._module.args.pkgs.symlinkJoin { 77 | inherit name; 78 | paths = builtins.attrValues eval.config.build; 79 | } 80 | ) (builtins.readDir ./examples) 81 | ); 82 | 83 | devShells = mapAttrs (system: pkgs: { 84 | default = pkgs.mkShell { 85 | packages = [ 86 | pkgs.lldb 87 | pkgs.qemu 88 | pkgs.swtpm 89 | pkgs.zig_0_14 90 | ]; 91 | env.TINYBOOT_KERNEL = 92 | with inputs.self.checks.${system}.disk.nodes.machine.tinyboot.build; 93 | ''${linux}/${linux.kernelFile}''; 94 | }; 95 | }) inputs.self.legacyPackages; 96 | 97 | checks = mapAttrs ( 98 | _: pkgs: 99 | let 100 | cross = 101 | { 102 | "x86_64-linux" = "gnu64"; 103 | "aarch64-linux" = "aarch64-multiplatform"; 104 | "armv7l-linux" = "armv7l-hf-multiplatform"; 105 | } 106 | .${pkgs.stdenv.hostPlatform.system}; 107 | in 108 | { 109 | disk = pkgs.callPackage ./tests/disk { }; 110 | ymodem = pkgs.callPackage ./tests/ymodem { }; 111 | tinyboot = pkgs.tinyboot; 112 | tinybootCross = pkgs.pkgsCross.${cross}.tinyboot; 113 | } 114 | ) inputs.self.legacyPackages; 115 | 116 | hydraJobs = recursiveUpdate inputs.self.checks inputs.self.packages; 117 | }; 118 | } 119 | -------------------------------------------------------------------------------- /modules/nixos/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: 7 | let 8 | cfg = config.tinyboot; 9 | 10 | inherit (lib) 11 | getExe' 12 | mkEnableOption 13 | mkForce 14 | mkIf 15 | mkMerge 16 | mkOption 17 | optionals 18 | types 19 | ; 20 | in 21 | { 22 | options.tinyboot = mkOption { 23 | type = types.submoduleWith { 24 | specialArgs = { inherit pkgs; }; 25 | modules = [ 26 | ../standalone 27 | { 28 | options = { 29 | enable = mkEnableOption "tinyboot bootloader"; 30 | extraInstallCommands = mkOption { 31 | type = types.lines; 32 | default = ""; 33 | }; 34 | maxFailedBootAttempts = mkOption { 35 | type = types.ints.positive; 36 | default = 3; 37 | }; 38 | verifiedBoot = { 39 | enable = mkEnableOption "verified boot"; 40 | certificate = mkOption { type = types.path; }; 41 | privateKey = mkOption { type = types.path; }; 42 | }; 43 | }; 44 | } 45 | ]; 46 | }; 47 | default = { }; 48 | }; 49 | 50 | config = mkIf cfg.enable (mkMerge [ 51 | { 52 | assertions = [ 53 | { 54 | assertion = config.boot.bootspec.enable; 55 | message = "Bootloader install program depends on bootspec"; 56 | } 57 | ]; 58 | boot.kernelPatches = [ 59 | pkgs.kernelPatches.ima_tpm_early_init 60 | { 61 | name = "enable-ima"; 62 | patch = null; 63 | extraStructuredConfig = { 64 | IMA = lib.kernel.yes; 65 | IMA_DEFAULT_HASH_SHA256 = lib.kernel.yes; 66 | }; 67 | } 68 | ]; 69 | boot.loader.supportsInitrdSecrets = mkForce false; 70 | boot.loader.efi.canTouchEfiVariables = mkForce false; 71 | boot.loader.external = { 72 | enable = true; 73 | installHook = pkgs.writeScript "install-bootloaderspec.sh" '' 74 | #!${pkgs.runtimeShell} 75 | ${ 76 | toString ( 77 | [ 78 | (getExe' pkgs.tinyboot "tboot-nixos-install") 79 | "--esp-mnt=${config.boot.loader.efi.efiSysMountPoint}" 80 | "--timeout=${toString config.boot.loader.timeout}" 81 | "--max-tries=${toString cfg.maxFailedBootAttempts}" 82 | ] 83 | ++ optionals cfg.verifiedBoot.enable [ 84 | "--private-key=${cfg.verifiedBoot.privateKey}" 85 | "--certificate=${cfg.verifiedBoot.certificate}" 86 | ] 87 | ) 88 | } "$@" 89 | ${cfg.extraInstallCommands} 90 | ''; 91 | }; 92 | systemd.additionalUpstreamSystemUnits = [ "boot-complete.target" ]; 93 | systemd.generators.tboot-bless-boot-generator = getExe' pkgs.tinyboot "tboot-bless-boot-generator"; 94 | systemd.services.tboot-bless-boot = { 95 | description = "Mark the current boot loader entry as good"; 96 | documentation = [ "https://github.com/jmbaur/tinyboot" ]; 97 | requires = [ "boot-complete.target" ]; 98 | conflicts = [ "shutdown.target" ]; 99 | before = [ "shutdown.target" ]; 100 | after = [ 101 | "local-fs.target" 102 | "boot-complete.target" 103 | ]; 104 | unitConfig.DefaultDependencies = false; 105 | restartIfChanged = false; # Only run at boot 106 | serviceConfig = { 107 | Type = "oneshot"; 108 | RemainAfterExit = true; 109 | ExecStart = "${getExe' pkgs.tinyboot "tboot-bless-boot"} --esp-mnt=${config.boot.loader.efi.efiSysMountPoint} good"; 110 | }; 111 | }; 112 | } 113 | ]); 114 | } 115 | -------------------------------------------------------------------------------- /modules/standalone/compress-firmware.nix: -------------------------------------------------------------------------------- 1 | # This is adapted from https://github.com/nixos/nixpkgs/blob/5e4947a31bd21b33ccabcb9ff06d685b68d1e9c4/pkgs/build-support/kernel/compress-firmware.nix, 2 | # but dereferences all symlinks so that the zig build system is capable of 3 | # including all paths we want. See https://github.com/ziglang/zig/blob/53216d2f22053ca94a68f5da234038c01f73d60f/lib/std/Build/Step/WriteFile.zig#L232. 4 | 5 | { runCommand, lib }: 6 | 7 | firmwares: 8 | 9 | let 10 | compressor = { 11 | ext = "xz"; 12 | nativeBuildInputs = [ ]; 13 | cmd = file: target: ''xz -9c -T1 -C crc32 --lzma2=dict=2MiB "${file}" > "${target}"''; 14 | }; 15 | in 16 | 17 | runCommand "firmware-xz" 18 | { 19 | allowedRequisites = [ ]; 20 | inherit (compressor) nativeBuildInputs; 21 | } 22 | ( 23 | '' 24 | mkdir -p $out/lib/firmware 25 | '' 26 | + lib.concatLines ( 27 | map (firmware: '' 28 | (cd ${firmware} && find lib/firmware -type d -print0) | 29 | (cd $out && xargs -0 mkdir -v --) 30 | (cd ${firmware} && find lib/firmware -type f -print0) | 31 | (cd $out && xargs -0rtP "$NIX_BUILD_CORES" -n1 \ 32 | sh -c '${compressor.cmd "${firmware}/$1" "$1.${compressor.ext}"}' --) 33 | (cd ${firmware} && find lib/firmware -type l) | while read link; do 34 | target="$(readlink "${firmware}/$link")" 35 | if [ -f "${firmware}/$link" ]; then 36 | cp -vL -- "''${target/^${firmware}/$out}.${compressor.ext}" "$out/$link.${compressor.ext}" 37 | else 38 | echo HI 39 | cp -vrL -- "''${target/^${firmware}/$out}" "$out/$link" 40 | fi 41 | done 42 | 43 | find $out 44 | 45 | echo "Checking for broken symlinks:" 46 | find -L $out -type l -print -execdir false -- '{}' '+' 47 | '') firmwares 48 | ) 49 | ) 50 | -------------------------------------------------------------------------------- /modules/standalone/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: 7 | 8 | let 9 | inherit (lib) 10 | concatLines 11 | hasPrefix 12 | isDerivation 13 | isPath 14 | isString 15 | kernel 16 | mapAttrsToList 17 | mkEnableOption 18 | mkOption 19 | optionals 20 | types 21 | ; 22 | 23 | kconfigOption = mkOption { 24 | type = types.attrsOf types.anything; 25 | default = { }; 26 | apply = 27 | attrs: 28 | concatLines ( 29 | mapAttrsToList ( 30 | kconfOption: answer: 31 | let 32 | optionName = "CONFIG_${kconfOption}"; 33 | kconfLine = 34 | if answer ? freeform then 35 | if 36 | ( 37 | (isPath answer.freeform || isDerivation answer.freeform) 38 | || (isString answer.freeform && builtins.match "[0-9]+" answer.freeform == null) 39 | ) 40 | && !(hasPrefix "0x" answer.freeform) 41 | then 42 | "${optionName}=\"${answer.freeform}\"" 43 | else 44 | "${optionName}=${toString answer.freeform}" 45 | else 46 | assert answer ? tristate; 47 | assert answer.tristate != "m"; 48 | if answer.tristate == null then 49 | "# ${optionName} is not set" 50 | else 51 | "${optionName}=${toString answer.tristate}"; 52 | in 53 | kconfLine 54 | ) attrs 55 | ); 56 | }; 57 | in 58 | { 59 | imports = [ ./kernel-configs ]; 60 | 61 | options = { 62 | hostPlatform = mkOption { 63 | type = types.nullOr types.unspecified; 64 | default = null; 65 | }; 66 | 67 | build = mkOption { 68 | default = { }; 69 | type = types.submoduleWith { 70 | modules = [ { freeformType = with types; lazyAttrsOf (uniq unspecified); } ]; 71 | }; 72 | }; 73 | 74 | debug = mkEnableOption "debug"; 75 | 76 | network = mkEnableOption "network"; 77 | 78 | chromebook = mkEnableOption "chromebook"; 79 | 80 | efi = mkEnableOption "efi"; 81 | 82 | platform = mkOption { 83 | type = types.nullOr ( 84 | types.attrTag { 85 | mediatek = mkEnableOption "mediatek"; 86 | qemu = mkEnableOption "qemu"; 87 | qualcomm = mkEnableOption "qualcomm"; 88 | } 89 | ); 90 | default = null; 91 | }; 92 | 93 | linux = { 94 | package = mkOption { 95 | type = types.package; 96 | default = pkgs.callPackage ./linux { inherit (config.linux) kconfig; }; 97 | defaultText = "tinyboot provided kernel"; 98 | }; 99 | consoles = mkOption { 100 | type = types.listOf types.str; 101 | default = [ ]; 102 | }; 103 | kconfig = kconfigOption; 104 | firmware = mkOption { 105 | type = types.listOf types.package; 106 | default = [ ]; 107 | apply = pkgs.callPackage ./compress-firmware.nix { }; 108 | }; 109 | }; 110 | }; 111 | 112 | config = { 113 | linux.kconfig.CMDLINE = kernel.freeform ( 114 | toString ( 115 | [ "printk.devkmsg=on" ] 116 | ++ optionals config.debug [ "debug" ] 117 | ++ map (c: "console=${c}") config.linux.consoles 118 | ) 119 | ); 120 | 121 | build = { 122 | initrd = pkgs.tinyboot.override { firmwareDirectory = config.linux.firmware; }; 123 | linux = config.linux.package; 124 | }; 125 | }; 126 | } 127 | -------------------------------------------------------------------------------- /modules/standalone/kernel-configs/aarch64.nix: -------------------------------------------------------------------------------- 1 | { lib, pkgs, ... }: 2 | { 3 | linux.kconfig = lib.mkIf pkgs.stdenv.hostPlatform.isAarch64 ( 4 | with lib.kernel; 5 | { 6 | ARM64_AMU_EXTN = yes; 7 | ARM64_ERRATUM_1024718 = yes; 8 | ARM64_ERRATUM_1165522 = yes; 9 | ARM64_ERRATUM_1319367 = yes; 10 | ARM64_ERRATUM_1463225 = yes; 11 | ARM64_ERRATUM_1508412 = yes; 12 | ARM64_ERRATUM_1530923 = yes; 13 | ARM64_ERRATUM_2051678 = yes; 14 | ARM64_ERRATUM_2054223 = yes; 15 | ARM64_ERRATUM_2067961 = yes; 16 | ARM64_ERRATUM_2077057 = yes; 17 | ARM64_ERRATUM_2658417 = yes; 18 | ARM64_ERRATUM_819472 = yes; 19 | ARM64_ERRATUM_824069 = yes; 20 | ARM64_ERRATUM_826319 = yes; 21 | ARM64_ERRATUM_827319 = yes; 22 | ARM64_ERRATUM_832075 = yes; 23 | ARM64_ERRATUM_843419 = yes; 24 | ARM_ARCH_TIMER = yes; 25 | ARM_PMU = yes; 26 | ARM_PSCI_CPUIDLE = yes; 27 | ARM_PSCI_FW = yes; 28 | ARM_SCMI_PROTOCOL = yes; 29 | ARM_SCMI_TRANSPORT_MAILBOX = yes; 30 | ARM_SCMI_TRANSPORT_SMC = yes; 31 | ARM_SCPI_PROTOCOL = yes; 32 | ARM_SMMU = yes; 33 | ARM_SMMU_V3 = yes; 34 | CMDLINE_FROM_BOOTLOADER = yes; 35 | CPU_FREQ = yes; 36 | CPU_IDLE = yes; 37 | CRYPTO_SHA256_ARM64 = yes; 38 | CRYPTO_SHA512_ARM64 = yes; 39 | DTC = yes; 40 | HW_PERF_EVENTS = yes; 41 | IIO = unset; 42 | MAILBOX = yes; 43 | MFD_SYSCON = yes; 44 | MMC_SDHCI_PLTFM = yes; 45 | MTD = yes; 46 | MTD_BLOCK = yes; 47 | NVMEM = yes; 48 | OF = yes; 49 | OF_ADDRESS = yes; 50 | OF_EARLY_FLATTREE = yes; 51 | OF_FLATTREE = yes; 52 | OF_IRQ = yes; 53 | OF_KOBJ = yes; 54 | OF_RESERVED_MEM = yes; 55 | PCI_ENDPOINT = yes; 56 | PCI_ENDPOINT_CONFIGFS = unset; 57 | PERF_EVENTS = yes; 58 | REGULATOR = yes; 59 | REGULATOR_FIXED_VOLTAGE = yes; 60 | REMOTEPROC = yes; 61 | RESET_CONTROLLER = yes; 62 | SERIAL_8250 = yes; 63 | SERIAL_OF_PLATFORM = yes; 64 | SPMI = yes; 65 | SRAM = unset; 66 | } 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /modules/standalone/kernel-configs/arm.nix: -------------------------------------------------------------------------------- 1 | { lib, pkgs, ... }: 2 | { 3 | linux.kconfig = lib.mkIf pkgs.stdenv.hostPlatform.isAarch32 ( 4 | with lib.kernel; 5 | { 6 | USE_OF = yes; 7 | ARCH_MULTI_V7 = yes; 8 | OF = yes; 9 | OF_ADDRESS = yes; 10 | OF_EARLY_FLATTREE = yes; 11 | OF_FLATTREE = yes; 12 | OF_IRQ = yes; 13 | OF_KOBJ = yes; 14 | OF_RESERVED_MEM = yes; 15 | SERIAL_8250 = yes; 16 | SERIAL_OF_PLATFORM = yes; 17 | 18 | # 19 | ARCH_ACTIONS = yes; 20 | ARCH_AIROHA = yes; 21 | ARCH_ALPINE = yes; 22 | ARCH_ARTPEC = yes; 23 | ARCH_ASPEED = yes; 24 | ARCH_AT91 = yes; 25 | ARCH_BCM = yes; 26 | ARCH_BCM2835 = yes; 27 | ARCH_BCMBCA = yes; 28 | ARCH_BCMBCA_BRAHMAB15 = yes; 29 | ARCH_BCMBCA_CORTEXA7 = yes; 30 | ARCH_BCMBCA_CORTEXA9 = yes; 31 | ARCH_BCM_21664 = yes; 32 | ARCH_BCM_23550 = yes; 33 | ARCH_BCM_281XX = yes; 34 | ARCH_BCM_5301X = yes; 35 | ARCH_BCM_53573 = yes; 36 | ARCH_BCM_CYGNUS = yes; 37 | ARCH_BCM_HR2 = yes; 38 | ARCH_BCM_NSP = yes; 39 | ARCH_BERLIN = yes; 40 | ARCH_BRCMSTB = yes; 41 | ARCH_DIGICOLOR = yes; 42 | ARCH_EXYNOS = yes; 43 | ARCH_HI3xxx = yes; 44 | ARCH_HIGHBANK = yes; 45 | ARCH_HIP01 = yes; 46 | ARCH_HIP04 = yes; 47 | ARCH_HISI = yes; 48 | ARCH_HIX5HD2 = yes; 49 | ARCH_HPE = yes; 50 | ARCH_HPE_GXP = yes; 51 | ARCH_MULTIPLATFORM = yes; 52 | ARCH_MXC = yes; 53 | ARCH_SUNPLUS = yes; 54 | ARCH_UNIPHIER = yes; 55 | ARCH_VIRT = yes; 56 | MACH_ARTPEC6 = yes; 57 | MACH_ASPEED_G6 = yes; 58 | MACH_BERLIN_BG2 = yes; 59 | MACH_BERLIN_BG2CD = yes; 60 | MACH_BERLIN_BG2Q = yes; 61 | SOC_LAN966 = yes; 62 | SOC_SAMA5D2 = yes; 63 | SOC_SAMA5D3 = yes; 64 | SOC_SAMA5D4 = yes; 65 | SOC_SAMA7G5 = yes; 66 | } 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /modules/standalone/kernel-configs/chromebook.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: 7 | { 8 | linux.kconfig = lib.mkIf config.chromebook ( 9 | with lib.kernel; 10 | { 11 | CHROME_PLATFORMS = yes; 12 | CROS_EC = yes; 13 | CROS_EC_I2C = yes; 14 | CROS_EC_LPC = lib.mkIf pkgs.stdenv.hostPlatform.isx86_64 yes; 15 | CROS_EC_PROTO = yes; 16 | CROS_EC_SPI = yes; 17 | GOOGLE_CBMEM = yes; 18 | GOOGLE_COREBOOT_TABLE = yes; 19 | GOOGLE_FIRMWARE = yes; 20 | GOOGLE_VPD = yes; 21 | HID_VIVALDI = yes; 22 | I2C_CROS_EC_TUNNEL = yes; 23 | KEYBOARD_CROS_EC = yes; 24 | TCG_TIS_I2C_CR50 = yes; 25 | TCG_TIS_SPI = yes; 26 | TCG_TIS_SPI_CR50 = yes; 27 | TYPEC = yes; 28 | USB_DWC3 = yes; 29 | USB_DWC3_HAPS = yes; 30 | USB_DWC3_PCI = lib.mkIf pkgs.stdenv.hostPlatform.isx86_64 yes; 31 | } 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /modules/standalone/kernel-configs/debug.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | { 3 | linux.kconfig = lib.mkIf config.debug ( 4 | with lib.kernel; 5 | { 6 | BUG = yes; 7 | DEBUG_BUGVERBOSE = yes; 8 | DEBUG_KERNEL = yes; 9 | DEBUG_MUTEXES = yes; 10 | DYNAMIC_DEBUG = yes; 11 | FTRACE = yes; 12 | GENERIC_BUG = yes; 13 | IKCONFIG = yes; 14 | KALLSYMS = yes; 15 | KALLSYMS_ALL = yes; 16 | SERIAL_EARLYCON = yes; 17 | SYMBOLIC_ERRNAME = yes; 18 | } 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /modules/standalone/kernel-configs/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | imports = [ 3 | ./aarch64.nix 4 | ./arm.nix 5 | ./chromebook.nix 6 | ./debug.nix 7 | ./efi.nix 8 | ./generic.nix 9 | ./ima.nix 10 | ./network.nix 11 | ./platform.nix 12 | ./x86_64.nix 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /modules/standalone/kernel-configs/efi.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | { 3 | linux.kconfig = lib.mkIf config.efi { 4 | EFI = lib.kernel.yes; 5 | EFI_STUB = lib.kernel.yes; 6 | EFI_EARLYCON = lib.kernel.yes; 7 | FB_EFI = lib.kernel.yes; 8 | EFIVAR_FS = lib.kernel.yes; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /modules/standalone/kernel-configs/generic.nix: -------------------------------------------------------------------------------- 1 | { lib, pkgs, ... }: 2 | { 3 | linux.kconfig = with lib.kernel; { 4 | "64BIT" = lib.mkIf pkgs.stdenv.hostPlatform.is64bit yes; 5 | ASYMMETRIC_KEY_TYPE = yes; 6 | ASYMMETRIC_PUBLIC_KEY_SUBTYPE = yes; 7 | ATA = yes; 8 | BINFMT_ELF = yes; 9 | BINFMT_SCRIPT = yes; 10 | BLK_DEV = yes; 11 | BLK_DEV_INITRD = yes; 12 | BLK_DEV_NVME = yes; 13 | BLK_DEV_SD = yes; 14 | BLOCK = yes; 15 | CC_OPTIMIZE_FOR_PERFORMANCE = unset; 16 | CC_OPTIMIZE_FOR_SIZE = yes; 17 | COMMON_CLK = yes; 18 | CRYPTO_HW = yes; 19 | CRYPTO_SHA256 = yes; 20 | CRYPTO_SHA512 = yes; 21 | DEBUG_FS = yes; # some drivers want to write here 22 | DEFAULT_HOSTNAME = freeform "tinyboot"; 23 | DEFAULT_INIT = freeform "/init"; 24 | DEVMEM = yes; 25 | DEVTMPFS = yes; 26 | DMADEVICES = yes; 27 | DUMMY_CONSOLE = yes; 28 | EPOLL = yes; 29 | EVENTFD = yes; 30 | EXPERT = yes; 31 | FAT_FS = yes; 32 | FB = yes; 33 | FRAMEBUFFER_CONSOLE = yes; 34 | FRAMEBUFFER_CONSOLE_DEFERRED_TAKEOVER = yes; 35 | FRAMEBUFFER_CONSOLE_DETECT_PRIMARY = yes; 36 | FUTEX = yes; 37 | FW_LOADER = yes; 38 | FW_LOADER_COMPRESS = yes; 39 | FW_LOADER_COMPRESS_ZSTD = yes; 40 | GPIOLIB = yes; 41 | HAS_IOMEM = yes; 42 | HID = yes; 43 | HID_GENERIC = yes; 44 | HID_SUPPORT = yes; 45 | I2C = yes; 46 | I2C_HID = yes; 47 | INPUT = yes; 48 | INPUT_KEYBOARD = yes; 49 | INPUT_MOUSE = unset; 50 | IOMMU_SUPPORT = yes; 51 | IRQ_POLL = yes; 52 | JUMP_LABEL = yes; 53 | KEXEC = yes; 54 | KEYS = yes; 55 | LSM = freeform "integrity"; 56 | LTO_NONE = yes; 57 | MMC = yes; 58 | MMC_BLOCK = yes; 59 | MMC_CQHCI = yes; 60 | MMC_HSQ = yes; 61 | MMC_SDHCI = yes; 62 | MMU = yes; 63 | MSDOS_FS = yes; 64 | MULTIUSER = yes; # not really needed 65 | NET = yes; # needed for unix domain sockets 66 | NLS = yes; 67 | NLS_CODEPAGE_437 = yes; 68 | NLS_ISO8859_1 = yes; 69 | PCI = yes; 70 | PINCTRL = yes; 71 | PM_SLEEP = yes; 72 | PM_SLEEP_SMP = yes; 73 | PRINTK = yes; 74 | PROC_FS = yes; 75 | RD_ZSTD = yes; 76 | RELOCATABLE = lib.mkIf pkgs.stdenv.hostPlatform.is64bit yes; # allows for this kernel itself to be kexec'ed 77 | RTC_CLASS = yes; 78 | SCSI = yes; 79 | SCSI_LOWLEVEL = yes; 80 | SECCOMP = unset; 81 | SECURITY = yes; 82 | SECURITYFS = yes; 83 | SHMEM = yes; 84 | SIGNALFD = yes; 85 | SLUB = yes; 86 | SLUB_TINY = yes; 87 | SMP = yes; 88 | SPI = yes; 89 | SUSPEND = yes; 90 | SYSFS = yes; 91 | SYSVIPC = yes; 92 | TCG_TIS = yes; 93 | TCG_TPM = yes; 94 | TIMERFD = yes; 95 | TMPFS = yes; 96 | TTY = yes; 97 | UNIX = yes; 98 | USB = yes; 99 | USB_EHCI_HCD = yes; 100 | USB_EHCI_PCI = yes; 101 | USB_HID = yes; 102 | USB_PCI = yes; 103 | USB_STORAGE = yes; 104 | USB_SUPPORT = yes; 105 | USB_XHCI_HCD = yes; 106 | USB_XHCI_PCI = yes; 107 | VFAT_FS = yes; 108 | VT = yes; 109 | VT_CONSOLE = yes; 110 | WATCHDOG = yes; 111 | WATCHDOG_HANDLE_BOOT_ENABLED = yes; 112 | WIRELESS = unset; 113 | X509_CERTIFICATE_PARSER = yes; 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /modules/standalone/kernel-configs/ima.nix: -------------------------------------------------------------------------------- 1 | { lib, pkgs, ... }: 2 | { 3 | # As of 2025-03-12, the linux kernel only allows for 4 | # kexec_file_load and ima/kexec integration on 64-bit 5 | # platforms. 6 | linux.kconfig = lib.mkIf (pkgs.stdenv.hostPlatform.is64bit) ( 7 | with lib.kernel; 8 | { 9 | IMA = yes; 10 | IMA_APPRAISE = yes; 11 | IMA_APPRAISE_MODSIG = yes; 12 | IMA_DEFAULT_HASH_SHA256 = yes; 13 | IMA_KEXEC = yes; 14 | IMA_MEASURE_ASYMMETRIC_KEYS = yes; 15 | KEXEC_FILE = yes; 16 | MODULE_SIG_FORMAT = yes; 17 | INTEGRITY = yes; 18 | INTEGRITY_ASYMMETRIC_KEYS = yes; 19 | INTEGRITY_SIGNATURE = yes; 20 | INTEGRITY_TRUSTED_KEYRING = unset; 21 | } 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /modules/standalone/kernel-configs/network.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | { 3 | linux.kconfig = lib.mkIf config.network ( 4 | with lib.kernel; 5 | { 6 | ETHERNET = yes; 7 | INET = yes; 8 | IPV6 = yes; 9 | NETDEVICES = yes; 10 | NET_CORE = yes; 11 | } 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /modules/standalone/kernel-configs/platform.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | { 3 | linux.kconfig = lib.mkIf (config.platform != null) ( 4 | with lib.kernel; 5 | { 6 | "qemu" = { 7 | "9P_FS" = yes; 8 | BLK_MQ_VIRTIO = yes; 9 | FW_CFG_SYSFS = yes; 10 | I2C_VIRTIO = yes; 11 | NETWORK_FILESYSTEMS = yes; 12 | NET_9P = yes; 13 | NET_9P_VIRTIO = yes; 14 | NET_VENDOR_INTEL = lib.mkIf config.network yes; 15 | SCSI_VIRTIO = yes; 16 | VIRTIO = yes; 17 | VIRTIO_ANCHOR = yes; 18 | VIRTIO_BLK = yes; 19 | VIRTIO_CONSOLE = yes; 20 | VIRTIO_INPUT = yes; 21 | VIRTIO_MENU = yes; 22 | VIRTIO_MMIO = yes; 23 | VIRTIO_NET = lib.mkIf config.network yes; 24 | VIRTIO_PCI = yes; 25 | VIRTIO_PCI_LIB = yes; 26 | }; 27 | "qualcomm" = { 28 | # PHY_ATH79_USB = yes; 29 | # QCOM_SPMI_ADC5 = unset; 30 | # QCOM_SPMI_ADC_TM5 = unset; 31 | # QCOM_SPMI_TEMP_ALARM = unset; 32 | ARCH_QCOM = yes; 33 | ARM_QCOM_CPUFREQ_HW = unset; 34 | ARM_SMMU_QCOM = yes; 35 | COMMON_CLK_QCOM = yes; 36 | CRYPTO_DEV_QCOM_RNG = yes; 37 | HWSPINLOCK = yes; 38 | HWSPINLOCK_QCOM = yes; 39 | HW_RANDOM = yes; 40 | I2C_QCOM_CCI = yes; 41 | I2C_QCOM_GENI = yes; 42 | I2C_QUP = yes; 43 | INTERCONNECT = yes; 44 | INTERCONNECT_QCOM = yes; 45 | INTERCONNECT_QCOM_SC7180 = yes; 46 | MFD_SPMI_PMIC = yes; 47 | MMC_SDHCI_MSM = yes; 48 | NVMEM_QCOM_QFPROM = yes; 49 | PHY_QCOM_APQ8064_SATA = unset; 50 | PHY_QCOM_EDP = unset; 51 | PHY_QCOM_EUSB2_REPEATER = yes; 52 | PHY_QCOM_IPQ4019_USB = unset; 53 | PHY_QCOM_IPQ806X_SATA = unset; 54 | PHY_QCOM_IPQ806X_USB = unset; 55 | PHY_QCOM_PCIE2 = yes; 56 | PHY_QCOM_QMP = yes; 57 | PHY_QCOM_QMP_COMBO = yes; 58 | PHY_QCOM_QMP_PCIE = yes; 59 | PHY_QCOM_QMP_PCIE_8996 = yes; 60 | PHY_QCOM_QMP_UFS = unset; 61 | PHY_QCOM_QMP_USB = unset; 62 | PHY_QCOM_QUSB2 = unset; 63 | PHY_QCOM_SGMII_ETH = unset; 64 | PHY_QCOM_SNPS_EUSB2 = unset; 65 | PHY_QCOM_USB_HS = yes; 66 | PHY_QCOM_USB_HSIC = yes; 67 | PHY_QCOM_USB_HS_28NM = yes; 68 | PHY_QCOM_USB_SNPS_FEMTO_V2 = unset; 69 | USB_ULPI_BUS = yes; 70 | PHY_QCOM_USB_SS = unset; 71 | PINCTRL_MSM = yes; 72 | PINCTRL_QCOM_SPMI_PMIC = unset; 73 | PINCTRL_SC7180 = yes; 74 | POWER_RESET_QCOM_PON = yes; 75 | QCOM_AOSS_QMP = yes; 76 | QCOM_APCS_IPC = yes; 77 | QCOM_APR = yes; 78 | QCOM_BAM_DMA = yes; 79 | QCOM_CLK_RPMH = yes; 80 | QCOM_CLK_SMD_RPM = yes; 81 | QCOM_COMMAND_DB = yes; 82 | QCOM_CPR = unset; 83 | QCOM_GDSC = yes; 84 | QCOM_GENI_SE = yes; 85 | QCOM_GPI_DMA = yes; 86 | QCOM_HFPLL = yes; 87 | QCOM_HIDMA = yes; 88 | QCOM_HIDMA_MGMT = yes; 89 | QCOM_ICC_BWMON = yes; 90 | QCOM_IOMMU = yes; 91 | QCOM_IPCC = yes; 92 | QCOM_LLCC = yes; 93 | QCOM_MPM = unset; 94 | QCOM_OCMEM = unset; 95 | QCOM_PDC = unset; 96 | QCOM_Q6V5_PAS = yes; 97 | QCOM_RMTFS_MEM = yes; 98 | QCOM_RPMH = yes; 99 | QCOM_RPMHPD = yes; 100 | QCOM_RPMPD = yes; 101 | QCOM_SMD_RPM = yes; 102 | QCOM_SMEM = yes; 103 | QCOM_SMP2P = yes; 104 | QCOM_SMSM = yes; 105 | QCOM_SPM = yes; 106 | QCOM_WCNSS_CTRL = yes; 107 | QCOM_WDT = yes; 108 | REGULATOR_QCOM_RPMH = yes; 109 | REGULATOR_QCOM_SMD_RPM = yes; 110 | REGULATOR_QCOM_SPMI = unset; 111 | RESET_QCOM_AOSS = yes; 112 | RESET_QCOM_PDC = yes; 113 | RPMSG_NS = yes; 114 | RPMSG_QCOM_GLINK = yes; 115 | RPMSG_QCOM_GLINK_RPM = yes; 116 | RPMSG_QCOM_GLINK_SMEM = yes; 117 | RPMSG_QCOM_SMD = yes; 118 | SC_DISPCC_7180 = yes; 119 | SC_GCC_7180 = yes; 120 | SC_GPUCC_7180 = yes; 121 | SC_LPASS_CORECC_7180 = yes; 122 | SERIAL_MSM = yes; 123 | SERIAL_MSM_CONSOLE = yes; 124 | SERIAL_QCOM_GENI = yes; 125 | SERIAL_QCOM_GENI_CONSOLE = yes; 126 | SPI_QCOM_GENI = yes; 127 | SPI_QCOM_QSPI = yes; 128 | SPMI_MSM_PMIC_ARB = unset; 129 | USB_DWC3_QCOM = yes; 130 | USB_ONBOARD_DEV = yes; 131 | }; 132 | "mediatek" = { 133 | ARCH_MEDIATEK = yes; 134 | ARM_MEDIATEK_CPUFREQ_HW = yes; 135 | BACKLIGHT_CLASS_DEVICE = yes; 136 | BACKLIGHT_MT6370 = yes; 137 | COMMON_CLK_MT8183 = yes; 138 | COMMON_CLK_MT8183_AUDIOSYS = yes; 139 | COMMON_CLK_MT8183_CAMSYS = yes; 140 | COMMON_CLK_MT8183_IMGSYS = yes; 141 | COMMON_CLK_MT8183_IPU_ADL = yes; 142 | COMMON_CLK_MT8183_IPU_CONN = yes; 143 | COMMON_CLK_MT8183_IPU_CORE0 = yes; 144 | COMMON_CLK_MT8183_IPU_CORE1 = yes; 145 | COMMON_CLK_MT8183_MFGCFG = yes; 146 | COMMON_CLK_MT8183_MMSYS = yes; 147 | COMMON_CLK_MT8183_VDECSYS = yes; 148 | COMMON_CLK_MT8183_VENCSYS = yes; 149 | COMMON_CLK_MT8192 = yes; 150 | COMMON_CLK_MT8192_AUDSYS = yes; 151 | COMMON_CLK_MT8192_CAMSYS = yes; 152 | COMMON_CLK_MT8192_IMGSYS = yes; 153 | COMMON_CLK_MT8192_IMP_IIC_WRAP = yes; 154 | COMMON_CLK_MT8192_IPESYS = yes; 155 | COMMON_CLK_MT8192_MDPSYS = yes; 156 | COMMON_CLK_MT8192_MFGCFG = yes; 157 | COMMON_CLK_MT8192_MSDC = yes; 158 | COMMON_CLK_MT8192_SCP_ADSP = yes; 159 | COMMON_CLK_MT8192_VDECSYS = yes; 160 | COMMON_CLK_MT8192_VENCSYS = yes; 161 | EINT_MTK = yes; 162 | I2C_MT65XX = yes; 163 | KEYBOARD_MT6779 = yes; 164 | MEDIATEK_WATCHDOG = yes; 165 | MFD_MT6360 = yes; 166 | MFD_MT6370 = yes; 167 | MFD_MT6397 = yes; 168 | MMC_MTK = yes; 169 | MTD_SPI_NOR = yes; 170 | MTK_CMDQ_MBOX = yes; 171 | MTK_IOMMU = yes; 172 | MTK_MMSYS = yes; 173 | MTK_PMIC_WRAP = yes; 174 | MTK_SCP = yes; 175 | MTK_SCPSYS_PM_DOMAINS = yes; 176 | MTK_SMI = yes; 177 | MTK_SVS = yes; 178 | NVMEM_MTK_EFUSE = yes; 179 | PCIE_MEDIATEK_GEN3 = yes; 180 | PHY_MTK_DP = yes; 181 | PHY_MTK_HDMI = yes; 182 | PHY_MTK_MIPI_DSI = yes; 183 | PHY_MTK_PCIE = yes; 184 | PHY_MTK_TPHY = yes; 185 | PHY_MTK_UFS = yes; 186 | PHY_MTK_XSPHY = yes; 187 | PINCTRL_MT6397 = yes; 188 | PINCTRL_MT8173 = yes; 189 | PINCTRL_MT8183 = yes; 190 | PINCTRL_MT8186 = yes; 191 | PINCTRL_MT8188 = yes; 192 | PINCTRL_MT8192 = yes; 193 | PINCTRL_MT8195 = yes; 194 | PINCTRL_MTK = yes; 195 | PINMUX = yes; 196 | PWM = yes; 197 | PWM_CROS_EC = yes; 198 | PWM_MTK_DISP = yes; 199 | REGULATOR_MT6311 = yes; 200 | REGULATOR_MT6315 = yes; 201 | REGULATOR_MT6323 = yes; 202 | REGULATOR_MT6331 = yes; 203 | REGULATOR_MT6332 = yes; 204 | REGULATOR_MT6357 = yes; 205 | REGULATOR_MT6358 = yes; 206 | REGULATOR_MT6359 = yes; 207 | REGULATOR_MT6360 = yes; 208 | REGULATOR_MT6370 = yes; 209 | REGULATOR_MT6380 = yes; 210 | REGULATOR_MT6397 = yes; 211 | RTC_DRV_MT6397 = yes; 212 | SERIAL_8250 = yes; 213 | SERIAL_8250_CONSOLE = yes; 214 | SERIAL_8250_MT6577 = yes; 215 | SPI_MT65XX = yes; 216 | SPI_MTK_NOR = yes; 217 | SPMI_MTK_PMIF = unset; 218 | USB_MTU3 = yes; 219 | USB_XHCI_MTK = yes; 220 | }; 221 | } 222 | .${lib.head (lib.attrNames config.platform)} 223 | ); 224 | } 225 | -------------------------------------------------------------------------------- /modules/standalone/kernel-configs/x86_64.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, ... }: 2 | { 3 | linux.kconfig = lib.mkIf pkgs.stdenv.hostPlatform.isx86_64 ( 4 | with lib.kernel; 5 | { 6 | ACPI = yes; 7 | ACPI_BUTTON = yes; 8 | ACPI_PROCESSOR = yes; 9 | ACPI_THERMAL = yes; 10 | ACPI_WMI = yes; 11 | ATA_ACPI = yes; 12 | CMDLINE_BOOL = yes; 13 | CPU_MITIGATIONS = yes; 14 | CPU_SUP_AMD = yes; 15 | CRYPTO_SHA256_SSSE3 = yes; 16 | CRYPTO_SHA512_SSSE3 = yes; 17 | DMA_ACPI = yes; 18 | DMI = yes; 19 | DW_DMAC = yes; 20 | FB_VESA = yes; 21 | GPIO_ACPI = yes; 22 | INTEL_IOMMU = yes; 23 | INTEL_IOMMU_DEFAULT_ON = yes; 24 | IRQ_REMAP = yes; 25 | KERNEL_BZIP2 = unset; 26 | KERNEL_GZIP = unset; 27 | KERNEL_LZ4 = unset; 28 | KERNEL_LZMA = unset; 29 | KERNEL_LZO = unset; 30 | KERNEL_XZ = yes; 31 | KEYBOARD_ATKBD = yes; 32 | MFD_INTEL_LPSS_ACPI = yes; 33 | MFD_INTEL_LPSS_PCI = yes; 34 | MITIGATION_RETHUNK = yes; 35 | MITIGATION_RETPOLINE = yes; 36 | MITIGATION_SRSO = yes; 37 | MTRR = yes; 38 | PCI_MSI = yes; 39 | PNP = yes; 40 | PNPACPI = yes; 41 | PREEMPT_VOLUNTARY = yes; 42 | RTC_DRV_CMOS = yes; 43 | SERIAL_8250 = yes; 44 | SERIAL_8250_CONSOLE = yes; 45 | SERIAL_8250_DMA = yes; 46 | SERIAL_8250_DW = yes; 47 | SERIAL_8250_EXAR = yes; 48 | SERIAL_8250_EXTENDED = yes; 49 | SERIAL_8250_LPSS = yes; 50 | SERIAL_8250_MID = yes; 51 | SERIAL_8250_PCI = yes; 52 | SERIAL_8250_PCILIB = yes; 53 | SERIAL_8250_PERICOM = yes; 54 | SERIAL_8250_PNP = yes; 55 | SERIAL_8250_SHARE_IRQ = yes; 56 | SPI_DESIGNWARE = yes; 57 | SPI_INTEL_PCI = yes; 58 | SPI_MEM = yes; 59 | SPI_PXA2XX = yes; 60 | SPI_PXA2XX_PCI = yes; 61 | UNIX98_PTYS = yes; 62 | UNWINDER_FRAME_POINTER = unset; 63 | UNWINDER_GUESS = yes; 64 | WMI_BMOF = yes; 65 | X86 = yes; 66 | X86_64 = yes; 67 | X86_INTEL_LPSS = yes; 68 | X86_IOPL_IOPERM = yes; 69 | X86_IO_APIC = yes; 70 | X86_LOCAL_APIC = yes; 71 | X86_PAT = yes; 72 | X86_PLATFORM_DEVICES = yes; 73 | X86_REROUTE_FOR_BROKEN_BOOT_IRQS = yes; 74 | X86_X2APIC = yes; 75 | } 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /modules/standalone/linux/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | kconfig ? "", 3 | strict ? true, 4 | bc, 5 | bison, 6 | buildPackages, 7 | elfutils, 8 | fetchurl, 9 | flex, 10 | gmp, 11 | hexdump, 12 | kmod, 13 | lib, 14 | libmpc, 15 | mpfr, 16 | nettools, 17 | openssl, 18 | perl, 19 | python3Minimal, 20 | rsync, 21 | stdenv, 22 | ubootTools, 23 | zstd, 24 | }: 25 | 26 | let 27 | kernelFile = 28 | { 29 | arm = "zImage"; 30 | arm64 = "Image"; 31 | x86_64 = "bzImage"; 32 | } 33 | .${stdenv.hostPlatform.linuxArch}; 34 | in 35 | stdenv.mkDerivation (finalAttrs: { 36 | pname = "tinyboot-linux"; 37 | version = "6.14.8"; 38 | 39 | src = fetchurl { 40 | url = "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${finalAttrs.version}.tar.xz"; 41 | hash = "sha256-YrEuzTB1o1frMgk1ZX3oTgFVKANxfa04P6fMOqSqKQU="; 42 | }; 43 | 44 | depsBuildBuild = [ buildPackages.stdenv.cc ]; 45 | nativeBuildInputs = [ 46 | bc 47 | bison 48 | elfutils 49 | flex 50 | gmp 51 | hexdump 52 | kmod 53 | libmpc 54 | mpfr 55 | nettools 56 | openssl 57 | perl 58 | python3Minimal 59 | rsync 60 | ubootTools 61 | zstd 62 | ]; 63 | 64 | buildFlags = [ 65 | "DTC_FLAGS=-@" 66 | "KBUILD_BUILD_VERSION=1-tinyboot" 67 | kernelFile 68 | ]; 69 | 70 | makeFlags = [ 71 | "O=$(buildRoot)" 72 | "ARCH=${stdenv.hostPlatform.linuxArch}" 73 | "CROSS_COMPILE=${stdenv.cc.targetPrefix}" 74 | ]; 75 | 76 | installFlags = 77 | [ "INSTALL_PATH=$(out)" ] 78 | ++ lib.optionals (with stdenv.hostPlatform; isAarch) [ 79 | "dtbs_install" 80 | "INSTALL_DTBS_PATH=$(out)/dtbs" 81 | ]; 82 | 83 | installTargets = [ (if kernelFile == "zImage" then "zinstall" else "install") ]; 84 | 85 | hardeningDisable = [ 86 | "bindnow" 87 | "format" 88 | "fortify" 89 | "stackprotector" 90 | "pic" 91 | "pie" 92 | ]; 93 | 94 | strictDeps = true; 95 | enableParallelBuilding = true; 96 | 97 | patches = [ ./tpm-probe.patch ]; 98 | 99 | inherit kconfig; 100 | passAsFile = [ "kconfig" ]; 101 | 102 | configurePhase = '' 103 | runHook preConfigure 104 | 105 | cat $kconfigPath >all.config 106 | make -j$NIX_BUILD_CORES $makeFlags KCONFIG_ALLCONFIG=1 allnoconfig 107 | 108 | start_config=all.config 109 | end_config=.config 110 | 111 | missing=() 112 | while read -r line; do 113 | if ! grep --silent "$line" "$end_config"; then 114 | missing+=("$line") 115 | fi 116 | done <"$start_config" 117 | 118 | if [[ ''${#missing[@]} -gt 0 ]]; then 119 | echo 120 | for line in "''${missing[@]}"; do 121 | echo "\"$line\" not found in final config!" 122 | done 123 | echo 124 | ${lib.optionalString strict '' 125 | exit 1 126 | ''} 127 | fi 128 | 129 | buildFlagsArray+=("KBUILD_BUILD_TIMESTAMP=$(date -u -d @$SOURCE_DATE_EPOCH)") 130 | 131 | runHook postConfigure 132 | ''; 133 | 134 | outputs = [ 135 | "out" 136 | "dev" 137 | ]; 138 | 139 | preInstall = 140 | let 141 | installkernel = buildPackages.writeShellScriptBin "installkernel" '' 142 | set -x 143 | echo $@ 144 | cp -av $2 $4 145 | cp -av $3 $4 146 | ''; 147 | in 148 | '' 149 | installFlagsArray+=("-j$NIX_BUILD_CORES") 150 | export HOME=${installkernel} 151 | ''; 152 | 153 | postInstall = '' 154 | install -D -m0644 -t $dev .config vmlinux 155 | ''; 156 | 157 | passthru = { inherit kernelFile; }; 158 | }) 159 | -------------------------------------------------------------------------------- /modules/standalone/linux/tpm-probe.patch: -------------------------------------------------------------------------------- 1 | diff -Naur a/drivers/char/tpm/tpm_tis_spi_main.c b/drivers/char/tpm/tpm_tis_spi_main.c 2 | --- a/drivers/char/tpm/tpm_tis_spi_main.c 2025-03-12 19:09:36.664664523 -0700 3 | +++ b/drivers/char/tpm/tpm_tis_spi_main.c 2025-03-12 19:10:33.484661242 -0700 4 | @@ -350,7 +350,11 @@ 5 | .pm = &tpm_tis_pm, 6 | .of_match_table = of_match_ptr(of_tis_spi_match), 7 | .acpi_match_table = ACPI_PTR(acpi_tis_spi_match), 8 | +#ifdef CONFIG_IMA 9 | + .probe_type = PROBE_FORCE_SYNCHRONOUS 10 | +#else 11 | .probe_type = PROBE_PREFER_ASYNCHRONOUS, 12 | +#endif 13 | }, 14 | .probe = tpm_tis_spi_driver_probe, 15 | .remove = tpm_tis_spi_remove, 16 | diff -Naur a/drivers/spi/spi.c b/drivers/spi/spi.c 17 | --- a/drivers/spi/spi.c 2025-03-12 19:09:37.054664199 -0700 18 | +++ b/drivers/spi/spi.c 2025-03-12 19:10:47.975673817 -0700 19 | @@ -4957,4 +4957,4 @@ 20 | * driver registration) _could_ be dynamically linked (modular) ... Costs 21 | * include needing to have boardinfo data structures be much more public. 22 | */ 23 | -postcore_initcall(spi_init); 24 | +core_initcall(spi_init); 25 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | { 2 | firmwareDirectory ? null, 3 | 4 | fetchzip, 5 | lib, 6 | linkFarm, 7 | stdenvNoCC, 8 | zig_0_14, 9 | }: 10 | 11 | let 12 | deps = linkFarm "tinyboot-deps" [ 13 | { 14 | name = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0"; 15 | path = fetchzip { 16 | url = "https://github.com/Hejsil/zig-clap/archive/e47028deaefc2fb396d3d9e9f7bd776ae0b2a43a.tar.gz"; 17 | hash = "sha256-leXnA97ITdvmBhD2YESLBZAKjBg+G4R/+PPPRslz/ec="; 18 | }; 19 | } 20 | { 21 | name = "N-V-__8AAMrlvQKMzsBG9BReeFEEQ2phyoN7lE8U1xNUEvgP"; 22 | path = fetchzip { 23 | url = "https://github.com/MBED-TLS/mbedtls/archive/v3.6.3.1.tar.gz"; 24 | hash = "sha256-koZAtExQguvfQ2Jf8xidKyLzCQoWrVIY73AYFjG0tMg="; 25 | }; 26 | } 27 | { 28 | name = "N-V-__8AAPZ7fwBg4JoCzM_0o2A8wxH2hsUUeiU1iuZv53L5"; 29 | path = fetchzip { 30 | url = "https://github.com/facebook/zstd/archive/v1.5.7.tar.gz"; 31 | hash = "sha256-tNFWIT9ydfozB8dWcmTMuZLCQmQudTFJIkSr0aG7S44="; 32 | }; 33 | } 34 | ]; 35 | in 36 | stdenvNoCC.mkDerivation { 37 | pname = "tinyboot"; 38 | version = "0.1.0"; 39 | 40 | src = lib.fileset.toSource { 41 | root = ./.; 42 | fileset = lib.fileset.unions [ 43 | ./build.zig 44 | ./build.zig.zon 45 | ./deps 46 | ./src 47 | ]; 48 | }; 49 | 50 | strictDeps = true; 51 | 52 | depsBuildBuild = [ zig_0_14 ]; 53 | 54 | dontInstall = true; 55 | doCheck = true; 56 | 57 | zigBuildFlags = 58 | [ 59 | "--color off" 60 | "-Doptimize=ReleaseSafe" 61 | "-Dtarget=${stdenvNoCC.hostPlatform.qemuArch}-${stdenvNoCC.hostPlatform.parsed.kernel.name}" 62 | ] 63 | ++ lib.optionals (firmwareDirectory != null) [ 64 | "-Dfirmware-directory=${firmwareDirectory}" 65 | ]; 66 | 67 | configurePhase = '' 68 | runHook preConfigure 69 | 70 | export ZIG_GLOBAL_CACHE_DIR=$TEMPDIR 71 | 72 | ln -s ${deps} $ZIG_GLOBAL_CACHE_DIR/p 73 | 74 | runHook postConfigure 75 | ''; 76 | 77 | buildPhase = '' 78 | runHook preBuild 79 | zig build install --prefix $out ''${zigBuildFlags[@]} 80 | runHook postBuild 81 | ''; 82 | 83 | checkPhase = '' 84 | runHook preCheck 85 | zig build test ''${zigBuildFlags[@]} 86 | runHook postCheck 87 | ''; 88 | 89 | passthru.initrdFile = "tboot-loader.cpio.zst"; 90 | 91 | meta.platforms = lib.platforms.linux; 92 | } 93 | -------------------------------------------------------------------------------- /src/autoboot.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const posix = std.posix; 3 | 4 | const BootLoader = @import("./boot/bootloader.zig"); 5 | const Console = @import("./console.zig"); 6 | 7 | const Autoboot = @This(); 8 | 9 | boot_loader: ?*BootLoader = null, 10 | 11 | pub fn init() Autoboot { 12 | return .{}; 13 | } 14 | 15 | pub fn run( 16 | self: *Autoboot, 17 | boot_loaders: *std.ArrayList(*BootLoader), 18 | timerfd: posix.fd_t, 19 | ) !?Console.Event { 20 | if (self.boot_loader) |boot_loader| { 21 | // After we attempt to boot with this boot loader, we unset it from the 22 | // autoboot structure so re-entry into this function does not attempt 23 | // to use it again. 24 | defer { 25 | boot_loader.boot_attempted = true; 26 | self.boot_loader = null; 27 | } 28 | 29 | std.log.info("autobooting {s}", .{boot_loader.device}); 30 | 31 | const entries = try boot_loader.probe(); 32 | 33 | for (entries) |entry| { 34 | boot_loader.load(entry) catch |err| { 35 | std.log.err( 36 | "failed to load entry {s}: {}", 37 | .{ entry.linux, err }, 38 | ); 39 | continue; 40 | }; 41 | 42 | return .kexec; 43 | } 44 | } else { 45 | if (boot_loaders.items.len == 0) { 46 | return error.NoBootloaders; 47 | } 48 | 49 | const head = boot_loaders.orderedRemove(0); 50 | 51 | // If we've already tried this boot loader, this means we've gone full 52 | // circle back to the first bootloader, so we are done. We insert it 53 | // back to the beginning to not mess with the original order. 54 | if (head.boot_attempted) { 55 | try boot_loaders.insert(0, head); 56 | 57 | return error.NoBootloaders; 58 | } 59 | 60 | try boot_loaders.append(head); 61 | 62 | if (!head.autoboot) { 63 | head.boot_attempted = true; 64 | 65 | return null; 66 | } 67 | 68 | self.boot_loader = head; 69 | 70 | std.debug.assert(self.boot_loader != null); 71 | const timeout = try self.boot_loader.?.timeout(); 72 | 73 | if (timeout == 0) { 74 | return self.run(boot_loaders, timerfd); 75 | } else { 76 | try posix.timerfd_settime(timerfd, .{}, &.{ 77 | // oneshot 78 | .it_interval = .{ .sec = 0, .nsec = 0 }, 79 | // wait for `timeout` seconds before continuing to boot 80 | .it_value = .{ .sec = timeout, .nsec = 0 }, 81 | }, null); 82 | 83 | std.log.info( 84 | "will boot in {} seconds without any user input", 85 | .{timeout}, 86 | ); 87 | } 88 | } 89 | 90 | return null; 91 | } 92 | -------------------------------------------------------------------------------- /src/boot/bootloader.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Device = @import("../device.zig"); 4 | const kexec = @import("../kexec/kexec.zig").kexec; 5 | 6 | const BootLoader = @This(); 7 | 8 | pub const Entry = struct { 9 | /// Will be passed to underlying boot loader after a successful kexec load. 10 | context: *anyopaque, 11 | /// Path to the linux kernel image. 12 | linux: []const u8, 13 | /// Optional path to the initrd. 14 | initrd: ?[]const u8 = null, 15 | /// Optional kernel parameters. 16 | cmdline: ?[]const u8 = null, 17 | }; 18 | 19 | allocator: std.mem.Allocator, 20 | 21 | /// Whether the bootloader can autoboot. 22 | autoboot: bool = true, 23 | 24 | /// Flag indicating if the underlying bootloader has been probed or not. 25 | probed: bool = false, 26 | 27 | /// Flag indicating if a boot has been attempted on this bootloader. This means 28 | /// load() was called at least once. 29 | boot_attempted: bool = false, 30 | 31 | /// The priority of the bootloader. The lowest priority bootloader will attempt 32 | /// to be booted first. 33 | priority: u8, 34 | 35 | /// The device this bootloader will operate on. 36 | device: Device, 37 | 38 | /// Entries obtained from the underlying bootloader on the device. Obtained 39 | /// after a probe(). 40 | entries: std.ArrayList(Entry), 41 | 42 | /// The underlying bootloader. 43 | inner: *anyopaque, 44 | 45 | /// Operations that can be ran on the underlying bootloader. 46 | vtable: *const struct { 47 | name: *const fn () []const u8, 48 | probe: *const fn (*anyopaque, *std.ArrayList(Entry), Device) anyerror!void, 49 | timeout: *const fn (*anyopaque) u8, 50 | entryLoaded: *const fn (*anyopaque, Entry) void, 51 | deinit: *const fn (*anyopaque, std.mem.Allocator) void, 52 | }, 53 | 54 | pub fn init( 55 | comptime T: type, 56 | allocator: std.mem.Allocator, 57 | device: Device, 58 | opts: struct { 59 | priority: u8, 60 | autoboot: bool, 61 | }, 62 | ) !BootLoader { 63 | const inner = try allocator.create(T); 64 | 65 | inner.* = T.init(); 66 | 67 | const wrapper = struct { 68 | pub fn deinit(ctx: *anyopaque, a: std.mem.Allocator) void { 69 | const self: *T = @ptrCast(@alignCast(ctx)); 70 | defer a.destroy(self); 71 | 72 | self.deinit(); 73 | } 74 | 75 | pub fn probe( 76 | ctx: *anyopaque, 77 | entries: *std.ArrayList(Entry), 78 | d: Device, 79 | ) !void { 80 | const self: *T = @ptrCast(@alignCast(ctx)); 81 | 82 | try self.probe(entries, d); 83 | } 84 | 85 | pub fn entryLoaded(ctx: *anyopaque, entry: Entry) void { 86 | const self: *T = @ptrCast(@alignCast(ctx)); 87 | 88 | self.entryLoaded(entry.context); 89 | } 90 | 91 | pub fn timeout(ctx: *anyopaque) u8 { 92 | const self: *T = @ptrCast(@alignCast(ctx)); 93 | 94 | return self.timeout(); 95 | } 96 | }; 97 | 98 | return .{ 99 | .autoboot = opts.autoboot, 100 | .priority = opts.priority, 101 | .device = device, 102 | .allocator = allocator, 103 | .entries = std.ArrayList(Entry).init(allocator), 104 | .inner = inner, 105 | .vtable = &.{ 106 | .name = T.name, 107 | .probe = wrapper.probe, 108 | .timeout = wrapper.timeout, 109 | .entryLoaded = wrapper.entryLoaded, 110 | .deinit = wrapper.deinit, 111 | }, 112 | }; 113 | } 114 | 115 | pub fn deinit(self: *BootLoader) void { 116 | defer self.entries.deinit(); 117 | 118 | self.vtable.deinit(self.inner, self.allocator); 119 | } 120 | 121 | pub fn name(self: *BootLoader) []const u8 { 122 | return self.vtable.name(); 123 | } 124 | 125 | pub fn timeout(self: *BootLoader) !u8 { 126 | _ = try self.probe(); 127 | 128 | return self.vtable.timeout(self.inner); 129 | } 130 | 131 | pub fn probe(self: *BootLoader) ![]const Entry { 132 | if (!self.probed) { 133 | std.log.debug("bootloader not yet probed on {}", .{self.device}); 134 | try self.vtable.probe(self.inner, &self.entries, self.device); 135 | self.probed = true; 136 | std.log.debug("bootloader probed on {}", .{self.device}); 137 | } 138 | 139 | return self.entries.items; 140 | } 141 | 142 | pub fn load(self: *BootLoader, entry: Entry) !void { 143 | self.boot_attempted = true; 144 | 145 | try kexec(self.allocator, entry.linux, entry.initrd, entry.cmdline); 146 | 147 | self.vtable.entryLoaded(self.inner, entry); 148 | } 149 | -------------------------------------------------------------------------------- /src/boot/ymodem.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const posix = std.posix; 3 | 4 | const BootLoader = @import("./bootloader.zig"); 5 | const Device = @import("../device.zig"); 6 | const TmpDir = @import("../tmpdir.zig"); 7 | const system = @import("../system.zig"); 8 | const ymodem = @import("../ymodem.zig"); 9 | 10 | const linux_headers = @import("linux_headers"); 11 | 12 | const YmodemBootLoader = @This(); 13 | 14 | pub const autoboot = false; 15 | 16 | arena: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator), 17 | tmpdir: ?TmpDir = null, 18 | 19 | fn serialDeviceIsConnected(fd: posix.fd_t) bool { 20 | var serial: c_int = 0; 21 | 22 | if (posix.system.ioctl( 23 | fd, 24 | linux_headers.TIOCMGET, 25 | @intFromPtr(&serial), 26 | ) != 0) { 27 | return false; 28 | } 29 | 30 | return serial & linux_headers.TIOCM_DTR == linux_headers.TIOCM_DTR; 31 | } 32 | 33 | pub fn match(device: *const Device) ?u8 { 34 | if (device.subsystem != .tty) { 35 | return null; 36 | } 37 | 38 | switch (device.type) { 39 | .node => |node| { 40 | const major, const minor = node; 41 | 42 | // https://www.kernel.org/doc/Documentation/admin-guide/devices.txt 43 | const nodeMatch = switch (major) { 44 | 4 => minor >= 64, 45 | 204 => minor >= 64, 46 | 229 => true, 47 | else => false, 48 | }; 49 | 50 | if (!nodeMatch) { 51 | return null; 52 | } 53 | }, 54 | else => return null, 55 | } 56 | 57 | var serial_path_buf: [std.fs.max_path_bytes]u8 = undefined; 58 | const serial_path = device.nodePath(&serial_path_buf) catch return null; 59 | var serial = std.fs.cwd().openFile( 60 | serial_path, 61 | .{ .mode = .read_write }, 62 | ) catch |err| { 63 | std.log.err("failed to open {}: {}", .{ device, err }); 64 | return null; 65 | }; 66 | defer serial.close(); 67 | 68 | // Prioritize serial devices that are already connected. 69 | if (!serialDeviceIsConnected(serial.handle)) { 70 | return 105; 71 | } 72 | 73 | return 100; 74 | } 75 | 76 | pub fn init() YmodemBootLoader { 77 | return .{}; 78 | } 79 | 80 | pub fn name() []const u8 { 81 | return "ymodem"; 82 | } 83 | 84 | pub fn timeout(self: *YmodemBootLoader) u8 { 85 | _ = self; 86 | 87 | return 0; 88 | } 89 | 90 | pub fn probe(self: *YmodemBootLoader, entries: *std.ArrayList(BootLoader.Entry), device: Device) !void { 91 | const allocator = self.arena.allocator(); 92 | 93 | var serial_path_buf: [std.fs.max_path_bytes]u8 = undefined; 94 | const serial_path = try device.nodePath(&serial_path_buf); 95 | 96 | var serial = try std.fs.cwd().openFile(serial_path, .{ .mode = .read_write }); 97 | defer serial.close(); 98 | 99 | var tty = system.Tty.init(serial.handle); 100 | defer { 101 | tty.deinit(); 102 | 103 | // If the TTY is being used for user input, this will allow for the 104 | // next message printed to the TTY to be legible. 105 | tty.writer().writeByte('\n') catch {}; 106 | } 107 | 108 | try tty.setMode(.file_transfer); 109 | 110 | self.tmpdir = try TmpDir.create(.{}); 111 | 112 | var tmpdir = self.tmpdir.?; 113 | 114 | { 115 | // Temporarily turn off the system console so that no kernel logs are 116 | // printed during the file transfer process. 117 | system.setConsole(.off) catch {}; 118 | defer system.setConsole(.on) catch {}; 119 | 120 | try ymodem.recv(&tty, tmpdir.dir); 121 | } 122 | 123 | var params_file = try tmpdir.dir.openFile("params", .{}); 124 | defer params_file.close(); 125 | 126 | const kernel_params_bytes = try params_file.readToEndAlloc( 127 | allocator, 128 | linux_headers.COMMAND_LINE_SIZE, 129 | ); 130 | 131 | const kernel_params = std.mem.trim(u8, kernel_params_bytes, &std.ascii.whitespace); 132 | 133 | const linux = try tmpdir.dir.realpathAlloc(allocator, "linux"); 134 | const initrd = b: { 135 | if (tmpdir.dir.realpathAlloc(allocator, "initrd")) |initrd| { 136 | break :b initrd; 137 | } else |err| { 138 | if (err == error.FileNotFound) { 139 | break :b null; 140 | } else { 141 | return err; 142 | } 143 | } 144 | }; 145 | 146 | try entries.append(.{ 147 | .context = try allocator.create(struct {}), 148 | .linux = linux, 149 | .initrd = initrd, 150 | .cmdline = if (kernel_params.len > 0) kernel_params else null, 151 | }); 152 | } 153 | 154 | pub fn entryLoaded(self: *YmodemBootLoader, ctx: *anyopaque) void { 155 | _ = self; 156 | _ = ctx; 157 | } 158 | 159 | pub fn deinit(self: *YmodemBootLoader) void { 160 | self.arena.deinit(); 161 | 162 | if (self.tmpdir) |*tmpdir| { 163 | tmpdir.cleanup(); 164 | self.tmpdir = null; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/bootspec.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const json = std.json; 3 | 4 | pub const BootSpecV1 = struct { 5 | allocator: std.mem.Allocator, 6 | 7 | name: ?[]const u8, 8 | 9 | init: []const u8, 10 | initrd: ?[]const u8 = null, 11 | initrd_secrets: ?[]const u8 = null, 12 | kernel: []const u8, 13 | kernel_params: []const []const u8, 14 | label: []const u8, 15 | system: std.Target.Cpu.Arch, 16 | toplevel: []const u8, 17 | 18 | const Error = error{ 19 | Invalid, 20 | }; 21 | 22 | fn ensureRequiredArch(val: ?json.Value) !std.Target.Cpu.Arch { 23 | if (val) |value| { 24 | switch (value) { 25 | .string => |string| { 26 | if (std.mem.eql(u8, string, "x86_64-linux")) { 27 | return std.Target.Cpu.Arch.x86_64; 28 | } else if (std.mem.eql(u8, string, "aarch64-linux")) { 29 | return std.Target.Cpu.Arch.aarch64; 30 | } else { 31 | return Error.Invalid; 32 | } 33 | }, 34 | else => return Error.Invalid, 35 | } 36 | } else { 37 | return Error.Invalid; 38 | } 39 | } 40 | 41 | fn ensureRequiredStringSlice(a: std.mem.Allocator, val: ?json.Value) ![]const []const u8 { 42 | if (val) |value| { 43 | switch (value) { 44 | .array => |array| { 45 | var new_list = std.ArrayList([]const u8).init(a); 46 | defer new_list.deinit(); 47 | 48 | for (array.items) |inner_val| { 49 | switch (inner_val) { 50 | .string => |string| try new_list.append(string), 51 | else => return Error.Invalid, 52 | } 53 | } 54 | 55 | return new_list.toOwnedSlice(); 56 | }, 57 | else => return Error.Invalid, 58 | } 59 | } else { 60 | return Error.Invalid; 61 | } 62 | } 63 | 64 | fn ensureOptionalString(val: ?json.Value) !?[]const u8 { 65 | if (val) |v| { 66 | switch (v) { 67 | .string => |string| return string, 68 | else => return Error.Invalid, 69 | } 70 | } 71 | 72 | return null; 73 | } 74 | 75 | fn ensureRequiredString(val: ?json.Value) ![]const u8 { 76 | return @This().ensureOptionalString(val) catch |err| { 77 | return err; 78 | } orelse return Error.Invalid; 79 | } 80 | 81 | pub fn parse(allocator: std.mem.Allocator, name: ?[]const u8, j: json.Value) !@This() { 82 | const object = o: { 83 | switch (j) { 84 | .object => |obj| break :o obj, 85 | else => return Error.Invalid, 86 | } 87 | }; 88 | 89 | return @This(){ 90 | .allocator = allocator, 91 | .name = name, 92 | .init = try @This().ensureRequiredString(object.get("init")), 93 | .initrd = try @This().ensureOptionalString(object.get("initrd")), 94 | .initrd_secrets = try @This().ensureOptionalString(object.get("initrdSecrets")), 95 | .kernel = try @This().ensureRequiredString(object.get("kernel")), 96 | .kernel_params = try @This().ensureRequiredStringSlice(allocator, object.get("kernelParams")), 97 | .label = try @This().ensureRequiredString(object.get("label")), 98 | .system = try @This().ensureRequiredArch(object.get("system")), 99 | .toplevel = try @This().ensureRequiredString(object.get("toplevel")), 100 | }; 101 | } 102 | 103 | pub fn deinit(self: *const @This()) void { 104 | self.allocator.free(self.kernel_params); 105 | } 106 | }; 107 | pub const BootJson = struct { 108 | spec: BootSpecV1, 109 | specialisations: ?[]BootSpecV1 = null, 110 | allocator: std.mem.Allocator, 111 | tree: json.Parsed(json.Value), 112 | 113 | const Error = error{ 114 | Invalid, 115 | }; 116 | 117 | pub fn parse(allocator: std.mem.Allocator, contents: []const u8) !@This() { 118 | const tree = try json.parseFromSlice(json.Value, allocator, contents, .{}); 119 | errdefer tree.deinit(); 120 | 121 | const toplevel_object = o: { 122 | switch (tree.value) { 123 | .object => |obj| break :o obj, 124 | else => return Error.Invalid, 125 | } 126 | }; 127 | 128 | const spec = try BootSpecV1.parse( 129 | allocator, 130 | null, 131 | toplevel_object.get("org.nixos.bootspec.v1") orelse return Error.Invalid, 132 | ); 133 | 134 | const specialisations: ?[]BootSpecV1 = s: { 135 | if (toplevel_object.get("org.nixos.specialisation.v1")) |special| switch (special) { 136 | .object => |obj| { 137 | var special_list = std.ArrayList(BootSpecV1).init(allocator); 138 | defer special_list.deinit(); 139 | 140 | var it = obj.iterator(); 141 | 142 | while (it.next()) |next| { 143 | const sub_obj = o: { 144 | switch (next.value_ptr.*) { 145 | .object => |o| break :o o, 146 | else => return Error.Invalid, 147 | } 148 | }; 149 | 150 | // Specialisations cannot be recursive, so we don't 151 | // have to look for specialisations of specialisations. 152 | const special_spec = try BootSpecV1.parse( 153 | allocator, 154 | next.key_ptr.*, 155 | sub_obj.get("org.nixos.bootspec.v1") orelse return Error.Invalid, 156 | ); 157 | try special_list.append(special_spec); 158 | } 159 | 160 | break :s try special_list.toOwnedSlice(); 161 | }, 162 | else => return Error.Invalid, 163 | } else { 164 | break :s null; 165 | } 166 | }; 167 | 168 | return @This(){ 169 | .spec = spec, 170 | .specialisations = specialisations, 171 | .allocator = allocator, 172 | .tree = tree, 173 | }; 174 | } 175 | 176 | pub fn deinit(self: *const @This()) void { 177 | self.spec.deinit(); 178 | 179 | if (self.specialisations) |specialisations| { 180 | for (specialisations) |s| { 181 | s.deinit(); 182 | } 183 | 184 | self.allocator.free(specialisations); 185 | } 186 | 187 | self.tree.deinit(); 188 | } 189 | }; 190 | 191 | test "boot spec parsing" { 192 | const json_contents = 193 | \\{ 194 | \\ "org.nixos.bootspec.v1": { 195 | \\ "init": "/nix/store/00000000000000000000000000000000-xxxxxxxxxx/init", 196 | \\ "initrd": "/nix/store/00000000000000000000000000000000-initrd-linux-x.x.xx/initrd", 197 | \\ "initrdSecrets": "/nix/store/00000000000000000000000000000000-append-initrd-secrets/bin/append-initrd-secrets", 198 | \\ "kernel": "/nix/store/00000000000000000000000000000000-linux-x.x.xx/bzImage", 199 | \\ "kernelParams": [ 200 | \\ "loglevel=4", 201 | \\ "nvidia-drm.modeset=1" 202 | \\ ], 203 | \\ "label": "foobar", 204 | \\ "system": "x86_64-linux", 205 | \\ "toplevel": "/nix/store/00000000000000000000000000000000-xxxxxxxxxx" 206 | \\ }, 207 | \\ "org.nixos.specialisation.v1": {} 208 | \\} 209 | ; 210 | 211 | const boot_json = try BootJson.parse(std.testing.allocator, json_contents); 212 | defer boot_json.deinit(); 213 | 214 | try std.testing.expectEqualStrings("/nix/store/00000000000000000000000000000000-xxxxxxxxxx/init", boot_json.spec.init); 215 | try std.testing.expectEqualStrings("/nix/store/00000000000000000000000000000000-initrd-linux-x.x.xx/initrd", boot_json.spec.initrd.?); 216 | try std.testing.expectEqualStrings("/nix/store/00000000000000000000000000000000-append-initrd-secrets/bin/append-initrd-secrets", boot_json.spec.initrd_secrets.?); 217 | try std.testing.expectEqualStrings("/nix/store/00000000000000000000000000000000-linux-x.x.xx/bzImage", boot_json.spec.kernel); 218 | try std.testing.expectEqual(@as(usize, 2), boot_json.spec.kernel_params.len); 219 | try std.testing.expectEqualStrings("loglevel=4", boot_json.spec.kernel_params[0]); 220 | try std.testing.expectEqualStrings("nvidia-drm.modeset=1", boot_json.spec.kernel_params[1]); 221 | try std.testing.expectEqualStrings("foobar", boot_json.spec.label); 222 | try std.testing.expectEqual(std.Target.Cpu.Arch.x86_64, boot_json.spec.system); 223 | try std.testing.expectEqualStrings("/nix/store/00000000000000000000000000000000-xxxxxxxxxx", boot_json.spec.toplevel); 224 | 225 | try std.testing.expectEqual(@as(usize, 0), boot_json.specialisations.?.len); 226 | } 227 | 228 | test "boot spec with specialisation" { 229 | const contents = 230 | \\{ 231 | \\ "org.nixos.bootspec.v1": { 232 | \\ "init": "/nix/store/00000000000000000000000000000000-xxxxxxxxxx/init", 233 | \\ "initrd": "/nix/store/00000000000000000000000000000000-initrd-linux-x.x.x/initrd", 234 | \\ "initrdSecrets": "/nix/store/00000000000000000000000000000000-append-initrd-secrets/bin/append-initrd-secrets", 235 | \\ "kernel": "/nix/store/00000000000000000000000000000000-linux-x.x.x/bzImage", 236 | \\ "kernelParams": [ 237 | \\ "console=ttyS0,115200", 238 | \\ "loglevel=4" 239 | \\ ], 240 | \\ "label": "foobar", 241 | \\ "system": "x86_64-linux", 242 | \\ "toplevel": "/nix/store/00000000000000000000000000000000-xxxxxxxxxx" 243 | \\ }, 244 | \\ "org.nixos.specialisation.v1": { 245 | \\ "alternate": { 246 | \\ "org.nixos.bootspec.v1": { 247 | \\ "init": "/nix/store/00000000000000000000000000000000-xxxxxxxxxx/init", 248 | \\ "initrd": "/nix/store/00000000000000000000000000000000-initrd-linux-x.x.x/initrd", 249 | \\ "initrdSecrets": "/nix/store/00000000000000000000000000000000-append-initrd-secrets/bin/append-initrd-secrets", 250 | \\ "kernel": "/nix/store/00000000000000000000000000000000-linux-x.x.x/bzImage", 251 | \\ "kernelParams": [ 252 | \\ "console=ttyS0,115200", 253 | \\ "console=tty1", 254 | \\ "loglevel=4" 255 | \\ ], 256 | \\ "label": "foobaz", 257 | \\ "system": "x86_64-linux", 258 | \\ "toplevel": "/nix/store/00000000000000000000000000000000-xxxxxxxxxx" 259 | \\ }, 260 | \\ "org.nixos.specialisation.v1": {} 261 | \\ } 262 | \\ } 263 | \\} 264 | ; 265 | 266 | const boot_json = try BootJson.parse(std.testing.allocator, contents); 267 | defer boot_json.deinit(); 268 | 269 | try std.testing.expectEqual(@as(usize, 1), boot_json.specialisations.?.len); 270 | } 271 | -------------------------------------------------------------------------------- /src/cpio.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const ASCII_CPIO_HEADER_SIZE = 110; 4 | const TRAILER = "TRAILER!!!"; 5 | 6 | const CpioHeader = struct { 7 | magic: u32 = 0x070701, 8 | ino: u32, 9 | mode: u32, 10 | uid: u32 = 0, 11 | gid: u32 = 0, 12 | nlink: u32, 13 | mtime: u32, 14 | filesize: u32 = 0, 15 | devmajor: u32 = 0, 16 | devminor: u32 = 0, 17 | rdevmajor: u32 = 0, 18 | rdevminor: u32 = 0, 19 | namesize: u32, 20 | check: u32 = 0, 21 | }; 22 | 23 | const CpioEntryType = enum { 24 | Directory, 25 | File, 26 | Symlink, 27 | 28 | fn toMode(self: @This(), perms: u32) u32 { 29 | return @as(u32, switch (self) { 30 | .Directory => std.os.linux.S.IFDIR | perms, 31 | .File => std.os.linux.S.IFREG | perms, 32 | .Symlink => std.os.linux.S.IFLNK | 0o777, // symlinks are always 0o777 33 | }); 34 | } 35 | }; 36 | 37 | pub const CpioArchive = @This(); 38 | 39 | destination: *std.io.StreamSource, 40 | ino: u32 = 0, 41 | total_written: usize = 0, 42 | 43 | const Error = error{ 44 | FileTooLarge, 45 | UnexpectedSource, 46 | }; 47 | 48 | pub fn init(destination: *std.io.StreamSource) !@This() { 49 | return @This(){ .destination = destination }; 50 | } 51 | 52 | pub fn addEntry( 53 | self: *@This(), 54 | source: ?*std.io.StreamSource, 55 | path: []const u8, 56 | entry_type: CpioEntryType, 57 | perms: u32, 58 | ) !void { 59 | if (entry_type == .Directory and source != null) { 60 | return Error.UnexpectedSource; 61 | } 62 | 63 | const filesize = b: { 64 | if (source) |s| { 65 | const pos = try s.getEndPos(); 66 | if (pos >= 1 << 32) { 67 | return Error.FileTooLarge; 68 | } else { 69 | break :b @as(u32, @intCast(pos)); 70 | } 71 | } else { 72 | break :b 0; 73 | } 74 | }; 75 | 76 | const filepath_len = path.len + 1; // null terminator 77 | 78 | // Zero by default for reproducibility, plus it's probably best to not 79 | // meaningfully use this field. The new ascii cpio format suffers from the 80 | // year 2038 problem with only being able to store 32 bits of time 81 | // information. 82 | var mtime_buf = [_]u8{0} ** (@sizeOf(u32) * 8); 83 | var fba = std.heap.FixedBufferAllocator.init(&mtime_buf); 84 | const mtime = b: { 85 | const mtime_string = std.process.getEnvVarOwned(fba.allocator(), "SOURCE_DATE_EPOCH") catch break :b 0; 86 | break :b try std.fmt.parseInt(u32, mtime_string, 10); 87 | }; 88 | 89 | // write entry to archive 90 | { 91 | const header = CpioHeader{ 92 | .ino = self.ino, 93 | .mode = entry_type.toMode(perms), 94 | .uid = 0, 95 | .gid = 0, 96 | .nlink = if (entry_type == .Directory) 2 else 1, 97 | .filesize = filesize, 98 | .devmajor = 0, 99 | .devminor = 0, 100 | .rdevmajor = 0, 101 | .rdevminor = 0, 102 | .mtime = mtime, 103 | .namesize = @intCast(filepath_len), 104 | }; 105 | 106 | try self.destination.writer().print("{X:0>6}{X:0>8}{X:0>8}{X:0>8}{X:0>8}{X:0>8}{X:0>8}{X:0>8}{X:0>8}{X:0>8}{X:0>8}{X:0>8}{X:0>8}{X:0>8}", .{ 107 | header.magic, 108 | header.ino, 109 | header.mode, 110 | header.uid, 111 | header.gid, 112 | header.nlink, 113 | header.mtime, 114 | header.filesize, 115 | header.devmajor, 116 | header.devminor, 117 | header.rdevmajor, 118 | header.rdevminor, 119 | header.namesize, 120 | header.check, 121 | }); 122 | self.total_written += ASCII_CPIO_HEADER_SIZE; 123 | 124 | try self.destination.writer().writeAll(path); 125 | try self.destination.writer().writeByte(0); // null terminator 126 | self.total_written += filepath_len; 127 | 128 | // pad the file name 129 | const header_padding = (4 - ((ASCII_CPIO_HEADER_SIZE + filepath_len) % 4)) % 4; 130 | try self.destination.writer().writeByteNTimes(0, header_padding); 131 | self.total_written += header_padding; 132 | 133 | if (source) |source_| { 134 | var pos: usize = 0; 135 | const end = try source_.getEndPos(); 136 | 137 | var buf = [_]u8{0} ** 4096; 138 | 139 | while (pos < end) { 140 | try source_.seekTo(pos); 141 | const bytes_read = try source_.read(&buf); 142 | try self.destination.writer().writeAll(buf[0..bytes_read]); 143 | self.total_written += bytes_read; 144 | pos += bytes_read; 145 | } 146 | 147 | // pad the file data 148 | const filedata_padding: usize = @intCast((4 - (end % 4)) % 4); 149 | try self.destination.writer().writeByteNTimes(0, filedata_padding); 150 | self.total_written += filedata_padding; 151 | } 152 | } 153 | 154 | self.ino += 1; 155 | } 156 | 157 | pub fn addFile(self: *@This(), path: []const u8, source: *std.io.StreamSource, perms: u32) !void { 158 | try self.addEntry(source, path, .File, perms); 159 | } 160 | 161 | pub fn addDirectory(self: *@This(), path: []const u8, perms: u32) !void { 162 | try self.addEntry(null, path, .Directory, perms); 163 | } 164 | 165 | pub fn addSymlink( 166 | self: *@This(), 167 | dstPath: []const u8, 168 | srcPath: []const u8, 169 | ) !void { 170 | var source = std.io.StreamSource{ 171 | .const_buffer = std.io.fixedBufferStream(srcPath), 172 | }; 173 | 174 | try self.addEntry( 175 | &source, 176 | dstPath, 177 | .Symlink, 178 | 0o777, // make symlinks always have 777 perms 179 | ); 180 | } 181 | 182 | pub fn finalize(self: *@This()) !void { 183 | try self.addEntry(null, TRAILER, .File, 0); 184 | 185 | // Maintain a block size of 512 by adding padding to the end of the 186 | // archive. 187 | try self.destination.writer().writeByteNTimes(0, (512 - (self.total_written % 512)) % 512); 188 | } 189 | 190 | fn handleFile( 191 | arena: *std.heap.ArenaAllocator, 192 | kind: std.fs.File.Kind, 193 | filename: []const u8, 194 | current_directory: []const u8, 195 | starting_directory: []const u8, 196 | archive: *CpioArchive, 197 | directory: *std.fs.Dir, 198 | ) anyerror!void { 199 | var fullpath_buf: [std.fs.max_path_bytes]u8 = undefined; 200 | const full_entry_path = try directory.realpath(filename, &fullpath_buf); 201 | const entry_path = try std.fs.path.relative(arena.allocator(), starting_directory, full_entry_path); 202 | 203 | switch (kind) { 204 | .directory => { 205 | var sub_directory = try directory.openDir( 206 | filename, 207 | .{ .iterate = true }, 208 | ); 209 | defer sub_directory.close(); 210 | 211 | try walkDirectory( 212 | arena, 213 | starting_directory, 214 | archive, 215 | &sub_directory, 216 | ); 217 | }, 218 | .file => { 219 | var file = try directory.openFile(filename, .{}); 220 | defer file.close(); 221 | 222 | const stat = try file.stat(); 223 | var source = std.io.StreamSource{ .file = file }; 224 | 225 | std.log.debug("adding file to archive at {s}", .{entry_path}); 226 | 227 | try archive.addFile(entry_path, &source, @intCast(stat.mode)); 228 | }, 229 | .sym_link => { 230 | const resolved_path = try std.fs.path.resolve( 231 | arena.allocator(), 232 | &.{ starting_directory, entry_path }, 233 | ); 234 | 235 | if (std.mem.startsWith(u8, resolved_path, starting_directory)) { 236 | const symlink_path = try std.fs.path.join( 237 | arena.allocator(), 238 | &.{ current_directory, filename }, 239 | ); 240 | 241 | try archive.addSymlink(symlink_path, entry_path); 242 | } else { 243 | const stat = try std.fs.cwd().statFile(resolved_path); 244 | 245 | try handleFile( 246 | arena, 247 | stat.kind, 248 | resolved_path, 249 | current_directory, 250 | starting_directory, 251 | archive, 252 | directory, 253 | ); 254 | } 255 | }, 256 | else => { 257 | std.log.warn( 258 | "Do not know how to add file {s} of kind {} to CPIO archive", 259 | .{ full_entry_path, kind }, 260 | ); 261 | }, 262 | } 263 | } 264 | 265 | pub fn walkDirectory( 266 | arena: *std.heap.ArenaAllocator, 267 | starting_directory: []const u8, 268 | archive: *CpioArchive, 269 | directory: *std.fs.Dir, 270 | ) anyerror!void { 271 | var iter = directory.iterate(); 272 | 273 | // Before iterating through the directory, first add the directory itself. 274 | var fullpath_buf: [std.fs.max_path_bytes]u8 = undefined; 275 | const full_directory_path = try directory.realpath(".", &fullpath_buf); 276 | const directory_path = try std.fs.path.relative(arena.allocator(), starting_directory, full_directory_path); 277 | 278 | // We don't need to add the root directory, as it will already exist. 279 | if (!std.mem.eql(u8, directory_path, "")) { 280 | // Before iterating through the directory, add the directory itself to 281 | // the archive. 282 | try archive.addDirectory(directory_path, 0o755); 283 | } 284 | 285 | while (try iter.next()) |dir_entry| { 286 | try handleFile( 287 | arena, 288 | dir_entry.kind, 289 | dir_entry.name, 290 | directory_path, 291 | starting_directory, 292 | archive, 293 | directory, 294 | ); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/device.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const utils = @import("./utils.zig"); 4 | 5 | const Device = @This(); 6 | 7 | subsystem: Subsystem, 8 | type: union(enum) { 9 | ifindex: u32, 10 | node: struct { u32, u32 }, 11 | }, 12 | 13 | pub fn nodePath(device: *const Device, buf: []u8) ![]u8 { 14 | std.debug.assert(device.type == .node); 15 | 16 | const major, const minor = device.type.node; 17 | 18 | return try std.fmt.bufPrint(buf, "/dev/{s}/{d}:{d}", .{ 19 | switch (device.subsystem) { 20 | .block => "block", 21 | else => "char", 22 | }, 23 | major, 24 | minor, 25 | }); 26 | } 27 | 28 | pub fn nodeSysfsPath(device: *const Device, buf: []u8) ![]u8 { 29 | std.debug.assert(device.type == .node); 30 | 31 | const major, const minor = device.type.node; 32 | 33 | return try std.fmt.bufPrint(buf, "/sys/dev/{s}/{d}:{d}", .{ 34 | switch (device.subsystem) { 35 | .block => "block", 36 | else => "char", 37 | }, 38 | major, 39 | minor, 40 | }); 41 | } 42 | 43 | pub fn format( 44 | self: Device, 45 | comptime fmt: []const u8, 46 | options: std.fmt.FormatOptions, 47 | writer: anytype, 48 | ) !void { 49 | _ = fmt; 50 | _ = options; 51 | 52 | switch (self.type) { 53 | .ifindex => |ifindex| try writer.print("{s} {}", .{ "ifindex", ifindex }), 54 | .node => |node| { 55 | const major, const minor = node; 56 | try writer.print("node {}:{}", .{ major, minor }); 57 | }, 58 | } 59 | } 60 | 61 | // ls -1 /sys/class 62 | // 63 | /// Subsystems we care about when acting as a bootloader. 64 | pub const Subsystem = enum { 65 | block, 66 | mem, 67 | mtd, 68 | net, 69 | rtc, 70 | tty, 71 | watchdog, 72 | 73 | pub fn fromStr(value: []const u8) !@This() { 74 | return utils.enumFromStr(@This(), value); 75 | } 76 | }; 77 | 78 | // grep --no-filename DEVTYPE /sys/class/*/*/uevent | cut -d'=' -f2 | sort | uniq 79 | // 80 | /// Device types we care about when acting as a bootloader. 81 | pub const DevType = enum { 82 | disk, 83 | mtd, 84 | partition, 85 | 86 | pub fn fromStr(value: []const u8) !@This() { 87 | return utils.enumFromStr(@This(), value); 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /src/disk/filesystem.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Filesystem = @This(); 4 | 5 | pub const Type = enum { 6 | Vfat, 7 | 8 | const vfat_signature_offset = 510; 9 | const vfat_signature = [_]u8{ 0x55, 0xaa }; 10 | 11 | /// Returns the filesystem type detected from a collection of bytes. 12 | pub fn detect(source: *std.io.StreamSource) !?@This() { 13 | try source.seekTo(vfat_signature_offset); 14 | var signature: [2]u8 = undefined; 15 | _ = try source.read(&signature); 16 | 17 | if (std.mem.eql(u8, &signature, &vfat_signature)) { 18 | return .Vfat; 19 | } 20 | 21 | return null; 22 | } 23 | }; 24 | 25 | test "vfat filesystem detection" { 26 | const FAT32_FILESYSTEM: []const u8 = &.{ 27 | 0xeb, 0x58, 0x90, 0x6d, 0x6b, 0x66, 0x73, 0x2e, 0x66, 0x61, 0x74, 0x00, 0x02, 0x08, 0x20, 0x00, 28 | 0x02, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x3f, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 29 | 0xfc, 0xff, 0x0f, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 30 | 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 31 | 0x80, 0x00, 0x29, 0x00, 0xa6, 0xce, 0x12, 0x4e, 0x4f, 0x20, 0x4e, 0x41, 0x4d, 0x45, 0x20, 0x20, 32 | 0x20, 0x20, 0x46, 0x41, 0x54, 0x33, 0x32, 0x20, 0x20, 0x20, 0x0e, 0x1f, 0xbe, 0x77, 0x7c, 0xac, 33 | 0x22, 0xc0, 0x74, 0x0b, 0x56, 0xb4, 0x0e, 0xbb, 0x07, 0x00, 0xcd, 0x10, 0x5e, 0xeb, 0xf0, 0x32, 34 | 0xe4, 0xcd, 0x16, 0xcd, 0x19, 0xeb, 0xfe, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x6e, 35 | 0x6f, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x64, 0x69, 36 | 0x73, 0x6b, 0x2e, 0x20, 0x20, 0x50, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x20, 0x69, 0x6e, 0x73, 0x65, 37 | 0x72, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x66, 0x6c, 38 | 0x6f, 0x70, 0x70, 0x79, 0x20, 0x61, 0x6e, 0x64, 0x0d, 0x0a, 0x70, 0x72, 0x65, 0x73, 0x73, 0x20, 39 | 0x61, 0x6e, 0x79, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x74, 0x72, 0x79, 0x20, 0x61, 40 | 0x67, 0x61, 0x69, 0x6e, 0x20, 0x2e, 0x2e, 0x2e, 0x20, 0x0d, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 41 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 42 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 43 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 44 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 45 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 46 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 47 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 48 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 49 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 50 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 51 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 52 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 53 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 54 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 55 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 56 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 57 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 58 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 59 | }; 60 | 61 | const FAT16_FILESYSTEM: []const u8 = &.{ 62 | 0xeb, 0x3c, 0x90, 0x6d, 0x6b, 0x66, 0x73, 0x2e, 0x66, 0x61, 0x74, 0x00, 0x02, 0x10, 0x10, 0x00, 63 | 0x02, 0x00, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x01, 0x3f, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 64 | 0xfc, 0xff, 0x0f, 0x00, 0x80, 0x00, 0x29, 0x00, 0xa6, 0xce, 0x12, 0x4e, 0x4f, 0x20, 0x4e, 0x41, 65 | 0x4d, 0x45, 0x20, 0x20, 0x20, 0x20, 0x46, 0x41, 0x54, 0x31, 0x36, 0x20, 0x20, 0x20, 0x0e, 0x1f, 66 | 0xbe, 0x5b, 0x7c, 0xac, 0x22, 0xc0, 0x74, 0x0b, 0x56, 0xb4, 0x0e, 0xbb, 0x07, 0x00, 0xcd, 0x10, 67 | 0x5e, 0xeb, 0xf0, 0x32, 0xe4, 0xcd, 0x16, 0xcd, 0x19, 0xeb, 0xfe, 0x54, 0x68, 0x69, 0x73, 0x20, 68 | 0x69, 0x73, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, 69 | 0x65, 0x20, 0x64, 0x69, 0x73, 0x6b, 0x2e, 0x20, 0x20, 0x50, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x20, 70 | 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, 71 | 0x65, 0x20, 0x66, 0x6c, 0x6f, 0x70, 0x70, 0x79, 0x20, 0x61, 0x6e, 0x64, 0x0d, 0x0a, 0x70, 0x72, 72 | 0x65, 0x73, 0x73, 0x20, 0x61, 0x6e, 0x79, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x74, 73 | 0x72, 0x79, 0x20, 0x61, 0x67, 0x61, 0x69, 0x6e, 0x20, 0x2e, 0x2e, 0x2e, 0x20, 0x0d, 0x0a, 0x00, 74 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 75 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 76 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 77 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 78 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 79 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 80 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 81 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 82 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 83 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 84 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 85 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 86 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 87 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 88 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 89 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 90 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 91 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 92 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 93 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 94 | }; 95 | 96 | const FAT12_FILESYSTEM: []const u8 = &.{ 97 | 0xeb, 0x3c, 0x90, 0x6d, 0x6b, 0x66, 0x73, 0x2e, 0x66, 0x61, 0x74, 0x00, 0x02, 0x80, 0x80, 0x00, 98 | 0x02, 0x00, 0x08, 0x00, 0x00, 0xf8, 0x80, 0x00, 0x20, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 99 | 0x00, 0x00, 0x04, 0x00, 0x80, 0x00, 0x29, 0x00, 0xa6, 0xce, 0x12, 0x4e, 0x4f, 0x20, 0x4e, 0x41, 100 | 0x4d, 0x45, 0x20, 0x20, 0x20, 0x20, 0x46, 0x41, 0x54, 0x31, 0x32, 0x20, 0x20, 0x20, 0x0e, 0x1f, 101 | 0xbe, 0x5b, 0x7c, 0xac, 0x22, 0xc0, 0x74, 0x0b, 0x56, 0xb4, 0x0e, 0xbb, 0x07, 0x00, 0xcd, 0x10, 102 | 0x5e, 0xeb, 0xf0, 0x32, 0xe4, 0xcd, 0x16, 0xcd, 0x19, 0xeb, 0xfe, 0x54, 0x68, 0x69, 0x73, 0x20, 103 | 0x69, 0x73, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, 104 | 0x65, 0x20, 0x64, 0x69, 0x73, 0x6b, 0x2e, 0x20, 0x20, 0x50, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x20, 105 | 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, 106 | 0x65, 0x20, 0x66, 0x6c, 0x6f, 0x70, 0x70, 0x79, 0x20, 0x61, 0x6e, 0x64, 0x0d, 0x0a, 0x70, 0x72, 107 | 0x65, 0x73, 0x73, 0x20, 0x61, 0x6e, 0x79, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x74, 108 | 0x72, 0x79, 0x20, 0x61, 0x67, 0x61, 0x69, 0x6e, 0x20, 0x2e, 0x2e, 0x2e, 0x20, 0x0d, 0x0a, 0x00, 109 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 110 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 111 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 112 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 113 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 114 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 115 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 116 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 117 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 118 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 119 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 120 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 121 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 122 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 123 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 124 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 125 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 126 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 127 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 128 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 129 | }; 130 | 131 | var fat32_source = std.io.StreamSource{ 132 | .const_buffer = std.io.fixedBufferStream(FAT32_FILESYSTEM), 133 | }; 134 | var fat16_source = std.io.StreamSource{ 135 | .const_buffer = std.io.fixedBufferStream(FAT16_FILESYSTEM), 136 | }; 137 | var fat12_source = std.io.StreamSource{ 138 | .const_buffer = std.io.fixedBufferStream(FAT12_FILESYSTEM), 139 | }; 140 | 141 | try std.testing.expectEqual(try Filesystem.Type.detect(&fat32_source), .Vfat); 142 | try std.testing.expectEqual(try Filesystem.Type.detect(&fat16_source), .Vfat); 143 | try std.testing.expectEqual(try Filesystem.Type.detect(&fat12_source), .Vfat); 144 | } 145 | -------------------------------------------------------------------------------- /src/disk/mbr.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// A read-only legacy Master Boot Record partition table 4 | const Mbr = @This(); 5 | 6 | const boot_magic = std.mem.bigToNative(u16, 0x55aa); 7 | 8 | header: Header, 9 | 10 | /// Caller is responsible for source. 11 | pub fn init(stream: *std.io.StreamSource) !Mbr { 12 | comptime std.debug.assert(@sizeOf(Header) == 512); 13 | 14 | const header = try stream.reader().readStructEndian(Header, .little); 15 | 16 | if (header.signature != boot_magic) { 17 | return error.InvalidMbr; 18 | } 19 | 20 | return .{ .header = header }; 21 | } 22 | 23 | pub fn identifier(self: *const Mbr) u32 { 24 | return std.mem.littleToNative(@TypeOf(self.header.unique_mbr_signature), self.header.unique_mbr_signature); 25 | } 26 | 27 | pub fn partitions(self: *const Mbr) [4]PartitionRecord { 28 | return self.header.partition_records; 29 | } 30 | 31 | const Header = extern struct { 32 | boot_code: [440]u8 align(1), 33 | unique_mbr_signature: u32 align(1), 34 | unknown: u16 align(1), 35 | partition_records: [4]PartitionRecord, 36 | signature: u16 align(1), 37 | }; 38 | 39 | const PartitionRecord = extern struct { 40 | boot_indicator: u8, 41 | start_head: u8, 42 | start_sector: u8, 43 | start_track: u8, 44 | os_type: u8, 45 | end_head: u8, 46 | end_sector: u8, 47 | end_track: u8, 48 | starting_lba: u32 align(1), 49 | size_in_lba: u32 align(1), 50 | 51 | const bootable_flag = 0x80; 52 | 53 | pub fn isBootable(self: *const @This()) bool { 54 | return self.boot_indicator == bootable_flag; 55 | } 56 | 57 | pub fn partType(self: *const @This()) u8 { 58 | return self.os_type; 59 | } 60 | }; 61 | 62 | pub const PartitionType = enum { 63 | Fat16, 64 | ProtectedMbr, 65 | LinuxExtendedBoot, 66 | EfiSystemPartition, 67 | 68 | pub fn fromValue(val: u8) ?@This() { 69 | return switch (val) { 70 | 0x06 => .Fat16, 71 | 0xea => .LinuxExtendedBoot, 72 | 0xee => .ProtectedMbr, 73 | 0xef => .EfiSystemPartition, 74 | else => return null, 75 | }; 76 | } 77 | }; 78 | 79 | test "mbr parsing" { 80 | // Disk /dev/sda: 504 MiB, 528482304 bytes, 1032192 sectors 81 | // Disk model: QEMU HARDDISK 82 | // Units: sectors of 1 * 512 = 512 bytes 83 | // Sector size (logical/physical): 512 bytes / 512 bytes 84 | // I/O size (minimum/optimal): 512 bytes / 512 bytes 85 | // Disklabel type: dos 86 | // Disk identifier: 0xbe1afdfa 87 | // 88 | // Device Boot Start End Sectors Size Id Type 89 | // /dev/sda1 * 63 1032191 1032129 504M 6 FAT16 90 | const partition_table= [_]u8{ 91 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 92 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 93 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 94 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 95 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 96 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 97 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 98 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 99 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 100 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 101 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 102 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 103 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 104 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 105 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 106 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 107 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 108 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 109 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 110 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 111 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 112 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 113 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 114 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 115 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 116 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 117 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 118 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0xfd, 0x1a, 0xbe, 0x00, 0x00, 0x80, 0x01, 119 | 0x01, 0x00, 0x06, 0x0f, 0xff, 0xff, 0x3f, 0x00, 0x00, 0x00, 0xc1, 0xbf, 0x0f, 0x00, 0x00, 0x00, 120 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 121 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 122 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 123 | }; 124 | 125 | var stream = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(&partition_table) }; 126 | 127 | var disk = try Mbr.init(&stream); 128 | 129 | try std.testing.expectEqual(@as(u32, 0xbe1afdfa), disk.identifier()); 130 | const mbr_partitions = disk.partitions(); 131 | try std.testing.expect(mbr_partitions[0].isBootable()); 132 | try std.testing.expectEqual(@as(u8, 0x06), mbr_partitions[0].partType()); 133 | } 134 | -------------------------------------------------------------------------------- /src/kexec/kexec.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const posix = std.posix; 3 | const builtin = @import("builtin"); 4 | 5 | const linux_headers = @import("linux_headers"); 6 | 7 | const KEXEC_LOADED = "/sys/kernel/kexec_loaded"; 8 | 9 | pub const MemoryType = enum(u8) { 10 | Ram = 0, 11 | Reserved = 1, 12 | Acpi = 2, 13 | AcpiNvs = 3, 14 | Uncached = 4, 15 | Pmem = 6, 16 | Pram = 11, 17 | }; 18 | 19 | pub const MemoryRange = struct { 20 | start: usize, 21 | end: usize, 22 | type: MemoryType, 23 | }; 24 | 25 | /// This structure is used to hold the arguments that 26 | /// are used when loading kernel binaries. 27 | /// https://github.com/torvalds/linux/blob/0d8d44db295ccad20052d6301ef49ff01fb8ae2d/include/uapi/linux/kexec.h#L59 28 | pub const KexecSegment = struct { 29 | buf: *anyopaque, 30 | buf_size: usize, 31 | mem: *anyopaque, 32 | mem_size: usize, 33 | }; 34 | 35 | /// Wait for up to 10 seconds for kernel to report for kexec to be loaded. 36 | fn waitForKexecKernelLoaded() !void { 37 | var f = try std.fs.cwd().openFile(KEXEC_LOADED, .{}); 38 | defer f.close(); 39 | 40 | var time_slept: usize = 0; 41 | while (time_slept < 10 * std.time.ns_per_s) : (time_slept += std.time.ns_per_s) { 42 | try f.seekTo(0); 43 | 44 | if (try f.reader().readByte() == '1') { 45 | return; 46 | } 47 | 48 | std.time.sleep(std.time.ns_per_s); 49 | } 50 | 51 | return error.Timeout; 52 | } 53 | 54 | const kexecLoad = switch (builtin.cpu.arch) { 55 | .arm => @import("./arm.zig").kexecLoad, 56 | else => @compileError("kexec_load not implemented for target architecture"), 57 | }; 58 | 59 | fn kexecFileLoad( 60 | allocator: std.mem.Allocator, 61 | linux: std.fs.File, 62 | initrd: ?std.fs.File, 63 | cmdline: ?[]const u8, 64 | ) !void { 65 | var flags: usize = 0; 66 | if (initrd == null) { 67 | flags |= linux_headers.KEXEC_FILE_NO_INITRAMFS; 68 | } 69 | 70 | // dupeZ() returns a null-terminated slice, however the null-terminator 71 | // is not included in the length of the slice, so we must add 1. 72 | const cmdline_z = try allocator.dupeZ(u8, cmdline orelse ""); 73 | defer allocator.free(cmdline_z); 74 | const cmdline_len = cmdline_z.len + 1; 75 | 76 | const rc = std.os.linux.syscall5( 77 | .kexec_file_load, 78 | @as(usize, @bitCast(@as(isize, linux.handle))), 79 | @as(usize, @bitCast(@as( 80 | isize, 81 | if (initrd) |initrd_| initrd_.handle else 0, 82 | ))), 83 | cmdline_len, 84 | @intFromPtr(cmdline_z.ptr), 85 | flags, 86 | ); 87 | 88 | switch (posix.errno(rc)) { 89 | .SUCCESS => {}, 90 | // IMA appraisal failed 91 | .ACCES => return error.PermissionDenied, 92 | // Invalid kernel image (CONFIG_RELOCATABLE not enabled?) 93 | .NOEXEC => return error.InvalidExe, 94 | // Another image is already loaded 95 | .BUSY => return error.FilesAlreadyRegistered, 96 | .NOMEM => return error.SystemResources, 97 | .BADF => return error.InvalidFileDescriptor, 98 | else => |err| { 99 | std.log.err("kexec load failed for unknown reason: {}", .{err}); 100 | return posix.unexpectedErrno(err); 101 | }, 102 | } 103 | } 104 | 105 | pub const kexec_file_load_available = switch (builtin.cpu.arch) { 106 | // TODO(jared): confirm there aren't any more. 107 | .aarch64, .riscv64, .x86_64 => true, 108 | else => false, 109 | }; 110 | 111 | pub fn kexecUnload() !void { 112 | const rc = if (kexec_file_load_available) std.os.linux.syscall5( 113 | .kexec_file_load, 114 | null, 115 | null, 116 | null, 117 | 0, 118 | linux_headers.KEXEC_FILE_NO_INITRAMFS, 119 | ) else std.os.linux.syscall4( 120 | .kexec_load, 121 | null, 122 | 0, 123 | null, 124 | 0, 125 | ); 126 | 127 | return switch (posix.errno(rc)) { 128 | .SUCESS => {}, 129 | else => |err| return posix.unexpectedErrno(err), 130 | }; 131 | } 132 | 133 | pub fn kexec( 134 | allocator: std.mem.Allocator, 135 | linux_filepath: []const u8, 136 | initrd_filepath: ?[]const u8, 137 | cmdline: ?[]const u8, 138 | ) !void { 139 | std.log.info("preparing kexec", .{}); 140 | std.log.info("loading linux {s}", .{linux_filepath}); 141 | std.log.info("loading initrd {s}", .{initrd_filepath orelse ""}); 142 | std.log.info("loading params {s}", .{cmdline orelse ""}); 143 | 144 | // Use a constant path for the kernel and initrd so that the IMA events 145 | // don't have differing random temporary paths each boot. 146 | std.fs.cwd().makeDir("/tinyboot") catch |err| switch (err) { 147 | error.PathAlreadyExists => {}, 148 | else => return err, 149 | }; 150 | 151 | var boot_dir = try std.fs.cwd().openDir("/tinyboot", .{}); 152 | defer { 153 | boot_dir.close(); 154 | std.fs.cwd().deleteTree("/tinyboot") catch {}; 155 | } 156 | 157 | try std.fs.cwd().copyFile(linux_filepath, std.fs.cwd(), "/tinyboot/kernel", .{}); 158 | if (initrd_filepath) |initrd| { 159 | try std.fs.cwd().copyFile(initrd, std.fs.cwd(), "/tinyboot/initrd", .{}); 160 | } 161 | 162 | const linux = try std.fs.cwd().openFile("/tinyboot/kernel", .{}); 163 | defer linux.close(); 164 | 165 | const initrd: ?std.fs.File = if (initrd_filepath != null) try std.fs.cwd().openFile("/tinyboot/initrd", .{}) else null; 166 | defer { 167 | if (initrd) |initrd_| { 168 | initrd_.close(); 169 | } 170 | } 171 | 172 | if (kexec_file_load_available) { 173 | try kexecFileLoad(allocator, linux, initrd, cmdline); 174 | } else { 175 | try kexecLoad(allocator, linux, initrd, cmdline); 176 | } 177 | 178 | try waitForKexecKernelLoaded(); 179 | 180 | std.log.info("kexec loaded", .{}); 181 | } 182 | -------------------------------------------------------------------------------- /src/kobject.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Device = @import("./device.zig"); 4 | const DeviceWatcher = @import("./watch.zig"); 5 | const utils = @import("./utils.zig"); 6 | 7 | pub fn parseUeventFileContents( 8 | subsystem: Device.Subsystem, 9 | contents: []const u8, 10 | ) ?Device { 11 | var iter = std.mem.splitSequence(u8, contents, "\n"); 12 | 13 | var major: ?u32 = null; 14 | var minor: ?u32 = null; 15 | var ifindex: ?u32 = null; 16 | 17 | while (iter.next()) |line| { 18 | var split = std.mem.splitSequence(u8, line, "="); 19 | const key = split.next() orelse continue; 20 | const value = split.next() orelse continue; 21 | 22 | if (std.mem.eql(u8, key, "IFINDEX")) { 23 | ifindex = std.fmt.parseInt(u32, value, 10) catch return null; 24 | } else if (std.mem.eql(u8, key, "MAJOR")) { 25 | major = std.fmt.parseInt(u32, value, 10) catch return null; 26 | } else if (std.mem.eql(u8, key, "MINOR")) { 27 | minor = std.fmt.parseInt(u32, value, 10) catch return null; 28 | } 29 | } 30 | 31 | return .{ 32 | .subsystem = subsystem, 33 | .type = if (ifindex) |ifidx| 34 | .{ .ifindex = ifidx } 35 | else 36 | .{ .node = .{ 37 | major orelse return null, 38 | minor orelse return null, 39 | } }, 40 | }; 41 | } 42 | 43 | pub fn parseUeventKobjectContents(contents: []const u8) ?DeviceWatcher.Event { 44 | var iter = std.mem.splitSequence(u8, contents, &.{0}); 45 | 46 | const first_line = iter.next().?; 47 | var first_line_split = std.mem.splitSequence(u8, first_line, "@"); 48 | const action = Action.fromStr(first_line_split.next().?) catch return null; 49 | 50 | var subsystem: ?Device.Subsystem = null; 51 | var major: ?u32 = null; 52 | var minor: ?u32 = null; 53 | var ifindex: ?u32 = null; 54 | 55 | while (iter.next()) |line| { 56 | var split = std.mem.splitSequence(u8, line, "="); 57 | const key = split.next() orelse continue; 58 | const value = split.next() orelse continue; 59 | 60 | if (std.mem.eql(u8, key, "SUBSYSTEM")) { 61 | subsystem = Device.Subsystem.fromStr(value) catch return null; 62 | } else if (std.mem.eql(u8, key, "IFINDEX")) { 63 | ifindex = std.fmt.parseInt(u32, value, 10) catch return null; 64 | } else if (std.mem.eql(u8, key, "MAJOR")) { 65 | major = std.fmt.parseInt(u32, value, 10) catch return null; 66 | } else if (std.mem.eql(u8, key, "MINOR")) { 67 | minor = std.fmt.parseInt(u32, value, 10) catch return null; 68 | } 69 | } 70 | 71 | const subsystem_ = subsystem orelse return null; 72 | 73 | return .{ 74 | .action = action, 75 | .device = .{ 76 | .subsystem = subsystem_, 77 | .type = b: { 78 | if (subsystem_ == .net) { 79 | // We expect IFINDEX to be set for the net subsystem. 80 | if (ifindex) |ifidx| { 81 | break :b .{ .ifindex = ifidx }; 82 | } else { 83 | return null; 84 | } 85 | } else { 86 | break :b .{ 87 | .node = .{ 88 | major orelse return null, 89 | minor orelse return null, 90 | }, 91 | }; 92 | } 93 | }, 94 | }, 95 | }; 96 | } 97 | 98 | test "uevent file content parsing" { 99 | const test_partition = 100 | \\MAJOR=259 101 | \\MINOR=1 102 | \\DEVNAME=nvme0n1p1 103 | \\DEVTYPE=partition 104 | \\DISKSEQ=1 105 | \\PARTN=1 106 | ; 107 | 108 | try std.testing.expectEqualDeep( 109 | Device{ .subsystem = .block, .type = .{ .node = .{ 259, 1 } } }, 110 | parseUeventFileContents(.block, test_partition) orelse unreachable, 111 | ); 112 | 113 | const test_disk = 114 | \\MAJOR=259 115 | \\MINOR=0 116 | \\DEVNAME=nvme0n1 117 | \\DEVTYPE=disk 118 | \\DISKSEQ=1 119 | ; 120 | 121 | try std.testing.expectEqualDeep( 122 | Device{ .subsystem = .block, .type = .{ .node = .{ 259, 0 } } }, 123 | parseUeventFileContents(.block, test_disk) orelse unreachable, 124 | ); 125 | } 126 | 127 | test "uevent kobject add chardev parsing" { 128 | const content = try std.mem.join(std.testing.allocator, &.{0}, &.{ 129 | "add@/devices/platform/serial8250/tty/ttyS6", 130 | "ACTION=add", 131 | "DEVPATH=/devices/platform/serial8250/tty/ttyS6", 132 | "SUBSYSTEM=tty", 133 | "SYNTH_UUID=0", 134 | "MAJOR=4", 135 | "MINOR=70", 136 | "DEVNAME=ttyS6", 137 | "SEQNUM=3469", 138 | }); 139 | defer std.testing.allocator.free(content); 140 | 141 | try std.testing.expectEqual( 142 | DeviceWatcher.Event{ 143 | .action = .add, 144 | .device = .{ 145 | .subsystem = .tty, 146 | .type = .{ .node = .{ 4, 70 } }, 147 | }, 148 | }, 149 | parseUeventKobjectContents(content) orelse unreachable, 150 | ); 151 | } 152 | 153 | test "uevent kobject remove chardev parsing" { 154 | const content = try std.mem.join(std.testing.allocator, &.{0}, &.{ 155 | "remove@/devices/platform/serial8250/tty/ttyS6", 156 | "ACTION=remove", 157 | "DEVPATH=/devices/platform/serial8250/tty/ttyS6", 158 | "SUBSYSTEM=tty", 159 | "SYNTH_UUID=0", 160 | "MAJOR=4", 161 | "MINOR=70", 162 | "DEVNAME=ttyS6", 163 | "SEQNUM=3471", 164 | }); 165 | defer std.testing.allocator.free(content); 166 | 167 | try std.testing.expectEqual( 168 | DeviceWatcher.Event{ 169 | .action = .remove, 170 | .device = .{ 171 | .subsystem = .tty, 172 | .type = .{ .node = .{ 4, 70 } }, 173 | }, 174 | }, 175 | parseUeventKobjectContents(content) orelse unreachable, 176 | ); 177 | } 178 | 179 | // https://github.com/torvalds/linux/blob/afcd48134c58d6af45fb3fdb648f1260b20f2326/lib/kobject_uevent.c#L50 180 | pub const Action = union(enum) { 181 | add, 182 | remove, 183 | 184 | fn fromStr(value: []const u8) !@This() { 185 | return utils.enumFromStr(@This(), value); 186 | } 187 | }; 188 | -------------------------------------------------------------------------------- /src/log.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const LOG_PREFIX = "boot"; 4 | 5 | const KMSG = "/dev/char/1:11"; 6 | 7 | const SYSLOG_FACILITY_USER = 1; 8 | 9 | // https://github.com/torvalds/linux/blob/55027e689933ba2e64f3d245fb1ff185b3e7fc81/kernel/printk/internal.h#L38C9-L38C28 10 | // https://github.com/torvalds/linux/blob/55027e689933ba2e64f3d245fb1ff185b3e7fc81/kernel/printk/printk.c#L735 11 | const PRINTKRB_RECORD_MAX = 1024; 12 | 13 | var mutex = std.Thread.Mutex{}; 14 | var kmsg: ?std.fs.File = null; 15 | 16 | // The Zig string formatter can make many individual writes to our 17 | // writer depending on the format string, so we do all the formatting 18 | // ahead of time here so we can perform the write all at once when the 19 | // log line goes to the kernel. 20 | var log_buf: [PRINTKRB_RECORD_MAX]u8 = undefined; 21 | var stream = std.io.fixedBufferStream(&log_buf); 22 | 23 | pub fn init() !void { 24 | kmsg = try std.fs.cwd().openFile(KMSG, .{ .mode = .write_only }); 25 | } 26 | 27 | pub fn deinit() void { 28 | if (kmsg) |file| { 29 | file.close(); 30 | } 31 | } 32 | 33 | pub fn logFn( 34 | comptime level: std.log.Level, 35 | comptime scope: @TypeOf(.EnumLiteral), 36 | comptime format: []const u8, 37 | args: anytype, 38 | ) void { 39 | _ = scope; 40 | 41 | const syslog_prefix = comptime b: { 42 | var buf: [2]u8 = undefined; 43 | var fbs = std.io.fixedBufferStream(&buf); 44 | 45 | // 0 KERN_EMERG 46 | // 1 KERN_ALERT 47 | // 2 KERN_CRIT 48 | // 3 KERN_ERR 49 | // 4 KERN_WARNING 50 | // 5 KERN_NOTICE 51 | // 6 KERN_INFO 52 | // 7 KERN_DEBUG 53 | 54 | // https://github.com/torvalds/linux/blob/f2661062f16b2de5d7b6a5c42a9a5c96326b8454/Documentation/ABI/testing/dev-kmsg#L1 55 | const syslog_level = ((SYSLOG_FACILITY_USER << 3) | switch (level) { 56 | .err => 3, 57 | .warn => 4, 58 | .info => 6, 59 | .debug => 7, 60 | }); 61 | 62 | std.fmt.formatIntValue(syslog_level, "", .{}, fbs.writer()) catch return; 63 | break :b fbs.getWritten(); 64 | }; 65 | 66 | const file = kmsg orelse return; 67 | 68 | mutex.lock(); 69 | defer mutex.unlock(); 70 | 71 | stream.writer().print( 72 | "<" ++ syslog_prefix ++ ">" ++ LOG_PREFIX ++ ": " ++ format, 73 | args, 74 | ) catch {}; 75 | 76 | file.writeAll(stream.getWritten()) catch {}; 77 | 78 | stream.reset(); 79 | } 80 | -------------------------------------------------------------------------------- /src/mbedtls.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const C = @cImport({ 4 | @cInclude("mbedtls/error.h"); 5 | }); 6 | 7 | var err_buf = [_]u8{0} ** 1024; 8 | 9 | pub fn wrapMulti(return_code: c_int) !c_int { 10 | return wrapReturnCode(.negative, c_int, return_code); 11 | } 12 | 13 | pub fn wrap(return_code: c_int) !void { 14 | return wrapReturnCode(.positive, void, return_code); 15 | } 16 | 17 | fn wrapReturnCode( 18 | comptime return_code_type: enum { negative, positive }, 19 | comptime T: type, 20 | return_code: c_int, 21 | ) !T { 22 | if ((return_code_type == .negative and return_code < 0) or (return_code_type == .positive and return_code != 0)) { 23 | C.mbedtls_strerror(return_code, &err_buf, err_buf.len); 24 | std.log.err("mbedtls error({}): {s}", .{ @abs(return_code), err_buf }); 25 | return error.MbedtlsError; 26 | } 27 | 28 | if (return_code_type == .negative) { 29 | return return_code; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/pkcs7.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const asn1 = std.crypto.asn1; 3 | 4 | const sequence_of_tag = asn1.Tag.universal(.sequence_of, true); 5 | const sequence_tag = asn1.Tag.universal(.sequence, true); 6 | const integer_tag = asn1.Tag.universal(.integer, false); 7 | const printable_string_tag = asn1.Tag.universal(.string_printable, false); 8 | const utf8_string_tag = asn1.Tag.universal(.string_utf8, false); 9 | const octetstring_tag = asn1.Tag.universal(.octetstring, false); 10 | 11 | const DigestAlgorithmIdentifier = struct { 12 | const Algorithm = enum { 13 | sha256, 14 | 15 | pub const oids = asn1.Oid.StaticMap(@This()).initComptime(.{ 16 | .sha256 = "2.16.840.1.101.3.4.2.1", 17 | }); 18 | }; 19 | 20 | algorithm: Algorithm, 21 | 22 | // TODO(jared): make encoding this work OOTB 23 | parameters: struct { 24 | pub fn encodeDer(self: @This(), encoder: *asn1.der.Encoder) !void { 25 | _ = self; 26 | try encoder.any(null); 27 | } 28 | }, 29 | }; 30 | 31 | const SignatureAlgorithmIdentifier = struct { 32 | const Algorithm = enum { 33 | rsa, 34 | 35 | pub const oids = asn1.Oid.StaticMap(@This()).initComptime(.{ 36 | .rsa = "1.2.840.113549.1.1.1", 37 | }); 38 | }; 39 | 40 | algorithm: Algorithm, 41 | 42 | // TODO(jared): make encoding this work OOTB 43 | parameters: struct { 44 | pub fn encodeDer(self: @This(), encoder: *asn1.der.Encoder) !void { 45 | _ = self; 46 | try encoder.any(null); 47 | } 48 | }, 49 | }; 50 | 51 | const ContentType = enum { 52 | signed_data, 53 | 54 | pub const oids = asn1.Oid.StaticMap(@This()).initComptime(.{ 55 | .signed_data = "1.2.840.113549.1.7.2", 56 | }); 57 | }; 58 | 59 | const Content = union(ContentType) { 60 | const SignedData = struct { 61 | const DigestAlgorithms = struct { 62 | pub const asn1_tag = sequence_of_tag; 63 | 64 | inner: DigestAlgorithmIdentifier, 65 | }; 66 | 67 | const EncapsulatedContentInfo = struct { 68 | const EncapsulatedContentType = enum { 69 | pkcs7, 70 | 71 | pub const oids = asn1.Oid.StaticMap(@This()).initComptime(.{ 72 | .pkcs7 = "1.2.840.113549.1.7.1", 73 | }); 74 | }; 75 | 76 | content_type: EncapsulatedContentType, 77 | }; 78 | 79 | const SignerInfos = struct { 80 | const SignerInfo = struct { 81 | const IssuerAndSerialNumber = struct { 82 | const Name = struct { 83 | const RelativeDistinguishedName = struct { 84 | const InnerName = struct { 85 | const AttributeTypeAndValue = struct { 86 | type: asn1.Oid, 87 | value: []const u8, 88 | 89 | const common_name_oid = asn1.Oid.fromDotComptime("2.5.4.3"); 90 | const organization_name_oid = asn1.Oid.fromDotComptime("2.5.4.10"); 91 | 92 | pub fn encodeDer(self: @This(), encoder: *asn1.der.Encoder) !void { 93 | const tag = if (std.mem.eql( 94 | u8, 95 | self.type.encoded, 96 | common_name_oid.encoded, 97 | ) or std.mem.eql( 98 | u8, 99 | self.type.encoded, 100 | organization_name_oid.encoded, 101 | )) utf8_string_tag else printable_string_tag; 102 | 103 | try encoder.tagBytes(tag, self.value); 104 | try encoder.any(self.type); 105 | } 106 | }; 107 | 108 | inner: AttributeTypeAndValue, 109 | }; 110 | 111 | inner: []const InnerName, 112 | 113 | pub fn encodeDer(self: @This(), encoder: *asn1.der.Encoder) !void { 114 | for (self.inner) |name| { 115 | const start = encoder.buffer.data.len; 116 | try encoder.any(name); 117 | try encoder.length(encoder.buffer.data.len - start); 118 | try encoder.tag(sequence_of_tag); 119 | } 120 | } 121 | }; 122 | 123 | relative_distinguished_name: RelativeDistinguishedName, 124 | }; 125 | 126 | rdn_sequence: Name, 127 | serial_number: []u8, 128 | 129 | pub fn encodeDer(self: @This(), encoder: *asn1.der.Encoder) !void { 130 | const start = encoder.buffer.data.len; 131 | 132 | try encoder.tagBytes(integer_tag, self.serial_number); 133 | try encoder.any(self.rdn_sequence); 134 | 135 | try encoder.length(encoder.buffer.data.len - start); 136 | try encoder.tag(sequence_tag); 137 | } 138 | }; 139 | 140 | const SignatureValue = struct { 141 | data: []const u8, 142 | 143 | pub fn encodeDer(self: @This(), encoder: *asn1.der.Encoder) !void { 144 | try encoder.tagBytes(octetstring_tag, self.data); 145 | } 146 | }; 147 | 148 | version: u8, 149 | issuer_and_serial_number: IssuerAndSerialNumber, 150 | digest_algorithm: DigestAlgorithmIdentifier, 151 | signature_algorithm: SignatureAlgorithmIdentifier, 152 | signature: SignatureValue, 153 | }; 154 | 155 | inner: []const SignerInfo, 156 | 157 | pub fn encodeDer(self: @This(), encoder: *asn1.der.Encoder) !void { 158 | const start = encoder.buffer.data.len; 159 | 160 | for (self.inner) |name| { 161 | try encoder.any(name); 162 | } 163 | 164 | try encoder.length(encoder.buffer.data.len - start); 165 | try encoder.tag(sequence_of_tag); 166 | } 167 | }; 168 | 169 | version: u8, 170 | digest_algorithms: DigestAlgorithms, 171 | encapsulated_content_info: EncapsulatedContentInfo, 172 | signer_infos: SignerInfos, 173 | }; 174 | 175 | signed_data: SignedData, 176 | 177 | pub fn encodeDer(self: @This(), encoder: *asn1.der.Encoder) !void { 178 | const start = encoder.buffer.data.len; 179 | 180 | switch (self) { 181 | inline else => |data| try encoder.any(data), 182 | } 183 | 184 | try encoder.length(encoder.buffer.data.len - start); 185 | try encoder.tag(asn1.FieldTag.initExplicit(0, .context_specific).toTag()); 186 | } 187 | }; 188 | 189 | content_type: ContentType, 190 | content: Content, 191 | -------------------------------------------------------------------------------- /src/runner.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const clap = @import("clap"); 4 | 5 | const utils = @import("./utils.zig"); 6 | 7 | pub fn main() !void { 8 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 9 | defer arena.deinit(); 10 | 11 | const arena_alloc = arena.allocator(); 12 | 13 | const params = comptime clap.parseParamsComptime( 14 | \\-h, --help Display this help and exit. 15 | \\ Architecture of the VM guest. 16 | \\ Temporary directory to store VM guest state. 17 | \\ Directory of keys used for verified boot within the VM guest. 18 | \\ Initrd file to use when spawning the VM guest. 19 | \\ Kernel file to use when spawning the VM guest. 20 | \\... Extra arguments passed to qemu. 21 | \\ 22 | ); 23 | 24 | const parsers = comptime .{ 25 | .ARCH = clap.parsers.enumeration(std.Target.Cpu.Arch), 26 | .TEMPDIR = clap.parsers.string, 27 | .KEYDIR = clap.parsers.string, 28 | .INITRD = clap.parsers.string, 29 | .KERNEL = clap.parsers.string, 30 | .QEMU_ARGS = clap.parsers.string, 31 | }; 32 | 33 | const stderr = std.io.getStdErr().writer(); 34 | 35 | var diag = clap.Diagnostic{}; 36 | var res = clap.parse(clap.Help, ¶ms, &parsers, .{ 37 | .diagnostic = &diag, 38 | .allocator = arena.allocator(), 39 | }) catch |err| { 40 | try diag.report(stderr, err); 41 | try clap.usage(stderr, clap.Help, ¶ms); 42 | return; 43 | }; 44 | defer res.deinit(); 45 | 46 | if (res.args.help != 0) { 47 | return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); 48 | } 49 | 50 | const arch = res.positionals[0].?; 51 | const tempdir = res.positionals[1].?; 52 | const keydir = res.positionals[2].?; 53 | const initrd = res.positionals[3].?; 54 | const kernel = res.positionals[4].?; 55 | const extra_qemu_args = res.positionals[5]; 56 | 57 | if (std.mem.eql(u8, kernel, "")) { 58 | std.log.err("Cannot execute runner without kernel", .{}); 59 | return error.InvalidArgument; 60 | } 61 | 62 | try std.posix.chdir(tempdir); 63 | 64 | var qemu_args = std.ArrayList([]const u8).init(arena_alloc); 65 | 66 | try qemu_args.append(switch (arch) { 67 | .aarch64 => "qemu-system-aarch64", 68 | .arm => "qemu-system-arm", 69 | .x86_64 => "qemu-system-x86_64", 70 | else => return error.UnknownArchitecture, 71 | }); 72 | 73 | if (builtin.target.os.tag == .linux and utils.absolutePathExists("/dev/kvm") and builtin.target.cpu.arch == arch) { 74 | try qemu_args.append("-enable-kvm"); 75 | } 76 | 77 | try qemu_args.appendSlice(&.{ "-machine", switch (arch) { 78 | .aarch64, .arm => "virt", 79 | .x86_64 => "q35", 80 | else => return error.UnknownArchitecture, 81 | } }); 82 | 83 | try qemu_args.appendSlice(&.{ 84 | "-display", 85 | "none", 86 | "-serial", 87 | "mon:stdio", 88 | "-cpu", 89 | "max", 90 | "-smp", 91 | "1", 92 | "-m", 93 | "1G", 94 | "-netdev", 95 | "user,id=n1", 96 | "-device", 97 | "virtio-net-pci,netdev=n1", 98 | "-initrd", 99 | initrd, 100 | "-kernel", 101 | kernel, 102 | }); 103 | 104 | if (!std.mem.eql(u8, keydir, "")) { 105 | try qemu_args.appendSlice(&.{ 106 | "-fw_cfg", 107 | try std.fmt.allocPrint(arena_alloc, "name=opt/org.tboot/pubkey,file={s}/tboot-certificate.der", .{keydir}), 108 | }); 109 | } 110 | 111 | const swtpm_sock_path = try std.fs.path.join(arena_alloc, &.{ tempdir, "swtpm.sock" }); 112 | try qemu_args.appendSlice(&.{ 113 | "-chardev", try std.fmt.allocPrint(arena_alloc, "socket,id=chrtpm,path={s}", .{swtpm_sock_path}), 114 | "-tpmdev", "emulator,id=tpm0,chardev=chrtpm", 115 | "-device", 116 | switch (arch) { 117 | .aarch64, .arm => "tpm-tis-device,tpmdev=tpm0", 118 | .x86_64 => "tpm-tis,tpmdev=tpm0", 119 | else => return error.UnknownArchitecture, 120 | }, 121 | }); 122 | 123 | for (extra_qemu_args) |arg| { 124 | try qemu_args.append(arg); 125 | } 126 | 127 | var swtpm_child = std.process.Child.init(&.{ 128 | "swtpm", 129 | "socket", 130 | "--terminate", 131 | "--tpmstate", 132 | try std.fmt.allocPrint(arena_alloc, "dir={s}", .{tempdir}), 133 | "--ctrl", 134 | try std.fmt.allocPrint(arena_alloc, "type=unixio,path={s}", .{swtpm_sock_path}), 135 | "--tpm2", 136 | }, arena_alloc); 137 | try swtpm_child.spawn(); 138 | defer _ = swtpm_child.kill() catch {}; 139 | 140 | var qemu_child = std.process.Child.init(try qemu_args.toOwnedSlice(), arena_alloc); 141 | _ = try qemu_child.spawnAndWait(); 142 | } 143 | -------------------------------------------------------------------------------- /src/security.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const base64 = std.base64.standard; 3 | const posix = std.posix; 4 | 5 | const kexec_file_load_available = @import("./kexec/kexec.zig").kexec_file_load_available; 6 | 7 | const linux_headers = @import("linux_headers"); 8 | 9 | const MEASURE_POLICY = 10 | PROC_SUPER_MAGIC ++ 11 | SYSFS_MAGIC ++ 12 | DEBUGFS_MAGIC ++ 13 | TMPFS_MAGIC ++ 14 | DEVPTS_SUPER_MAGIC ++ 15 | BINFMTFS_MAGIC ++ 16 | SECURITYFS_MAGIC ++ 17 | SELINUX_MAGIC ++ 18 | SMACK_MAGIC ++ 19 | CGROUP_SUPER_MAGIC ++ 20 | CGROUP2_SUPER_MAGIC ++ 21 | NSFS_MAGIC ++ 22 | KEY_CHECK ++ 23 | POLICY_CHECK ++ 24 | KEXEC_KERNEL_CHECK ++ 25 | KEXEC_INITRAMFS_CHECK ++ 26 | KEXEC_CMDLINE; 27 | 28 | const APPRAISE_POLICY = KEXEC_KERNEL_CHECK_APPRAISE ++ KEXEC_INITRAMFS_CHECK_APPRAISE; 29 | 30 | const MEASURE_AND_APPRAISE_POLICY = MEASURE_POLICY ++ APPRAISE_POLICY; 31 | 32 | const IMA_POLICY_PATH = "/sys/kernel/security/ima/policy"; 33 | 34 | // Individual IMA policy lines below 35 | 36 | // PROC_SUPER_MAGIC = 0x9fa0 37 | const PROC_SUPER_MAGIC = withNewline("dont_measure fsmagic=0x9fa0"); 38 | 39 | // SYSFS_MAGIC = 0x62656572 40 | const SYSFS_MAGIC = withNewline("dont_measure fsmagic=0x62656572"); 41 | 42 | // DEBUGFS_MAGIC = 0x64626720 43 | const DEBUGFS_MAGIC = withNewline("dont_measure fsmagic=0x64626720"); 44 | 45 | // TMPFS_MAGIC = 0x01021994 46 | const TMPFS_MAGIC = withNewline("dont_measure fsmagic=0x1021994"); 47 | 48 | // DEVPTS_SUPER_MAGIC=0x1cd1 49 | const DEVPTS_SUPER_MAGIC = withNewline("dont_measure fsmagic=0x1cd1"); 50 | 51 | // BINFMTFS_MAGIC=0x42494e4d 52 | const BINFMTFS_MAGIC = withNewline("dont_measure fsmagic=0x42494e4d"); 53 | 54 | // SECURITYFS_MAGIC=0x73636673 55 | const SECURITYFS_MAGIC = withNewline("dont_measure fsmagic=0x73636673"); 56 | 57 | // SELINUX_MAGIC=0xf97cff8c 58 | const SELINUX_MAGIC = withNewline("dont_measure fsmagic=0xf97cff8c"); 59 | 60 | // SMACK_MAGIC=0x43415d53 61 | const SMACK_MAGIC = withNewline("dont_measure fsmagic=0x43415d53"); 62 | 63 | // CGROUP_SUPER_MAGIC=0x27e0eb 64 | const CGROUP_SUPER_MAGIC = withNewline("dont_measure fsmagic=0x27e0eb"); 65 | 66 | // CGROUP2_SUPER_MAGIC=0x63677270 67 | const CGROUP2_SUPER_MAGIC = withNewline("dont_measure fsmagic=0x63677270"); 68 | 69 | // NSFS_MAGIC=0x6e736673 70 | const NSFS_MAGIC = withNewline("dont_measure fsmagic=0x6e736673"); 71 | 72 | const KEY_CHECK = withNewline("measure func=KEY_CHECK pcr=7"); 73 | 74 | const POLICY_CHECK = withNewline("measure func=POLICY_CHECK pcr=7"); 75 | 76 | const KEXEC_KERNEL_CHECK = withNewline("measure func=KEXEC_KERNEL_CHECK pcr=8"); 77 | 78 | const KEXEC_INITRAMFS_CHECK = withNewline("measure func=KEXEC_INITRAMFS_CHECK pcr=9"); 79 | 80 | const KEXEC_CMDLINE = withNewline("measure func=KEXEC_CMDLINE pcr=12"); 81 | 82 | const KEXEC_KERNEL_CHECK_APPRAISE = withNewline("appraise func=KEXEC_KERNEL_CHECK appraise_type=imasig|modsig"); 83 | 84 | const KEXEC_INITRAMFS_CHECK_APPRAISE = withNewline("appraise func=KEXEC_INITRAMFS_CHECK appraise_type=imasig|modsig"); 85 | 86 | fn installImaPolicy(policy: []const u8) !void { 87 | var policy_file = try std.fs.cwd().openFile(IMA_POLICY_PATH, .{ .mode = .write_only }); 88 | defer policy_file.close(); 89 | 90 | std.log.debug("writing IMA policy", .{}); 91 | 92 | try policy_file.writeAll(policy); 93 | } 94 | 95 | const MAX_KEY_SIZE = 8192; 96 | 97 | // The public key is held in VPD as a base64 encoded string. 98 | // https://github.com/torvalds/linux/blob/master/drivers/firmware/google/vpd.c#L193 99 | const VPD_KEY = "/sys/firmware/vpd/ro/pubkey"; 100 | 101 | fn loadVpdKey(allocator: std.mem.Allocator) ![]const u8 { 102 | const vpd_key = std.fs.cwd().openFile( 103 | VPD_KEY, 104 | .{}, 105 | ) catch |err| switch (err) { 106 | error.FileNotFound => return error.MissingKey, 107 | else => return err, 108 | }; 109 | defer vpd_key.close(); 110 | 111 | const contents = try vpd_key.readToEndAlloc(allocator, MAX_KEY_SIZE); 112 | defer allocator.free(contents); 113 | const out_size = try base64.Decoder.calcSizeForSlice(contents); 114 | var out_buf = try allocator.alloc(u8, out_size); 115 | 116 | try base64.Decoder.decode(out_buf[0..], contents); 117 | return out_buf; 118 | } 119 | 120 | // https://qemu-project.gitlab.io/qemu/specs/fw_cfg.html 121 | const QEMU_FW_CFG_KEY = "/sys/firmware/qemu_fw_cfg/by_name/opt/org.tboot/pubkey/raw"; 122 | 123 | fn loadQemuFwCfgKey(allocator: std.mem.Allocator) ![]const u8 { 124 | const fw_cfg_key = std.fs.cwd().openFile( 125 | QEMU_FW_CFG_KEY, 126 | .{}, 127 | ) catch |err| switch (err) { 128 | error.FileNotFound => return error.MissingKey, 129 | else => return err, 130 | }; 131 | defer fw_cfg_key.close(); 132 | 133 | return try fw_cfg_key.readToEndAlloc(allocator, MAX_KEY_SIZE); 134 | } 135 | 136 | // https://github.com/torvalds/linux/blob/3b517966c5616ac011081153482a5ba0e91b17ff/security/integrity/digsig.c#L193 137 | fn loadVerificationKey(allocator: std.mem.Allocator) !void { 138 | const keyring_id = try addKeyring(IMA_KEYRING_NAME, KeySerial.User); 139 | std.log.info("added ima keyring (id=0x{x})", .{keyring_id}); 140 | 141 | inline for (.{ loadVpdKey, loadQemuFwCfgKey }) |load_key_fn| { 142 | if (load_key_fn(allocator)) |pubkey| { 143 | defer allocator.free(pubkey); 144 | 145 | const key_id = try addKey(keyring_id, pubkey); 146 | 147 | std.log.info("added verification key (id=0x{x})", .{key_id}); 148 | 149 | return; 150 | } else |err| switch (err) { 151 | error.MissingKey => {}, 152 | else => return err, 153 | } 154 | } 155 | 156 | return error.MissingKey; 157 | } 158 | 159 | // Initialize the IMA subsystem in linux to perform measurements and optionally 160 | // appraisals (verification) of boot components. We always do measured boot 161 | // with IMA since we basically get it for free; measurements are held in memory 162 | // and persisted across kexecs, and the measurements are extended to the 163 | // system's TPM if one is available. 164 | pub fn initializeSecurity(allocator: std.mem.Allocator) !void { 165 | if (!kexec_file_load_available) { 166 | std.log.warn("platform does not have kexec_file_load(), skipping security setup", .{}); 167 | return; 168 | } 169 | 170 | if (loadVerificationKey(allocator)) { 171 | try installImaPolicy(MEASURE_AND_APPRAISE_POLICY); 172 | std.log.info("boot measurement and verification is enabled", .{}); 173 | } else |err| { 174 | std.log.warn("failed to load verification key, cannot perform boot verification: {}", .{err}); 175 | try installImaPolicy(MEASURE_POLICY); 176 | std.log.info("boot measurement is enabled", .{}); 177 | } 178 | } 179 | 180 | // Each line in an IMA policy, including the last line, needs to be terminated 181 | // with a single line feed. 182 | fn withNewline(comptime line: []const u8) []const u8 { 183 | return line ++ "\n"; 184 | } 185 | 186 | // We are using the "_ima" keyring and not the ".ima" keyring since we do not use 187 | // CONFIG_INTEGRITY_TRUSTED_KEYRING=y in our kernel config. 188 | const IMA_KEYRING_NAME = "_ima"; 189 | 190 | const KeySerial = enum { 191 | User, 192 | 193 | fn to_keyring(self: @This()) i32 { 194 | return switch (self) { 195 | .User => linux_headers.KEY_SPEC_USER_KEYRING, 196 | }; 197 | } 198 | }; 199 | 200 | // https://git.kernel.org/pub/scm/linux/kernel/git/dhowells/keyutils.git/tree/keyctl.c#n705 201 | fn addKeyring(name: [*:0]const u8, key_serial: KeySerial) !usize { 202 | const key_type: [*:0]const u8 = "keyring"; 203 | 204 | const keyring = key_serial.to_keyring(); 205 | 206 | const key_content: ?[*:0]const u8 = null; 207 | 208 | const rc = std.os.linux.syscall5( 209 | .add_key, 210 | @intFromPtr(key_type), 211 | @intFromPtr(name), 212 | @intFromPtr(key_content), 213 | 0, 214 | @as(u32, @bitCast(keyring)), 215 | ); 216 | 217 | switch (posix.errno(rc)) { 218 | .SUCCESS => { 219 | return rc; 220 | }, 221 | else => |err| { 222 | return posix.unexpectedErrno(err); 223 | }, 224 | } 225 | } 226 | 227 | fn addKey(keyring_id: usize, key_content: []const u8) !usize { 228 | const key_type: [*:0]const u8 = "asymmetric"; 229 | 230 | const key_desc: ?[*:0]const u8 = null; 231 | 232 | // see https://github.com/torvalds/linux/blob/59f3fd30af355dc893e6df9ccb43ace0b9033faa/security/keys/keyctl.c#L74 233 | const rc = std.os.linux.syscall5( 234 | .add_key, 235 | @intFromPtr(key_type), 236 | @intFromPtr(key_desc), 237 | @intFromPtr(key_content.ptr), 238 | key_content.len, 239 | keyring_id, 240 | ); 241 | 242 | switch (posix.errno(rc)) { 243 | .SUCCESS => { 244 | return rc; 245 | }, 246 | else => |err| return posix.unexpectedErrno(err), 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/system.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const posix = std.posix; 4 | const linux = std.os.linux; 5 | const MS = std.os.linux.MS; 6 | 7 | const linux_headers = @import("linux_headers"); 8 | 9 | const ioctl = std.posix.system.ioctl; 10 | 11 | fn mountPseudoFs( 12 | path: [*:0]const u8, 13 | fstype: [*:0]const u8, 14 | flags: u32, 15 | ) !void { 16 | const rc = linux.mount(fstype, path, fstype, flags, 0); 17 | 18 | switch (posix.errno(rc)) { 19 | .SUCCESS => {}, 20 | else => |err| return posix.unexpectedErrno(err), 21 | } 22 | } 23 | 24 | /// Mounts basic psuedo-filesystems (/dev, /proc, /sys, etc.). 25 | pub fn mountPseudoFilesystems() !void { 26 | try std.fs.cwd().makePath("/proc"); 27 | try mountPseudoFs("/proc", "proc", MS.NOSUID | MS.NODEV | MS.NOEXEC); 28 | 29 | try std.fs.cwd().makePath("/sys"); 30 | try mountPseudoFs("/sys", "sysfs", MS.NOSUID | MS.NODEV | MS.NOEXEC | MS.RELATIME); 31 | try mountPseudoFs("/sys/kernel/security", "securityfs", MS.NOSUID | MS.NODEV | MS.NOEXEC | MS.RELATIME); 32 | try mountPseudoFs("/sys/kernel/debug", "debugfs", MS.NOSUID | MS.NODEV | MS.NOEXEC | MS.RELATIME); 33 | 34 | try std.fs.cwd().makePath("/dev"); 35 | try mountPseudoFs("/dev", "devtmpfs", MS.SILENT | MS.NOSUID | MS.NOEXEC); 36 | 37 | try std.fs.cwd().makePath("/run"); 38 | try mountPseudoFs("/run", "tmpfs", MS.NOSUID | MS.NODEV); 39 | } 40 | 41 | const TCFLSH = linux_headers.TCFLSH; 42 | const TCIOFLUSH = linux_headers.TCIOFLUSH; 43 | const TCOON = linux_headers.TCOON; 44 | const TCXONC = linux_headers.TCXONC; 45 | const VEOF = linux_headers.VEOF; 46 | const VERASE = linux_headers.VERASE; 47 | const VINTR = linux_headers.VINTR; 48 | const VKILL = linux_headers.VKILL; 49 | const VMIN = linux_headers.VMIN; 50 | const VQUIT = linux_headers.VQUIT; 51 | const VSTART = linux_headers.VSTART; 52 | const VSTOP = linux_headers.VSTOP; 53 | const VSUSP = linux_headers.VSUSP; 54 | const VTIME = linux_headers.VTIME; 55 | 56 | fn setBaudRate(t: *posix.termios, speed: posix.speed_t) void { 57 | // indicate that we want to set a new baud rate 58 | t.*.ispeed = speed; 59 | t.*.ospeed = speed; 60 | } 61 | 62 | fn cfmakeraw(t: *posix.termios) void { 63 | t.iflag.IGNBRK = false; 64 | t.iflag.BRKINT = false; 65 | t.iflag.PARMRK = false; 66 | t.iflag.INLCR = false; 67 | t.iflag.IGNCR = false; 68 | t.iflag.IXON = false; 69 | 70 | t.lflag.ECHO = false; 71 | t.lflag.ECHONL = false; 72 | t.lflag.ICANON = false; 73 | t.lflag.ISIG = false; 74 | t.lflag.IEXTEN = false; 75 | 76 | t.cflag.CSIZE = .CS8; 77 | t.cflag.PARENB = false; 78 | t.cc[VMIN] = 1; 79 | t.cc[VTIME] = 0; 80 | } 81 | 82 | pub const Tty = struct { 83 | handle: std.fs.File.Handle, 84 | original: ?State = null, 85 | mode: ?Mode = null, 86 | 87 | const State = posix.termios; 88 | 89 | pub const Mode = enum { 90 | no_echo, 91 | user_input, 92 | file_transfer, 93 | }; 94 | 95 | pub fn init(handle: std.fs.File.Handle) @This() { 96 | return .{ .handle = handle }; 97 | } 98 | 99 | pub fn current(self: *@This()) !State { 100 | return try posix.tcgetattr(self.handle); 101 | } 102 | 103 | pub fn setMode(self: *@This(), mode: Mode) !void { 104 | if (self.original == null) { 105 | self.original = try self.current(); 106 | } 107 | 108 | var termios = self.original.?; 109 | 110 | switch (mode) { 111 | .no_echo => { 112 | termios.lflag.ECHO = false; 113 | }, 114 | .user_input => { 115 | termios.cc[VINTR] = 3; // C-c 116 | termios.cc[VQUIT] = 28; // C-\ 117 | termios.cc[VERASE] = 127; // C-? 118 | termios.cc[VKILL] = 21; // C-u 119 | termios.cc[VEOF] = 4; // C-d 120 | termios.cc[VSTART] = 17; // C-q 121 | termios.cc[VSTOP] = 19; // C-s 122 | termios.cc[VSUSP] = 26; // C-z 123 | 124 | termios.cflag.CSIZE = .CS8; 125 | termios.cflag.CSTOPB = true; 126 | termios.cflag.PARENB = true; 127 | termios.cflag.PARODD = true; 128 | termios.cflag.CREAD = true; 129 | termios.cflag.HUPCL = true; 130 | termios.cflag.CLOCAL = true; 131 | 132 | // input modes 133 | termios.iflag.ICRNL = true; 134 | termios.iflag.IXON = true; 135 | termios.iflag.IXOFF = true; 136 | 137 | // output modes 138 | termios.oflag.OPOST = true; 139 | termios.oflag.ONLCR = true; 140 | 141 | // local modes 142 | termios.lflag.ISIG = true; 143 | termios.lflag.ICANON = true; 144 | termios.lflag.ECHO = true; 145 | termios.lflag.ECHOE = true; 146 | termios.lflag.ECHOK = true; 147 | termios.lflag.IEXTEN = true; 148 | 149 | cfmakeraw(&termios); 150 | 151 | setBaudRate(&termios, posix.speed_t.B115200); 152 | }, 153 | .file_transfer => { 154 | termios.iflag = .{ 155 | .IGNBRK = true, 156 | .IXOFF = true, 157 | }; 158 | 159 | termios.lflag.ECHO = false; 160 | termios.lflag.ICANON = false; 161 | termios.lflag.ISIG = false; 162 | termios.lflag.IEXTEN = false; 163 | 164 | termios.oflag = .{}; 165 | 166 | termios.cflag.PARENB = false; 167 | termios.cflag.CSIZE = .CS8; 168 | termios.cflag.CREAD = true; 169 | termios.cflag.CLOCAL = true; 170 | 171 | // https://www.unixwiz.net/techtips/termios-vmin-vtime.html 172 | termios.cc[VMIN] = 0; // allow timeout with zero bytes obtained 173 | termios.cc[VTIME] = 50; // 5-second timeout 174 | 175 | setBaudRate(&termios, posix.speed_t.B3000000); 176 | }, 177 | } 178 | 179 | self.setState(termios); 180 | 181 | self.mode = mode; 182 | } 183 | 184 | pub const ReadError = error{Timeout} || posix.ReadError; 185 | pub const Reader = std.io.GenericReader(*@This(), ReadError, read); 186 | 187 | pub fn read(self: *@This(), buffer: []u8) ReadError!usize { 188 | const n_read = try posix.read(self.handle, buffer); 189 | 190 | if (n_read == 0) { 191 | return ReadError.Timeout; 192 | } 193 | 194 | return n_read; 195 | } 196 | 197 | pub fn reader(self: *@This()) Reader { 198 | return .{ .context = self }; 199 | } 200 | 201 | pub const WriteError = posix.WriteError; 202 | pub const Writer = std.io.GenericWriter(*@This(), WriteError, write); 203 | 204 | pub fn write(self: *@This(), bytes: []const u8) WriteError!usize { 205 | return try posix.write(self.handle, bytes); 206 | } 207 | 208 | pub fn writer(self: *@This()) Writer { 209 | return .{ .context = self }; 210 | } 211 | 212 | fn setState(self: *@This(), state: State) void { 213 | // wait until everything is sent 214 | _ = linux.tcdrain(self.handle); 215 | 216 | // flush input queue 217 | _ = ioctl(self.handle, TCFLSH, TCIOFLUSH); 218 | 219 | posix.tcsetattr(self.handle, posix.TCSA.DRAIN, state) catch {}; 220 | 221 | // restart output 222 | _ = ioctl(self.handle, TCXONC, TCOON); 223 | } 224 | 225 | pub fn deinit(self: *@This()) void { 226 | if (self.original) |state| { 227 | self.setState(state); 228 | } 229 | } 230 | }; 231 | 232 | // These aren't defined in the UAPI linux headers for some odd reason. 233 | const SYSLOG_ACTION_READ_ALL = 3; 234 | const SYSLOG_ACTION_CONSOLE_OFF = 6; 235 | const SYSLOG_ACTION_CONSOLE_ON = 7; 236 | const SYSLOG_ACTION_UNREAD = 9; 237 | 238 | /// Read kernel logs (AKA syslog/dmesg). Caller is responsible for returned 239 | /// slice. 240 | pub fn printKernelLogs( 241 | allocator: std.mem.Allocator, 242 | filter: u3, 243 | writer: std.io.AnyWriter, 244 | ) !void { 245 | const bytes_available = std.os.linux.syscall3(.syslog, SYSLOG_ACTION_UNREAD, 0, 0); 246 | const buf = try allocator.alloc(u8, bytes_available); 247 | defer allocator.free(buf); 248 | 249 | switch (posix.errno(std.os.linux.syscall3( 250 | .syslog, 251 | SYSLOG_ACTION_READ_ALL, 252 | @intFromPtr(buf.ptr), 253 | buf.len, 254 | ))) { 255 | .SUCCESS => {}, 256 | .PERM => return error.PermissionDenied, 257 | // We don't need to capture the bytes read since we only request for the 258 | // exact number of bytes available. 259 | else => |err| return posix.unexpectedErrno(err), 260 | } 261 | 262 | var split = std.mem.splitScalar(u8, buf, '\n'); 263 | while (split.next()) |line| { 264 | if (line.len <= 2 or line[0] != '<') { 265 | break; 266 | } 267 | 268 | if (std.mem.indexOf(u8, line[0..5], ">")) |right_chevron_index| { 269 | const syslog_prefix = try std.fmt.parseInt(u32, line[1..right_chevron_index], 10); 270 | const log_level = 0x7 & syslog_prefix; // lower 3 bits 271 | if (log_level <= filter) { 272 | try writer.print("{s}\n", .{line[right_chevron_index + 1 ..]}); 273 | } 274 | } 275 | } 276 | } 277 | 278 | pub fn setConsole(toggle: enum { on, off }) !void { 279 | switch (posix.errno(std.os.linux.syscall3( 280 | .syslog, 281 | switch (toggle) { 282 | .on => SYSLOG_ACTION_CONSOLE_ON, 283 | .off => SYSLOG_ACTION_CONSOLE_OFF, 284 | }, 285 | 0, // ignored 286 | 0, // ignored 287 | ))) { 288 | .SUCCESS => {}, 289 | .PERM => return error.PermissionDenied, 290 | else => |err| return posix.unexpectedErrno(err), 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/tboot-bless-boot-generator.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const clap = @import("clap"); 4 | 5 | pub const std_options = std.Options{ .log_level = if (builtin.mode == .Debug) .debug else .info }; 6 | 7 | pub fn main() !void { 8 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 9 | defer arena.deinit(); 10 | 11 | const allocator = arena.allocator(); 12 | 13 | const params = comptime clap.parseParamsComptime( 14 | \\-h, --help Display this help and exit. 15 | \\ The normal generator directory. 16 | \\ The early generator directory. 17 | \\ The late generator directory. 18 | \\ 19 | ); 20 | 21 | const parsers = comptime .{ .DIR = clap.parsers.string }; 22 | 23 | const stderr = std.io.getStdErr().writer(); 24 | 25 | var diag = clap.Diagnostic{}; 26 | var res = clap.parse(clap.Help, ¶ms, &parsers, .{ 27 | .diagnostic = &diag, 28 | .allocator = arena.allocator(), 29 | }) catch |err| { 30 | try diag.report(stderr, err); 31 | try clap.usage(stderr, clap.Help, ¶ms); 32 | return; 33 | }; 34 | defer res.deinit(); 35 | 36 | if (res.args.help != 0) { 37 | return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); 38 | } 39 | 40 | if (res.positionals[0] == null or 41 | res.positionals[1] == null or 42 | res.positionals[2] == null) 43 | { 44 | try diag.report(stderr, error.InvalidArgument); 45 | try clap.usage(std.io.getStdErr().writer(), clap.Help, ¶ms); 46 | return; 47 | } 48 | 49 | const normal_dir = res.positionals[0].?; 50 | const early_dir = res.positionals[1].?; 51 | const late_dir = res.positionals[2].?; 52 | 53 | _ = normal_dir; 54 | _ = late_dir; 55 | 56 | var env_map = try std.process.getEnvMap(allocator); 57 | const in_initrd = b: { 58 | const env_value = env_map.get("SYSTEMD_IN_INITRD") orelse break :b false; 59 | break :b std.mem.eql(u8, env_value, "1"); 60 | }; 61 | 62 | if (in_initrd) { 63 | std.log.debug("skipping tboot-bless-boot-generator, running in the initrd", .{}); 64 | return; 65 | } 66 | 67 | var kernel_cmdline_file = try std.fs.cwd().openFile("/proc/cmdline", .{}); 68 | defer kernel_cmdline_file.close(); 69 | 70 | const kernel_cmdline = try kernel_cmdline_file.readToEndAlloc(allocator, 1024); 71 | 72 | if (std.mem.count(u8, kernel_cmdline, "tboot.bls-entry=") > 0) { 73 | const basic_target_path = try std.fs.path.join( 74 | allocator, 75 | &.{ early_dir, "basic.target.wants" }, 76 | ); 77 | 78 | std.fs.cwd().makeDir(basic_target_path) catch |err| switch (err) { 79 | error.PathAlreadyExists => {}, 80 | else => return err, 81 | }; 82 | 83 | var basic_target_dir = try std.fs.cwd().openDir(basic_target_path, .{}); 84 | defer basic_target_dir.close(); 85 | 86 | try basic_target_dir.symLink( 87 | "/etc/systemd/system/tboot-bless-boot.service", 88 | "tboot-bless-boot.service", 89 | .{}, 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/tboot-bless-boot.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const clap = @import("clap"); 4 | 5 | pub const std_options = std.Options{ .log_level = if (builtin.mode == .Debug) .debug else .info }; 6 | 7 | const DiskBootLoader = @import("./boot/disk.zig"); 8 | 9 | const BlsEntryFile = DiskBootLoader.BlsEntryFile; 10 | 11 | const Error = error{ 12 | InvalidAction, 13 | MissingBlsEntry, 14 | }; 15 | 16 | const Action = enum { 17 | good, 18 | bad, 19 | status, 20 | 21 | pub fn fromStr(str: []const u8) !@This() { 22 | if (std.mem.eql(u8, str, "good")) { 23 | return .good; 24 | } else if (std.mem.eql(u8, str, "bad")) { 25 | return .bad; 26 | } else if (std.mem.eql(u8, str, "status")) { 27 | return .status; 28 | } else { 29 | return Error.InvalidAction; 30 | } 31 | } 32 | }; 33 | 34 | fn markAsGood( 35 | allocator: std.mem.Allocator, 36 | parent_dir: std.fs.Dir, 37 | original_entry_filename: []const u8, 38 | bls_entry_file: BlsEntryFile, 39 | ) !void { 40 | if (bls_entry_file.tries_left) |tries_left| { 41 | _ = tries_left; 42 | 43 | const new_filename = try std.fmt.allocPrint( 44 | allocator, 45 | "{s}.conf", 46 | .{bls_entry_file.name}, 47 | ); 48 | 49 | try parent_dir.rename(original_entry_filename, new_filename); 50 | } 51 | } 52 | 53 | fn markAsBad( 54 | allocator: std.mem.Allocator, 55 | parent_dir: std.fs.Dir, 56 | original_entry_filename: []const u8, 57 | bls_entry_file: BlsEntryFile, 58 | ) !void { 59 | const new_filename = b: { 60 | if (bls_entry_file.tries_done) |tries_done| { 61 | break :b try std.fmt.allocPrint( 62 | allocator, 63 | "{s}+0-{}.conf", 64 | .{ bls_entry_file.name, tries_done }, 65 | ); 66 | } else { 67 | break :b try std.fmt.allocPrint( 68 | allocator, 69 | "{s}+0.conf", 70 | .{bls_entry_file.name}, 71 | ); 72 | } 73 | }; 74 | 75 | try parent_dir.rename(original_entry_filename, new_filename); 76 | } 77 | 78 | fn printStatus( 79 | original_entry_filename: []const u8, 80 | bls_entry_file: BlsEntryFile, 81 | ) !void { 82 | var stdout = std.io.getStdOut().writer(); 83 | 84 | try stdout.print("{s}:\n", .{original_entry_filename}); 85 | 86 | if (bls_entry_file.tries_left) |tries_left| { 87 | if (tries_left > 0) { 88 | try stdout.print("\t{} tries left until entry is bad\n", .{tries_left}); 89 | } else if (bls_entry_file.tries_done) |tries_done| { 90 | try stdout.print("\tentry is bad, {} tries attempted\n", .{tries_done}); 91 | } else { 92 | try stdout.print("\tentry is bad\n", .{}); 93 | } 94 | 95 | if (bls_entry_file.tries_done) |tries_done| { 96 | try stdout.print("\t{} tries done\n", .{tries_done}); 97 | } 98 | } else { 99 | try stdout.print("\tentry is good\n", .{}); 100 | } 101 | } 102 | 103 | fn findEntry( 104 | allocator: std.mem.Allocator, 105 | esp_mnt: []const u8, 106 | entry_name: []const u8, 107 | action: Action, 108 | ) !void { 109 | const entries_path = try std.fs.path.join( 110 | allocator, 111 | &.{ esp_mnt, "loader", "entries" }, 112 | ); 113 | 114 | var entries_dir = try std.fs.cwd().openDir( 115 | entries_path, 116 | .{ .iterate = true }, 117 | ); 118 | defer entries_dir.close(); 119 | 120 | var iter = entries_dir.iterate(); 121 | while (try iter.next()) |dir_entry| { 122 | if (dir_entry.kind != .file) { 123 | continue; 124 | } 125 | 126 | const bls_entry = BlsEntryFile.parse(dir_entry.name) catch |err| { 127 | std.log.debug( 128 | "failed to parse boot entry {s}: {}", 129 | .{ dir_entry.name, err }, 130 | ); 131 | continue; 132 | }; 133 | 134 | if (std.mem.eql(u8, bls_entry.name, entry_name)) { 135 | return switch (action) { 136 | .good => try markAsGood(allocator, entries_dir, dir_entry.name, bls_entry), 137 | .bad => try markAsBad(allocator, entries_dir, dir_entry.name, bls_entry), 138 | .status => try printStatus(dir_entry.name, bls_entry), 139 | }; 140 | } 141 | } 142 | 143 | return Error.MissingBlsEntry; 144 | } 145 | 146 | pub fn main() !void { 147 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 148 | defer arena.deinit(); 149 | const allocator = arena.allocator(); 150 | 151 | const params = comptime clap.parseParamsComptime( 152 | \\-h, --help Display this help and exit. 153 | \\--esp-mnt UEFI system partition mountpoint (default /boot). 154 | \\ Action to take against current boot entry (mark as "good"/"bad", or print "status"). 155 | \\ 156 | ); 157 | 158 | const parsers = comptime .{ 159 | .ACTION = clap.parsers.string, 160 | .DIR = clap.parsers.string, 161 | }; 162 | 163 | const stderr = std.io.getStdErr().writer(); 164 | 165 | var diag = clap.Diagnostic{}; 166 | var res = clap.parse(clap.Help, ¶ms, &parsers, .{ 167 | .diagnostic = &diag, 168 | .allocator = arena.allocator(), 169 | }) catch |err| { 170 | try diag.report(stderr, err); 171 | try clap.usage(stderr, clap.Help, ¶ms); 172 | return; 173 | }; 174 | defer res.deinit(); 175 | 176 | if (res.args.help != 0) { 177 | return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); 178 | } 179 | 180 | const esp_mnt = res.args.@"esp-mnt" orelse std.fs.path.sep_str ++ "boot"; 181 | const action = if (res.positionals[0]) |action| try Action.fromStr(action) else Action.status; 182 | 183 | const kernel_cmdline_file = try std.fs.cwd().openFile("/proc/cmdline", .{}); 184 | defer kernel_cmdline_file.close(); 185 | 186 | const kernel_cmdline = try kernel_cmdline_file.readToEndAlloc(allocator, 1024); 187 | 188 | var split = std.mem.splitScalar(u8, kernel_cmdline, ' '); 189 | const tboot_bls_entry = b: { 190 | while (split.next()) |kernel_param| { 191 | if (std.mem.startsWith(u8, kernel_param, "tboot.bls-entry=")) { 192 | var param_split = std.mem.splitScalar(u8, kernel_param, '='); 193 | _ = param_split.next().?; 194 | 195 | // /proc/cmdline contains newline at the end of the file 196 | break :b std.mem.trimRight( 197 | u8, 198 | param_split.next() orelse return Error.MissingBlsEntry, 199 | "\n", 200 | ); 201 | } 202 | } 203 | 204 | return Error.MissingBlsEntry; 205 | }; 206 | 207 | try findEntry( 208 | allocator, 209 | esp_mnt, 210 | tboot_bls_entry, 211 | action, 212 | ); 213 | } 214 | -------------------------------------------------------------------------------- /src/tboot-efi-stub.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const uefi = std.os.uefi; 3 | 4 | var con_out: *uefi.protocol.SimpleTextOutput = undefined; 5 | var boot_services: *uefi.tables.BootServices = undefined; 6 | 7 | fn puts(msg: []const u8) void { 8 | for (msg) |c| { 9 | const c_ = [2]u16{ c, 0 }; // work around https://github.com/ziglang/zig/issues/4372 10 | _ = con_out.outputString(@ptrCast(&c_)); 11 | } 12 | } 13 | 14 | var print_buf: [256]u8 = undefined; 15 | fn printf(comptime fmt: []const u8, args: anytype) void { 16 | var fbs = std.io.fixedBufferStream(&print_buf); 17 | 18 | const truncated = if (std.fmt.format(fbs.writer().any(), fmt, args)) false else |err| switch (err) { 19 | // Ignore NoSpaceLeft errors, since the writer will still have written 20 | // enough bytes for us to get something on the screen to likely still 21 | // be useful. In this case, the output will just be truncated. This is 22 | // the same as what the Linux EFI stub does. 23 | error.NoSpaceLeft => true, 24 | else => unreachable, 25 | }; 26 | 27 | puts(fbs.getWritten()); 28 | 29 | // Indicate that we truncated the output 30 | if (truncated) { 31 | puts(">"); 32 | } 33 | } 34 | 35 | fn println(comptime fmt: []const u8, args: anytype) void { 36 | printf(fmt, args); 37 | 38 | // Do this in another call to puts since the printf formatted string might 39 | // get truncated. 40 | puts("\r\n"); 41 | } 42 | 43 | const LINUX_INITRD_MEDIA_GUID align(8) = uefi.Guid{ 44 | .time_low = 0x5568e427, 45 | .time_mid = 0x68fc, 46 | .time_high_and_version = 0x4f3d, 47 | .clock_seq_high_and_reserved = 0xac, 48 | .clock_seq_low = 0x74, 49 | .node = [_]u8{ 0xca, 0x55, 0x52, 0x31, 0xcc, 0x68 }, 50 | }; 51 | 52 | const EFI_LOAD_FILE2_PROTOCOL_GUID align(8) = uefi.Guid{ 53 | .time_low = 0x4006c0c1, 54 | .time_mid = 0xfcb3, 55 | .time_high_and_version = 0x403e, 56 | .clock_seq_high_and_reserved = 0x99, 57 | .clock_seq_low = 0x6d, 58 | .node = [_]u8{ 0x4a, 0x6c, 0x87, 0x24, 0xe0, 0x6d }, 59 | }; 60 | 61 | const LoadFile = *const fn ( 62 | *LoadFileProtocol, 63 | *uefi.protocol.DevicePath, 64 | bool, 65 | *usize, 66 | ?*anyopaque, 67 | ) callconv(uefi.cc) uefi.Status; 68 | 69 | const LoadFileProtocol = extern struct { 70 | load_file: LoadFile, 71 | }; 72 | 73 | const InitrdLoader = struct { 74 | load_file: LoadFileProtocol, 75 | address: *const anyopaque, 76 | length: usize, 77 | }; 78 | 79 | fn initrd_load_file( 80 | this: *LoadFileProtocol, 81 | filepath: *uefi.protocol.DevicePath, 82 | boot_policy: bool, 83 | buffer_size: *usize, 84 | buffer: ?*anyopaque, 85 | ) callconv(uefi.cc) uefi.Status { 86 | _ = filepath; 87 | 88 | if (boot_policy) { 89 | return .unsupported; 90 | } 91 | 92 | const loader: *InitrdLoader = @ptrCast(this); 93 | 94 | if (loader.length == 0) { 95 | return .not_found; 96 | } 97 | 98 | if (buffer == null or buffer_size.* < loader.length) { 99 | buffer_size.* = loader.length; 100 | return .buffer_too_small; 101 | } 102 | 103 | const dest: [*]u8 = @ptrCast(buffer); 104 | const source: [*]u8 = @ptrCast(@constCast(loader.address)); 105 | @memcpy(dest, source[0..loader.length]); 106 | buffer_size.* = loader.length; 107 | 108 | return .success; 109 | } 110 | 111 | const efi_initrd_device_path: extern struct { 112 | vendor: uefi.DevicePath.Media.VendorDevicePath, 113 | end: uefi.protocol.DevicePath, 114 | } = .{ 115 | .vendor = .{ 116 | .type = .media, 117 | .subtype = .vendor, 118 | .length = @sizeOf(uefi.DevicePath.Media.VendorDevicePath), 119 | .guid = LINUX_INITRD_MEDIA_GUID, 120 | }, 121 | .end = .{ 122 | .type = .end, 123 | .subtype = @intFromEnum(uefi.DevicePath.End.Subtype.end_entire), 124 | .length = @sizeOf(uefi.protocol.DevicePath), 125 | }, 126 | }; 127 | 128 | const TbootStubError = error{ 129 | MissingSection, 130 | OutOfMemory, 131 | EndOfStream, 132 | MissingPEHeader, 133 | } || uefi.Status.Error; 134 | 135 | fn run() TbootStubError!void { 136 | var self_loaded_image_: ?*uefi.protocol.LoadedImage = undefined; 137 | try uefi.Status.err(boot_services.handleProtocol( 138 | uefi.handle, 139 | &uefi.protocol.LoadedImage.guid, 140 | @ptrCast(&self_loaded_image_), 141 | )); 142 | 143 | const self_loaded_image = self_loaded_image_.?; 144 | 145 | const coff = try std.coff.Coff.init( 146 | self_loaded_image.image_base[0..self_loaded_image.image_size], 147 | true, 148 | ); 149 | 150 | const linux = coff.getSectionByName(".linux") orelse { 151 | return TbootStubError.MissingSection; 152 | }; 153 | 154 | const linux_data = coff.getSectionData(linux); 155 | 156 | var linux_image_handle: ?uefi.Handle = null; 157 | try uefi.Status.err(boot_services.loadImage( 158 | false, 159 | uefi.handle, 160 | null, 161 | linux_data.ptr, 162 | linux_data.len, 163 | &linux_image_handle, 164 | )); 165 | 166 | var linux_loaded_image_: ?*uefi.protocol.LoadedImage = undefined; 167 | try uefi.Status.err(boot_services.handleProtocol( 168 | linux_image_handle.?, 169 | &uefi.protocol.LoadedImage.guid, 170 | @ptrCast(&linux_loaded_image_), 171 | )); 172 | 173 | const initrd = coff.getSectionByName(".initrd") orelse { 174 | return TbootStubError.MissingSection; 175 | }; 176 | 177 | const initrd_data = coff.getSectionData(initrd); 178 | 179 | const loader = try uefi.pool_allocator.create(InitrdLoader); 180 | loader.* = InitrdLoader{ 181 | .load_file = .{ .load_file = initrd_load_file }, 182 | .address = @ptrCast(initrd_data.ptr), 183 | .length = initrd_data.len, 184 | }; 185 | 186 | // In the happy path, this doesn't get cleaned up by us, since it needs to 187 | // outlive our application so linux can use it. 188 | errdefer uefi.pool_allocator.destroy(loader); 189 | 190 | // TODO(jared): if StartImage() fails, we need to unregister the initrd. 191 | // See https://github.com/systemd/systemd/blob/0015502168b868e8b6380765bdce3abee33b856c/src/boot/initrd.c#L112. 192 | var initrd_image_handle: ?uefi.Handle = null; 193 | 194 | const efi_initrd_device_path_: [*]uefi.protocol.DevicePath = @constCast(@ptrCast(&efi_initrd_device_path)); 195 | 196 | // TODO(jared): Use InstallMultipleProtocolInterfaces() 197 | try uefi.Status.err(boot_services.installProtocolInterface( 198 | @ptrCast(&initrd_image_handle), 199 | &uefi.protocol.DevicePath.guid, 200 | .efi_native_interface, 201 | efi_initrd_device_path_, 202 | )); 203 | 204 | try uefi.Status.err(boot_services.installProtocolInterface( 205 | @ptrCast(&initrd_image_handle), 206 | @alignCast(&EFI_LOAD_FILE2_PROTOCOL_GUID), 207 | .efi_native_interface, 208 | loader, 209 | )); 210 | 211 | try uefi.Status.err(boot_services.startImage(linux_image_handle.?, null, null)); 212 | } 213 | 214 | pub fn main() uefi.Status { 215 | con_out = uefi.system_table.con_out.?; 216 | boot_services = uefi.system_table.boot_services.?; 217 | 218 | const status: uefi.Status = if (run()) .aborted else |err| switch (err) { 219 | error.EndOfStream => .end_of_file, 220 | error.MissingPEHeader => .not_found, 221 | error.MissingSection => .not_found, 222 | error.OutOfMemory => .out_of_resources, 223 | 224 | // Errors from std.os.uefi.Status.Error 225 | error.LoadError => .load_error, 226 | error.InvalidParameter => .invalid_parameter, 227 | error.Unsupported => .unsupported, 228 | error.BadBufferSize => .bad_buffer_size, 229 | error.BufferTooSmall => .buffer_too_small, 230 | error.NotReady => .not_ready, 231 | error.DeviceError => .device_error, 232 | error.WriteProtected => .write_protected, 233 | error.OutOfResources => .out_of_resources, 234 | error.VolumeCorrupted => .volume_corrupted, 235 | error.VolumeFull => .volume_full, 236 | error.NoMedia => .no_media, 237 | error.MediaChanged => .media_changed, 238 | error.NotFound => .not_found, 239 | error.AccessDenied => .access_denied, 240 | error.NoResponse => .no_response, 241 | error.NoMapping => .no_mapping, 242 | error.Timeout => .timeout, 243 | error.NotStarted => .not_started, 244 | error.AlreadyStarted => .already_started, 245 | error.Aborted => .aborted, 246 | error.IcmpError => .icmp_error, 247 | error.TftpError => .tftp_error, 248 | error.ProtocolError => .protocol_error, 249 | error.IncompatibleVersion => .incompatible_version, 250 | error.SecurityViolation => .security_violation, 251 | error.CrcError => .crc_error, 252 | error.EndOfMedia => .end_of_media, 253 | error.EndOfFile => .end_of_file, 254 | error.InvalidLanguage => .invalid_language, 255 | error.CompromisedData => .compromised_data, 256 | error.IpAddressConflict => .ip_address_conflict, 257 | error.HttpError => .http_error, 258 | error.NetworkUnreachable => .network_unreachable, 259 | error.HostUnreachable => .host_unreachable, 260 | error.ProtocolUnreachable => .protocol_unreachable, 261 | error.PortUnreachable => .port_unreachable, 262 | error.ConnectionFin => .connection_fin, 263 | error.ConnectionReset => .connection_reset, 264 | error.ConnectionRefused => .connection_refused, 265 | }; 266 | 267 | println("Failed to run tinyboot EFI stub: {}", .{status}); 268 | 269 | // Stall so that the user has some time to see what happened. 270 | _ = boot_services.stall(5 * std.time.ms_per_s); 271 | 272 | return status; 273 | } 274 | -------------------------------------------------------------------------------- /src/tboot-initrd.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const clap = @import("clap"); 4 | const CpioArchive = @import("./cpio.zig"); 5 | const zstd = @import("./zstd.zig"); 6 | 7 | pub const std_options = std.Options{ .log_level = if (builtin.mode == .Debug) .debug else .info }; 8 | 9 | const BoolArgument = enum { 10 | yes, 11 | no, 12 | true, 13 | false, 14 | 15 | pub fn to_bool(self: @This()) bool { 16 | return switch (self) { 17 | .yes, .true => true, 18 | else => false, 19 | }; 20 | } 21 | }; 22 | 23 | fn compress( 24 | arena: *std.heap.ArenaAllocator, 25 | output: []const u8, 26 | archive_file: std.fs.File, 27 | ) !void { 28 | try archive_file.seekTo(0); 29 | 30 | const archive_file_buf = try archive_file.readToEndAlloc(arena.allocator(), std.math.maxInt(usize)); 31 | const compressed = try zstd.compress(arena.allocator(), archive_file_buf); 32 | defer compressed.deinit(); 33 | 34 | const compressed_output = try std.fmt.allocPrint( 35 | arena.allocator(), 36 | "{s}.tmp", 37 | .{output}, 38 | ); 39 | 40 | var compressed_file = try std.fs.cwd().createFile(compressed_output, .{ .mode = 0o444 }); 41 | defer compressed_file.close(); 42 | 43 | try compressed_file.writer().writeAll(compressed.content()); 44 | 45 | try std.fs.cwd().rename(compressed_output, output); 46 | } 47 | 48 | pub fn main() !void { 49 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 50 | defer arena.deinit(); 51 | 52 | const params = comptime clap.parseParamsComptime( 53 | \\-h, --help Display this help and exit. 54 | \\-c, --compress Specify whether archive should be compressed. 55 | \\-i, --init File to add to archive as /init. 56 | \\-d, --directory ... Directory to add to archive (as-is). 57 | \\-o, --output Archive output filepath. 58 | \\ 59 | ); 60 | 61 | const parsers = comptime .{ 62 | .FILE = clap.parsers.string, 63 | .DIR = clap.parsers.string, 64 | .BOOL = clap.parsers.enumeration(BoolArgument), 65 | }; 66 | 67 | const stderr = std.io.getStdErr().writer(); 68 | 69 | var diag = clap.Diagnostic{}; 70 | var res = clap.parse(clap.Help, ¶ms, &parsers, .{ 71 | .diagnostic = &diag, 72 | .allocator = arena.allocator(), 73 | }) catch |err| { 74 | try diag.report(stderr, err); 75 | try clap.usage(stderr, clap.Help, ¶ms); 76 | return; 77 | }; 78 | defer res.deinit(); 79 | 80 | if (res.args.help != 0) { 81 | return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); 82 | } 83 | 84 | if (res.args.init == null or res.args.output == null) { 85 | try diag.report(stderr, error.InvalidArgument); 86 | try clap.usage(std.io.getStdErr().writer(), clap.Help, ¶ms); 87 | return; 88 | } 89 | 90 | const do_compress: bool = if (res.args.compress) |do_compress| do_compress.to_bool() else true; 91 | const init: []const u8 = res.args.init.?; 92 | const directories: []const []const u8 = res.args.directory; 93 | const output: []const u8 = res.args.output.?; 94 | 95 | var archive_file = try std.fs.cwd().createFile( 96 | output, 97 | .{ .read = true, .mode = 0o444 }, 98 | ); 99 | defer archive_file.close(); 100 | 101 | var archive_file_source = std.io.StreamSource{ .file = archive_file }; 102 | var archive = try CpioArchive.init(&archive_file_source); 103 | 104 | var init_file = try std.fs.cwd().openFile(init, .{}); 105 | defer init_file.close(); 106 | 107 | var init_source = std.io.StreamSource{ .file = init_file }; 108 | try archive.addFile("init", &init_source, 0o755); 109 | 110 | for (directories) |directory_path| { 111 | var dir = try std.fs.cwd().openDir( 112 | directory_path, 113 | .{ .iterate = true }, 114 | ); 115 | defer dir.close(); 116 | try CpioArchive.walkDirectory(&arena, directory_path, &archive, &dir); 117 | } 118 | 119 | try archive.finalize(); 120 | 121 | if (do_compress) { 122 | try compress(&arena, output, archive_file); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/tboot-keygen.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const clap = @import("clap"); 3 | const builtin = @import("builtin"); 4 | 5 | const mbedtls = @import("./mbedtls.zig"); 6 | 7 | const C = @cImport({ 8 | @cInclude("mbedtls/ctr_drbg.h"); 9 | @cInclude("mbedtls/pk.h"); 10 | @cInclude("mbedtls/x509_crt.h"); 11 | @cInclude("mbedtls/x509_csr.h"); 12 | @cInclude("time.h"); 13 | }); 14 | 15 | /// Returns the generalized time (https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5.2) of an 16 | /// instant, requires the input buffer's length to be >= 15. 17 | fn generalizedTime(epoch_seconds: std.time.epoch.EpochSeconds, buf: []u8) ![]u8 { 18 | const epoch_day = epoch_seconds.getEpochDay(); 19 | const day_seconds = epoch_seconds.getDaySeconds(); 20 | const year_day = epoch_day.calculateYearDay(); 21 | const month_day = year_day.calculateMonthDay(); 22 | 23 | // YYYYMMDDHHMMSSZ 24 | return std.fmt.bufPrint(buf, "{:0>4}{:0>2}{:0>2}{:0>2}{:0>2}{:0>2}Z", .{ 25 | year_day.year, 26 | month_day.month.numeric(), 27 | month_day.day_index, 28 | day_seconds.getHoursIntoDay(), 29 | day_seconds.getMinutesIntoHour(), 30 | day_seconds.getSecondsIntoMinute(), 31 | }); 32 | } 33 | 34 | test "generalized time" { 35 | var buf = [_]u8{0} ** "YYYYMMDDHHMMSSZ".len; 36 | 37 | try std.testing.expectEqualStrings( 38 | "19700100000000Z", 39 | try generalizedTime(.{ .secs = 0 }, &buf), 40 | ); 41 | 42 | try std.testing.expectEqualStrings( 43 | "20250512050449Z", 44 | try generalizedTime(.{ .secs = 1747112689 }, &buf), 45 | ); 46 | } 47 | 48 | pub fn main() !void { 49 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 50 | defer arena.deinit(); 51 | 52 | const params = comptime clap.parseParamsComptime( 53 | \\-h, --help Display this help and exit. 54 | \\-n, --common-name Common name for certificate. 55 | \\-o, --organization Organization for certificate. 56 | \\-c, --country Country for certificate. 57 | \\-s, --valid-seconds Number of seconds the certificate is valid for (defaults to 31536000, 1 year). 58 | \\-t, --time-now Number of seconds past the Unix epoch (defaults to current time, only set if reproducibility is needed). 59 | \\ 60 | ); 61 | 62 | const parsers = comptime .{ 63 | .STR = clap.parsers.string, 64 | .NUM = clap.parsers.int(u64, 10), 65 | }; 66 | 67 | const stderr = std.io.getStdErr().writer(); 68 | 69 | var diag = clap.Diagnostic{}; 70 | var res = clap.parse(clap.Help, ¶ms, &parsers, .{ 71 | .diagnostic = &diag, 72 | .allocator = arena.allocator(), 73 | }) catch |err| { 74 | try diag.report(stderr, err); 75 | try clap.usage(stderr, clap.Help, ¶ms); 76 | return; 77 | }; 78 | defer res.deinit(); 79 | 80 | if (res.args.help != 0) { 81 | return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); 82 | } 83 | 84 | const time_now: u64 = res.args.@"time-now" orelse @intCast(C.time(null)); 85 | const valid_seconds: u64 = res.args.@"valid-seconds" orelse 60 * 60 * 24 * 365; 86 | const common_name: []const u8 = res.args.@"common-name" orelse { 87 | try clap.usage(stderr, clap.Help, ¶ms); 88 | return; 89 | }; 90 | const organization: []const u8 = res.args.organization orelse { 91 | try clap.usage(stderr, clap.Help, ¶ms); 92 | return; 93 | }; 94 | const country: []const u8 = res.args.country orelse { 95 | try clap.usage(stderr, clap.Help, ¶ms); 96 | return; 97 | }; 98 | 99 | var entropy: C.mbedtls_entropy_context = undefined; 100 | C.mbedtls_entropy_init(&entropy); 101 | defer C.mbedtls_entropy_free(&entropy); 102 | 103 | var ctr_drbg: C.mbedtls_ctr_drbg_context = undefined; 104 | C.mbedtls_ctr_drbg_init(&ctr_drbg); 105 | try mbedtls.wrap(C.mbedtls_ctr_drbg_seed( 106 | &ctr_drbg, 107 | C.mbedtls_entropy_func, 108 | &entropy, 109 | "tboot-keygen", 110 | "tboot-keygen".len, 111 | )); 112 | defer C.mbedtls_ctr_drbg_free(&ctr_drbg); 113 | 114 | // generate RSA key 115 | var key: C.mbedtls_pk_context = undefined; 116 | C.mbedtls_pk_init(&key); 117 | defer C.mbedtls_pk_free(&key); 118 | 119 | try mbedtls.wrap(C.mbedtls_pk_setup(&key, C.mbedtls_pk_info_from_type(C.MBEDTLS_PK_RSA))); 120 | 121 | try mbedtls.wrap(C.mbedtls_rsa_gen_key(C.mbedtls_pk_rsa(key), C.mbedtls_ctr_drbg_random, &ctr_drbg, 4096, 65537)); 122 | 123 | var key_buf = [_]u8{0} ** 16000; 124 | 125 | // NOTE: When we write out PEM files, ensure there is a trailing null byte 126 | // so that MBEDTLS detects these as PEM files, see https://github.com/Mbed-TLS/mbedtls/blob/6fb5120fde4ab889bea402f5ab230c720b0a3b9a/library/pkparse.c#L994. 127 | 128 | // write out public key 129 | { 130 | try mbedtls.wrap(C.mbedtls_pk_write_pubkey_pem(&key, &key_buf, key_buf.len)); 131 | 132 | const pub_out = try std.fs.cwd().createFile("tboot-public.pem", .{ .mode = 0o444 }); 133 | defer pub_out.close(); 134 | 135 | try pub_out.writer().writeAll(std.mem.trim(u8, &key_buf, &.{0})); 136 | try pub_out.writer().writeByte(0); 137 | } 138 | 139 | key_buf = std.mem.zeroes(@TypeOf(key_buf)); 140 | 141 | // write out private key 142 | { 143 | try mbedtls.wrap(C.mbedtls_pk_write_key_pem(&key, &key_buf, key_buf.len)); 144 | 145 | const priv_out = try std.fs.cwd().createFile("tboot-private.pem", .{ .mode = 0o444 }); 146 | defer priv_out.close(); 147 | 148 | try priv_out.writer().writeAll(std.mem.trim(u8, &key_buf, &.{0})); 149 | try priv_out.writer().writeByte(0); 150 | } 151 | 152 | // generate x509 cert 153 | var issuer_crt: C.mbedtls_x509_crt = undefined; 154 | C.mbedtls_x509_crt_init(&issuer_crt); 155 | 156 | var crt: C.mbedtls_x509write_cert = undefined; 157 | C.mbedtls_x509write_crt_init(&crt); 158 | 159 | var csr: C.mbedtls_x509_csr = undefined; 160 | C.mbedtls_x509_csr_init(&csr); 161 | 162 | // self-signed 163 | C.mbedtls_x509write_crt_set_subject_key(&crt, &key); 164 | C.mbedtls_x509write_crt_set_issuer_key(&crt, &key); 165 | 166 | const name = try std.fmt.allocPrint(arena.allocator(), "CN={s},O={s},C={s}", .{ common_name, organization, country }); 167 | try mbedtls.wrap(C.mbedtls_x509write_crt_set_subject_name(&crt, try arena.allocator().dupeZ(u8, name))); 168 | try mbedtls.wrap(C.mbedtls_x509write_crt_set_issuer_name(&crt, try arena.allocator().dupeZ(u8, name))); 169 | 170 | C.mbedtls_x509write_crt_set_md_alg(&crt, C.MBEDTLS_MD_SHA256); 171 | 172 | var serial = "1"; 173 | try mbedtls.wrap(C.mbedtls_x509write_crt_set_serial_raw(&crt, @ptrCast(&serial), 1)); 174 | 175 | const not_before_seconds = time_now; 176 | const not_after_seconds = not_before_seconds + valid_seconds; 177 | 178 | var before_buf = [_]u8{0} ** 15; 179 | var after_buf = [_]u8{0} ** 15; 180 | 181 | // mbedtls expects the 'Z' to not be present 182 | const not_before_time = try arena.allocator().dupeZ(u8, (try generalizedTime(.{ .secs = not_before_seconds }, &before_buf))[0..14]); 183 | const not_after_time = try arena.allocator().dupeZ(u8, (try generalizedTime(.{ .secs = not_after_seconds }, &after_buf))[0..14]); 184 | 185 | try mbedtls.wrap(C.mbedtls_x509write_crt_set_validity( 186 | &crt, 187 | @ptrCast(not_before_time), 188 | @ptrCast(not_after_time), 189 | )); 190 | 191 | try mbedtls.wrap(C.mbedtls_x509write_crt_set_basic_constraints(&crt, 1, -1)); 192 | 193 | try mbedtls.wrap(C.mbedtls_x509write_crt_set_subject_key_identifier(&crt)); 194 | try mbedtls.wrap(C.mbedtls_x509write_crt_set_authority_key_identifier(&crt)); 195 | 196 | var cert_buf = [_]u8{0} ** 4096; 197 | 198 | // write out certificate in DER format 199 | { 200 | const len: usize = @intCast(try mbedtls.wrapMulti(C.mbedtls_x509write_crt_der( 201 | &crt, 202 | &cert_buf, 203 | cert_buf.len, 204 | C.mbedtls_ctr_drbg_random, 205 | &ctr_drbg, 206 | ))); 207 | 208 | const cert_der_out = try std.fs.cwd().createFile("tboot-certificate.der", .{ .mode = 0o444 }); 209 | defer cert_der_out.close(); 210 | 211 | const start: usize = cert_buf.len - len; 212 | try cert_der_out.writer().writeAll(std.mem.trim(u8, cert_buf[start .. start + len], &.{0})); 213 | try cert_der_out.writer().writeByte(0); 214 | } 215 | 216 | cert_buf = std.mem.zeroes(@TypeOf(cert_buf)); 217 | 218 | // write out certificate in PEM format 219 | { 220 | try mbedtls.wrap(C.mbedtls_x509write_crt_pem( 221 | &crt, 222 | &cert_buf, 223 | cert_buf.len, 224 | C.mbedtls_ctr_drbg_random, 225 | &ctr_drbg, 226 | )); 227 | 228 | const cert_pem_out = try std.fs.cwd().createFile("tboot-certificate.pem", .{ .mode = 0o444 }); 229 | defer cert_pem_out.close(); 230 | 231 | try cert_pem_out.writer().writeAll(&cert_buf); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/tboot-sign.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const clap = @import("clap"); 3 | 4 | const asn1 = std.crypto.asn1; 5 | const sha2 = std.crypto.hash.sha2; 6 | 7 | const Pkcs7 = @import("./pkcs7.zig"); 8 | 9 | const mbedtls = @import("./mbedtls.zig"); 10 | 11 | const C = @cImport({ 12 | @cInclude("mbedtls/ctr_drbg.h"); 13 | @cInclude("mbedtls/entropy.h"); 14 | @cInclude("mbedtls/pk.h"); 15 | @cInclude("mbedtls/rsa.h"); 16 | @cInclude("mbedtls/x509_crt.h"); 17 | }); 18 | 19 | const MODULE_SIG_STRING = "~Module signature appended~\n"; 20 | 21 | // https://github.com/torvalds/linux/blob/ec9eeb89e60d86fcc0243f47c2383399ce0de8f8/include/linux/module_signature.h#L17 22 | const PkeyIdType = enum(u2) { 23 | /// OpenPGP generated key ID 24 | PkeyIdPgp, 25 | /// X.509 arbitrary subjectKeyIdentifier 26 | PkeyIdX509, 27 | /// Signature in PKCS#7 message 28 | PkeyIdPkcs7, 29 | }; 30 | 31 | // https://github.com/torvalds/linux/blob/ec9eeb89e60d86fcc0243f47c2383399ce0de8f8/include/linux/module_signature.h#L33 32 | const ModuleSignature = extern struct { 33 | /// Public-key crypto algorithm [0] 34 | algo: u8, 35 | /// Digest algorithm [0] 36 | hash: u8, 37 | /// Key identifier type [PKEY_ID_PKCS7] 38 | id_type: u8, 39 | /// Length of signer's name [0] 40 | signer_len: u8, 41 | /// Length of key identifier [0] 42 | key_id_len: u8, 43 | __pad: [3]u8, 44 | /// Length of signature data 45 | sig_len: u32, // be32 46 | }; 47 | 48 | const country_name_oid = asn1.Oid.fromDotComptime("2.5.4.6"); 49 | const common_name_oid = asn1.Oid.fromDotComptime("2.5.4.3"); 50 | const organization_name_oid = asn1.Oid.fromDotComptime("2.5.4.10"); 51 | 52 | pub fn signFile( 53 | arena_alloc: std.mem.Allocator, 54 | in_filepath: []const u8, 55 | out_filepath: []const u8, 56 | private_key_filepath: []const u8, 57 | certificate_filepath: []const u8, 58 | ) !void { 59 | var scratch_buf = [_]u8{0} ** 4096; 60 | 61 | const input_file = try std.fs.cwd().openFile(in_filepath, .{}); 62 | defer input_file.close(); 63 | 64 | errdefer std.fs.cwd().deleteFile(out_filepath) catch {}; 65 | 66 | const output_file = try std.fs.cwd().createFile(out_filepath, .{}); 67 | defer output_file.close(); 68 | 69 | var entropy: C.mbedtls_entropy_context = undefined; 70 | C.mbedtls_entropy_init(&entropy); 71 | defer C.mbedtls_entropy_free(&entropy); 72 | 73 | var ctr_drbg: C.mbedtls_ctr_drbg_context = undefined; 74 | C.mbedtls_ctr_drbg_init(&ctr_drbg); 75 | defer C.mbedtls_ctr_drbg_free(&ctr_drbg); 76 | 77 | var pk: C.mbedtls_pk_context = undefined; 78 | C.mbedtls_pk_init(&pk); 79 | defer C.mbedtls_pk_free(&pk); 80 | 81 | try mbedtls.wrap(C.mbedtls_ctr_drbg_seed( 82 | &ctr_drbg, 83 | C.mbedtls_entropy_func, 84 | &entropy, 85 | "tinyboot", 86 | "tinyboot".len, 87 | )); 88 | 89 | const certificate_file = try std.fs.cwd().openFile(certificate_filepath, .{}); 90 | defer certificate_file.close(); 91 | const certificate_bytes = try certificate_file.readToEndAlloc(arena_alloc, std.math.maxInt(usize)); 92 | 93 | var x509: C.mbedtls_x509_crt = undefined; 94 | C.mbedtls_x509_crt_init(&x509); 95 | try mbedtls.wrap(C.mbedtls_x509_crt_parse(&x509, @ptrCast(certificate_bytes), certificate_bytes.len)); 96 | defer C.mbedtls_x509_crt_free(&x509); 97 | 98 | const common_name = getAttribute(&x509, .commonName) orelse return error.MissingCommonName; 99 | const organization_name = getAttribute(&x509, .organizationName) orelse return error.MissingOrganizationName; 100 | const country_name = getAttribute(&x509, .countryName) orelse return error.MissingCountryName; 101 | const serial_number = x509.serial.p[0..x509.serial.len]; 102 | 103 | const private_key_file = try std.fs.cwd().openFile(private_key_filepath, .{}); 104 | defer private_key_file.close(); 105 | const private_key_bytes = try private_key_file.readToEndAlloc(arena_alloc, std.math.maxInt(usize)); 106 | 107 | try mbedtls.wrap(C.mbedtls_pk_parse_key( 108 | &pk, 109 | @ptrCast(private_key_bytes), 110 | private_key_bytes.len, 111 | null, 112 | 0, 113 | C.mbedtls_ctr_drbg_random, 114 | &ctr_drbg, 115 | )); 116 | 117 | if (C.mbedtls_pk_can_do(&pk, C.MBEDTLS_PK_RSA) == 0) { 118 | std.log.err("detected non RSA key", .{}); 119 | return error.InvalidPrivateKey; 120 | } 121 | 122 | try mbedtls.wrap(C.mbedtls_rsa_set_padding( 123 | C.mbedtls_pk_rsa(pk), 124 | C.MBEDTLS_RSA_PKCS_V15, 125 | C.MBEDTLS_MD_SHA256, 126 | )); 127 | 128 | var hash = [_]u8{0} ** sha2.Sha256.digest_length; 129 | var hasher = sha2.Sha256.init(.{}); 130 | while (true) { 131 | const bytes_read = try input_file.reader().read(&scratch_buf); 132 | if (bytes_read == 0) { 133 | break; 134 | } 135 | 136 | hasher.update(scratch_buf[0..bytes_read]); 137 | } 138 | hasher.final(&hash); 139 | 140 | scratch_buf = std.mem.zeroes(@TypeOf(scratch_buf)); 141 | 142 | var signature_len: usize = 0; 143 | var signature_buf = [_]u8{0} ** C.MBEDTLS_MPI_MAX_SIZE; 144 | try mbedtls.wrap(C.mbedtls_pk_sign( 145 | &pk, 146 | C.MBEDTLS_MD_SHA256, 147 | &hash, 148 | hash.len, 149 | &signature_buf, 150 | signature_buf.len, 151 | &signature_len, 152 | C.mbedtls_ctr_drbg_random, 153 | &ctr_drbg, 154 | )); 155 | 156 | const signature = signature_buf[0..signature_len]; 157 | 158 | var encoder = asn1.der.Encoder.init(arena_alloc); 159 | defer encoder.deinit(); 160 | 161 | try encoder.any(Pkcs7{ 162 | .content_type = .signed_data, 163 | .content = .{ 164 | .signed_data = .{ 165 | .version = 1, 166 | .digest_algorithms = .{ .inner = .{ .algorithm = .sha256, .parameters = .{} } }, 167 | .encapsulated_content_info = .{ .content_type = .pkcs7 }, 168 | .signer_infos = .{ 169 | .inner = &.{ 170 | .{ 171 | .version = 1, 172 | .issuer_and_serial_number = .{ 173 | .serial_number = serial_number, 174 | .rdn_sequence = .{ 175 | .relative_distinguished_name = .{ 176 | .inner = &.{ 177 | .{ .inner = .{ .type = country_name_oid, .value = country_name } }, 178 | .{ .inner = .{ .type = organization_name_oid, .value = organization_name } }, 179 | .{ .inner = .{ .type = common_name_oid, .value = common_name } }, 180 | }, 181 | }, 182 | }, 183 | }, 184 | .digest_algorithm = .{ .algorithm = .sha256, .parameters = .{} }, 185 | .signature_algorithm = .{ .algorithm = .rsa, .parameters = .{} }, 186 | .signature = .{ .data = signature }, 187 | }, 188 | }, 189 | }, 190 | }, 191 | }, 192 | }); 193 | 194 | const pkcs7_encoded = encoder.buffer.data; 195 | 196 | try input_file.seekTo(0); 197 | while (true) { 198 | const bytes_read = try input_file.reader().read(&scratch_buf); 199 | if (bytes_read == 0) { 200 | break; 201 | } 202 | 203 | try output_file.writer().writeAll(scratch_buf[0..bytes_read]); 204 | } 205 | 206 | try output_file.writer().writeAll(pkcs7_encoded); 207 | 208 | const sig_info = ModuleSignature{ 209 | .sig_len = std.mem.nativeToBig(u32, @intCast(pkcs7_encoded.len)), 210 | .id_type = @intFromEnum(PkeyIdType.PkeyIdPkcs7), 211 | .algo = 0, 212 | .hash = 0, 213 | .__pad = [_]u8{0} ** 3, 214 | .signer_len = 0, 215 | .key_id_len = 0, 216 | }; 217 | 218 | try output_file.writer().writeAll(std.mem.asBytes(&sig_info)); 219 | try output_file.writer().writeAll(MODULE_SIG_STRING); 220 | } 221 | 222 | fn getAttribute(x509: *C.mbedtls_x509_crt, attribute: std.crypto.Certificate.Attribute) ?[]const u8 { 223 | var issuer = x509.issuer; 224 | 225 | while (true) { 226 | if (std.crypto.Certificate.Attribute.map.get(issuer.oid.p[0..issuer.oid.len])) |a| { 227 | if (a == attribute) { 228 | return issuer.val.p[0..issuer.val.len]; 229 | } 230 | } 231 | 232 | if (issuer.next == null) { 233 | break; 234 | } else { 235 | issuer = issuer.next.*; 236 | } 237 | } 238 | 239 | return null; 240 | } 241 | 242 | pub fn main() !void { 243 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 244 | defer arena.deinit(); 245 | 246 | const params = comptime clap.parseParamsComptime( 247 | \\-h, --help Display this help and exit. 248 | \\--private-key Private key to sign with. 249 | \\--certificate X509 certificate to sign with. 250 | \\ Input file. 251 | \\ Output file. 252 | \\ 253 | ); 254 | 255 | const parsers = comptime .{ 256 | .FILE = clap.parsers.string, 257 | }; 258 | 259 | const stderr = std.io.getStdErr().writer(); 260 | 261 | var diag = clap.Diagnostic{}; 262 | var res = clap.parse(clap.Help, ¶ms, &parsers, .{ 263 | .diagnostic = &diag, 264 | .allocator = arena.allocator(), 265 | }) catch |err| { 266 | try diag.report(stderr, err); 267 | try clap.usage(stderr, clap.Help, ¶ms); 268 | return; 269 | }; 270 | defer res.deinit(); 271 | 272 | if (res.args.help != 0) { 273 | return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); 274 | } 275 | 276 | if (res.positionals[0] == null or 277 | res.positionals[1] == null or 278 | res.args.@"private-key" == null or 279 | res.args.certificate == null) 280 | { 281 | try diag.report(stderr, error.InvalidArgument); 282 | try clap.usage(stderr, clap.Help, ¶ms); 283 | return; 284 | } 285 | 286 | const in_file = res.positionals[0].?; 287 | const out_file = res.positionals[1].?; 288 | const private_key_filepath = res.args.@"private-key".?; 289 | const certificate_filepath = res.args.certificate.?; 290 | 291 | return signFile( 292 | arena.allocator(), 293 | in_file, 294 | out_file, 295 | private_key_filepath, 296 | certificate_filepath, 297 | ); 298 | } 299 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | test { 2 | _ = @import("./autoboot.zig"); 3 | _ = @import("./boot/bootloader.zig"); 4 | _ = @import("./boot/disk.zig"); 5 | _ = @import("./boot/ymodem.zig"); 6 | _ = @import("./bootspec.zig"); 7 | _ = @import("./console.zig"); 8 | _ = @import("./cpio.zig"); 9 | _ = @import("./device.zig"); 10 | _ = @import("./disk/filesystem.zig"); 11 | _ = @import("./disk/gpt.zig"); 12 | _ = @import("./disk/mbr.zig"); 13 | _ = @import("./fdt.zig"); 14 | _ = @import("./kexec/arm.zig"); 15 | _ = @import("./kexec/kexec.zig"); 16 | _ = @import("./kobject.zig"); 17 | _ = @import("./log.zig"); 18 | _ = @import("./mbedtls.zig"); 19 | _ = @import("./pkcs7.zig"); 20 | _ = @import("./runner.zig"); 21 | _ = @import("./security.zig"); 22 | _ = @import("./system.zig"); 23 | _ = @import("./tboot-bless-boot-generator.zig"); 24 | _ = @import("./tboot-bless-boot.zig"); 25 | _ = @import("./tboot-efi-stub.zig"); 26 | _ = @import("./tboot-initrd.zig"); 27 | _ = @import("./tboot-keygen.zig"); 28 | _ = @import("./tboot-loader.zig"); 29 | _ = @import("./tboot-nixos-install.zig"); 30 | _ = @import("./tboot-sign.zig"); 31 | _ = @import("./tmpdir.zig"); 32 | _ = @import("./utils.zig"); 33 | _ = @import("./vpd.zig"); 34 | _ = @import("./watch.zig"); 35 | _ = @import("./ymodem.zig"); 36 | _ = @import("./zstd.zig"); 37 | } 38 | -------------------------------------------------------------------------------- /src/tmpdir.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const random_bytes_count = 12; 4 | const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count); 5 | 6 | pub const TmpDir = @This(); 7 | 8 | dir: std.fs.Dir, 9 | parent_dir: std.fs.Dir, 10 | sub_path: [sub_path_len]u8, 11 | 12 | pub fn cleanup(self: *TmpDir) void { 13 | self.dir.close(); 14 | self.parent_dir.deleteTree(&self.sub_path) catch {}; 15 | self.parent_dir.close(); 16 | self.* = undefined; 17 | } 18 | 19 | pub fn create(opts: std.fs.Dir.OpenDirOptions) !TmpDir { 20 | var random_bytes: [TmpDir.random_bytes_count]u8 = undefined; 21 | std.crypto.random.bytes(&random_bytes); 22 | var sub_path: [sub_path_len]u8 = undefined; 23 | _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); 24 | 25 | const parent_dir = try std.fs.cwd().makeOpenPath("/run", .{}); 26 | 27 | const dir = try parent_dir.makeOpenPath(&sub_path, opts); 28 | 29 | return .{ 30 | .dir = dir, 31 | .parent_dir = parent_dir, 32 | .sub_path = sub_path, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/utils.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn pathExists(d: *std.fs.Dir, p: []const u8) bool { 4 | d.access(p, .{}) catch { 5 | return false; 6 | }; 7 | 8 | return true; 9 | } 10 | 11 | pub fn absolutePathExists(p: []const u8) bool { 12 | std.fs.cwd().access(p, .{}) catch { 13 | return false; 14 | }; 15 | 16 | return true; 17 | } 18 | 19 | pub fn enumFromStr(T: anytype, value: []const u8) !T { 20 | inline for (std.meta.fields(T)) |field| { 21 | if (std.mem.eql(u8, field.name, value)) { 22 | return @field(T, field.name); 23 | } 24 | } 25 | 26 | return error.NotFound; 27 | } 28 | -------------------------------------------------------------------------------- /src/watch.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const posix = std.posix; 3 | const epoll_event = std.os.linux.epoll_event; 4 | 5 | const Device = @import("./device.zig"); 6 | const kobject = @import("./kobject.zig"); 7 | 8 | const DeviceWatcher = @This(); 9 | 10 | pub const Event = struct { 11 | action: kobject.Action, 12 | device: Device, 13 | }; 14 | 15 | const Queue = std.DoublyLinkedList(Event); 16 | 17 | fn makedev(major: u32, minor: u32) u32 { 18 | return std.math.shl(u32, major & 0xfffff000, 32) | 19 | std.math.shl(u32, major & 0x00000fff, 8) | 20 | std.math.shl(u32, minor & 0xffffff00, 12) | 21 | std.math.shl(u32, minor & 0x000000ff, 0); 22 | } 23 | 24 | const NodeType = enum { block, char }; 25 | 26 | // busybox uses these buffer sizes 27 | const USER_RCVBUF = 3 * 1024; 28 | const KERN_RCVBUF = 128 * 1024 * 1024; 29 | 30 | arena: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator), 31 | 32 | is_pid1: bool, 33 | 34 | block_dir: std.fs.Dir, 35 | char_dir: std.fs.Dir, 36 | 37 | /// An epoll file descriptor for receiving events on other file descriptors for 38 | /// the watcher. 39 | epoll: posix.fd_t, 40 | 41 | /// Netlink socket fd for subscribing to new device events. 42 | nl: posix.fd_t, 43 | 44 | /// An eventfd file descriptor used to indicate when new device events are 45 | /// available on the device queue. 46 | event: posix.fd_t, 47 | 48 | mutex: std.Thread.Mutex = .{}, 49 | queue: Queue = .{}, 50 | 51 | pub fn init(is_pid1: bool) !DeviceWatcher { 52 | var self = DeviceWatcher{ 53 | .is_pid1 = is_pid1, 54 | .event = try posix.eventfd(0, 0), 55 | .block_dir = try std.fs.cwd().makeOpenPath("/dev/block", .{}), 56 | .char_dir = try std.fs.cwd().makeOpenPath("/dev/char", .{}), 57 | .epoll = try posix.epoll_create1(std.os.linux.EPOLL.CLOEXEC), 58 | .nl = try posix.socket( 59 | posix.system.AF.NETLINK, 60 | posix.system.SOCK.DGRAM, 61 | std.os.linux.NETLINK.KOBJECT_UEVENT, 62 | ), 63 | }; 64 | 65 | try posix.setsockopt( 66 | self.nl, 67 | posix.SOL.SOCKET, 68 | posix.SO.RCVBUF, 69 | &std.mem.toBytes(@as(c_int, KERN_RCVBUF)), 70 | ); 71 | 72 | try posix.setsockopt( 73 | self.nl, 74 | posix.SOL.SOCKET, 75 | posix.SO.RCVBUFFORCE, 76 | &std.mem.toBytes(@as(c_int, KERN_RCVBUF)), 77 | ); 78 | 79 | const nls = posix.sockaddr.nl{ 80 | .groups = 1, // KOBJECT_UEVENT groups bitmask must be 1 81 | .pid = @bitCast(posix.system.getpid()), 82 | }; 83 | try posix.bind(self.nl, @ptrCast(&nls), @sizeOf(posix.sockaddr.nl)); 84 | 85 | var netlink_event = epoll_event{ 86 | .data = .{ .fd = self.nl }, 87 | .events = std.os.linux.EPOLL.IN, 88 | }; 89 | 90 | try posix.epoll_ctl( 91 | self.epoll, 92 | std.os.linux.EPOLL.CTL_ADD, 93 | self.nl, 94 | &netlink_event, 95 | ); 96 | 97 | try self.scanAndCreateExistingDevices(); 98 | 99 | return self; 100 | } 101 | 102 | pub fn watch(self: *DeviceWatcher, done: posix.fd_t) !void { 103 | defer self.deinit(); 104 | 105 | var done_event = epoll_event{ 106 | .data = .{ .fd = done }, 107 | .events = std.os.linux.EPOLL.IN, 108 | }; 109 | 110 | try posix.epoll_ctl( 111 | self.epoll, 112 | std.os.linux.EPOLL.CTL_ADD, 113 | done, 114 | &done_event, 115 | ); 116 | 117 | while (true) { 118 | var events = [_]posix.system.epoll_event{undefined} ** (2 << 4); 119 | 120 | const n_events = posix.epoll_wait(self.epoll, &events, -1); 121 | 122 | var i_event: usize = 0; 123 | while (i_event < n_events) : (i_event += 1) { 124 | const event = events[i_event]; 125 | 126 | if (event.data.fd == done) { 127 | std.log.debug("done watching devices", .{}); 128 | return; 129 | } else if (event.data.fd == self.nl) { 130 | self.handleNewEvent() catch |err| { 131 | std.log.err("failed to handle new device: {}", .{err}); 132 | }; 133 | } else { 134 | std.debug.panic("unknown event: {}", .{event}); 135 | } 136 | } 137 | } 138 | } 139 | 140 | fn handleNewEvent(self: *DeviceWatcher) !void { 141 | var recv_bytes: [USER_RCVBUF]u8 = undefined; 142 | 143 | const bytes_read = try posix.read(self.nl, &recv_bytes); 144 | 145 | const event = kobject.parseUeventKobjectContents( 146 | recv_bytes[0..bytes_read], 147 | ) orelse return; 148 | 149 | switch (event.action) { 150 | .add => try self.addDevice(event), 151 | .remove => try self.removeDevice(event), 152 | } 153 | } 154 | 155 | pub fn nextEvent(self: *DeviceWatcher) ?Event { 156 | self.mutex.lock(); 157 | defer self.mutex.unlock(); 158 | 159 | const node = self.queue.pop() orelse return null; 160 | defer self.arena.allocator().destroy(node); 161 | 162 | return node.data; 163 | } 164 | 165 | pub fn deinit(self: *DeviceWatcher) void { 166 | defer self.arena.deinit(); 167 | 168 | self.block_dir.close(); 169 | self.char_dir.close(); 170 | 171 | posix.close(self.nl); 172 | posix.close(self.event); 173 | posix.close(self.epoll); 174 | } 175 | 176 | fn mknod(self: *DeviceWatcher, node_type: NodeType, major: u32, minor: u32) !void { 177 | var buf: [10]u8 = undefined; 178 | const device = try std.fmt.bufPrintZ(&buf, "{}:{}", .{ major, minor }); 179 | 180 | const rc = std.os.linux.mknodat( 181 | switch (node_type) { 182 | .block => self.block_dir.fd, 183 | .char => self.char_dir.fd, 184 | }, 185 | device, 186 | switch (node_type) { 187 | .block => posix.system.S.IFBLK, 188 | .char => posix.system.S.IFCHR, 189 | }, 190 | makedev(major, minor), 191 | ); 192 | 193 | switch (posix.errno(rc)) { 194 | .SUCCESS => {}, 195 | .EXIST => {}, 196 | else => |err| return posix.unexpectedErrno(err), 197 | } 198 | } 199 | 200 | // stat() on any uevent file always returns 4096 201 | const UEVENT_FILE_SIZE = 4096; 202 | 203 | /// Scan sysfs and create all nodes of interest that currently exist on the 204 | /// system. 205 | pub fn scanAndCreateExistingDevices(self: *DeviceWatcher) !void { 206 | inline for (std.meta.fields(Device.Subsystem)) |field| { 207 | try self.scanAndCreateExistingDevicesForSubsystem(field.name); 208 | } 209 | } 210 | 211 | pub fn scanAndCreateExistingDevicesForSubsystem( 212 | self: *DeviceWatcher, 213 | comptime subsystem: []const u8, 214 | ) !void { 215 | var subsystem_dir = std.fs.cwd().openDir( 216 | "/sys/class/" ++ subsystem, 217 | .{ .iterate = true }, 218 | ) catch return; // don't hard fail if the subsystem does not exist 219 | defer subsystem_dir.close(); 220 | 221 | var iter = subsystem_dir.iterate(); 222 | 223 | while (try iter.next()) |entry| { 224 | // We expect all files in every directory under /sys/class to be a 225 | // symlink. 226 | if (entry.kind != .sym_link) { 227 | continue; 228 | } 229 | 230 | var device_dir = subsystem_dir.openDir(entry.name, .{}) catch continue; 231 | defer device_dir.close(); 232 | 233 | var device_uevent = device_dir.openFile("uevent", .{}) catch continue; 234 | defer device_uevent.close(); 235 | 236 | var buf: [UEVENT_FILE_SIZE]u8 = undefined; 237 | const n_read = device_uevent.readAll(&buf) catch continue; 238 | 239 | const device = kobject.parseUeventFileContents( 240 | @field(Device.Subsystem, subsystem), 241 | buf[0..n_read], 242 | ) orelse continue; 243 | 244 | try self.addDevice(.{ .action = .add, .device = device }); 245 | } 246 | } 247 | 248 | fn addDevice(self: *DeviceWatcher, event: Event) !void { 249 | if (self.is_pid1) { 250 | switch (event.device.type) { 251 | .node => |node| { 252 | const major, const minor = node; 253 | try self.mknod(switch (event.device.subsystem) { 254 | .block => .block, 255 | else => .char, 256 | }, major, minor); 257 | }, 258 | else => {}, 259 | } 260 | } 261 | 262 | { 263 | self.mutex.lock(); 264 | defer self.mutex.unlock(); 265 | 266 | const node = try self.arena.allocator().create(Queue.Node); 267 | node.* = .{ .data = event }; 268 | 269 | self.queue.append(node); 270 | 271 | _ = try posix.write(self.event, std.mem.asBytes(&@as(u64, 1))); 272 | } 273 | } 274 | 275 | fn removeDevice(self: *DeviceWatcher, event: Event) !void { 276 | if (self.is_pid1) { 277 | switch (event.device.type) { 278 | .node => |node| { 279 | const major, const minor = node; 280 | try self.removeNode(switch (event.device.subsystem) { 281 | .block => .block, 282 | else => .char, 283 | }, major, minor); 284 | }, 285 | else => {}, 286 | } 287 | } 288 | 289 | { 290 | self.mutex.lock(); 291 | defer self.mutex.unlock(); 292 | 293 | const node = try self.arena.allocator().create(Queue.Node); 294 | 295 | node.* = .{ .data = event }; 296 | self.queue.append(node); 297 | 298 | _ = try posix.write(self.event, std.mem.asBytes(&@as(u64, 1))); 299 | } 300 | } 301 | 302 | /// Assumes we are PID1. 303 | fn removeNode(self: *DeviceWatcher, node_type: NodeType, major: u32, minor: u32) !void { 304 | var buf: [10]u8 = undefined; 305 | const device = try std.fmt.bufPrint(&buf, "{}:{}", .{ major, minor }); 306 | 307 | var dir = switch (node_type) { 308 | .block => self.block_dir, 309 | .char => self.char_dir, 310 | }; 311 | 312 | dir.deleteFile(device) catch |err| switch (err) { 313 | error.FileNotFound => {}, 314 | else => return err, 315 | }; 316 | } 317 | -------------------------------------------------------------------------------- /src/zstd.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const C = @cImport({ 4 | @cInclude("zstd.h"); 5 | }); 6 | 7 | pub const Compressed = struct { 8 | allocator: std.mem.Allocator, 9 | buf: []u8, 10 | end: usize, 11 | 12 | pub fn content(self: *const @This()) []u8 { 13 | return self.buf[0..self.end]; 14 | } 15 | 16 | pub fn deinit(self: *const @This()) void { 17 | self.allocator.free(self.buf); 18 | } 19 | }; 20 | 21 | pub fn compress(allocator: std.mem.Allocator, buf: []const u8) !Compressed { 22 | const compressed_size = C.ZSTD_compressBound(buf.len); 23 | const compressed_buf = try allocator.alloc(u8, compressed_size); 24 | errdefer allocator.free(compressed_buf); 25 | 26 | const rc = C.ZSTD_compress( 27 | @ptrCast(compressed_buf), 28 | compressed_size, 29 | @ptrCast(buf), 30 | buf.len, 31 | 1, 32 | ); 33 | 34 | if (C.ZSTD_isError(rc) == 1) { 35 | std.log.err("failed to compress: {s}", .{C.ZSTD_getErrorName(rc)}); 36 | return error.CompressFail; 37 | } 38 | 39 | return .{ 40 | .allocator = allocator, 41 | .buf = compressed_buf, 42 | .end = rc, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /tests/disk/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | testers, 3 | tinyboot, 4 | runCommand, 5 | }: 6 | 7 | testers.runNixOSTest { 8 | name = "disk"; 9 | defaults.imports = [ ../module.nix ]; 10 | nodes.machine = { 11 | virtualisation.fileSystems."/boot" = { 12 | device = "/dev/disk/by-label/QEMU\\x20VVFAT"; 13 | fsType = "vfat"; 14 | }; 15 | virtualisation.qemu.options = [ "-fw_cfg name=opt/org.tboot/pubkey,file=$TBOOT_CERT_DER" ]; 16 | virtualisation.qemu.drives = [ 17 | { 18 | file = "fat:rw:$ESP"; 19 | driveExtraOpts = { 20 | "if" = "virtio"; 21 | format = "raw"; 22 | }; 23 | } 24 | ]; 25 | }; 26 | testScript = 27 | { nodes, ... }: 28 | let 29 | testArtifacts = runCommand "tinyboot-test-artifacts" { nativeBuildInputs = [ tinyboot ]; } '' 30 | tboot-keygen --common-name test --organization org.tboot --country US 31 | tboot-sign --private-key tboot-private.pem --certificate tboot-certificate.pem ${nodes.machine.system.build.kernel}/${nodes.machine.system.boot.loader.kernelFile} kernel.signed 32 | tboot-sign --private-key tboot-private.pem --certificate tboot-certificate.pem ${nodes.machine.system.build.initialRamdisk}/${nodes.machine.system.boot.loader.initrdFile} initrd.signed 33 | install -Dm0444 -t $out *.der *.pem *.signed 34 | ''; 35 | in 36 | '' 37 | import os 38 | import shutil 39 | import tempfile 40 | 41 | os.environ["TBOOT_CERT_DER"] = "${testArtifacts}/tboot-certificate.der" 42 | 43 | def populate_esp(tmpdir, title, tries_left=None, tries_done=None): 44 | os.makedirs(os.path.join(tmpdir, "loader/entries")) 45 | 46 | entry_name = title 47 | if tries_left is not None: 48 | entry_name = entry_name + f"+{tries_left}" 49 | if tries_done is not None: 50 | entry_name = entry_name + f"-{tries_done}" 51 | entry_name = entry_name + ".conf" 52 | 53 | with open(os.path.join(tmpdir, "loader/entries", entry_name), "x") as entry: 54 | entry.write(f"title {title}\n") 55 | shutil.copyfile("${testArtifacts}/kernel.signed", os.path.join(tmpdir, "linux")) 56 | entry.write("linux /linux\n") 57 | shutil.copyfile("${testArtifacts}/initrd.signed", os.path.join(tmpdir, "initrd")) 58 | entry.write("initrd /initrd\n") 59 | entry.write("options init=${nodes.machine.system.build.toplevel}/init ${toString nodes.machine.boot.kernelParams}\n") 60 | 61 | with subtest("simple"): 62 | with tempfile.TemporaryDirectory() as esp: 63 | os.environ["ESP"] = esp 64 | populate_esp( 65 | esp, 66 | "foo", 67 | ) 68 | machine.wait_for_unit("multi-user.target") 69 | 70 | machine.shutdown() 71 | machine.wait_for_shutdown() 72 | 73 | with subtest("boot-counting"): 74 | # TODO(jared): qemu bug: https://gitlab.com/qemu-project/qemu/-/issues/2786 75 | if False: 76 | with tempfile.TemporaryDirectory() as esp: 77 | os.environ["ESP"] = esp 78 | populate_esp( 79 | esp, 80 | "foo", 81 | tries_left=3, 82 | tries_done=0, 83 | ) 84 | machine.wait_for_unit("boot-complete.target") 85 | assert "active" == machine.succeed("systemctl is-active tboot-bless-boot.service").strip() 86 | machine.succeed("test -e /boot/loader/entries/foo.conf") 87 | ''; 88 | } 89 | -------------------------------------------------------------------------------- /tests/module.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: 7 | 8 | let 9 | inherit (pkgs.stdenv.hostPlatform) isx86_64 isAarch64; 10 | in 11 | { 12 | imports = [ ../modules/nixos ]; 13 | tinyboot = { 14 | enable = true; 15 | platform.qemu = true; 16 | network = true; 17 | debug = true; 18 | linux = { 19 | consoles = lib.mkIf isx86_64 [ "ttyS0,115200n8" ]; 20 | kconfig = lib.mkIf isAarch64 ( 21 | with lib.kernel; 22 | { 23 | ARM_SCMI_TRANSPORT_VIRTIO = yes; 24 | GPIO_PL061 = yes; 25 | MEMORY_HOTPLUG = yes; 26 | MEMORY_HOTREMOVE = yes; 27 | MIGRATION = yes; 28 | PCI_HOST_GENERIC = yes; 29 | PCI_PRI = yes; 30 | PL330_DMA = yes; 31 | RTC_DRV_PL031 = yes; 32 | SERIAL_AMBA_PL011 = yes; 33 | SERIAL_AMBA_PL011_CONSOLE = yes; 34 | } 35 | ); 36 | }; 37 | }; 38 | 39 | # can't use this cause this doesn't let us customize our kernel 40 | virtualisation.directBoot.enable = false; 41 | 42 | virtualisation.graphics = false; 43 | virtualisation.qemu.consoles = config.tinyboot.linux.consoles; 44 | virtualisation.qemu.options = 45 | [ 46 | "-kernel ${config.tinyboot.build.linux}/${config.tinyboot.build.linux.kernelFile}" 47 | "-initrd ${config.tinyboot.build.initrd}/${config.tinyboot.build.initrd.initrdFile}" 48 | ] 49 | ++ lib.optionals isx86_64 [ "-machine q35" ] 50 | # TODO(jared): make work with -cpu max 51 | ++ lib.optionals isAarch64 [ "-cpu cortex-a53" ]; 52 | } 53 | -------------------------------------------------------------------------------- /tests/ymodem/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | lrzsz, 4 | testers, 5 | tinyboot, 6 | }: 7 | 8 | testers.runNixOSTest { 9 | name = "ymodem"; 10 | defaults.imports = [ ../module.nix ]; 11 | nodes.machine = { }; 12 | testScript = 13 | { nodes, ... }: 14 | '' 15 | import os 16 | import re 17 | import shutil 18 | import subprocess 19 | import tempfile 20 | import time 21 | 22 | host_boot_dir = tempfile.TemporaryDirectory() 23 | 24 | linux = os.path.join(host_boot_dir.name, "linux") 25 | initrd = os.path.join(host_boot_dir.name, "initrd") 26 | params = os.path.join(host_boot_dir.name, "params") 27 | 28 | shutil.copyfile("${nodes.machine.system.build.kernel}/${nodes.machine.system.boot.loader.kernelFile}", linux) 29 | shutil.copyfile("${nodes.machine.system.build.initialRamdisk}/${nodes.machine.system.boot.loader.initrdFile}", initrd) 30 | with open(params, "x") as f: 31 | f.write("init=${nodes.machine.system.build.toplevel}/init ${toString nodes.machine.boot.kernelParams}") 32 | 33 | def tboot_ymodem(pty): 34 | subprocess.run(["${lib.getExe' tinyboot "tboot-ymodem"}", "send", "--tty", pty, "--directory", host_boot_dir.name]) 35 | 36 | def lrzsz(pty): 37 | subprocess.run(f"${lib.getExe' lrzsz "sx"} --ymodem --1k --binary {linux} {initrd} {params} > {pty} < {pty}", shell=True) 38 | 39 | for fn in [tboot_ymodem, lrzsz]: 40 | machine.start() 41 | chardev = machine.send_monitor_command("chardev-add pty,id=ymodem") 42 | machine.send_monitor_command("device_add virtconsole,chardev=ymodem") 43 | machine.wait_for_console_text("press ENTER to interrupt") 44 | machine.send_console("\n") # interrupt boot process 45 | time.sleep(1) 46 | machine.send_console("list\n") 47 | machine.send_console("select 229:1\n") # /dev/hvc1 has major:minor of 229:1 48 | time.sleep(1) 49 | machine.send_console("probe\n") 50 | 51 | pty = re.findall(r"/dev/pts/[0-9]+", chardev)[0] 52 | print(f"using pty {pty}") 53 | fn(pty) 54 | 55 | machine.send_console("boot\n") 56 | machine.wait_for_unit("multi-user.target") 57 | machine.shutdown() 58 | machine.wait_for_shutdown() 59 | ''; 60 | } 61 | --------------------------------------------------------------------------------