diff --git a/.gitignore b/.gitignore index 4812d58..90acb92 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ result +/testvm* +/target +/micro-activate .direnv/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index af2cfdd..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "muvm"] - path = muvm - url = https://github.com/valpackett/muvm -[submodule "libkrun"] - path = libkrun - url = https://github.com/valpackett/libkrun diff --git a/README.md b/README.md index 6a770f9..e98d7df 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,69 @@ WIP: A microVM runner for NixOS systems with desktop integration, powered by muvm/libkrun. +## Quick Start + +**1. Build a test VM:** +```bash +nix build '.#nixosConfigurations.testvm-x86_64.config.system.build.toplevel' -o testvm +``` + +**2. Run the VM:** +```bash +nix run '.#munix' -- testvm +``` + +This will start an interactive bash session inside the microVM. + +**Run a specific command:** +```bash +nix run '.#munix' -- testvm fastfetch +``` + +**Create a custom VM:** + +Use the template to bootstrap a new munix project: + +```bash +mkdir my-vm && cd my-vm +nix flake init -t 'git+https://git.clan.lol/clan/munix#musictest' +git init && git add flake.nix +nix run +``` + +This creates a `flake.nix` with a music player demo (MPD + Euphonica). Edit the module to customize your VM. + +## munix Options + +- `--uid UID`, `-u UID` - Set microVM UID (default: 1337) +- `--gid GID`, `-g GID` - Set microVM GID (default: 1337) +- `--no-gpu` - Disable GPU acceleration +- `--no-wayland` - Disable Wayland support +- `--no-pipewire` - Disable PipeWire audio +- `--x11` - Enable X11 support +- `--bind SRC DST` - Bind mount SRC to DST in the VM +- `--ro-bind SRC DST` - Read-only bind mount +- `--expose PATH` - Expose PATH in the VM at the same location +- `--ro-expose PATH` - Expose PATH read-only + +Example with options: +```bash +nix run '.#munix' -- --no-gpu --ro-expose /home/user/data testvm htop +``` + ## Development -Building an example closure: - -``` -nix build '.#nixosConfigurations.x86_64-linux.testvm.config.system.build.toplevel' -``` - -Running the nix build: - -``` -nix run '.#packages.x86_64-linux.munix' $(readlink result) -``` - Working on muvm & munix locally (not built into the nix store): -``` +```bash cd muvm && cargo build --locked --release -PATH=$PWD/muvm/target/release:$PATH ./munix $(readlink result) +PATH=$PWD/muvm/target/release:$PATH ./munix testvm ``` + +## Requirements + +- Linux system with KVM support (`/dev/kvm`) +- For GPU acceleration: Kernel 6.13+ with compatible drivers (amdgpu, msm) +- For Wayland: `XDG_RUNTIME_DIR` and `WAYLAND_DISPLAY` set + +## Known Issues diff --git a/devShells/default.nix b/devShells/default.nix new file mode 100644 index 0000000..c288f30 --- /dev/null +++ b/devShells/default.nix @@ -0,0 +1,49 @@ +{ + mkShell, + lib, + systemd, + cargo, + rust-analyzer, + rustfmt, + passt, + bubblewrap, + libkrun, + muvm, + sidebus-broker, + wl-cross-domain-proxy, + wl-backdrop, + pkgs, +}: + +let + projects = [ + libkrun + muvm + ]; +in +mkShell { + MUVM_UDEVD_PATH = "${systemd}/lib/systemd/systemd-udevd"; + nativeBuildInputs = lib.concatMap (pkg: pkg.nativeBuildInputs) projects; + buildInputs = + (lib.concatMap (pkg: pkg.buildInputs) projects) + ++ [ + # virglrenderer + cargo + rust-analyzer + rustfmt + passt + bubblewrap + sidebus-broker + wl-cross-domain-proxy + wl-backdrop + ] + ++ (with pkgs; [ + meson + wayland + wayland-protocols + wayland-scanner + cairo + libgbm + ]); + # Enough things to compile wl-cross-domain-proxy, muvm, etc. in development +} diff --git a/flake.lock b/flake.lock index becdabc..234a180 100644 --- a/flake.lock +++ b/flake.lock @@ -1,120 +1,133 @@ { "nodes": { - "flake-utils": { + "flake-parts": { "inputs": { - "systems": "systems" + "nixpkgs-lib": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "lastModified": 1769996383, + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "hercules-ci", + "repo": "flake-parts", "type": "github" } }, - "flake-utils_2": { - "inputs": { - "systems": "systems_2" - }, + "libkrun-src": { + "flake": false, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "lastModified": 1772170018, + "narHash": "sha256-pi4Mrx9wFE8zT8lx+0su2bP1tTkJBh5FaGNQjAFUAx4=", + "owner": "valpackett", + "repo": "libkrun", + "rev": "eeafbc55379b60379414168e7a22d5f95b73d7a0", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "valpackett", + "repo": "libkrun", + "rev": "eeafbc55379b60379414168e7a22d5f95b73d7a0", + "type": "github" + } + }, + "muvm-src": { + "flake": false, + "locked": { + "lastModified": 1772176363, + "narHash": "sha256-aSWulv3ml4XmMYnFOkZCd2YBLIY0Rr8CUHK1NDYk5jw=", + "owner": "valpackett", + "repo": "muvm", + "rev": "c68742bcedb96deb6f23ed5a83188022d1cdf71d", + "type": "github" + }, + "original": { + "owner": "valpackett", + "repo": "muvm", + "rev": "c68742bcedb96deb6f23ed5a83188022d1cdf71d", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1758078198, - "narHash": "sha256-60ojlXp42UZYTOAgsiKPZUab1Aa8pXTIo+rz0zoaynI=", - "ref": "val/tsvwswkqrrsr", - "rev": "1f0fc70eb049852a18a5203af2204bd9f5729f29", - "shallow": true, - "submodules": true, - "type": "git", - "url": "https://github.com/valpackett/nixpkgs" + "lastModified": 1770197578, + "narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "type": "github" }, "original": { - "ref": "val/tsvwswkqrrsr", - "shallow": true, - "submodules": true, - "type": "git", - "url": "https://github.com/valpackett/nixpkgs" + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" } }, "root": { "inputs": { - "flake-utils": "flake-utils", + "flake-parts": "flake-parts", + "libkrun-src": "libkrun-src", + "muvm-src": "muvm-src", "nixpkgs": "nixpkgs", - "virtwl": "virtwl" + "sidebus": "sidebus", + "wl-backdrop": "wl-backdrop" } }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "virtwl": { + "sidebus": { "inputs": { - "flake-utils": "flake-utils_2", + "flake-parts": [ + "flake-parts" + ], "nixpkgs": [ "nixpkgs" ] }, "locked": { - "lastModified": 1758075582, - "narHash": "sha256-o2lpXQLaM9QcZVr+sAxvh83CqJW1QkFhfja6K40ndmA=", - "ref": "wip", - "rev": "5a5df73a11b2f6bf671a8fc89926ac993e0fbb78", + "lastModified": 1772183103, + "narHash": "sha256-9jbqBtaLUdOeT95PVUMz45JdUpVeJ25ZYZHEOQn9XsI=", + "ref": "main", + "rev": "c42eaef55440e2594677ede5279bd8c3eaf128f2", "shallow": true, - "submodules": true, "type": "git", - "url": "https://github.com/valpackett/wayland-proxy-virtwl" + "url": "https://git.clan.lol/clan/sidebus" }, "original": { - "ref": "wip", + "ref": "main", "shallow": true, - "submodules": true, "type": "git", - "url": "https://github.com/valpackett/wayland-proxy-virtwl" + "url": "https://git.clan.lol/clan/sidebus" + } + }, + "wl-backdrop": { + "inputs": { + "flake-parts": [ + "flake-parts" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1772781884, + "narHash": "sha256-epVLmAHnJi4EoZea5DUmxB3r6SPSMu3Nzki7JzbK0sA=", + "ref": "main", + "rev": "20e3910ef60aa627933a5d750b60cc3dcd0653cf", + "shallow": true, + "type": "git", + "url": "https://git.clan.lol/valpackett/wl-backdrop" + }, + "original": { + "ref": "main", + "shallow": true, + "type": "git", + "url": "https://git.clan.lol/valpackett/wl-backdrop" } } }, diff --git a/flake.nix b/flake.nix index 9ab4655..6b9191b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,167 +1,146 @@ { - inputs = { - self.submodules = true; - # nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - # https://github.com/NixOS/nixpkgs/pull/444133 - nixpkgs.url = "git+https://github.com/valpackett/nixpkgs?shallow=1&submodules=1&ref=val/tsvwswkqrrsr"; - flake-utils.url = "github:numtide/flake-utils"; - virtwl.url = "git+https://github.com/valpackett/wayland-proxy-virtwl?shallow=1&submodules=1&ref=wip"; - virtwl.inputs.nixpkgs.follows = "nixpkgs"; + nixConfig = { + extra-substituters = [ "https://cache.clan.lol" ]; + extra-trusted-public-keys = [ "cache.clan.lol-1:3KztgSAB5R1M+Dz7vzkBGzXdodizbgLXGXKXlcQLA28=" ]; }; - outputs = {self, nixpkgs, flake-utils, virtwl, ...}: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs { - inherit system; - }; - in { + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - packages.libkrunfw = pkgs.libkrunfw; - # packages.libkrunfw = (pkgs.libkrunfw.overrideAttrs { - # version = "4.10.0"; - # src = pkgs.fetchFromGitHub { - # owner = "containers"; - # repo = "libkrunfw"; - # tag = "v4.10.0"; - # hash = "sha256-mq2gw0+xL6qUZE/fk0vLT3PEpzPV8p+iwRFJHXVOMnk="; - # }; - # kernelSrc = pkgs.fetchurl { - # url = "mirror://kernel/linux/kernel/v6.x/linux-6.12.34.tar.xz"; - # hash = "sha256-p/P+OB9n7KQXLptj77YaFL1/nhJ44DYD0P9ak/Jwwk0="; - # }; - # cargoHash = ""; - # }); + flake-parts.url = "github:hercules-ci/flake-parts"; + flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; - packages.libkrun = (pkgs.libkrun.override { - withBlk = true; - withGpu = true; - withSound = true; - withNet = true; - libkrunfw = self.packages.${system}.libkrunfw; - }).overrideAttrs (old: { - src = ./libkrun; - cargoDeps = pkgs.rustPlatform.importCargoLock { - lockFile = ./libkrun/Cargo.lock; - }; - # mesonFlags = [ (pkgs.lib.mesonOption "decoders" "gles,vulkan,composer") ]; # no magma(?) - }); + sidebus.url = "git+https://git.clan.lol/clan/sidebus?shallow=1&ref=main"; + sidebus.inputs.nixpkgs.follows = "nixpkgs"; + sidebus.inputs.flake-parts.follows = "flake-parts"; - packages.muvm = (pkgs.muvm.override { - libkrun = self.packages.${system}.libkrun; - }).overrideAttrs (old: { - postPatch = ""; # no more sysctl; udevd now takes the var anyway; XXX: fex - MUVM_UDEVD_PATH = "${pkgs.systemd}/lib/systemd/systemd-udevd"; - src = ./muvm; - cargoDeps = pkgs.rustPlatform.importCargoLock { - lockFile = ./muvm/Cargo.lock; - }; - }); + wl-backdrop.url = "git+https://git.clan.lol/valpackett/wl-backdrop?shallow=1&ref=main"; + wl-backdrop.inputs.nixpkgs.follows = "nixpkgs"; + wl-backdrop.inputs.flake-parts.follows = "flake-parts"; - packages.munix = let - munixScript = (pkgs.writeScriptBin "munix" (builtins.readFile ./munix)).overrideAttrs(old: { - buildCommand = "${old.buildCommand}\n patchShebangs $out"; - }); - munixInitRootHook = (pkgs.writeScriptBin "munix-init-root" (builtins.readFile ./munix-init-root)).overrideAttrs(old: { - buildCommand = "${old.buildCommand}\n patchShebangs $out"; - }); - munixInitUserHook = (pkgs.writeScriptBin "munix-init-user" (builtins.readFile ./munix-init-user)).overrideAttrs(old: { - buildCommand = "${old.buildCommand}\n patchShebangs $out"; - }); - in pkgs.symlinkJoin { - name = "munix"; - paths = [ munixScript munixInitRootHook munixInitUserHook self.packages.${system}.muvm pkgs.passt pkgs.bubblewrap ]; - buildInputs = [ pkgs.makeWrapper ]; - postBuild = '' - wrapProgram $out/bin/munix --prefix PATH : $out/bin - ''; + # To override with local checkouts during development, use the --override-input CLI flag! + muvm-src = { + url = "github:valpackett/muvm/c68742bcedb96deb6f23ed5a83188022d1cdf71d"; # v0.5.0+custom-init+dbus + flake = false; + }; + libkrun-src = { + url = "github:valpackett/libkrun/eeafbc55379b60379414168e7a22d5f95b73d7a0"; # PR #558 (map permission fix) + D-Bus WIP + flake = false; + }; + # libkrunfw-src = { + # url = "github:containers/libkrunfw/20484a2e60290acb74c43ccfd6e1ea4caf41d470"; # v5.1.0 + # flake = false; + # }; + }; + + outputs = + inputs@{ + self, + nixpkgs, + flake-parts, + sidebus, + wl-backdrop, + muvm-src, + libkrun-src, + # libkrunfw-src, + ... + }: + flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; + + flake = { + nixosModules.testvm = nixpkgs.lib.modules.importApply ./nixosModules/testvm.nix { }; + nixosModules.default = nixpkgs.lib.modules.importApply ./nixosModules/default.nix { + inherit self; }; - devShells.default = let - projects = with self.packages.${system}; [ libkrun muvm ]; - in pkgs.mkShell { - MUVM_UDEVD_PATH = "${pkgs.systemd}/lib/systemd/systemd-udevd"; - nativeBuildInputs = pkgs.lib.concatMap (pkg: pkg.nativeBuildInputs) projects; - buildInputs = (pkgs.lib.concatMap (pkg: pkg.buildInputs) projects) ++ (with self.packages.${system}; [ - # virglrenderer - ]) ++ (with pkgs; [ - cargo - rust-analyzer - rustfmt - passt - bubblewrap - ]); + templates.musictest = { + description = "Music player demo VM with MPD and Euphonica"; + path = ./templates/musictest; }; - packages.mesa = (pkgs.mesa.override { - vulkanDrivers = [ - "amd" - "intel" - "microsoft-experimental" # removing this breaks the build - "nouveau" - "swrast" - "virtio" - "gfxstream" # probably not going to use this though - ]; - }).overrideAttrs (new: old: { - mesonFlags = old.mesonFlags ++ [ (pkgs.lib.mesonBool "amdgpu-virtio" true) ]; - patches = old.patches ++ [ ./radvmmio.patch ]; # already merged to git - }); - - nixosConfigurations.testvm = nixpkgs.lib.nixosSystem { - inherit system; + nixosConfigurations.testvm-x86_64 = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; modules = [ - { - system.stateVersion = "25.11"; - fileSystems."/".device = pkgs.lib.mkDefault "/dev/sda"; - boot.isContainer = true; - - users.mutableUsers = false; - users.users.appvm = { - uid = 1337; - isNormalUser = true; - home = "/home/appvm"; - description = "microVM User"; - extraGroups = [ "wheel" "video" "input" ]; - }; - users.groups.appvm.gid = 1337; - users.allowNoPasswordLogin = true; - - hardware.graphics.enable = true; - hardware.graphics.package = self.packages.${system}.mesa; - system.replaceDependencies.replacements = [ - { - original = pkgs.mesa; - replacement = self.packages.${system}.mesa; - } - ]; - environment.systemPackages = [ - pkgs.fastfetch - pkgs.htop - pkgs.radeontop - virtwl.packages.x86_64-linux.proxy - pkgs.wayland-utils - pkgs.weston - pkgs.waycheck - pkgs.vulkan-tools - pkgs.glxinfo - pkgs.glmark2 - pkgs.mesa-demos - pkgs.xorg.xeyes - pkgs.xterm - pkgs.vkquake - pkgs.kdePackages.kate - pkgs.adwaita-fonts - pkgs.adwaita-icon-theme - pkgs.gnome-text-editor - pkgs.firefox - pkgs.ffmpeg-full - pkgs.mpv - pkgs.libva-utils - ]; - } + self.nixosModules.default + self.nixosModules.testvm + { nixpkgs.config.allowUnfree = true; } ]; }; - }); + nixosConfigurations.testvm-aarch64 = nixpkgs.lib.nixosSystem { + system = "aarch64-linux"; + modules = [ + self.nixosModules.default + self.nixosModules.testvm + { nixpkgs.config.allowUnfree = true; } + ]; + }; + }; + + perSystem = + { + pkgs, + system, + self', + ... + }: + { + _module.args.pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + + checks = + (pkgs.lib.mapAttrs' (n: pkgs.lib.nameValuePair "package-${n}") self'.packages) + // (pkgs.lib.mapAttrs' (n: pkgs.lib.nameValuePair "devShell-${n}") self'.devShells) + // (pkgs.lib.optionalAttrs (system == "x86_64-linux") { + nixos-testvm = self.nixosConfigurations.testvm-x86_64.config.system.build.toplevel; + }) + // (pkgs.lib.optionalAttrs (system == "aarch64-linux") { + nixos-testvm = self.nixosConfigurations.testvm-aarch64.config.system.build.toplevel; + }); + + packages = { + # Packages support variant parameter: null (default), "sev", or "tdx" + # To build a variant: packages.libkrunfw.override { variant = "sev"; } + libkrunfw = pkgs.callPackage ./packages/libkrunfw { + # libkrunfw-src = libkrunfw-src; + }; + + libkrun = pkgs.callPackage ./packages/libkrun { + libkrunfw = self'.packages.libkrunfw; + libkrun-src = libkrun-src; + }; + + mesa = pkgs.callPackage ./packages/mesa { }; + + muvm = pkgs.callPackage ./packages/muvm { + libkrun = self'.packages.libkrun; + muvm-src = muvm-src; + }; + + munix = pkgs.callPackage ./packages/munix { + mesa = self'.packages.mesa; + muvm = self'.packages.muvm; + wl-cross-domain-proxy = self'.packages.wl-cross-domain-proxy; + wl-backdrop = wl-backdrop.packages.${system}.wl-backdrop; + sidebus-broker = sidebus.packages.${system}.sidebus-broker; + }; + + wl-cross-domain-proxy = pkgs.callPackage ./packages/wl-cross-domain-proxy { }; + }; + + devShells.default = pkgs.callPackage ./devShells { + libkrun = self'.packages.libkrun; + muvm = self'.packages.muvm; + wl-cross-domain-proxy = self'.packages.wl-cross-domain-proxy; + wl-backdrop = wl-backdrop.packages.${system}.wl-backdrop; + sidebus-broker = sidebus.packages.${system}.sidebus-broker; + }; + }; + }; } diff --git a/libkrun b/libkrun deleted file mode 160000 index bd97a39..0000000 --- a/libkrun +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bd97a39bfaa46147933beca7e16513b6820bb031 diff --git a/micro-activate.rs b/micro-activate.rs new file mode 100644 index 0000000..765fb30 --- /dev/null +++ b/micro-activate.rs @@ -0,0 +1,155 @@ +use std::os::raw::{c_char, c_int, c_ulong, c_void}; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::process::CommandExt; + +const MS_RDONLY: c_ulong = 0x01; +const MS_NOSUID: c_ulong = 0x02; +const MS_NODEV: c_ulong = 0x04; +const MS_RELATIME: c_ulong = 0x200000; +const MS_STRICTATIME: c_ulong = 0x1000000; + +const CLONE_NEWTIME: c_int = 0x80; + +unsafe extern "C" { + fn mount( + src: *const c_char, + target: *const c_char, + fstype: *const c_char, + flags: c_ulong, + data: *const c_void, + ) -> c_int; + fn getrandom(buf: *mut u8, buflen: usize, flags: u32) -> c_int; + fn unshare(flags: c_int) -> c_int; +} + +fn gen_machine_id() -> String { + use std::fmt::Write as _; + let mut bytes: [u8; 16] = [0; 16]; + if unsafe { getrandom(bytes.as_mut_ptr(), 16, 0) } == -1 { + eprintln!("[micro-activate] getrandom failed!"); + } + let mut result = String::with_capacity(32); + for b in bytes { + let _ = write!(result, "{:02x}", b); + } + result +} + +fn parse_tmpfiles_line(line: &str) -> Option<(&str, &str)> { + // NOTE: does not support actual whitespace inside quotes + // (that's not gonna appear in these files we parse) + let mut it = line + .split_whitespace() + .map(|s| s.trim_start_matches('\'').trim_end_matches('\'')); + let instr = it.next()?; + if !instr.starts_with('L') { + return None; + } + let src = it.next()?; + let _ = it.next()?; + let _ = it.next()?; + let _ = it.next()?; + let _ = it.next()?; + let dst = it.next()?; + Some((src, dst)) +} + +fn link_tmpfiles(contents: &[u8]) -> Result<(), std::io::Error> { + for (src, dst) in str::from_utf8(contents) + .unwrap() + .lines() + .flat_map(parse_tmpfiles_line) + { + std::os::unix::fs::symlink(dst, src)?; + } + Ok(()) +} + +fn main() -> Result<(), std::io::Error> { + let closure = std::env::var("MICROVM_CLOSURE").unwrap(); + + // systemd really wants /run to be a mountpoint and will mount a tmpfs on its own + // if it's not already a mountpoint. Well, it's correct: reaching into virtiofs + // (which is what not-mounting would entail) for /run stuff is not great. + // + // Let's preserve the fixed passed-in files and set up the NixOS symlinks in the new mount. + let resolv_conf = std::fs::read("/run/resolv.conf")?; + let localtime = std::fs::read("/run/localtime")?; + assert_eq!( + unsafe { + mount( + c"tmpfs".as_ptr(), + c"/run".as_ptr(), + c"tmpfs".as_ptr(), + MS_NOSUID | MS_NODEV | MS_STRICTATIME, + std::ptr::null(), + ) + }, + 0 + ); + std::fs::write("/run/localtime", &localtime)?; + std::fs::write("/run/resolv.conf", &resolv_conf)?; + std::fs::write("/run/machine-id", &gen_machine_id())?; + std::fs::create_dir("/run/systemd")?; + std::os::unix::fs::symlink("/opt/systemd", "/run/systemd/system")?; + std::os::unix::fs::symlink(&closure, "/run/current-system")?; + if let Ok(tmp_graphics) = + std::fs::read(format!("{closure}/etc/tmpfiles.d/graphics-driver.conf")) + { + link_tmpfiles(&tmp_graphics)?; + } else { + eprintln!("[micro-activate] Could not find the closure's graphics-driver.conf!"); + } + + // We need the /etc metadata overlay not just for abstract correctness, but even just to + // allow the regular user to run systemctl (it doesn't like passwd being owned by non-root).. + let metadata_img = std::fs::read_link(format!("{closure}/etc-metadata-image")) + .expect("The closure must use an immutable /etc overlay!"); + let basedir = std::fs::read_link(format!("{closure}/etc-basedir")) + .expect("The closure must use an immutable /etc overlay!"); + let overlay_opts = std::ffi::CString::new(format!( + "redirect_dir=on,metacopy=on,lowerdir=/run/etc.meta::{}", + basedir.display() + )) + .unwrap(); + std::fs::create_dir("/run/etc.meta")?; + std::fs::remove_file("/etc")?; + std::fs::create_dir("/etc")?; + unsafe { + assert_eq!( + mount( + metadata_img.as_os_str().as_bytes().as_ptr() as *const c_char, + c"/run/etc.meta".as_ptr(), + c"erofs".as_ptr(), + MS_RDONLY | MS_NODEV | MS_NOSUID, + std::ptr::null(), + ), + 0 + ); + assert_eq!( + mount( + c"overlay".as_ptr(), + c"/etc".as_ptr(), + c"overlay".as_ptr(), + MS_NODEV | MS_NOSUID | MS_RELATIME, + overlay_opts.as_ptr() as *const c_void, + ), + 0 + ); + } + + if let Ok(offset) = std::env::var("BOOT_TIME_OFFSET") { + if unsafe { unshare(CLONE_NEWTIME) } != 0 { + eprintln!("[micro-activate] Could not unshare time!"); + } else { + std::fs::write( + "/proc/self/timens_offsets", + format!("monotonic {offset}\nboottime {offset}\n"), + )?; + } + } + + let mut args = std::env::args_os().skip(1); + let cmd = args.next().unwrap(); + Err(std::process::Command::new(cmd).args(args).exec()) +} diff --git a/munix b/munix index e828fa3..619a38f 100755 --- a/munix +++ b/munix @@ -2,19 +2,46 @@ SCRIPT_PATH=$(dirname $(realpath -s $0)) MUVM_PATH=$(dirname $(which muvm)) PASST_PATH=$(dirname $(which passt)) +WL_PROXY_PATH=$(dirname $(which wl-cross-domain-proxy)) +WL_BACKDROP_PATH=$(dirname $(which wl-backdrop)) HOST_OPENGL_DRIVER=/run/opengl-driver +: "${MICROVM_DEFAULT_COMMAND:=bash}" +: "${MUNIX_SYSTEMD_UNITS:="${SCRIPT_PATH}/systemd"}" MICROVM_CLOSURE= MICROVM_COMMAND=() MICROVM_UID=1337 MICROVM_GID=1337 BWRAP_ARGS=() MUVM_ARGS=() +SIDEBUS_ARGS=() GPU=1 WAYLAND=1 PIPEWIRE=1 X11=0 +ENV_DEFAULTS=1 +USING_PUBLISH=0 export TMP=/tmp TMPDIR=/tmp TEMP=/tmp TEMPDIR=/tmp LC_ALL=C +if [ ! -e "$HOST_OPENGL_DRIVER" ]; then + HOST_OPENGL_DRIVER="$FALLBACK_OPENGL_DRIVER" +fi + +# A little bit cursed: pre-pass to relaunch with new env var.. +BG_COLOR= +pargs=() +while [ "$#" -gt 0 ]; do + case "$1" in + --bg-color) BG_COLOR="$2"; shift 2;; + --wl-backdrop-bin-dir) WL_BACKDROP_PATH="$2"; shift 2;; + *) pargs+=("$1"); shift;; + esac +done +if [ "$BG_COLOR" = "" ]; then + set -- "${pargs[@]}" +else + exec "$WL_BACKDROP_PATH/wl-backdrop" --background "$BG_COLOR" -- "$(readlink -f "$0")" "${pargs[@]}" +fi + while [ "$#" -gt 0 ]; do case "$1" in -u|--uid) MICROVM_UID="$2"; shift 2;; @@ -22,15 +49,20 @@ while [ "$#" -gt 0 ]; do --no-gpu) GPU=0; shift 1;; --no-wayland) WAYLAND=0; shift 1;; --no-pipewire) PIPEWIRE=0; shift 1;; + --no-env-defaults) ENV_DEFAULTS=0; shift 1;; --x11) X11=1; shift 1;; - --bind) BWRAP_ARGS+=("--bind" "$2" "$3"); shift 3;; - --ro-bind) BWRAP_ARGS+=("--ro-bind" "$2" "$3"); shift 3;; - --expose) BWRAP_ARGS+=("--bind" "$2" "$2"); shift 2;; - --ro-expose) BWRAP_ARGS+=("--ro-bind" "$2" "$2"); shift 2;; + -b|--bind) BWRAP_ARGS+=("--bind" "$2" "$3"); SIDEBUS_ARGS+=("--path-mapping" "$3=$2"); shift 3;; + --ro-bind) BWRAP_ARGS+=("--ro-bind" "$2" "$3"); SIDEBUS_ARGS+=("--path-mapping" "$3=$2"); shift 3;; + -e|--expose) BWRAP_ARGS+=("--bind" "$2" "$2"); SIDEBUS_ARGS+=("--path-mapping" "$2=$2"); shift 2;; + --ro-expose) BWRAP_ARGS+=("--ro-bind" "$2" "$2"); SIDEBUS_ARGS+=("--path-mapping" "$2=$2"); shift 2;; + -p|--publish) USING_PUBLISH=1; MUVM_ARGS+=("--publish=$2"); shift 2;; --host-opengl-driver) HOST_OPENGL_DRIVER="$2"; shift 2;; --munix-bin-dir) SCRIPT_PATH="$2"; shift 2;; --muvm-bin-dir) MUVM_PATH="$2"; shift 2;; --passt-bin-dir) PASST_PATH="$2"; shift 2;; + --wl-proxy-bin-dir) WL_PROXY_PATH="$2"; shift 2;; + --wl-backdrop-bin-dir) shift 2;; + --) shift 1; MICROVM_COMMAND+=("$@"); break;; -*) echo "munix: unknown option: $1" >&2; exit 1;; *) if [ "$MICROVM_CLOSURE" = "" ]; then @@ -47,8 +79,36 @@ if [ "$MICROVM_CLOSURE" = "" ]; then exit 1 fi +if [ "$MUVM_PATH" = "" ]; then + echo "munix: muvm not found, provide a --muvm-bin-dir or fix \$PATH" >&2 + exit 1 +fi + +if [ "$PASST_PATH" = "" ]; then + echo "munix: passt not found, provide a --passt-bin-dir or fix \$PATH" >&2 + exit 1 +fi + +if [ "$WL_PROXY_PATH" = "" ]; then + echo "munix: wl-cross-domain-proxy not found, provide a --wl-proxy-bin-dir or fix \$PATH" >&2 + exit 1 +fi + +if [ "$WL_BACKDROP_PATH" = "" ]; then + echo "munix: wl-backdrop not found, provide a --wl-backdrop-bin-dir or fix \$PATH" >&2 + exit 1 +fi + +if [ ! -e "$HOST_OPENGL_DRIVER" ]; then + echo "munix: host graphics driver not found, provide a --host-opengl-driver" >&2 + exit 1 +fi + +# Resolve symlinks automatically +MICROVM_CLOSURE=$(realpath "$MICROVM_CLOSURE") + if [ ${#MICROVM_COMMAND[@]} -eq 0 ]; then - MICROVM_COMMAND=("bash") + MICROVM_COMMAND=("$MICROVM_DEFAULT_COMMAND") fi if [ "$GPU" -eq 1 ]; then @@ -69,7 +129,7 @@ if [ "$GPU" -eq 1 ]; then driver_mod="$(readlink "$driver_link")" driver_name="${driver_mod##*/}" case "$driver_name" in - amdgpu|msm) # TODO: i915 + amdgpu|msm_dpu) # TODO: i915 echo "munix: ${card##*/} gpu driver is '$driver_name', using vdrm" >&2; GPU_MODE=drm break;; @@ -84,9 +144,6 @@ if [ "$GPU" -eq 1 ]; then GPU_MODE=software fi MUVM_ARGS+=("--gpu-mode=$GPU_MODE") - if [ "$GPU_MODE" = "venus" ]; then - MUVM_ARGS+=("-e" "MESA_LOADER_DRIVER_OVERRIDE=zink") - fi else BWRAP_ARGS+=("--dir" "/dev/dri") MUVM_ARGS+=("--gpu-mode=software") @@ -101,7 +158,15 @@ if [ "$WAYLAND" -eq 1 ]; then "--bind" "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" "--setenv" "WAYLAND_DISPLAY" "$WAYLAND_DISPLAY" ) - MUVM_ARGS+=("-e" "WAYLAND_DISPLAY=wayland-1") # the proxy is managed by us, not muvm + if [ "$ENV_DEFAULTS" -eq 1 ]; then + MUVM_ARGS+=( + "-e" "XDG_SESSION_TYPE=wayland" + "-e" "SDL_VIDEODRIVER=wayland" + "-e" "QT_QPA_PLATFORM=wayland" + "-e" "_JAVA_AWT_WM_NONREPARENTING=1" # e.g. with xwayland-satellite + "-e" "ELECTRON_OZONE_PLATFORM_HINT=wayland" # 28 < Electron < 39; newer should work by default + ) + fi fi if [ "$PIPEWIRE" -eq 1 ]; then @@ -119,6 +184,11 @@ if [ "$PIPEWIRE" -eq 1 ]; then PIPEWIRE_REMOTE=pipewire-0 fi BWRAP_ARGS+=("--bind" "$PIPEWIRE_RUNTIME_DIR/$PIPEWIRE_REMOTE" "$PIPEWIRE_RUNTIME_DIR/$PIPEWIRE_REMOTE") + if [ "$ENV_DEFAULTS" -eq 1 ]; then + MUVM_ARGS+=( + "-e" "SDL_AUDIO_DRIVER=pipewire" + ) + fi fi if [ "$X11" -eq 1 ]; then @@ -130,37 +200,87 @@ else unset DISPLAY XAUTHORITY fi -exec bwrap --unshare-all --share-net \ +declare -a BG_PIDS + +cleanup() { + for pid in "${BG_PIDS[@]}"; do + if kill -0 "$pid" 2>/dev/null; then + echo "Killing process $pid" + kill -INT "$pid" + wait "$pid" 2>/dev/null + fi + done + exit +} + +trap cleanup EXIT INT TERM + +HOST_RUNTIME_DIR="$XDG_RUNTIME_DIR/munix.$$" +mkdir -p $HOST_RUNTIME_DIR +rm $HOST_RUNTIME_DIR/* +mkdir -p "$HOST_RUNTIME_DIR/home" +RUST_LOG=debug sidebus-broker \ + --path-mapping "/home=$HOST_RUNTIME_DIR/home" \ + "${SIDEBUS_ARGS[@]}" \ + --guest-mountpoint /mnt/munix-doc-portal/doc \ + --runtime-dir "$HOST_RUNTIME_DIR" \ + --unix-path "$HOST_RUNTIME_DIR/port.sock" & # >/dev/null 2>&1 & +BG_PIDS+=("$!") +while [ ! -S "$HOST_RUNTIME_DIR/port.sock" ]; do sleep 0.1; done +BWRAP_ARGS=( # prepend home mount before other mounts to not override custom bind mounts under /home + --bind "$HOST_RUNTIME_DIR/home" /home + "${BWRAP_ARGS[@]}" + --bind "$HOST_RUNTIME_DIR" /mnt/munix-doc-portal + --setenv "RUTABAGA_DBUS_CLIENT_SOCKET" /mnt/munix-doc-portal/port.sock +) +if [ "$ENV_DEFAULTS" -eq 1 ]; then + MUVM_ARGS+=( + "-e" "GTK_USE_PORTAL=1" # GTK 3 including Firefox + "-e" "QT_QPA_PLATFORMTHEME=xdgdesktopportal" + ) +fi + +if [ "$USING_PUBLISH" -eq 1 ]; then + printf "\n\n\n\e[1mNote: due to a \e[31mBUG\e[39m with port publishing (-p) you have to send the first outgoing packet (e.g. ping -c1 8.8.8.8) before your ports start receiving traffic. Sorry for the inconvenience!\e[39;0m\n\n\n\n" >&2 +fi + +# xxx: some time is lost to the starting process.. +[[ "$(/dev/null # perl, calm down -PATH=$MICROVM_CLOSURE/sw/bin - -echo "=> Initializing microVM environment" - -mkdir /run/log - -# Prepare mounts for paths that activation writes to -mount -t tmpfs tmpfs /etc -cp /run/muvm-host/etc/resolv.conf /etc/resolv.conf -mount -t tmpfs tmpfs /usr/bin -cp /run/muvm-host/usr/bin/munix-init-user /usr/bin/munix-init-user -mount -t tmpfs tmpfs /dev/shm # tries to remount, fails on virtiofs due to unexpected opts - -$MICROVM_CLOSURE/activate >/run/log/activate.spam 2>&1 - -umount /dev/shm # restore the original virtiofs dax shm though - -chown $MICROVM_UID:$MICROVM_GID /run /dev # avoid "Detected unsafe path transition" -systemd-tmpfiles --create >/run/log/tmpfiles.spam 2>&1 - -systemd-machine-id-setup >/dev/null 2>&1 - -$MICROVM_CLOSURE/sw/lib/systemd/systemd-udevd >/run/log/udevd.spam 2>&1 & - -echo "=> microVM environment ready!" diff --git a/munix-init-user b/munix-init-user deleted file mode 100755 index 2e7b672..0000000 --- a/munix-init-user +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -PATH=$MICROVM_CLOSURE/sw/bin -mkdir -p ~/.var/log -wayland-proxy-virtwl --virtio-gpu >~/.var/log/wayland-proxy.log 2>&1 & diff --git a/muvm b/muvm deleted file mode 160000 index 53d416f..0000000 --- a/muvm +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 53d416fb3f96b04a5bd3dd40f94fa4d8fbf1d7f1 diff --git a/nixosModules/default.nix b/nixosModules/default.nix new file mode 100644 index 0000000..35398b8 --- /dev/null +++ b/nixosModules/default.nix @@ -0,0 +1,204 @@ +{ + self, +}: +{ + pkgs, + lib, + utils, + config, + ... +}: +let + runtimeDir = "/run/vm-user"; + system = pkgs.stdenv.hostPlatform.system; +in +{ + options.virtualisation.munix.defaultCommand = lib.mkOption { + type = lib.types.str; + default = "bash"; + description = "Default command to run when starting the VM without arguments."; + }; + + config = { + boot.isContainer = true; + fileSystems."/".device = lib.mkDefault "/dev/sda"; # dummy + + # Disable unused things + environment.defaultPackages = lib.mkDefault [ ]; + documentation = { + enable = lib.mkDefault false; + doc.enable = lib.mkDefault false; + info.enable = lib.mkDefault false; + man.enable = lib.mkDefault false; + nixos.enable = lib.mkDefault false; + }; + services.logrotate.enable = false; + services.udisks2.enable = false; + system.tools.nixos-generate-config.enable = false; + system.activationScripts.specialfs = lib.mkForce ""; + systemd.coredump.enable = false; + networking.firewall.enable = false; + powerManagement.enable = false; + boot.kexec.enable = false; + console.enable = false; + + # Configure activation / systemd + boot.initrd.systemd.enable = true; # for etc.overlay, but we don't have initrd + system.etc.overlay.enable = true; # erofs + system.etc.overlay.mutable = false; + system.systemBuilderCommands = # XXX: removed with the introduction of nixos-init + '' + ln -s ${config.system.build.etcMetadataImage} $out/etc-metadata-image + ln -s ${config.system.build.etcBasedir} $out/etc-basedir + ''; + system.switch.enable = false; + services.udev.enable = lib.mkDefault true; + services.udev.packages = lib.mkDefault [ ]; + services.resolved.enable = false; + environment.etc."resolv.conf".source = "/run/resolv.conf"; + environment.etc."machine-id".source = "/run/machine-id"; + environment.etc."localtime".source = "/run/localtime"; + environment.etc."systemd/system".source = lib.mkForce ( + utils.systemdUtils.lib.generateUnits { + type = "system"; + units = config.systemd.units; + upstreamUnits = [ + "sysinit.target" + "local-fs.target" + "nss-user-lookup.target" + "umount.target" + "sockets.target" + "shutdown.target" + "reboot.target" + "exit.target" + "final.target" + "systemd-exit.service" + "systemd-journald.socket" + "systemd-journald-audit.socket" + "systemd-journald-dev-log.socket" + "systemd-journald.service" + "systemd-udevd-kernel.socket" + "systemd-udevd-control.socket" + "user.slice" + ]; + upstreamWants = [ "multi-user.target.wants" ]; + } + ); + + # systemd.package = pkgs.systemdMinimal; # no analyze + systemd.defaultUnit = "microvm.target"; + systemd.targets.microvm = { + description = "Minimal microVM system"; + wants = [ + "systemd-journald.socket" + "systemd-udevd.service" + "dbus.socket" + ]; + unitConfig.AllowIsolate = "yes"; + }; + systemd.services.generate-shutdown-ramfs.enable = lib.mkForce false; + systemd.services.systemd-remount-fs.enable = lib.mkForce false; + systemd.services.systemd-pstore.enable = lib.mkForce false; + systemd.services.lastlog2-import.enable = lib.mkForce false; + systemd.services.suid-sgid-wrappers.enable = lib.mkForce false; + systemd.services.systemd-udevd = { + # Redefine to remove the Before deps and get out of the critical chain + enable = true; + description = "Rule-based Manager for Device Events and Files"; + unitConfig.DefaultDependencies = "no"; + serviceConfig = { + CapabilityBoundingSet = "~CAP_SYS_TIME CAP_WAKE_ALARM"; + Delegate = ""; + DelegateSubgroup = "udev"; + Type = "notify-reload"; + OOMScoreAdjust = "-1000"; + Sockets = "systemd-udevd-control.socket systemd-udevd-kernel.socket systemd-udevd-varlink.socket"; + Restart = "always"; + RestartSec = "0"; + ExecStart = "${pkgs.systemd}/lib/systemd/systemd-udevd"; + FileDescriptorStoreMax = "512"; + FileDescriptorStorePreserve = "yes"; + KillMode = "mixed"; + TasksMax = "infinity"; + PrivateMounts = "yes"; + ProtectHostname = "yes"; + MemoryDenyWriteExecute = "yes"; + RestrictAddressFamilies = "AF_UNIX AF_NETLINK AF_INET AF_INET6"; + RestrictRealtime = "yes"; + RestrictSUIDSGID = "yes"; + SystemCallFilter = [ + "@system-service @module @raw-io bpf" + "~@clock" + ]; + SystemCallErrorNumber = "EPERM"; + SystemCallArchitectures = "native"; + LockPersonality = "yes"; + IPAddressDeny = "any"; + WatchdogSec = "3min"; + }; + }; + + # Configure user accounts + systemd.sysusers.enable = false; + services.userborn.enable = true; + services.userborn.static = true; + users.mutableUsers = false; + users.users.appvm = { + uid = 1337; + group = "appvm"; + isNormalUser = false; + isSystemUser = true; + home = "/home/appvm"; + description = "microVM User"; + shell = pkgs.bash; # not nologin, despite being a "system" user + extraGroups = [ + "wheel" + "video" + "input" + "systemd-journal" + ]; + }; + users.groups.appvm.gid = 1337; + users.allowNoPasswordLogin = true; + + # Configure services + + systemd.settings.Manager.DefaultEnvironment = "XDG_RUNTIME_DIR=${runtimeDir}"; + + systemd.sockets.session-bus = { + enable = true; + description = "D-Bus session bus socket"; + wantedBy = [ "microvm.target" ]; + partOf = [ "session-bus.service" ]; + listenStreams = [ "${runtimeDir}/dbus.sock" ]; + socketConfig = { + SocketUser = "appvm"; + SocketGroup = "appvm"; + }; + }; + systemd.services.session-bus = { + enable = true; + description = "D-Bus session bus"; + requires = [ "session-bus.socket" ]; + serviceConfig = { + ExecStart = "${pkgs.dbus}/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --syslog-only"; # no systemd activation, we don't run a *session* systemd + User = "appvm"; + Group = "appvm"; + }; + }; + + hardware.graphics.enable = true; + hardware.graphics.package = self.packages.${system}.mesa; + + system.build.munix = pkgs.symlinkJoin { + name = "munix"; + paths = [ self.packages.${system}.munix ]; + buildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/munix \ + --add-flags ${config.system.build.toplevel} \ + --set MICROVM_DEFAULT_COMMAND ${lib.escapeShellArg config.virtualisation.munix.defaultCommand} + ''; + }; + }; +} diff --git a/nixosModules/testvm.nix b/nixosModules/testvm.nix new file mode 100644 index 0000000..aaf30d3 --- /dev/null +++ b/nixosModules/testvm.nix @@ -0,0 +1,44 @@ +{ }: +{ pkgs, ... }: { + system.stateVersion = "26.05"; + + fonts.packages = [ pkgs.adwaita-fonts pkgs.dejavu_fonts ]; + programs.dconf.enable = true; + environment.systemPackages = [ + pkgs.fastfetch + pkgs.htop + pkgs.wayland-utils + pkgs.weston + pkgs.waycheck + pkgs.vulkan-tools + pkgs.glmark2 + pkgs.mesa-demos + pkgs.xorg.xeyes + pkgs.xterm + pkgs.vkquake + # pkgs.veloren + pkgs.kdePackages.kate + pkgs.adwaita-icon-theme + pkgs.amberol + pkgs.bustle + pkgs.d-spy + pkgs.gnome-text-editor + pkgs.firefox + pkgs.ffmpeg-full + pkgs.mpv + pkgs.libva-utils + pkgs.pipewire # cli + pkgs.tailscale + pkgs.zerotierone + pkgs.localsend + pkgs.ashpd-demo + pkgs.nautilus + pkgs.rewaita + pkgs.wl-clipboard-rs + pkgs.snapshot + pkgs.gst_all_1.gst-plugins-base + pkgs.gst_all_1.gst-plugins-good + pkgs.gst_all_1.gst-plugins-bad + pkgs.gst_all_1.gstreamer + ]; +} diff --git a/packages/libkrun/default.nix b/packages/libkrun/default.nix new file mode 100644 index 0000000..00d655c --- /dev/null +++ b/packages/libkrun/default.nix @@ -0,0 +1,32 @@ +{ + libkrun, + libkrunfw, + libkrun-src, + rustPlatform, + libcap_ng, + variant ? null, + ... +}: + +let + libkrunfw' = libkrunfw.override { inherit variant; }; + libkrun' = libkrun.override { + withBlk = true; + withNet = true; + withGpu = true; + # --- stick to the override used in nixpkgs' muvm package to reuse nixos.org cache when not overriding src --- + # withSound = true; # not for pipewire forwarding, anyway + # withTimesync = true; # why not?.. + # --------- + inherit variant; + libkrunfw = libkrunfw'; + }; +in +# libkrun' +libkrun'.overrideAttrs (old: { + src = libkrun-src; + cargoDeps = rustPlatform.importCargoLock { + lockFile = "${libkrun-src}/Cargo.lock"; + }; + buildInputs = old.buildInputs ++ [ libcap_ng ]; # new dep +}) diff --git a/packages/libkrunfw/default.nix b/packages/libkrunfw/default.nix new file mode 100644 index 0000000..c805728 --- /dev/null +++ b/packages/libkrunfw/default.nix @@ -0,0 +1,22 @@ +{ + libkrunfw, + # libkrunfw-src, + # fetchurl, + variant ? null, + ... +}: + +let + libkrunfw' = libkrunfw.override { + inherit variant; + }; +in +libkrunfw' +# libkrunfw'.overrideAttrs (old: { +# version = "5.1.0"; +# src = libkrunfw-src; +# kernelSrc = fetchurl { +# url = "mirror://kernel/linux/kernel/v6.x/linux-6.12.62.tar.xz"; +# hash = "sha256-E+LGhayPq13Zkt0QVzJVTa5RSu81DCqMdBjnt062LBM="; +# }; +# }) diff --git a/packages/mesa/default.nix b/packages/mesa/default.nix new file mode 100644 index 0000000..d76f1b5 --- /dev/null +++ b/packages/mesa/default.nix @@ -0,0 +1,10 @@ +{ mesa, lib, stdenv }: + +(mesa.override { + # nothing currently +}).overrideAttrs (new: old: { + mesonFlags = old.mesonFlags ++ + lib.optionals stdenv.hostPlatform.isx86_64 [ (lib.mesonBool "amdgpu-virtio" true) ]; + # not that amdgpu can't be found on aarch64 but let's avoid rebuilds for now + # patches = old.patches ++ [ ]; +}) diff --git a/packages/munix/default.nix b/packages/munix/default.nix new file mode 100644 index 0000000..e0b848e --- /dev/null +++ b/packages/munix/default.nix @@ -0,0 +1,36 @@ +{ stdenv, writeScriptBin, symlinkJoin, makeWrapper, muvm, passt, bubblewrap, sidebus-broker, wl-cross-domain-proxy, wl-backdrop, mesa, rustc }: + +let + munixScript = (writeScriptBin "munix" (builtins.readFile ../../munix)).overrideAttrs(old: { + buildCommand = "${old.buildCommand}\n patchShebangs $out"; + }); + munixSystemd = stdenv.mkDerivation { + name = "munix-systemd"; + src = ../../systemd; + dontUnpack = true; + installPhase = '' + mkdir -p $out + cp -aR $src/* $out + ''; + }; + microActivate = stdenv.mkDerivation { + name = "micro-activate"; + src = ../../micro-activate.rs; + dontUnpack = true; + nativeBuildInputs = [ rustc ]; + buildPhase = '' + rustc -C opt-level=s -C panic=abort --edition 2024 -o micro-activate $src + ''; + installPhase = '' + mkdir -p $out/bin + mv micro-activate $out/bin + ''; + }; +in symlinkJoin { + name = "munix"; + paths = [ munixScript microActivate muvm passt bubblewrap sidebus-broker wl-cross-domain-proxy wl-backdrop ]; + buildInputs = [ makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/munix --prefix PATH : $out/bin --set FALLBACK_OPENGL_DRIVER ${mesa} --set MUNIX_SYSTEMD_UNITS ${munixSystemd} + ''; +} diff --git a/packages/muvm/default.nix b/packages/muvm/default.nix new file mode 100644 index 0000000..dd678d8 --- /dev/null +++ b/packages/muvm/default.nix @@ -0,0 +1,12 @@ +{ muvm, libkrun, muvm-src, systemd, rustPlatform }: + +(muvm.override { + libkrun = libkrun; +}).overrideAttrs (old: { + postPatch = ""; # no more sysctl; udevd now takes the var anyway; XXX: fex + MUVM_UDEVD_PATH = "${systemd}/lib/systemd/systemd-udevd"; + src = muvm-src; + cargoDeps = rustPlatform.importCargoLock { + lockFile = "${muvm-src}/Cargo.lock"; + }; +}) diff --git a/packages/wl-cross-domain-proxy/default.nix b/packages/wl-cross-domain-proxy/default.nix new file mode 100644 index 0000000..0ca436b --- /dev/null +++ b/packages/wl-cross-domain-proxy/default.nix @@ -0,0 +1,44 @@ +{ + lib, + rustPlatform, + # fetchpatch, + fetchFromGitea, + pkg-config, + libdrm, +}: + +# TODO: upstream +rustPlatform.buildRustPackage { + pname = "wl-cross-domain-proxy"; + version = "0-unstable-2026-01-30"; + + src = fetchFromGitea { + domain = "codeberg.org"; + owner = "drakulix"; + repo = "wl-cross-domain-proxy"; + rev = "c6ce1ca89fb4d6f4f18d3aaf88324d40d4589177"; + hash = "sha256-ydyT4DFzWzhzOZR591UOgLjVQt/v6hRSNjzM3QtohlU="; + }; + + nativeBuildInputs = [ pkg-config ]; + + buildInputs = [ libdrm ]; + + cargoHash = "sha256-k3dmxIuCQoOrn/VwauTdzuRw/XKQB6LPLgO5ql0rE7E="; + cargoPatches = [ + # (fetchpatch { + # name = "XXX.patch"; + # url = "https://codeberg.org/drakulix/wl-cross-domain-proxy/pulls/XXX.patch"; + # hash = lib.fakeHash; + # }) + ]; + + meta = { + homepage = "https://codeberg.org/drakulix/wl-cross-domain-proxy"; + description = "Proxy for the wayland protocol across virtio-gpu cross-domain context"; + mainProgram = "wl-cross-domain-proxy"; + platforms = lib.platforms.linux; + license = [ lib.licenses.mit ]; + maintainers = [ lib.maintainers.valpackett ]; + }; +} diff --git a/radvmmio.patch b/radvmmio.patch deleted file mode 100644 index c3ff0bf..0000000 --- a/radvmmio.patch +++ /dev/null @@ -1,42 +0,0 @@ -From c2fd030644cf2074f9d2ffd155839b2e943473d0 Mon Sep 17 00:00:00 2001 -From: Val Packett -Date: Wed, 10 Sep 2025 15:44:41 -0300 -Subject: [PATCH] radv: detect platform:virtio-mmio devices for virtgpu native - context - -VirtIO devices can be configured as platform devices instead of PCI, -which is especially common in microVM projects like libkrun. - -Let's allow RADV to probe MMIO virtgpu devices. - -Signed-off-by: Val Packett ---- - src/amd/vulkan/radv_physical_device.c | 7 ++++--- - 1 file changed, 4 insertions(+), 3 deletions(-) - -diff --git a/src/amd/vulkan/radv_physical_device.c b/src/amd/vulkan/radv_physical_device.c -index 07b5b000b3ae..cd6f4c3488a1 100644 ---- a/src/amd/vulkan/radv_physical_device.c -+++ b/src/amd/vulkan/radv_physical_device.c -@@ -2507,14 +2507,15 @@ create_drm_physical_device(struct vk_instance *vk_instance, struct _drmDevice *d - #ifndef _WIN32 - bool supported_device = false; - -- if (!(device->available_nodes & (1 << DRM_NODE_RENDER)) || device->bustype != DRM_BUS_PCI) -+ if (!(device->available_nodes & (1 << DRM_NODE_RENDER))) - return VK_ERROR_INCOMPATIBLE_DRIVER; - - #ifdef HAVE_AMDGPU_VIRTIO -- supported_device |= device->deviceinfo.pci->vendor_id == VIRTGPU_PCI_VENDOR_ID; -+ supported_device |= device->bustype == DRM_BUS_PCI && device->deviceinfo.pci->vendor_id == VIRTGPU_PCI_VENDOR_ID; -+ supported_device |= device->bustype == DRM_BUS_PLATFORM; /* virtio-mmio */ - #endif - -- supported_device |= device->deviceinfo.pci->vendor_id == ATI_VENDOR_ID; -+ supported_device |= device->bustype == DRM_BUS_PCI && device->deviceinfo.pci->vendor_id == ATI_VENDOR_ID; - - if (!supported_device) - return VK_ERROR_INCOMPATIBLE_DRIVER; --- -2.50.1 - diff --git a/systemd/microvm.target.wants/muvm-configure-network.service b/systemd/microvm.target.wants/muvm-configure-network.service new file mode 100644 index 0000000..ca93c89 --- /dev/null +++ b/systemd/microvm.target.wants/muvm-configure-network.service @@ -0,0 +1 @@ +../muvm-configure-network.service \ No newline at end of file diff --git a/systemd/microvm.target.wants/muvm-remote.service b/systemd/microvm.target.wants/muvm-remote.service new file mode 100644 index 0000000..4f9f550 --- /dev/null +++ b/systemd/microvm.target.wants/muvm-remote.service @@ -0,0 +1 @@ +../muvm-remote.service \ No newline at end of file diff --git a/systemd/microvm.target.wants/pipewire-bridge.socket b/systemd/microvm.target.wants/pipewire-bridge.socket new file mode 100644 index 0000000..95e1850 --- /dev/null +++ b/systemd/microvm.target.wants/pipewire-bridge.socket @@ -0,0 +1 @@ +../pipewire-bridge.socket \ No newline at end of file diff --git a/systemd/microvm.target.wants/session-bus-bridge.service b/systemd/microvm.target.wants/session-bus-bridge.service new file mode 100644 index 0000000..faa559d --- /dev/null +++ b/systemd/microvm.target.wants/session-bus-bridge.service @@ -0,0 +1 @@ +../session-bus-bridge.service \ No newline at end of file diff --git a/systemd/microvm.target.wants/wayland-bridge.socket b/systemd/microvm.target.wants/wayland-bridge.socket new file mode 100644 index 0000000..4bcb084 --- /dev/null +++ b/systemd/microvm.target.wants/wayland-bridge.socket @@ -0,0 +1 @@ +../wayland-bridge.socket \ No newline at end of file diff --git a/systemd/muvm-configure-network.service b/systemd/muvm-configure-network.service new file mode 100644 index 0000000..3a67ff4 --- /dev/null +++ b/systemd/muvm-configure-network.service @@ -0,0 +1,6 @@ +[Unit] +Description=microVM Network configuration + +[Service] +Type=oneshot +ExecStart=/opt/bin/muvm-configure-network diff --git a/systemd/muvm-remote.service b/systemd/muvm-remote.service new file mode 100644 index 0000000..6164434 --- /dev/null +++ b/systemd/muvm-remote.service @@ -0,0 +1,33 @@ +[Unit] +After=sockets.target +Description=microVM Application runner +OnFailure=exit.target +OnSuccess=exit.target +Wants=sockets.target + +[Service] +Type=exec +# Environment="LOCALE_ARCHIVE=/nix/store/1hilqf0v1nm2w8xj87pwpm0rgq6lvhlv-glibc-locales-2.42-47/lib/locale/locale-archive" +# Environment="TZDIR=/nix/store/80izpiglrlqv2zb7rd7m5274k2dr142l-tzdata-2025c/share/zoneinfo" +Environment=WAYLAND_DISPLAY=wayland-1 +Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/vm-user/dbus.sock +Environment=PATH=/run/current-system/sw/bin +PassEnvironment=MESA_LOADER_DRIVER_OVERRIDE +PassEnvironment=MUVM_REMOTE_CONFIG +PassEnvironment=TERM +PassEnvironment=XDG_SESSION_TYPE +PassEnvironment=SDL_VIDEODRIVER +PassEnvironment=QT_QPA_PLATFORM +PassEnvironment=_JAVA_AWT_WM_NONREPARENTING +PassEnvironment=ELECTRON_OZONE_PLATFORM_HINT +PassEnvironment=GTK_USE_PORTAL +PassEnvironment=QT_QPA_PLATFORMTHEME +ExecStartPre=+/run/current-system/sw/bin/chown appvm:appvm /run/vm-user +ExecStart=/opt/bin/muvm-remote +ExecStopPost=+/nix/store/ygzfhw5nxrn7qqfqmz2s9p6cikcjqqkh-python3-3.13.11/bin/python -c "import os,fcntl,struct;print(os.getenv('EXIT_STATUS', '1'));fcntl.ioctl(os.open('/', os.O_RDONLY), 0x7602, int(os.getenv('EXIT_STATUS', '1')))" +User=appvm +Group=appvm +StandardError=tty +StandardInput=tty +StandardOutput=tty +TTYPath=/dev/hvc0 diff --git a/systemd/pipewire-bridge.service b/systemd/pipewire-bridge.service new file mode 100644 index 0000000..25c53e2 --- /dev/null +++ b/systemd/pipewire-bridge.service @@ -0,0 +1,12 @@ +[Unit] +Description=PipeWire VM bridge +Requires=pipewire-bridge.socket + +[Service] +Type=exec +ExecStart=/opt/bin/muvm-pwbridge + +# Environment=RUST_LOG=debug +# StandardError=tty +# StandardOutput=tty +# TTYPath=/dev/hvc0 diff --git a/systemd/pipewire-bridge.socket b/systemd/pipewire-bridge.socket new file mode 100644 index 0000000..998e661 --- /dev/null +++ b/systemd/pipewire-bridge.socket @@ -0,0 +1,8 @@ +[Unit] +Description=PipeWire VM bridge socket +PartOf=pipewire-bridge.service + +[Socket] +SocketGroup=appvm +SocketUser=appvm +ListenStream=/run/vm-user/pipewire-0 diff --git a/systemd/session-bus-bridge.service b/systemd/session-bus-bridge.service new file mode 100644 index 0000000..9b12a85 --- /dev/null +++ b/systemd/session-bus-bridge.service @@ -0,0 +1,11 @@ +[Unit] +After=session-bus.service +Description=D-Bus VM integration bridge +Requires=session-bus.socket session-bus.service + +[Service] +Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/vm-user/dbus.sock +ExecStartPre=+/run/current-system/sw/bin/chmod 0666 /dev/dri/card0 /dev/dri/renderD128 +ExecStart=/opt/bin/muvm-dbusbridge +Group=appvm +User=appvm diff --git a/systemd/wayland-bridge.service b/systemd/wayland-bridge.service new file mode 100644 index 0000000..c74ba4e --- /dev/null +++ b/systemd/wayland-bridge.service @@ -0,0 +1,13 @@ +[Unit] +Description=Wayland VM bridge +Requires=wayland-bridge.socket + +[Service] +ExecStartPre=+/run/current-system/sw/bin/chmod 0666 /dev/dri/card0 /dev/dri/renderD128 +ExecStart=/opt/bin/wl-cross-domain-proxy --listen-fd --filter-global wp_presentation +User=appvm +Group=appvm + +StandardError=tty +StandardOutput=tty +TTYPath=/dev/hvc0 diff --git a/systemd/wayland-bridge.socket b/systemd/wayland-bridge.socket new file mode 100644 index 0000000..2706a2b --- /dev/null +++ b/systemd/wayland-bridge.socket @@ -0,0 +1,9 @@ +[Unit] +Description=Wayland VM bridge socket +PartOf=wayland-bridge.service + +[Socket] +FileDescriptorName=wayland +SocketGroup=appvm +SocketUser=appvm +ListenStream=/run/vm-user/wayland-1 diff --git a/templates/musictest/flake.nix b/templates/musictest/flake.nix new file mode 100644 index 0000000..406049b --- /dev/null +++ b/templates/musictest/flake.nix @@ -0,0 +1,71 @@ +{ + nixConfig = { + extra-substituters = [ "https://cache.clan.lol" ]; + extra-trusted-public-keys = [ + "cache.clan.lol-1:3KztgSAB5R1M+Dz7vzkBGzXdodizbgLXGXKXlcQLA28=" + ]; + }; + + inputs = { + munix.url = "git+https://git.clan.lol/clan/munix?shallow=1&ref=main"; + nixpkgs.follows = "munix/nixpkgs"; + }; + + outputs = + { + nixpkgs, + munix, + ... + }: + let + forAllSystems = nixpkgs.lib.genAttrs [ + "x86_64-linux" + "aarch64-linux" + ]; + musictest-vm = + system: + nixpkgs.lib.nixosSystem { + modules = [ + munix.nixosModules.default + ( + { pkgs, ... }: + { + system.stateVersion = "26.05"; + virtualisation.munix.defaultCommand = "euphonica"; + + nixpkgs.hostPlatform = system; + + programs.dconf.enable = true; + fonts.packages = with pkgs; [ adwaita-fonts ]; + environment.systemPackages = with pkgs; [ euphonica ]; + + # Local background service as a demo that doesn't require network creds :) + services.mpd = { + enable = true; + startWhenNeeded = true; + musicDirectory = "/etc/demo-music"; + user = "appvm"; + group = "appvm"; + settings.audio_output = [ + { + type = "pipewire"; + name = "Pipewire Output"; + } + ]; + }; + environment.etc."demo-music/0101GhostsI.ogg".source = pkgs.fetchurl { + # just a CC-BY-SA licensed example + url = "https://archive.org/download/NineInchNailsGhostsI-Iv24bit48khz/0101GhostsI.ogg"; + sha256 = "0iijm1c191aqkxybl4a4gvlpnf72hk4896lwvp0xixkhds88qzxi"; + }; + } + ) + ]; + }; + in + { + packages = forAllSystems (system: { + default = (musictest-vm system).config.system.build.munix; + }); + }; +}