[PATCH 0/7] System updates based on systemd-sysupdate
This implements updates via systemd-sysupdate. See individual commit messages for details. There are major changes to the image build process. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Demi Marie Obenour (7): host/rootfs: Use full util-linux and systemd release/combined: Compress installation image tools: Add directory checker for updates Adjust partition layout to support updates release: add install step Factor out dm-verity build rules Support updates via systemd-sysupdate host/initramfs/Makefile | 17 ++-- host/initramfs/default.nix | 2 + host/initramfs/etc/init | 17 ++-- host/initramfs/etc/probe | 20 +++-- host/initramfs/shell.nix | 1 + host/rootfs/Makefile | 54 ++++++------- host/rootfs/default.nix | 64 +++++++-------- host/rootfs/file-list.mk | 5 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 21 +++++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 21 +++++ .../image/etc/sysupdate.d/70-kernel.transfer | 25 ++++++ host/rootfs/image/usr/bin/run-update | 54 +++++++++++++ host/rootfs/image/usr/bin/update | 56 +++++++++++++ host/rootfs/image/usr/bin/vm-start | 25 +++++- host/rootfs/os-release.in | 13 +++ host/rootfs/os-release.in.license | 2 + host/rootfs/shell.nix | 3 +- img/app/Makefile | 2 +- img/app/default.nix | 5 +- lib/kcmdline-utils.mk | 8 ++ lib/verity.mk | 18 +++++ lib/version.nix | 15 ++++ release/checks/integration/default.nix | 2 +- release/checks/integration/meson.build | 2 +- release/checks/no-roothash.nix | 2 +- release/combined/eosimages.nix | 16 ++-- release/live/Makefile | 46 +++++------ release/live/default.nix | 17 ++-- release/live/shell.nix | 4 +- scripts/format-uuid.awk | 35 ++++++++ scripts/format-uuid.sh | 1 + scripts/make-gpt.bash | 72 +++++++++++++++++ scripts/make-gpt.sh | 67 +-------------- scripts/make-live-image.sh | 41 ++++++++++ scripts/sfdisk-field.awk | 3 +- tools/default.nix | 1 + tools/meson.build | 1 + tools/updates-dir-check/meson.build | 4 + tools/updates-dir-check/updates-dir-check.c | 94 ++++++++++++++++++++++ update-signing-keys.gpg | 1 + update-signing-keys.gpg.license | 2 + update-url | 1 + update-url.license | 2 + version | 1 + version.license | 2 + vm/app/sysupdate.d/50-verity.transfer | 18 +++++ vm/app/sysupdate.d/60-root.transfer | 18 +++++ vm/app/sysupdate.d/70-kernel.transfer | 18 +++++ vm/app/updates.nix | 57 +++++++++++++ vm/sys/net/Makefile | 2 +- vm/sys/net/default.nix | 5 +- 52 files changed, 782 insertions(+), 202 deletions(-) --- base-commit: 11c411139e006ddf6ce074c22c30a0bb9b6fb76e change-id: 20250928-updates-92e99849e231 -- Sincerely, Demi Marie Obenour (she/her/hers)
Busybox provides a broken fdisk that doesn't support GPT, only MBR. The systemd built against musl doesn't include systemd-pull, so systemd-sysupdate doesn't work. Therefore, use all of util-linux's command-line tools, and use systemd built against glibc. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/default.nix | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 0d79f7ca54ccc86eb0fa6e743f2011237d365f24..00052222507077b9e94a5ed0a3fbddd27caeefc3 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -4,20 +4,20 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src -, pkgsMusl, pkgsStatic, linux_latest +, pkgsMusl, pkgsStatic, linux_latest, systemd }: pkgsStatic.callPackage ( { busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils, execline , inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos , runCommand, s6, s6-linux-init, s6-rc, socat, spectrum-host-tools -, stdenvNoCC, util-linuxMinimal, virtiofsd, writeClosure +, stdenvNoCC, util-linux, virtiofsd, writeClosure , xdg-desktop-portal-spectrum-host, xorg }: let inherit (lib) - concatMapStringsSep concatStrings escapeShellArgs fileset - mapAttrsToList systems trivial; + concatMapStringsSep concatStrings escapeShellArg + escapeShellArgs fileset mapAttrsToList systems trivial; pkgsGui = pkgsMusl.extend ( _final: super: (lib.optionalAttrs (systems.equals pkgsMusl.stdenv.hostPlatform super.stdenv.hostPlatform) { @@ -32,7 +32,7 @@ in # systemd, so might as well use it. pkgsGui.callPackage ( { cosmic-files, crosvm, dejavu_fonts, foot, kmod, mesa -, systemd, westonLite, xdg-desktop-portal, xdg-desktop-portal-gtk +, westonLite, xdg-desktop-portal, xdg-desktop-portal-gtk }: let @@ -83,7 +83,7 @@ let # (not just their bin/* files). usrPackages = [ appvm kernel.modules firmware kmod kmod.lib - netvm mesa dejavu_fonts westonLite + netvm mesa dejavu_fonts systemd util-linux westonLite ]; appvms = { @@ -99,36 +99,29 @@ let mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps + for pkg in ${escapeShellArgs usrPackages}; do + lndir -ignorelinks -silent "$pkg" "$out/usr" + done + # Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \ ${cosmic-files}/share/icons/hicolor/24x24/apps/com.system76.CosmicFiles.svg - ln -st $out/usr/bin \ - ${concatMapStringsSep " " (p: "${p}/bin/*") packages} \ - ${xdg-desktop-portal}/libexec/xdg-document-portal \ - ${xdg-desktop-portal-gtk}/libexec/xdg-desktop-portal-gtk + ln -sft "$out/usr/bin" \ + ${concatMapStringsSep " " (p: "${escapeShellArg p}/bin/*") packages} \ + ${escapeShellArg xdg-desktop-portal}/libexec/xdg-document-portal \ + ${escapeShellArg xdg-desktop-portal-gtk}/libexec/xdg-desktop-portal-gtk ln -st $out/usr/share/dbus-1 \ - ${dbus}/share/dbus-1/session.conf + ${escapeShellArg dbus}/share/dbus-1/session.conf ln -st $out/usr/share/dbus-1/services \ - ${xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service - - for pkg in ${escapeShellArgs usrPackages}; do - lndir -ignorelinks -silent "$pkg" "$out/usr" - done + ${escapeShellArg xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service + # clobber any conflicting files from busybox + ln -sft "$out/usr/bin" ${escapeShellArg util-linux}/bin/* ${concatStrings (mapAttrsToList (name: path: '' ln -s ${path} $out/usr/lib/spectrum/vm/${name} '') appvms)} - - # TODO: this is a hack and we should just build the util-linux - # programs we want. - # https://lore.kernel.org/util-linux/87zgrl6ufb.fsf@alyssa.is/ - ln -s ${util-linuxMinimal}/bin/{findfs,uuidgen,lsblk,mount} $out/usr/bin - - # TODO: this is another hack and it should be possible - # to build systemd without this. - ln -s -- ${lib.escapeShellArg systemd}/bin/udevadm "$out/usr/bin" ''; in -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
Busybox provides a broken fdisk that doesn't support GPT, only MBR. The systemd built against musl doesn't include systemd-pull, so systemd-sysupdate doesn't work. Therefore, use all of util-linux's command-line tools, and use systemd built against glibc.
That's a problem that's going to need to be fixed. We're not mixing two different libcs on the host.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/default.nix | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-)
Okay idea overall. I like the idea of less busybox. Busybox → util-linux should probably be a patch of its own.
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 0d79f7ca54ccc86eb0fa6e743f2011237d365f24..00052222507077b9e94a5ed0a3fbddd27caeefc3 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -4,20 +4,20 @@
import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src -, pkgsMusl, pkgsStatic, linux_latest +, pkgsMusl, pkgsStatic, linux_latest, systemd }: pkgsStatic.callPackage (
{ busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils, execline , inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos , runCommand, s6, s6-linux-init, s6-rc, socat, spectrum-host-tools -, stdenvNoCC, util-linuxMinimal, virtiofsd, writeClosure +, stdenvNoCC, util-linux, virtiofsd, writeClosure
util-linuxMinimal = util-linux.override { cryptsetupSupport = false; nlsSupport = false; ncursesSupport = false; pamSupport = false; shadowSupport = false; systemdSupport = false; translateManpages = false; }; So how come we need the non-minimal version?
# Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \ ${cosmic-files}/share/icons/hicolor/24x24/apps/com.system76.CosmicFiles.svg
- ln -st $out/usr/bin \ - ${concatMapStringsSep " " (p: "${p}/bin/*") packages} \ - ${xdg-desktop-portal}/libexec/xdg-document-portal \ - ${xdg-desktop-portal-gtk}/libexec/xdg-desktop-portal-gtk + ln -sft "$out/usr/bin" \ + ${concatMapStringsSep " " (p: "${escapeShellArg p}/bin/*") packages} \ + ${escapeShellArg xdg-desktop-portal}/libexec/xdg-document-portal \ + ${escapeShellArg xdg-desktop-portal-gtk}/libexec/xdg-desktop-portal-gtk ln -st $out/usr/share/dbus-1 \ - ${dbus}/share/dbus-1/session.conf + ${escapeShellArg dbus}/share/dbus-1/session.conf ln -st $out/usr/share/dbus-1/services \ - ${xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service - - for pkg in ${escapeShellArgs usrPackages}; do - lndir -ignorelinks -silent "$pkg" "$out/usr" - done + ${escapeShellArg xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service
Unrelated changes. We don't need to escape Nix store paths, because we rely on Nixpkgs, which would break if store paths had weird characters in them.
+ # clobber any conflicting files from busybox + ln -sft "$out/usr/bin" ${escapeShellArg util-linux}/bin/*
The approach we've taken so far is to disable those tools in Busybox, and avoid conflicting symlinks, and I like that better. Alternatively, if you want to figure out which Busybox tools are actually needed, we could switch to a minimal build and enable only what we use.
${concatStrings (mapAttrsToList (name: path: '' ln -s ${path} $out/usr/lib/spectrum/vm/${name} '') appvms)} - - # TODO: this is a hack and we should just build the util-linux - # programs we want. - # https://lore.kernel.org/util-linux/87zgrl6ufb.fsf@alyssa.is/ - ln -s ${util-linuxMinimal}/bin/{findfs,uuidgen,lsblk,mount} $out/usr/bin - - # TODO: this is another hack and it should be possible - # to build systemd without this. - ln -s -- ${lib.escapeShellArg systemd}/bin/udevadm "$out/usr/bin" ''; in
-- 2.51.2
On 10/29/25 07:36, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Busybox provides a broken fdisk that doesn't support GPT, only MBR. The systemd built against musl doesn't include systemd-pull, so systemd-sysupdate doesn't work. Therefore, use all of util-linux's command-line tools, and use systemd built against glibc.
That's a problem that's going to need to be fixed. We're not mixing two different libcs on the host.
Unfortunately, systemd-pull can't be built with musl libc. That leaves four options: 1. Try to fix the build problem, knowing that it could come back. 2. Mix two libcs on the host. 3. Try to fix the build (but this could take quite a while to upstream). 4. Use glibc on the host. I don't think we should ship with option 2, but I think it might be acceptable until the underlying problem is solved.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/default.nix | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-)
Okay idea overall. I like the idea of less busybox. Busybox → util-linux should probably be a patch of its own.
Will fix in v2.
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 0d79f7ca54ccc86eb0fa6e743f2011237d365f24..00052222507077b9e94a5ed0a3fbddd27caeefc3 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -4,20 +4,20 @@
import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src -, pkgsMusl, pkgsStatic, linux_latest +, pkgsMusl, pkgsStatic, linux_latest, systemd }: pkgsStatic.callPackage (
{ busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils, execline , inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos , runCommand, s6, s6-linux-init, s6-rc, socat, spectrum-host-tools -, stdenvNoCC, util-linuxMinimal, virtiofsd, writeClosure +, stdenvNoCC, util-linux, virtiofsd, writeClosure
util-linuxMinimal = util-linux.override { cryptsetupSupport = false; nlsSupport = false; ncursesSupport = false; pamSupport = false; shadowSupport = false; systemdSupport = false; translateManpages = false; };
So how come we need the non-minimal version?
I didn't check.
# Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \ ${cosmic-files}/share/icons/hicolor/24x24/apps/com.system76.CosmicFiles.svg
- ln -st $out/usr/bin \ - ${concatMapStringsSep " " (p: "${p}/bin/*") packages} \ - ${xdg-desktop-portal}/libexec/xdg-document-portal \ - ${xdg-desktop-portal-gtk}/libexec/xdg-desktop-portal-gtk + ln -sft "$out/usr/bin" \ + ${concatMapStringsSep " " (p: "${escapeShellArg p}/bin/*") packages} \ + ${escapeShellArg xdg-desktop-portal}/libexec/xdg-document-portal \ + ${escapeShellArg xdg-desktop-portal-gtk}/libexec/xdg-desktop-portal-gtk ln -st $out/usr/share/dbus-1 \ - ${dbus}/share/dbus-1/session.conf + ${escapeShellArg dbus}/share/dbus-1/session.conf ln -st $out/usr/share/dbus-1/services \ - ${xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service - - for pkg in ${escapeShellArgs usrPackages}; do - lndir -ignorelinks -silent "$pkg" "$out/usr" - done + ${escapeShellArg xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service
Unrelated changes. We don't need to escape Nix store paths, because we rely on Nixpkgs, which would break if store paths had weird characters in them.
Will drop in v2.
+ # clobber any conflicting files from busybox + ln -sft "$out/usr/bin" ${escapeShellArg util-linux}/bin/*
The approach we've taken so far is to disable those tools in Busybox, and avoid conflicting symlinks, and I like that better. Alternatively, if you want to figure out which Busybox tools are actually needed, we could switch to a minimal build and enable only what we use.
I don't want to block on either of those, and I'd rather not risk breaking the build whenever util-linux adds a new tool. It's pretty clear that util-linux is to be preferred over Busybox in the event of a conflict. This does add bloat, but there are far *more* sources of bloat right now, so I think that should be part of a more general debloating effort.
${concatStrings (mapAttrsToList (name: path: '' ln -s ${path} $out/usr/lib/spectrum/vm/${name} '') appvms)} - - # TODO: this is a hack and we should just build the util-linux - # programs we want. - # https://lore.kernel.org/util-linux/87zgrl6ufb.fsf@alyssa.is/ - ln -s ${util-linuxMinimal}/bin/{findfs,uuidgen,lsblk,mount} $out/usr/bin - - # TODO: this is another hack and it should be possible - # to build systemd without this. - ln -s -- ${lib.escapeShellArg systemd}/bin/udevadm "$out/usr/bin" ''; in
-- 2.51.2
-- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 10/29/25 07:36, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Busybox provides a broken fdisk that doesn't support GPT, only MBR. The systemd built against musl doesn't include systemd-pull, so systemd-sysupdate doesn't work. Therefore, use all of util-linux's command-line tools, and use systemd built against glibc.
That's a problem that's going to need to be fixed. We're not mixing two different libcs on the host.
Unfortunately, systemd-pull can't be built with musl libc. That leaves four options:
1. Try to fix the build problem, knowing that it could come back. 2. Mix two libcs on the host. 3. Try to fix the build (but this could take quite a while to upstream). 4. Use glibc on the host.
I don't think we should ship with option 2, but I think it might be acceptable until the underlying problem is solved.
Depends how hard it is to fix I suppose. What's the actual issue?
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 0d79f7ca54ccc86eb0fa6e743f2011237d365f24..00052222507077b9e94a5ed0a3fbddd27caeefc3 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -4,20 +4,20 @@
import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src -, pkgsMusl, pkgsStatic, linux_latest +, pkgsMusl, pkgsStatic, linux_latest, systemd }: pkgsStatic.callPackage (
{ busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils, execline , inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos , runCommand, s6, s6-linux-init, s6-rc, socat, spectrum-host-tools -, stdenvNoCC, util-linuxMinimal, virtiofsd, writeClosure +, stdenvNoCC, util-linux, virtiofsd, writeClosure
util-linuxMinimal = util-linux.override { cryptsetupSupport = false; nlsSupport = false; ncursesSupport = false; pamSupport = false; shadowSupport = false; systemdSupport = false; translateManpages = false; };
So how come we need the non-minimal version?
I didn't check.
Let's stick with the minimal one until there's a need, then.
+ # clobber any conflicting files from busybox + ln -sft "$out/usr/bin" ${escapeShellArg util-linux}/bin/*
The approach we've taken so far is to disable those tools in Busybox, and avoid conflicting symlinks, and I like that better. Alternatively, if you want to figure out which Busybox tools are actually needed, we could switch to a minimal build and enable only what we use.
I don't want to block on either of those, and I'd rather not risk breaking the build whenever util-linux adds a new tool. It's pretty clear that util-linux is to be preferred over Busybox in the event of a conflict.
This does add bloat, but there are far *more* sources of bloat right now, so I think that should be part of a more general debloating effort.
How would disabling the conflicting tools in Busybox like we've done so far block?
On 11/1/25 08:13, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 10/29/25 07:36, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Busybox provides a broken fdisk that doesn't support GPT, only MBR. The systemd built against musl doesn't include systemd-pull, so systemd-sysupdate doesn't work. Therefore, use all of util-linux's command-line tools, and use systemd built against glibc.
That's a problem that's going to need to be fixed. We're not mixing two different libcs on the host.
Unfortunately, systemd-pull can't be built with musl libc. That leaves four options:
1. Try to fix the build problem, knowing that it could come back. 2. Mix two libcs on the host. 3. Try to fix the build (but this could take quite a while to upstream). 4. Use glibc on the host.
I don't think we should ship with option 2, but I think it might be acceptable until the underlying problem is solved.
Depends how hard it is to fix I suppose. What's the actual issue?
The problem went away in your recent Nixpkgs update. It’s only a problem with the older Nixpkgs. I'll keep using glibc systemd for testing but will not include it in subsequent versions.
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 0d79f7ca54ccc86eb0fa6e743f2011237d365f24..00052222507077b9e94a5ed0a3fbddd27caeefc3 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -4,20 +4,20 @@
import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src -, pkgsMusl, pkgsStatic, linux_latest +, pkgsMusl, pkgsStatic, linux_latest, systemd }: pkgsStatic.callPackage (
{ busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils, execline , inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos , runCommand, s6, s6-linux-init, s6-rc, socat, spectrum-host-tools -, stdenvNoCC, util-linuxMinimal, virtiofsd, writeClosure +, stdenvNoCC, util-linux, virtiofsd, writeClosure
util-linuxMinimal = util-linux.override { cryptsetupSupport = false; nlsSupport = false; ncursesSupport = false; pamSupport = false; shadowSupport = false; systemdSupport = false; translateManpages = false; };
So how come we need the non-minimal version?
I didn't check.
Let's stick with the minimal one until there's a need, then.
Will fix in v2.
+ # clobber any conflicting files from busybox + ln -sft "$out/usr/bin" ${escapeShellArg util-linux}/bin/*
The approach we've taken so far is to disable those tools in Busybox, and avoid conflicting symlinks, and I like that better. Alternatively, if you want to figure out which Busybox tools are actually needed, we could switch to a minimal build and enable only what we use.
I don't want to block on either of those, and I'd rather not risk breaking the build whenever util-linux adds a new tool. It's pretty clear that util-linux is to be preferred over Busybox in the event of a conflict.
This does add bloat, but there are far *more* sources of bloat right now, so I think that should be part of a more general debloating effort.
How would disabling the conflicting tools in Busybox like we've done so far block? It wouldn't. I thought it would be too much work, but thankfully one can save the errors to a text file and run some vim commands to turn them into something one can paste into host/rootfs/default.nix. -- Sincerely, Demi Marie Obenour (she/her/hers)
This will be needed once the B partitions are added. Otherwise, tar2ext4's size limit is exceeded. The timeout is increased to account for the very slow compression process. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release/checks/integration/meson.build | 2 +- release/combined/eosimages.nix | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/release/checks/integration/meson.build b/release/checks/integration/meson.build index 7bf8f51e4c762d2279ed6064ae1a87cb9b07494c..eb2860c6871b1067891c07ff7f4ac634cb4af458 100644 --- a/release/checks/integration/meson.build +++ b/release/checks/integration/meson.build @@ -13,6 +13,6 @@ lib = static_library('spectrum-integration-test', 'lib.c') foreach test : ['appimage', 'late-serial', 'networking', 'portal'] test(test, executable(test, test + '.c', link_with : lib), - timeout : 400, + timeout : 800, args : [run_qemu]) endforeach diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..ba44d9cd82d55d491293ed36cc0402db8ebd3ffe 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -12,11 +12,15 @@ runCommand "eosimages.img" { unsafeDiscardReferences = { out = true; }; dontFixup = true; } '' + set -o pipefail mkdir dir cd dir - ln -s $image $imageName - sha256sum $imageName > $imageName.sha256 - tar -chf $NIX_BUILD_TOP/eosimages.tar * - tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out - e2label $out eosimages + ln -s -- "$image" "$imageName" + sha256sum -- "$imageName" > "$imageName.sha256" & + pid=$! + gzip -9 < "$image" > "$imageName.gz" + sha256sum -- "$imageName.gz" > "$imageName.gz.sha256" + wait "$pid" + tar -ch -- "$imageName.gz" "$imageName.gz.sha256" "$imageName.sha256" | tar2ext4 -o "$out" + e2label "$out" eosimages '') (_: {}) -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
This will be needed once the B partitions are added. Otherwise, tar2ext4's size limit is exceeded.
The timeout is increased to account for the very slow compression process.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release/checks/integration/meson.build | 2 +- release/combined/eosimages.nix | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-)
I haven't built this yet, so maybe I'm wrong somehow, but doesn't this break "Try Spectrum"? GRUB isn't going to be able to loopback mount a compressed image, I assume. That's why I keep asking what GNOME OS does. We currently produce an image that lets you install Spectrum, or try it out in a live image. Do they do that too? If so, how do they make it so that live image is bootable without being huge? Does their installer resize partitions, perhaps? (I reviewed the rest of the patch anyway, but I think we're going to need a different approach here.)
diff --git a/release/checks/integration/meson.build b/release/checks/integration/meson.build index 7bf8f51e4c762d2279ed6064ae1a87cb9b07494c..eb2860c6871b1067891c07ff7f4ac634cb4af458 100644 --- a/release/checks/integration/meson.build +++ b/release/checks/integration/meson.build @@ -13,6 +13,6 @@ lib = static_library('spectrum-integration-test', 'lib.c')
foreach test : ['appimage', 'late-serial', 'networking', 'portal'] test(test, executable(test, test + '.c', link_with : lib), - timeout : 400, + timeout : 800,
Did you measure this, and find that it was actually roughly twice as long? I'd like to keep the timeouts grounded in measurement, so this should be increased by an actual measured factor.
args : [run_qemu]) endforeach diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..ba44d9cd82d55d491293ed36cc0402db8ebd3ffe 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -12,11 +12,15 @@ runCommand "eosimages.img" { unsafeDiscardReferences = { out = true; }; dontFixup = true; } '' + set -o pipefail
This is set by stdenv.
mkdir dir cd dir - ln -s $image $imageName - sha256sum $imageName > $imageName.sha256 - tar -chf $NIX_BUILD_TOP/eosimages.tar * - tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out - e2label $out eosimages + ln -s -- "$image" "$imageName" + sha256sum -- "$imageName" > "$imageName.sha256" &
What's this used for? The eos-installer README indicates that it looks at the hash of the compressed image.
+ pid=$! + gzip -9 < "$image" > "$imageName.gz" + sha256sum -- "$imageName.gz" > "$imageName.gz.sha256" + wait "$pid" + tar -ch -- "$imageName.gz" "$imageName.gz.sha256" "$imageName.sha256" | tar2ext4 -o "$out" + e2label "$out" eosimages '') (_: {})
Please separate quoting/escaping changes to keep functional diffs easier to identify, and check if they're actually necessary. They never are for store paths because Nixpkgs assumes that.
Alyssa Ross <hi@alyssa.is> writes:
Demi Marie Obenour <demiobenour@gmail.com> writes:
This will be needed once the B partitions are added. Otherwise, tar2ext4's size limit is exceeded.
The timeout is increased to account for the very slow compression process.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release/checks/integration/meson.build | 2 +- release/combined/eosimages.nix | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-)
I haven't built this yet, so maybe I'm wrong somehow, but doesn't this break "Try Spectrum"? GRUB isn't going to be able to loopback mount a compressed image, I assume. That's why I keep asking what GNOME OS does. We currently produce an image that lets you install Spectrum, or try it out in a live image. Do they do that too? If so, how do they make it so that live image is bootable without being huge? Does their installer resize partitions, perhaps?
(I reviewed the rest of the patch anyway, but I think we're going to need a different approach here.)
Okay, I've finally got the answers I wanted about the GNOME OS installer on Matrix. It sounds like it doesn't copy a whole disk image like eos-installer does; rather it copies individual partition images using systemd-repart. This means they can distribute small partition images, and install them into partitions with room to grow, which would solve this problem. Reusing GNOME OS's installer sounds like it would be good then, but I don't know how much work it would be, and don't want to block this work on that, so I suggest we go ahead with uncompressed, small partitions for now — either sized to content or slightly bigger than content — and then later on we switch to GNOME OS's installer, and then increase the sizes of the installed partitions. Only at that point would we consider Spectrum installs "stable".
On 10/29/25 12:51, Alyssa Ross wrote:
Alyssa Ross <hi@alyssa.is> writes:
Demi Marie Obenour <demiobenour@gmail.com> writes:
This will be needed once the B partitions are added. Otherwise, tar2ext4's size limit is exceeded.
The timeout is increased to account for the very slow compression process.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release/checks/integration/meson.build | 2 +- release/combined/eosimages.nix | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-)
I haven't built this yet, so maybe I'm wrong somehow, but doesn't this break "Try Spectrum"? GRUB isn't going to be able to loopback mount a compressed image, I assume. That's why I keep asking what GNOME OS does. We currently produce an image that lets you install Spectrum, or try it out in a live image. Do they do that too? If so, how do they make it so that live image is bootable without being huge? Does their installer resize partitions, perhaps?
(I reviewed the rest of the patch anyway, but I think we're going to need a different approach here.)
Okay, I've finally got the answers I wanted about the GNOME OS installer on Matrix. It sounds like it doesn't copy a whole disk image like eos-installer does; rather it copies individual partition images using systemd-repart. This means they can distribute small partition images, and install them into partitions with room to grow, which would solve this problem.
Reusing GNOME OS's installer sounds like it would be good then, but I don't know how much work it would be, and don't want to block this work on that, so I suggest we go ahead with uncompressed, small partitions for now — either sized to content or slightly bigger than content — and then later on we switch to GNOME OS's installer, and then increase the sizes of the installed partitions. Only at that point would we consider Spectrum installs "stable".
I agree in the long term, but I found a short-term workaround: use erofs instead of ext4. That compresses the giant runs of zeros down to almost nothing, and its mkfs tool doesn't have the same file size limitations. The only difficulty is that if we should have dm-verity protection in the installer for ext4, we _really_ ought to have it for erofs. That's a separate change, though. -- Sincerely, Demi Marie Obenour (she/her/hers)
On 11/1/25 18:15, Demi Marie Obenour wrote:
On 10/29/25 12:51, Alyssa Ross wrote:
Alyssa Ross <hi@alyssa.is> writes:
Demi Marie Obenour <demiobenour@gmail.com> writes:
This will be needed once the B partitions are added. Otherwise, tar2ext4's size limit is exceeded.
The timeout is increased to account for the very slow compression process.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release/checks/integration/meson.build | 2 +- release/combined/eosimages.nix | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-)
I haven't built this yet, so maybe I'm wrong somehow, but doesn't this break "Try Spectrum"? GRUB isn't going to be able to loopback mount a compressed image, I assume. That's why I keep asking what GNOME OS does. We currently produce an image that lets you install Spectrum, or try it out in a live image. Do they do that too? If so, how do they make it so that live image is bootable without being huge? Does their installer resize partitions, perhaps?
(I reviewed the rest of the patch anyway, but I think we're going to need a different approach here.)
Okay, I've finally got the answers I wanted about the GNOME OS installer on Matrix. It sounds like it doesn't copy a whole disk image like eos-installer does; rather it copies individual partition images using systemd-repart. This means they can distribute small partition images, and install them into partitions with room to grow, which would solve this problem.
Reusing GNOME OS's installer sounds like it would be good then, but I don't know how much work it would be, and don't want to block this work on that, so I suggest we go ahead with uncompressed, small partitions for now — either sized to content or slightly bigger than content — and then later on we switch to GNOME OS's installer, and then increase the sizes of the installed partitions. Only at that point would we consider Spectrum installs "stable".
I agree in the long term, but I found a short-term workaround: use erofs instead of ext4. That compresses the giant runs of zeros down to almost nothing, and its mkfs tool doesn't have the same file size limitations. The only difficulty is that if we should have dm-verity protection in the installer for ext4, we _really_ ought to have it for erofs. That's a separate change, though.
Actually, that doesn't work either. The installer doesn't find the erofs image. I suspect this is a udisks bug but am not particularly interested in fixing it, especially as this installer is going to be replaced. Using small installation images also doesn't work. Even with very little room to grow, the image is too big for mkfs.ext4 and tar2ext4 to handle. Given this, I think the best option is to drop the live image for now. It can come back after switching to the GNOME OS installer. Having a live image isn't strictly necessary for users to use Spectrum, whereas an updater is. -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/1/25 18:15, Demi Marie Obenour wrote:
On 10/29/25 12:51, Alyssa Ross wrote:
Alyssa Ross <hi@alyssa.is> writes:
Demi Marie Obenour <demiobenour@gmail.com> writes:
This will be needed once the B partitions are added. Otherwise, tar2ext4's size limit is exceeded.
The timeout is increased to account for the very slow compression process.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release/checks/integration/meson.build | 2 +- release/combined/eosimages.nix | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-)
I haven't built this yet, so maybe I'm wrong somehow, but doesn't this break "Try Spectrum"? GRUB isn't going to be able to loopback mount a compressed image, I assume. That's why I keep asking what GNOME OS does. We currently produce an image that lets you install Spectrum, or try it out in a live image. Do they do that too? If so, how do they make it so that live image is bootable without being huge? Does their installer resize partitions, perhaps?
(I reviewed the rest of the patch anyway, but I think we're going to need a different approach here.)
Okay, I've finally got the answers I wanted about the GNOME OS installer on Matrix. It sounds like it doesn't copy a whole disk image like eos-installer does; rather it copies individual partition images using systemd-repart. This means they can distribute small partition images, and install them into partitions with room to grow, which would solve this problem.
Reusing GNOME OS's installer sounds like it would be good then, but I don't know how much work it would be, and don't want to block this work on that, so I suggest we go ahead with uncompressed, small partitions for now — either sized to content or slightly bigger than content — and then later on we switch to GNOME OS's installer, and then increase the sizes of the installed partitions. Only at that point would we consider Spectrum installs "stable".
I agree in the long term, but I found a short-term workaround: use erofs instead of ext4. That compresses the giant runs of zeros down to almost nothing, and its mkfs tool doesn't have the same file size limitations. The only difficulty is that if we should have dm-verity protection in the installer for ext4, we _really_ ought to have it for erofs. That's a separate change, though.
Actually, that doesn't work either. The installer doesn't find the erofs image. I suspect this is a udisks bug but am not particularly interested in fixing it, especially as this installer is going to be replaced.
Using small installation images also doesn't work. Even with very little room to grow, the image is too big for mkfs.ext4 and tar2ext4 to handle.
You mean that adding the B partitions makes the image too big?
Given this, I think the best option is to drop the live image for now. It can come back after switching to the GNOME OS installer. Having a live image isn't strictly necessary for users to use Spectrum, whereas an updater is.
Alyssa Ross <hi@alyssa.is> writes:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/1/25 18:15, Demi Marie Obenour wrote:
On 10/29/25 12:51, Alyssa Ross wrote:
Alyssa Ross <hi@alyssa.is> writes:
Demi Marie Obenour <demiobenour@gmail.com> writes:
This will be needed once the B partitions are added. Otherwise, tar2ext4's size limit is exceeded.
The timeout is increased to account for the very slow compression process.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release/checks/integration/meson.build | 2 +- release/combined/eosimages.nix | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-)
I haven't built this yet, so maybe I'm wrong somehow, but doesn't this break "Try Spectrum"? GRUB isn't going to be able to loopback mount a compressed image, I assume. That's why I keep asking what GNOME OS does. We currently produce an image that lets you install Spectrum, or try it out in a live image. Do they do that too? If so, how do they make it so that live image is bootable without being huge? Does their installer resize partitions, perhaps?
(I reviewed the rest of the patch anyway, but I think we're going to need a different approach here.)
Okay, I've finally got the answers I wanted about the GNOME OS installer on Matrix. It sounds like it doesn't copy a whole disk image like eos-installer does; rather it copies individual partition images using systemd-repart. This means they can distribute small partition images, and install them into partitions with room to grow, which would solve this problem.
Reusing GNOME OS's installer sounds like it would be good then, but I don't know how much work it would be, and don't want to block this work on that, so I suggest we go ahead with uncompressed, small partitions for now — either sized to content or slightly bigger than content — and then later on we switch to GNOME OS's installer, and then increase the sizes of the installed partitions. Only at that point would we consider Spectrum installs "stable".
I agree in the long term, but I found a short-term workaround: use erofs instead of ext4. That compresses the giant runs of zeros down to almost nothing, and its mkfs tool doesn't have the same file size limitations. The only difficulty is that if we should have dm-verity protection in the installer for ext4, we _really_ ought to have it for erofs. That's a separate change, though.
Actually, that doesn't work either. The installer doesn't find the erofs image. I suspect this is a udisks bug but am not particularly interested in fixing it, especially as this installer is going to be replaced.
Using small installation images also doesn't work. Even with very little room to grow, the image is too big for mkfs.ext4 and tar2ext4 to handle.
You mean that adding the B partitions makes the image too big?
Given this, I think the best option is to drop the live image for now. It can come back after switching to the GNOME OS installer. Having a live image isn't strictly necessary for users to use Spectrum, whereas an updater is.
FYI: I've just pushed an integration test for the "Try Spectrum" feature of the combined image, because I'd already written it yesterday before your message came in. If we do end up temporarily dropping the live image, we can just drop that test for now as well. (But I'd like to discuss a little further before deciding to do that, in case we can identify a solution.)
On 11/2/25 07:05, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/1/25 18:15, Demi Marie Obenour wrote:
On 10/29/25 12:51, Alyssa Ross wrote:
Alyssa Ross <hi@alyssa.is> writes:
Demi Marie Obenour <demiobenour@gmail.com> writes:
This will be needed once the B partitions are added. Otherwise, tar2ext4's size limit is exceeded.
The timeout is increased to account for the very slow compression process.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release/checks/integration/meson.build | 2 +- release/combined/eosimages.nix | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-)
I haven't built this yet, so maybe I'm wrong somehow, but doesn't this break "Try Spectrum"? GRUB isn't going to be able to loopback mount a compressed image, I assume. That's why I keep asking what GNOME OS does. We currently produce an image that lets you install Spectrum, or try it out in a live image. Do they do that too? If so, how do they make it so that live image is bootable without being huge? Does their installer resize partitions, perhaps?
(I reviewed the rest of the patch anyway, but I think we're going to need a different approach here.)
Okay, I've finally got the answers I wanted about the GNOME OS installer on Matrix. It sounds like it doesn't copy a whole disk image like eos-installer does; rather it copies individual partition images using systemd-repart. This means they can distribute small partition images, and install them into partitions with room to grow, which would solve this problem.
Reusing GNOME OS's installer sounds like it would be good then, but I don't know how much work it would be, and don't want to block this work on that, so I suggest we go ahead with uncompressed, small partitions for now — either sized to content or slightly bigger than content — and then later on we switch to GNOME OS's installer, and then increase the sizes of the installed partitions. Only at that point would we consider Spectrum installs "stable".
I agree in the long term, but I found a short-term workaround: use erofs instead of ext4. That compresses the giant runs of zeros down to almost nothing, and its mkfs tool doesn't have the same file size limitations. The only difficulty is that if we should have dm-verity protection in the installer for ext4, we _really_ ought to have it for erofs. That's a separate change, though.
Actually, that doesn't work either. The installer doesn't find the erofs image. I suspect this is a udisks bug but am not particularly interested in fixing it, especially as this installer is going to be replaced.
Using small installation images also doesn't work. Even with very little room to grow, the image is too big for mkfs.ext4 and tar2ext4 to handle.
You mean that adding the B partitions makes the image too big?
This is correct. I tried for a day to find workarounds but without success. -- Sincerely, Demi Marie Obenour (she/her/hers)
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either. Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. It also ensures that both SHA256SUMS and SHA256SUMS.gpg are present. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 6 +- lib/kcmdline-utils.mk | 6 ++ tools/default.nix | 1 + tools/meson.build | 1 + tools/updates-dir-check/meson.build | 4 ++ tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 00d125774bb7b98736d0928c69cb307740cee034..15752286f5924291768f0655a12b90c702730520 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -62,6 +62,9 @@ build/fifo: build/empty: mkdir -p $@ +build/etc: + mkdir -p $@ + # s6-rc-compile's input is a directory, but that doesn't play nice # with Make, because it won't know to update if some file in the # directory is changed, or a file is created or removed in a @@ -69,8 +72,7 @@ build/empty: # including files that aren't intended to be part of the input, like # temporary editor files or .license files. So for all these reasons, # only explicitly listed files are made available to s6-rc-compile. -build/etc/s6-rc: $(S6_RC_FILES) file-list.mk - mkdir -p $$(dirname $@) +build/etc/s6-rc: $(S6_RC_FILES) file-list.mk build/etc rm -rf $@ set -uo pipefail && dir=$$(mktemp -d) && \ { tar -c $(S6_RC_FILES) | tar -C $$dir -x --strip-components 3; } && \ diff --git a/lib/kcmdline-utils.mk b/lib/kcmdline-utils.mk new file mode 100644 index 0000000000000000000000000000000000000000..fa228552e583f15fc77a746985060ad5d04cdf2c --- /dev/null +++ b/lib/kcmdline-utils.mk @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +READ_ROOTHASH = { \ + set -eufo pipefail; \ + read -r version < ../../version; \ + LC_ALL=C expr "x$$version" : '^\(x0\|x[1-9][0-9]*\)\(\.\(0\|[1-9][0-9]*\)\)\{2\}$$' >/dev/null; } diff --git a/tools/default.nix b/tools/default.nix index ca165b5ec8ae1a63b75af4a34f33e320b262ba7b..e644f4e710e56f32de27ea10047cba3cffd0ecdf 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -78,6 +78,7 @@ stdenv.mkDerivation (finalAttrs: { ./start-vmm ./subprojects ./sd-notify-adapter + ./updates-dir-check ] ++ lib.optionals driverSupport [ ./xdp-forwarder ])); diff --git a/tools/meson.build b/tools/meson.build index 5d0ae81042fd3d77646594500f32cb1d48a6af0c..7da3bb451a5f1a797bc7e50a67c44dbd37ba60bf 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -28,6 +28,7 @@ if get_option('host') subdir('lsvm') subdir('start-vmm') subdir('sd-notify-adapter') + subdir('updates-dir-check') endif if get_option('app') diff --git a/tools/updates-dir-check/meson.build b/tools/updates-dir-check/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..e19691d0e35f8a051e897990f0376384b3625c1a --- /dev/null +++ b/tools/updates-dir-check/meson.build @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +executable('updates-dir-check', 'updates-dir-check.c', install: true, c_args: ['-D_GNU_SOURCE=1', '-UNDEBUG', '-Wno-error=pedantic']) diff --git a/tools/updates-dir-check/updates-dir-check.c b/tools/updates-dir-check/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..94c7d54bec38d6efbd5b8aca257f3ec4ad3fae35 --- /dev/null +++ b/tools/updates-dir-check/updates-dir-check.c @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <string.h> + +#include <sysexits.h> +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> + +#include <linux/openat2.h> +#include <sys/syscall.h> +#include <unistd.h> + +#include <err.h> + +static void checkdir(int fd) +{ + DIR *d = fdopendir(fd); + if (d == NULL) + err(1, "fdopendir"); + bool found_sha256sums = false; + bool found_sha256sums_gpg = false; + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(1, "readdir"); + break; + } + assert(entry->d_reclen > offsetof(struct dirent, d_name)); + size_t len = strnlen(entry->d_name, entry->d_reclen - offsetof(struct dirent, d_name)); + assert(len < entry->d_reclen - offsetof(struct dirent, d_name)); + assert(len > 0); + if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + if (strcmp(entry->d_name, "SHA256SUMS") == 0) { + found_sha256sums = true; + continue; + } + if (strcmp(entry->d_name, "SHA256SUMS.gpg") == 0) { + found_sha256sums_gpg = true; + continue; + } + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(1, "Filename must begin with an ASCII letter"); + for (size_t i = 1; i < len; ++i) { + c = (unsigned char)entry->d_name[i]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) { + if (c >= 0x20 && c <= 0x7E) + errx(1, "Forbidden subsequent character in filename: '%c'", (int)c); + else + errx(1, "Forbidden subsequent character in filename: byte %d", (int)c); + } + } + if (entry->d_name[len - 1] == '.') + errx(1, "Filename must not end with a '.'"); + if (entry->d_type != DT_REG) + errx(1, "Entry contains non-regular file %s", entry->d_name); + } + if (!found_sha256sums) + errx(1, "SHA256SUMS not found"); + if (!found_sha256sums_gpg) + errx(1, "SHA256SUMS.gpg not found"); + closedir(d); +} + +int main(int argc, char **argv) +{ + for (int i = 1; i < argc; ++i) { + // Avoid symlink attacks. + struct open_how how = { + .flags = O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_NOFOLLOW, + .resolve = RESOLVE_NO_SYMLINKS|RESOLVE_NO_MAGICLINKS, + }; + int fd = (int)syscall((long)SYS_openat2, (long)AT_FDCWD, (long)argv[i], + (long)&how, (long)sizeof(how)); + if (fd < 0) + err(1, "open(%s)", argv[i]); + checkdir(fd); + } + return 0; +} -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either.
Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. It also ensures that both SHA256SUMS and SHA256SUMS.gpg are present.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 6 +- lib/kcmdline-utils.mk | 6 ++ tools/default.nix | 1 + tools/meson.build | 1 + tools/updates-dir-check/meson.build | 4 ++ tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-)
I still don't really understand why this needs to be a C program instead of find -H /path/to/dir -not -type f. None of the other checks seem very necessary?
diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 00d125774bb7b98736d0928c69cb307740cee034..15752286f5924291768f0655a12b90c702730520 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -62,6 +62,9 @@ build/fifo: build/empty: mkdir -p $@
+build/etc: + mkdir -p $@ + # s6-rc-compile's input is a directory, but that doesn't play nice # with Make, because it won't know to update if some file in the # directory is changed, or a file is created or removed in a @@ -69,8 +72,7 @@ build/empty: # including files that aren't intended to be part of the input, like # temporary editor files or .license files. So for all these reasons, # only explicitly listed files are made available to s6-rc-compile. -build/etc/s6-rc: $(S6_RC_FILES) file-list.mk - mkdir -p $$(dirname $@) +build/etc/s6-rc: $(S6_RC_FILES) file-list.mk build/etc rm -rf $@ set -uo pipefail && dir=$$(mktemp -d) && \ { tar -c $(S6_RC_FILES) | tar -C $$dir -x --strip-components 3; } && \ diff --git a/lib/kcmdline-utils.mk b/lib/kcmdline-utils.mk new file mode 100644 index 0000000000000000000000000000000000000000..fa228552e583f15fc77a746985060ad5d04cdf2c --- /dev/null +++ b/lib/kcmdline-utils.mk @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +READ_ROOTHASH = { \ + set -eufo pipefail; \ + read -r version < ../../version; \ + LC_ALL=C expr "x$$version" : '^\(x0\|x[1-9][0-9]*\)\(\.\(0\|[1-9][0-9]*\)\)\{2\}$$' >/dev/null; }
None of these changes seem to have anything to do with this patch. Did they end up in here by mistake?
diff --git a/tools/default.nix b/tools/default.nix index ca165b5ec8ae1a63b75af4a34f33e320b262ba7b..e644f4e710e56f32de27ea10047cba3cffd0ecdf 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -78,6 +78,7 @@ stdenv.mkDerivation (finalAttrs: { ./start-vmm ./subprojects ./sd-notify-adapter + ./updates-dir-check ] ++ lib.optionals driverSupport [ ./xdp-forwarder ])); diff --git a/tools/meson.build b/tools/meson.build index 5d0ae81042fd3d77646594500f32cb1d48a6af0c..7da3bb451a5f1a797bc7e50a67c44dbd37ba60bf 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -28,6 +28,7 @@ if get_option('host') subdir('lsvm') subdir('start-vmm') subdir('sd-notify-adapter') + subdir('updates-dir-check') endif
if get_option('app') diff --git a/tools/updates-dir-check/meson.build b/tools/updates-dir-check/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..e19691d0e35f8a051e897990f0376384b3625c1a --- /dev/null +++ b/tools/updates-dir-check/meson.build @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +executable('updates-dir-check', 'updates-dir-check.c', install: true, c_args: ['-D_GNU_SOURCE=1', '-UNDEBUG', '-Wno-error=pedantic'])
How are -Werror=pedantic and -DNDEBUG getting enabled in the first place?
diff --git a/tools/updates-dir-check/updates-dir-check.c b/tools/updates-dir-check/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..94c7d54bec38d6efbd5b8aca257f3ec4ad3fae35 --- /dev/null +++ b/tools/updates-dir-check/updates-dir-check.c @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <string.h> + +#include <sysexits.h> +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> + +#include <linux/openat2.h> +#include <sys/syscall.h> +#include <unistd.h> + +#include <err.h> + +static void checkdir(int fd) +{ + DIR *d = fdopendir(fd); + if (d == NULL) + err(1, "fdopendir");
Usually we use EXIT_FAILURE.
+ bool found_sha256sums = false; + bool found_sha256sums_gpg = false; + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(1, "readdir"); + break; + } + assert(entry->d_reclen > offsetof(struct dirent, d_name)); + size_t len = strnlen(entry->d_name, entry->d_reclen - offsetof(struct dirent, d_name)); + assert(len < entry->d_reclen - offsetof(struct dirent, d_name)); + assert(len > 0);
We do not need to second guess the kernel/libc.
+ if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + if (strcmp(entry->d_name, "SHA256SUMS") == 0) { + found_sha256sums = true; + continue; + } + if (strcmp(entry->d_name, "SHA256SUMS.gpg") == 0) { + found_sha256sums_gpg = true; + continue; + } + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(1, "Filename must begin with an ASCII letter"); + for (size_t i = 1; i < len; ++i) { + c = (unsigned char)entry->d_name[i]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) { + if (c >= 0x20 && c <= 0x7E) + errx(1, "Forbidden subsequent character in filename: '%c'", (int)c); + else + errx(1, "Forbidden subsequent character in filename: byte %d", (int)c); + } + }
Why do we care? Surely we don't expect systemd-sysupdate to put filenames unescaped into a shell or something.
+ if (entry->d_name[len - 1] == '.') + errx(1, "Filename must not end with a '.'"); + if (entry->d_type != DT_REG) + errx(1, "Entry contains non-regular file %s", entry->d_name); + } + if (!found_sha256sums) + errx(1, "SHA256SUMS not found"); + if (!found_sha256sums_gpg) + errx(1, "SHA256SUMS.gpg not found"); + closedir(d); +} + +int main(int argc, char **argv) +{ + for (int i = 1; i < argc; ++i) { + // Avoid symlink attacks. + struct open_how how = { + .flags = O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_NOFOLLOW, + .resolve = RESOLVE_NO_SYMLINKS|RESOLVE_NO_MAGICLINKS, + };
For opening files given on the command line, wouldn't we want to use the mount's symlink behaviour? The VM presumably can't replace the root directory shared with it with a symlink.
+ int fd = (int)syscall((long)SYS_openat2, (long)AT_FDCWD, (long)argv[i], + (long)&how, (long)sizeof(how)); + if (fd < 0) + err(1, "open(%s)", argv[i]); + checkdir(fd); + } + return 0; +}
On 10/29/25 08:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either.
Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. It also ensures that both SHA256SUMS and SHA256SUMS.gpg are present.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 6 +- lib/kcmdline-utils.mk | 6 ++ tools/default.nix | 1 + tools/meson.build | 1 + tools/updates-dir-check/meson.build | 4 ++ tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-)
I still don't really understand why this needs to be a C program instead of find -H /path/to/dir -not -type f. None of the other checks seem very necessary?
I trust this code more than I trust (especially) the Busybox implementation of find.
diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 00d125774bb7b98736d0928c69cb307740cee034..15752286f5924291768f0655a12b90c702730520 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -62,6 +62,9 @@ build/fifo: build/empty: mkdir -p $@
+build/etc: + mkdir -p $@ + # s6-rc-compile's input is a directory, but that doesn't play nice # with Make, because it won't know to update if some file in the # directory is changed, or a file is created or removed in a @@ -69,8 +72,7 @@ build/empty: # including files that aren't intended to be part of the input, like # temporary editor files or .license files. So for all these reasons, # only explicitly listed files are made available to s6-rc-compile. -build/etc/s6-rc: $(S6_RC_FILES) file-list.mk - mkdir -p $$(dirname $@) +build/etc/s6-rc: $(S6_RC_FILES) file-list.mk build/etc rm -rf $@ set -uo pipefail && dir=$$(mktemp -d) && \ { tar -c $(S6_RC_FILES) | tar -C $$dir -x --strip-components 3; } && \ diff --git a/lib/kcmdline-utils.mk b/lib/kcmdline-utils.mk new file mode 100644 index 0000000000000000000000000000000000000000..fa228552e583f15fc77a746985060ad5d04cdf2c --- /dev/null +++ b/lib/kcmdline-utils.mk @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +READ_ROOTHASH = { \ + set -eufo pipefail; \ + read -r version < ../../version; \ + LC_ALL=C expr "x$$version" : '^\(x0\|x[1-9][0-9]*\)\(\.\(0\|[1-9][0-9]*\)\)\{2\}$$' >/dev/null; }
None of these changes seem to have anything to do with this patch. Did they end up in here by mistake?
Correct.
diff --git a/tools/default.nix b/tools/default.nix index ca165b5ec8ae1a63b75af4a34f33e320b262ba7b..e644f4e710e56f32de27ea10047cba3cffd0ecdf 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -78,6 +78,7 @@ stdenv.mkDerivation (finalAttrs: { ./start-vmm ./subprojects ./sd-notify-adapter + ./updates-dir-check ] ++ lib.optionals driverSupport [ ./xdp-forwarder ])); diff --git a/tools/meson.build b/tools/meson.build index 5d0ae81042fd3d77646594500f32cb1d48a6af0c..7da3bb451a5f1a797bc7e50a67c44dbd37ba60bf 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -28,6 +28,7 @@ if get_option('host') subdir('lsvm') subdir('start-vmm') subdir('sd-notify-adapter') + subdir('updates-dir-check') endif
if get_option('app') diff --git a/tools/updates-dir-check/meson.build b/tools/updates-dir-check/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..e19691d0e35f8a051e897990f0376384b3625c1a --- /dev/null +++ b/tools/updates-dir-check/meson.build @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +executable('updates-dir-check', 'updates-dir-check.c', install: true, c_args: ['-D_GNU_SOURCE=1', '-UNDEBUG', '-Wno-error=pedantic'])
How are -Werror=pedantic and -DNDEBUG getting enabled in the first place?
I believe Meson sets -DNDEBUG in some cases.
diff --git a/tools/updates-dir-check/updates-dir-check.c b/tools/updates-dir-check/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..94c7d54bec38d6efbd5b8aca257f3ec4ad3fae35 --- /dev/null +++ b/tools/updates-dir-check/updates-dir-check.c @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <string.h> + +#include <sysexits.h> +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> + +#include <linux/openat2.h> +#include <sys/syscall.h> +#include <unistd.h> + +#include <err.h> + +static void checkdir(int fd) +{ + DIR *d = fdopendir(fd); + if (d == NULL) + err(1, "fdopendir");
Usually we use EXIT_FAILURE.
Will fix in v2.
+ bool found_sha256sums = false; + bool found_sha256sums_gpg = false; + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(1, "readdir"); + break; + } + assert(entry->d_reclen > offsetof(struct dirent, d_name)); + size_t len = strnlen(entry->d_name, entry->d_reclen - offsetof(struct dirent, d_name)); + assert(len < entry->d_reclen - offsetof(struct dirent, d_name)); + assert(len > 0);
Will fix in v2.
+ if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + if (strcmp(entry->d_name, "SHA256SUMS") == 0) { + found_sha256sums = true; + continue; + } + if (strcmp(entry->d_name, "SHA256SUMS.gpg") == 0) { + found_sha256sums_gpg = true; + continue; + } + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(1, "Filename must begin with an ASCII letter"); + for (size_t i = 1; i < len; ++i) { + c = (unsigned char)entry->d_name[i]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) { + if (c >= 0x20 && c <= 0x7E) + errx(1, "Forbidden subsequent character in filename: '%c'", (int)c); + else + errx(1, "Forbidden subsequent character in filename: byte %d", (int)c); + } + }
Why do we care? Surely we don't expect systemd-sysupdate to put filenames unescaped into a shell or something.
Prevent escape sequence injection into terminals and logs is the main reason. Qubes OS has similar checks in some places, though they are off by default for file copying.
+ if (entry->d_name[len - 1] == '.') + errx(1, "Filename must not end with a '.'"); + if (entry->d_type != DT_REG) + errx(1, "Entry contains non-regular file %s", entry->d_name); + } + if (!found_sha256sums) + errx(1, "SHA256SUMS not found"); + if (!found_sha256sums_gpg) + errx(1, "SHA256SUMS.gpg not found"); + closedir(d); +} + +int main(int argc, char **argv) +{ + for (int i = 1; i < argc; ++i) { + // Avoid symlink attacks. + struct open_how how = { + .flags = O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_NOFOLLOW, + .resolve = RESOLVE_NO_SYMLINKS|RESOLVE_NO_MAGICLINKS, + };
For opening files given on the command line, wouldn't we want to use the mount's symlink behaviour? The VM presumably can't replace the root directory shared with it with a symlink.
Right now, the directory shared is actually the parent of the directory with the updates. This is because of a limitation in the updated start-vm script. As you point out later, that's not the best approach. Much better to bind-mount only the directories needed.
+ int fd = (int)syscall((long)SYS_openat2, (long)AT_FDCWD, (long)argv[i], + (long)&how, (long)sizeof(how)); + if (fd < 0) + err(1, "open(%s)", argv[i]); + checkdir(fd); + } + return 0; +}
-- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 10/29/25 08:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either.
Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. It also ensures that both SHA256SUMS and SHA256SUMS.gpg are present.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 6 +- lib/kcmdline-utils.mk | 6 ++ tools/default.nix | 1 + tools/meson.build | 1 + tools/updates-dir-check/meson.build | 4 ++ tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-)
I still don't really understand why this needs to be a C program instead of find -H /path/to/dir -not -type f. None of the other checks seem very necessary?
I trust this code more than I trust (especially) the Busybox implementation of find.
This doesn't really make sense to me. All of this is quite trivial find behaviour — not the sort of thing that's unlikely to have been widely tested. No objection to GNU find though if it helps.
How are -Werror=pedantic and -DNDEBUG getting enabled in the first place?
I believe Meson sets -DNDEBUG in some cases.
Yes, if the user explicitly asks for it.
+ if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + if (strcmp(entry->d_name, "SHA256SUMS") == 0) { + found_sha256sums = true; + continue; + } + if (strcmp(entry->d_name, "SHA256SUMS.gpg") == 0) { + found_sha256sums_gpg = true; + continue; + } + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(1, "Filename must begin with an ASCII letter"); + for (size_t i = 1; i < len; ++i) { + c = (unsigned char)entry->d_name[i]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) { + if (c >= 0x20 && c <= 0x7E) + errx(1, "Forbidden subsequent character in filename: '%c'", (int)c); + else + errx(1, "Forbidden subsequent character in filename: byte %d", (int)c); + } + }
Why do we care? Surely we don't expect systemd-sysupdate to put filenames unescaped into a shell or something.
Prevent escape sequence injection into terminals and logs is the main reason. Qubes OS has similar checks in some places, though they are off by default for file copying.
Doing this in a tool that's only used by sysupdate is a very ad-hoc way to protect against that. I think if we want to protect against that sort of thing it should be done in one place, probably in virtiofsd.
Alyssa Ross <hi@alyssa.is> writes:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 10/29/25 08:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either.
Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. It also ensures that both SHA256SUMS and SHA256SUMS.gpg are present.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 6 +- lib/kcmdline-utils.mk | 6 ++ tools/default.nix | 1 + tools/meson.build | 1 + tools/updates-dir-check/meson.build | 4 ++ tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-)
I still don't really understand why this needs to be a C program instead of find -H /path/to/dir -not -type f. None of the other checks seem very necessary?
I trust this code more than I trust (especially) the Busybox implementation of find.
This doesn't really make sense to me. All of this is quite trivial find behaviour — not the sort of thing that's unlikely to have been widely tested. No objection to GNU find though if it helps.
(Or even uutils find?)
On 11/1/25 08:17, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 10/29/25 08:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either.
Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. It also ensures that both SHA256SUMS and SHA256SUMS.gpg are present.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 6 +- lib/kcmdline-utils.mk | 6 ++ tools/default.nix | 1 + tools/meson.build | 1 + tools/updates-dir-check/meson.build | 4 ++ tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-)
I still don't really understand why this needs to be a C program instead of find -H /path/to/dir -not -type f. None of the other checks seem very necessary?
I trust this code more than I trust (especially) the Busybox implementation of find.
This doesn't really make sense to me. All of this is quite trivial find behaviour — not the sort of thing that's unlikely to have been widely tested. No objection to GNU find though if it helps.
I see: find with a -exec false to return an error if anything matching is found? I'm way more familiar with C than with find, which is why I missed this.
How are -Werror=pedantic and -DNDEBUG getting enabled in the first place?
I believe Meson sets -DNDEBUG in some cases.
Yes, if the user explicitly asks for it.
I thought it was default for release builds.
+ if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + if (strcmp(entry->d_name, "SHA256SUMS") == 0) { + found_sha256sums = true; + continue; + } + if (strcmp(entry->d_name, "SHA256SUMS.gpg") == 0) { + found_sha256sums_gpg = true; + continue; + } + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(1, "Filename must begin with an ASCII letter"); + for (size_t i = 1; i < len; ++i) { + c = (unsigned char)entry->d_name[i]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) { + if (c >= 0x20 && c <= 0x7E) + errx(1, "Forbidden subsequent character in filename: '%c'", (int)c); + else + errx(1, "Forbidden subsequent character in filename: byte %d", (int)c); + } + }
Why do we care? Surely we don't expect systemd-sysupdate to put filenames unescaped into a shell or something.
Prevent escape sequence injection into terminals and logs is the main reason. Qubes OS has similar checks in some places, though they are off by default for file copying.
Doing this in a tool that's only used by sysupdate is a very ad-hoc way to protect against that. I think if we want to protect against that sort of thing it should be done in one place, probably in virtiofsd.
I think sysupdate is more likely to log unsanitized data, especially as systemd-journald has no problems with it. -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/1/25 08:17, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 10/29/25 08:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either.
Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. It also ensures that both SHA256SUMS and SHA256SUMS.gpg are present.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 6 +- lib/kcmdline-utils.mk | 6 ++ tools/default.nix | 1 + tools/meson.build | 1 + tools/updates-dir-check/meson.build | 4 ++ tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-)
I still don't really understand why this needs to be a C program instead of find -H /path/to/dir -not -type f. None of the other checks seem very necessary?
I trust this code more than I trust (especially) the Busybox implementation of find.
This doesn't really make sense to me. All of this is quite trivial find behaviour — not the sort of thing that's unlikely to have been widely tested. No objection to GNU find though if it helps.
I see: find with a -exec false to return an error if anything matching is found?
I'm way more familiar with C than with find, which is why I missed this.
Hmm, thinking about it some more I suppose there's a problem with find: there's no way to get it to exit as soon as it finds a matching file, with a failing error code, so it could end up running way too long. So the C program is fine, I guess.
How are -Werror=pedantic and -DNDEBUG getting enabled in the first place?
I believe Meson sets -DNDEBUG in some cases.
Yes, if the user explicitly asks for it.
I thought it was default for release builds.
Doesn't look like it: https://github.com/mesonbuild/meson/blob/d00f840c573103c2d51aed2b169386f7acf... b_ndebug defaults to false.
+ if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + if (strcmp(entry->d_name, "SHA256SUMS") == 0) { + found_sha256sums = true; + continue; + } + if (strcmp(entry->d_name, "SHA256SUMS.gpg") == 0) { + found_sha256sums_gpg = true; + continue; + } + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(1, "Filename must begin with an ASCII letter"); + for (size_t i = 1; i < len; ++i) { + c = (unsigned char)entry->d_name[i]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) { + if (c >= 0x20 && c <= 0x7E) + errx(1, "Forbidden subsequent character in filename: '%c'", (int)c); + else + errx(1, "Forbidden subsequent character in filename: byte %d", (int)c); + } + }
Why do we care? Surely we don't expect systemd-sysupdate to put filenames unescaped into a shell or something.
Prevent escape sequence injection into terminals and logs is the main reason. Qubes OS has similar checks in some places, though they are off by default for file copying.
Doing this in a tool that's only used by sysupdate is a very ad-hoc way to protect against that. I think if we want to protect against that sort of thing it should be done in one place, probably in virtiofsd.
I think sysupdate is more likely to log unsanitized data, especially as systemd-journald has no problems with it.
What's the difference between systemd-journald's behaviour and the logging we have?
On Sun, Nov 02, 2025 at 01:18:02PM +0100, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/1/25 08:17, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 10/29/25 08:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either.
Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. It also ensures that both SHA256SUMS and SHA256SUMS.gpg are present.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 6 +- lib/kcmdline-utils.mk | 6 ++ tools/default.nix | 1 + tools/meson.build | 1 + tools/updates-dir-check/meson.build | 4 ++ tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-)
I still don't really understand why this needs to be a C program instead of find -H /path/to/dir -not -type f. None of the other checks seem very necessary?
I trust this code more than I trust (especially) the Busybox implementation of find.
This doesn't really make sense to me. All of this is quite trivial find behaviour — not the sort of thing that's unlikely to have been widely tested. No objection to GNU find though if it helps.
I see: find with a -exec false to return an error if anything matching is found?
I'm way more familiar with C than with find, which is why I missed this.
Hmm, thinking about it some more I suppose there's a problem with find: there's no way to get it to exit as soon as it finds a matching file, with a failing error code, so it could end up running way too long.
So the C program is fine, I guess.
Actually, we can do it. We just need to make find not responsible for exiting. foreground { pipeline { find -H /path/to/dir -mindepth 1 -not -type f -prune } grep -q . } importas -iu ? ? if { test $? -eq 1 } # We have only regular files. When find prints a line, grep will exit, and find will receive SIGPIPE and exit.
On 11/2/25 07:43, Alyssa Ross wrote:
On Sun, Nov 02, 2025 at 01:18:02PM +0100, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/1/25 08:17, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 10/29/25 08:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
> Spectrum OS's host has no network access. Updates must be downloaded by > VMs. The downloads are placed into a bind-mounted directory. The VM > can write whatever it wants into that directory. This includes symlinks > that subsequent code might open, which would create a path traversal > vulnerability. It also includes paths with names containing containing > terminal escape sequences, newlines, or other nastiness. Furthermore, > the directory should not have any subdirectories either. > > Add a simple C program that checks for such ugliness and indicates > (via its exit code) if the VM misbehaved. It also ensures that both > SHA256SUMS and SHA256SUMS.gpg are present. > > Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> > --- > host/rootfs/Makefile | 6 +- > lib/kcmdline-utils.mk | 6 ++ > tools/default.nix | 1 + > tools/meson.build | 1 + > tools/updates-dir-check/meson.build | 4 ++ > tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ > 6 files changed, 110 insertions(+), 2 deletions(-)
I still don't really understand why this needs to be a C program instead of find -H /path/to/dir -not -type f. None of the other checks seem very necessary?
I trust this code more than I trust (especially) the Busybox implementation of find.
This doesn't really make sense to me. All of this is quite trivial find behaviour — not the sort of thing that's unlikely to have been widely tested. No objection to GNU find though if it helps.
I see: find with a -exec false to return an error if anything matching is found?
I'm way more familiar with C than with find, which is why I missed this.
Hmm, thinking about it some more I suppose there's a problem with find: there's no way to get it to exit as soon as it finds a matching file, with a failing error code, so it could end up running way too long.
So the C program is fine, I guess.
Actually, we can do it. We just need to make find not responsible for exiting.
foreground { pipeline { find -H /path/to/dir -mindepth 1 -not -type f -prune } grep -q . } importas -iu ? ? if { test $? -eq 1 } # We have only regular files.
When find prints a line, grep will exit, and find will receive SIGPIPE and exit.
This version also has a bug: if find exits with an error without printing anything, the exit status will be ignored. Something like this (not tested) might work: pipeline { find -H /path/to/dir -mindepth 1 -not -type f -prune } importas -iu ! ! foreground { grep -q . } if { importas -iu ? ? test $? -eq 1 } wait $! importas -iu ? ? if { test $? -eq 0 } However, it's all way way way way too subtle for me. It's short, but it's also extremely error-prone. The C program is longer, but it's also much easier to understand and modify. -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/2/25 07:43, Alyssa Ross wrote:
On Sun, Nov 02, 2025 at 01:18:02PM +0100, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/1/25 08:17, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 10/29/25 08:01, Alyssa Ross wrote: > Demi Marie Obenour <demiobenour@gmail.com> writes: > >> Spectrum OS's host has no network access. Updates must be downloaded by >> VMs. The downloads are placed into a bind-mounted directory. The VM >> can write whatever it wants into that directory. This includes symlinks >> that subsequent code might open, which would create a path traversal >> vulnerability. It also includes paths with names containing containing >> terminal escape sequences, newlines, or other nastiness. Furthermore, >> the directory should not have any subdirectories either. >> >> Add a simple C program that checks for such ugliness and indicates >> (via its exit code) if the VM misbehaved. It also ensures that both >> SHA256SUMS and SHA256SUMS.gpg are present. >> >> Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> >> --- >> host/rootfs/Makefile | 6 +- >> lib/kcmdline-utils.mk | 6 ++ >> tools/default.nix | 1 + >> tools/meson.build | 1 + >> tools/updates-dir-check/meson.build | 4 ++ >> tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ >> 6 files changed, 110 insertions(+), 2 deletions(-) > > I still don't really understand why this needs to be a C program instead > of find -H /path/to/dir -not -type f. None of the other checks seem > very necessary?
I trust this code more than I trust (especially) the Busybox implementation of find.
This doesn't really make sense to me. All of this is quite trivial find behaviour — not the sort of thing that's unlikely to have been widely tested. No objection to GNU find though if it helps.
I see: find with a -exec false to return an error if anything matching is found?
I'm way more familiar with C than with find, which is why I missed this.
Hmm, thinking about it some more I suppose there's a problem with find: there's no way to get it to exit as soon as it finds a matching file, with a failing error code, so it could end up running way too long.
So the C program is fine, I guess.
Actually, we can do it. We just need to make find not responsible for exiting.
foreground { pipeline { find -H /path/to/dir -mindepth 1 -not -type f -prune } grep -q . } importas -iu ? ? if { test $? -eq 1 } # We have only regular files.
When find prints a line, grep will exit, and find will receive SIGPIPE and exit.
This version also has a bug: if find exits with an error without printing anything, the exit status will be ignored. Something like this (not tested) might work:
pipeline { find -H /path/to/dir -mindepth 1 -not -type f -prune } importas -iu ! ! foreground { grep -q . } if { importas -iu ? ? test $? -eq 1 } wait $! importas -iu ? ? if { test $? -eq 0 }
However, it's all way way way way too subtle for me. It's short, but it's also extremely error-prone. The C program is longer, but it's also much easier to understand and modify.
Okay. :)
On 11/2/25 07:18, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/1/25 08:17, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 10/29/25 08:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either.
Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. It also ensures that both SHA256SUMS and SHA256SUMS.gpg are present.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 6 +- lib/kcmdline-utils.mk | 6 ++ tools/default.nix | 1 + tools/meson.build | 1 + tools/updates-dir-check/meson.build | 4 ++ tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-)
I still don't really understand why this needs to be a C program instead of find -H /path/to/dir -not -type f. None of the other checks seem very necessary?
I trust this code more than I trust (especially) the Busybox implementation of find.
This doesn't really make sense to me. All of this is quite trivial find behaviour — not the sort of thing that's unlikely to have been widely tested. No objection to GNU find though if it helps.
I see: find with a -exec false to return an error if anything matching is found?
I'm way more familiar with C than with find, which is why I missed this.
Hmm, thinking about it some more I suppose there's a problem with find: there's no way to get it to exit as soon as it finds a matching file, with a failing error code, so it could end up running way too long.
So the C program is fine, I guess.
How are -Werror=pedantic and -DNDEBUG getting enabled in the first place?
I believe Meson sets -DNDEBUG in some cases.
Yes, if the user explicitly asks for it.
I thought it was default for release builds.
Doesn't look like it:
https://github.com/mesonbuild/meson/blob/d00f840c573103c2d51aed2b169386f7acf...
b_ndebug defaults to false.
Got it, thanks!
+ if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + if (strcmp(entry->d_name, "SHA256SUMS") == 0) { + found_sha256sums = true; + continue; + } + if (strcmp(entry->d_name, "SHA256SUMS.gpg") == 0) { + found_sha256sums_gpg = true; + continue; + } + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(1, "Filename must begin with an ASCII letter"); + for (size_t i = 1; i < len; ++i) { + c = (unsigned char)entry->d_name[i]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) { + if (c >= 0x20 && c <= 0x7E) + errx(1, "Forbidden subsequent character in filename: '%c'", (int)c); + else + errx(1, "Forbidden subsequent character in filename: byte %d", (int)c); + } + }
Why do we care? Surely we don't expect systemd-sysupdate to put filenames unescaped into a shell or something.
Prevent escape sequence injection into terminals and logs is the main reason. Qubes OS has similar checks in some places, though they are off by default for file copying.
Doing this in a tool that's only used by sysupdate is a very ad-hoc way to protect against that. I think if we want to protect against that sort of thing it should be done in one place, probably in virtiofsd.
I think sysupdate is more likely to log unsanitized data, especially as systemd-journald has no problems with it.
What's the difference between systemd-journald's behaviour and the logging we have?
I'm not familiar with s6 at all, but I think it is at least worth investigating. Also, all else equal it is best to reject invalid untrusted input as early as possible. -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/2/25 07:18, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/1/25 08:17, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 10/29/25 08:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
> Spectrum OS's host has no network access. Updates must be downloaded by > VMs. The downloads are placed into a bind-mounted directory. The VM > can write whatever it wants into that directory. This includes symlinks > that subsequent code might open, which would create a path traversal > vulnerability. It also includes paths with names containing containing > terminal escape sequences, newlines, or other nastiness. Furthermore, > the directory should not have any subdirectories either. > > Add a simple C program that checks for such ugliness and indicates > (via its exit code) if the VM misbehaved. It also ensures that both > SHA256SUMS and SHA256SUMS.gpg are present. > > Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> > --- > host/rootfs/Makefile | 6 +- > lib/kcmdline-utils.mk | 6 ++ > tools/default.nix | 1 + > tools/meson.build | 1 + > tools/updates-dir-check/meson.build | 4 ++ > tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ > 6 files changed, 110 insertions(+), 2 deletions(-)
I still don't really understand why this needs to be a C program instead of find -H /path/to/dir -not -type f. None of the other checks seem very necessary?
I trust this code more than I trust (especially) the Busybox implementation of find.
This doesn't really make sense to me. All of this is quite trivial find behaviour — not the sort of thing that's unlikely to have been widely tested. No objection to GNU find though if it helps.
I see: find with a -exec false to return an error if anything matching is found?
I'm way more familiar with C than with find, which is why I missed this.
Hmm, thinking about it some more I suppose there's a problem with find: there's no way to get it to exit as soon as it finds a matching file, with a failing error code, so it could end up running way too long.
So the C program is fine, I guess.
How are -Werror=pedantic and -DNDEBUG getting enabled in the first place?
I believe Meson sets -DNDEBUG in some cases.
Yes, if the user explicitly asks for it.
I thought it was default for release builds.
Doesn't look like it:
https://github.com/mesonbuild/meson/blob/d00f840c573103c2d51aed2b169386f7acf...
b_ndebug defaults to false.
Got it, thanks!
> + if (entry->d_name[0] == '.') > + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) > + continue; > + if (strcmp(entry->d_name, "SHA256SUMS") == 0) { > + found_sha256sums = true; > + continue; > + } > + if (strcmp(entry->d_name, "SHA256SUMS.gpg") == 0) { > + found_sha256sums_gpg = true; > + continue; > + } > + unsigned char c = (unsigned char)entry->d_name[0]; > + if (!((c >= 'A' && c <= 'Z') || > + (c >= 'a' && c <= 'z'))) > + errx(1, "Filename must begin with an ASCII letter"); > + for (size_t i = 1; i < len; ++i) { > + c = (unsigned char)entry->d_name[i]; > + if (!((c >= 'A' && c <= 'Z') || > + (c >= 'a' && c <= 'z') || > + (c >= '0' && c <= '9') || > + (c == '_') || > + (c == '-') || > + (c == '.'))) { > + if (c >= 0x20 && c <= 0x7E) > + errx(1, "Forbidden subsequent character in filename: '%c'", (int)c); > + else > + errx(1, "Forbidden subsequent character in filename: byte %d", (int)c); > + } > + }
Why do we care? Surely we don't expect systemd-sysupdate to put filenames unescaped into a shell or something.
Prevent escape sequence injection into terminals and logs is the main reason. Qubes OS has similar checks in some places, though they are off by default for file copying.
Doing this in a tool that's only used by sysupdate is a very ad-hoc way to protect against that. I think if we want to protect against that sort of thing it should be done in one place, probably in virtiofsd.
I think sysupdate is more likely to log unsanitized data, especially as systemd-journald has no problems with it.
What's the difference between systemd-journald's behaviour and the logging we have?
I'm not familiar with s6 at all, but I think it is at least worth investigating. Also, all else equal it is best to reject invalid untrusted input as early as possible.
As early as possible would be in virtiofsd, not ad-hoc for this one service here.
On 11/4/25 10:27, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/2/25 07:18, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/1/25 08:17, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 10/29/25 08:01, Alyssa Ross wrote: > Demi Marie Obenour <demiobenour@gmail.com> writes: > >> Spectrum OS's host has no network access. Updates must be downloaded by >> VMs. The downloads are placed into a bind-mounted directory. The VM >> can write whatever it wants into that directory. This includes symlinks >> that subsequent code might open, which would create a path traversal >> vulnerability. It also includes paths with names containing containing >> terminal escape sequences, newlines, or other nastiness. Furthermore, >> the directory should not have any subdirectories either. >> >> Add a simple C program that checks for such ugliness and indicates >> (via its exit code) if the VM misbehaved. It also ensures that both >> SHA256SUMS and SHA256SUMS.gpg are present. >> >> Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> >> --- >> host/rootfs/Makefile | 6 +- >> lib/kcmdline-utils.mk | 6 ++ >> tools/default.nix | 1 + >> tools/meson.build | 1 + >> tools/updates-dir-check/meson.build | 4 ++ >> tools/updates-dir-check/updates-dir-check.c | 94 +++++++++++++++++++++++++++++ >> 6 files changed, 110 insertions(+), 2 deletions(-) > > I still don't really understand why this needs to be a C program instead > of find -H /path/to/dir -not -type f. None of the other checks seem > very necessary?
I trust this code more than I trust (especially) the Busybox implementation of find.
This doesn't really make sense to me. All of this is quite trivial find behaviour — not the sort of thing that's unlikely to have been widely tested. No objection to GNU find though if it helps.
I see: find with a -exec false to return an error if anything matching is found?
I'm way more familiar with C than with find, which is why I missed this.
Hmm, thinking about it some more I suppose there's a problem with find: there's no way to get it to exit as soon as it finds a matching file, with a failing error code, so it could end up running way too long.
So the C program is fine, I guess.
> How are -Werror=pedantic and -DNDEBUG getting enabled in the first place?
I believe Meson sets -DNDEBUG in some cases.
Yes, if the user explicitly asks for it.
I thought it was default for release builds.
Doesn't look like it:
https://github.com/mesonbuild/meson/blob/d00f840c573103c2d51aed2b169386f7acf...
b_ndebug defaults to false.
Got it, thanks!
>> + if (entry->d_name[0] == '.') >> + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) >> + continue; >> + if (strcmp(entry->d_name, "SHA256SUMS") == 0) { >> + found_sha256sums = true; >> + continue; >> + } >> + if (strcmp(entry->d_name, "SHA256SUMS.gpg") == 0) { >> + found_sha256sums_gpg = true; >> + continue; >> + } >> + unsigned char c = (unsigned char)entry->d_name[0]; >> + if (!((c >= 'A' && c <= 'Z') || >> + (c >= 'a' && c <= 'z'))) >> + errx(1, "Filename must begin with an ASCII letter"); >> + for (size_t i = 1; i < len; ++i) { >> + c = (unsigned char)entry->d_name[i]; >> + if (!((c >= 'A' && c <= 'Z') || >> + (c >= 'a' && c <= 'z') || >> + (c >= '0' && c <= '9') || >> + (c == '_') || >> + (c == '-') || >> + (c == '.'))) { >> + if (c >= 0x20 && c <= 0x7E) >> + errx(1, "Forbidden subsequent character in filename: '%c'", (int)c); >> + else >> + errx(1, "Forbidden subsequent character in filename: byte %d", (int)c); >> + } >> + } > > Why do we care? Surely we don't expect systemd-sysupdate to put > filenames unescaped into a shell or something.
Prevent escape sequence injection into terminals and logs is the main reason. Qubes OS has similar checks in some places, though they are off by default for file copying.
Doing this in a tool that's only used by sysupdate is a very ad-hoc way to protect against that. I think if we want to protect against that sort of thing it should be done in one place, probably in virtiofsd.
I think sysupdate is more likely to log unsanitized data, especially as systemd-journald has no problems with it.
What's the difference between systemd-journald's behaviour and the logging we have?
I'm not familiar with s6 at all, but I think it is at least worth investigating. Also, all else equal it is best to reject invalid untrusted input as early as possible.
As early as possible would be in virtiofsd, not ad-hoc for this one service here.
That’s actually an interesting idea, but I don’t know if it would be upstreamable. -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/4/25 10:27, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/2/25 07:18, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/1/25 08:17, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
> On 10/29/25 08:01, Alyssa Ross wrote: >> Demi Marie Obenour <demiobenour@gmail.com> writes: >> >>> + if (entry->d_name[0] == '.') >>> + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) >>> + continue; >>> + if (strcmp(entry->d_name, "SHA256SUMS") == 0) { >>> + found_sha256sums = true; >>> + continue; >>> + } >>> + if (strcmp(entry->d_name, "SHA256SUMS.gpg") == 0) { >>> + found_sha256sums_gpg = true; >>> + continue; >>> + } >>> + unsigned char c = (unsigned char)entry->d_name[0]; >>> + if (!((c >= 'A' && c <= 'Z') || >>> + (c >= 'a' && c <= 'z'))) >>> + errx(1, "Filename must begin with an ASCII letter"); >>> + for (size_t i = 1; i < len; ++i) { >>> + c = (unsigned char)entry->d_name[i]; >>> + if (!((c >= 'A' && c <= 'Z') || >>> + (c >= 'a' && c <= 'z') || >>> + (c >= '0' && c <= '9') || >>> + (c == '_') || >>> + (c == '-') || >>> + (c == '.'))) { >>> + if (c >= 0x20 && c <= 0x7E) >>> + errx(1, "Forbidden subsequent character in filename: '%c'", (int)c); >>> + else >>> + errx(1, "Forbidden subsequent character in filename: byte %d", (int)c); >>> + } >>> + } >> >> Why do we care? Surely we don't expect systemd-sysupdate to put >> filenames unescaped into a shell or something. > > Prevent escape sequence injection into terminals and logs is the > main reason. Qubes OS has similar checks in some places, though they > are off by default for file copying.
Doing this in a tool that's only used by sysupdate is a very ad-hoc way to protect against that. I think if we want to protect against that sort of thing it should be done in one place, probably in virtiofsd.
I think sysupdate is more likely to log unsanitized data, especially as systemd-journald has no problems with it.
What's the difference between systemd-journald's behaviour and the logging we have?
I'm not familiar with s6 at all, but I think it is at least worth investigating. Also, all else equal it is best to reject invalid untrusted input as early as possible.
As early as possible would be in virtiofsd, not ad-hoc for this one service here.
That’s actually an interesting idea, but I don’t know if it would be upstreamable.
I imagine this could fit with the work that's being done on pluggable backends[1][2]. [1]: https://youtu.be/qsFc234tzz4?si=Qw2b4MzerLWCX39J&t=239 [2]: https://gitlab.com/virtio-fs/virtiofsd/-/issues/147
systemd-sysupdate has strict requirements on the partition layout: - The label of the active partition must match the template in the .transfer file. For instance, the root filesystem of Spectrum OS 0.0.0 will be in a partition with label Spectrum_OS_0.0.0. - The label of the inactive partition must either be that of the old version of the OS or "_empty". The former indicates an incomplete update. - The partition type UUID must conform to the Discoverable Partition Specification. After installing an image to a partition, systemd-sysupdate updates the label of the partition to match the image's version. However, it does not update the partition UUID. Therefore, use the partition label, not the partition UUID, to find the root filesystem and its verity metadata. systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, make the partitions very large so that there is plenty of room for the OS to grow. This requires rewriting the code that calculates the partition sizes. Since the partition label includes the OS version, add an OS version number. Use 0.0.0 to indicate that Spectrum OS is still in very early development and should not be used. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/initramfs/Makefile | 17 +++++----- host/initramfs/default.nix | 2 ++ host/initramfs/etc/init | 17 +++------- host/initramfs/etc/probe | 20 ++++++++---- host/initramfs/shell.nix | 1 + host/rootfs/Makefile | 23 ++++++++------ host/rootfs/default.nix | 3 ++ host/rootfs/shell.nix | 1 + img/app/Makefile | 2 +- img/app/default.nix | 5 +-- lib/kcmdline-utils.mk | 10 +++--- lib/version.nix | 15 +++++++++ release/checks/no-roothash.nix | 2 +- release/live/Makefile | 15 ++++----- release/live/default.nix | 11 +++++-- release/live/shell.nix | 4 ++- scripts/format-uuid.awk | 35 ++++++++++++++++++++ scripts/format-uuid.sh | 1 + scripts/make-gpt.bash | 72 ++++++++++++++++++++++++++++++++++++++++++ scripts/make-gpt.sh | 67 ++------------------------------------- scripts/make-live-image.sh | 41 ++++++++++++++++++++++++ scripts/sfdisk-field.awk | 3 +- version | 1 + version.license | 2 ++ vm/sys/net/Makefile | 2 +- vm/sys/net/default.nix | 5 +-- 26 files changed, 252 insertions(+), 125 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index cb13fbb35f065b67d291d4a35591d6f12720060c..798f675eb4f2ffde1c2eadc0a7b08ca59b65f347 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -4,6 +4,7 @@ .POSIX: include ../../lib/common.mk +include ../../lib/kcmdline-utils.mk dest = build/initramfs @@ -51,16 +52,13 @@ build/rootfs.verity.roothash: build/rootfs.verity build/rootfs.verity.superblock: build/rootfs.verity tail -n +2 build/rootfs.verity > $@ -build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/rootfs.verity.superblock build/rootfs.verity.roothash $(ROOT_FS) - ../../scripts/make-gpt.sh $@.tmp \ - build/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=build/rootfs.verity.roothash bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 build/rootfs.verity.roothash)") - mv $@.tmp $@ +build/live.img: $(LIVE_IMAGE_DEPS) $(ROOT_FS) + ../../scripts/make-live-image.sh live $@ $(ROOT_FS) build/loop.tar: build/live.img $(TAR) -cf $@ build/live.img -build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 +build/loop.img: ../../scripts/make-gpt.sh ../../scripts/make-gpt.bash ../../scripts/sfdisk-field.awk build/loop.ext4 ../../scripts/make-gpt.sh $@.tmp \ build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e mv $@.tmp $@ @@ -69,12 +67,13 @@ clean: rm -rf build .PHONY: clean -run: $(dest) build/rootfs.verity.roothash $(RUN_IMAGE) - @../../scripts/run-qemu.sh -m 4G \ +run: $(dest) build/rootfs.verity.roothash $(RUN_IMAGE) ../../lib/kcmdline-utils.mk + $(READ_ROOTHASH); \ + ../../scripts/run-qemu.sh -m 4G \ -machine virtualization=on \ -kernel $(KERNEL) \ -initrd $(dest) \ - -append "ro earlycon console=hvc0 intel_iommu=on roothash=$$(< build/rootfs.verity.roothash) nokaslr" \ + -append "ro earlycon console=hvc0 intel_iommu=on nokaslr x-spectrum-roothash=$$roothash x-spectrum-version=$$VERSION" \ -cpu max \ -gdb unix:build/gdb.sock,server,nowait \ -parallel none \ diff --git a/host/initramfs/default.nix b/host/initramfs/default.nix index ac7efbe3e19ee73d757d041a6a1051fe06c1d069..a1a1abbf5395f5650585901e5d2a13a72100ac09 100644 --- a/host/initramfs/default.nix +++ b/host/initramfs/default.nix @@ -99,12 +99,14 @@ stdenvNoCC.mkDerivation { fileset = lib.fileset.intersection src (lib.fileset.unions [ ./. ../../lib/common.mk + ../../lib/kcmdline-utils.mk ]); }; sourceRoot = "source/host/initramfs"; env = { PACKAGES_CPIO = packagesCpio; + VERSION = import ../../lib/version.nix; } // lib.optionalAttrs stdenvNoCC.hostPlatform.isx86_64 { MICROCODE = microcode; }; diff --git a/host/initramfs/etc/init b/host/initramfs/etc/init index 719488741b6d31564c2c17c0e41f15d16b1c0a08..8f36cd68e0450ff1a77ed5338e992323577a9f87 100755 --- a/host/initramfs/etc/init +++ b/host/initramfs/etc/init @@ -6,22 +6,16 @@ export PATH /bin if { mount -a } -piperw 3 4 -if { fdmove 1 4 /etc/getuuids } -fdclose 4 -# head -1 would be clearer, but it might use buffered I/O and consume -# too much from the fifo. Ideally we'd have line(1) from illumos. -backtick ROOTFS_UUID { fdmove 0 3 dd count=1 bs=37 status=none } -backtick VERITY_UUID { fdmove 0 3 dd count=1 bs=37 status=none } -fdclose 3 - if { mkfifo /dev/rootfs.poll } background { - fdclose 3 mdevd -C -b134217728 } -importas -iu mdevd_pid ! + +multisubstitute { + importas -iu mdevd_pid ! + importas -i roothash x-spectrum-roothash +} if { modprobe erofs } @@ -36,7 +30,6 @@ background { kill $mdevd_pid } background { rm /dev/rootfs.poll } if { - importas -Si roothash veritysetup open /dev/rootfs root-verity /dev/verity $roothash } diff --git a/host/initramfs/etc/probe b/host/initramfs/etc/probe index 4cbd00db52c1a7128b5c619a43d415675feaee0b..34e82fe9fa81316f21125b8eb058cc2917de69d7 100755 --- a/host/initramfs/etc/probe +++ b/host/initramfs/etc/probe @@ -14,9 +14,13 @@ if -n { forx -pE module { ext4 loop } modprobe $module } - backtick -E uuid { lsblk -lnpo PARTUUID $mdev } + backtick uuid { lsblk -lnpo PARTUUID $mdev } + multisubstiute { + define mdev_ $mdev + importas -Si uuid + } if { mkdir -p /mnt/${uuid} } - if { mount $mdev /mnt/${uuid} } + if { mount $mdev_ /mnt/${uuid} } find /mnt/${uuid} -name *.img -exec losetup -Pf {} ; @@ -24,11 +28,13 @@ if -n { # Check whether we now have all the partitions we need to boot. -importas -i rootfs_uuid ROOTFS_UUID -importas -i verity_uuid VERITY_UUID - -backtick -E rootfs_dev { findfs PARTUUID=${rootfs_uuid} } -backtick -E verity_dev { findfs PARTUUID=${verity_uuid} } +importas -i version x-spectrum-version +backtick rootfs_dev { findfs PARTLABEL=Spectrum_OS_${version} } +backtick verity_dev { findfs PARTLABEL=Spectrum_OS_${version}.verity } +multisubstitute { + importas -iS rootfs_dev + importas -iS verity_dev +} if { ln -s $rootfs_dev /dev/rootfs } if { ln -s $verity_dev /dev/verity } diff --git a/host/initramfs/shell.nix b/host/initramfs/shell.nix index eeba865e3ac793f67ae1808a92cf5eb1b37d57af..fa628f9c09eb266de247241b233286e756bd01d4 100644 --- a/host/initramfs/shell.nix +++ b/host/initramfs/shell.nix @@ -18,5 +18,6 @@ initramfs.overrideAttrs ({ nativeBuildInputs ? [], env ? {}, ... }: { env = env // { KERNEL = "${rootfs.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; ROOT_FS = rootfs; + VERSION = import ../../lib/version.nix; }; })) (_: {}) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 15752286f5924291768f0655a12b90c702730520..84f1b385198ecfa5905b69e4901e56150ea1b424 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -4,6 +4,7 @@ .POSIX: include ../../lib/common.mk +-include ../../lib/kcmdline-utils.mk include file-list.mk dest = build/rootfs.erofs @@ -38,9 +39,11 @@ DIRS = \ etc/s6-linux-init/run-image/vm/by-id \ etc/s6-linux-init/run-image/vm/by-name \ ext \ + home \ proc \ run \ - sys + sys \ + tmp FIFOS = etc/s6-linux-init/run-image/service/s6-svscan-log/fifo @@ -99,11 +102,8 @@ build/rootfs.verity.roothash: build/rootfs.verity build/rootfs.verity.superblock: build/rootfs.verity tail -n +2 build/rootfs.verity > $@ -build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/rootfs.verity.superblock build/rootfs.verity.roothash $(dest) - ../../scripts/make-gpt.sh $@.tmp \ - build/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=build/rootfs.verity.roothash bs=32 skip=1 count=1 status=none)") \ - $(dest):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 build/rootfs.verity.roothash)") - mv $@.tmp $@ +build/live.img: $(LIVE_IMAGE_DEPS) $(dest) + ../../scripts/make-live-image.sh live $@ $(dest) debug: $(GDB) -q \ @@ -112,14 +112,17 @@ debug: $(VMLINUX) .PHONY: debug -run: build/live.img $(EXT_FS) build/rootfs.verity.roothash +run: build/live.img $(EXT_FS) build/rootfs.verity.roothash ../../lib/kcmdline-utils.mk @set -x && \ ext="$$(mktemp build/spectrum-rootfs-extfs.XXXXXXXXXX.img)" && \ truncate -s 10G "$$ext" && \ - mkfs.btrfs "$$ext" && \ + dir=$$(mktemp -d) && \ + mkdir -- "$$dir/tmp" "$$dir/home" && \ + mkfs.btrfs --rootdir "$$dir" --subvol tmp --subvol home -- "$$ext" && \ + rm -rf -- "$$dir" && \ exec 3<>"$$ext" && \ rm -f "$$ext" && \ - set +x && \ + set -x && $(READ_ROOTHASH) && \ exec ../../scripts/run-qemu.sh -cpu max -m 4G \ -machine virtualization=on \ -kernel $(KERNEL) \ @@ -133,7 +136,7 @@ run: build/live.img $(EXT_FS) build/rootfs.verity.roothash -device virtconsole,chardev=virtiocon0 \ -drive file=build/live.img,if=virtio,format=raw,readonly=on \ -drive file=/proc/self/fd/3,if=virtio,format=raw \ - -append "earlycon console=hvc0 roothash=$$(< build/rootfs.verity.roothash) intel_iommu=on nokaslr" \ + -append "earlycon console=hvc0 intel_iommu=on nokaslr x-spectrum-roothash=$$roothash x-spectrum-version=$$VERSION" \ -device virtio-keyboard \ -device virtio-mouse \ -device virtio-gpu \ diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 00052222507077b9e94a5ed0a3fbddd27caeefc3..bc364b930b30e00c55b17b5e4248a303392cf3a0 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -133,7 +133,9 @@ stdenvNoCC.mkDerivation { fileset = fileset.intersection src (fileset.unions [ ./. ../../lib/common.mk + ../../lib/kcmdline-utils.mk ../../scripts/make-erofs.sh + ../../version ]); }; sourceRoot = "source/host/rootfs"; @@ -145,6 +147,7 @@ stdenvNoCC.mkDerivation { printf "%s\n/\n" ${packagesSysroot} >$out sed p ${writeClosure [ packagesSysroot] } >>$out ''; + VERSION = import ../../lib/version.nix; }; makeFlags = [ "dest=$(out)" ]; diff --git a/host/rootfs/shell.nix b/host/rootfs/shell.nix index 3d986f7327823cb855e5980759ad2a3935793340..bd234e90ee19bdfa6591d29c518cb0dc393b01c8 100644 --- a/host/rootfs/shell.nix +++ b/host/rootfs/shell.nix @@ -20,5 +20,6 @@ rootfs.overrideAttrs ( KERNEL = "${passthru.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; LINUX_SRC = srcOnly passthru.kernel; VMLINUX = "${passthru.kernel.dev}/vmlinux"; + VERSION = import ../../lib/version.nix; }; })) (_: {}) diff --git a/img/app/Makefile b/img/app/Makefile index 981889ebe55d9ba03228977f3dc0ea3f26d5c4fb..e380fc173f580f00e9f4008da36533b645345f9b 100644 --- a/img/app/Makefile +++ b/img/app/Makefile @@ -24,7 +24,7 @@ $(imgdir)/appvm/vmlinux: $(KERNEL) mkdir -p $$(dirname $@) cp $(KERNEL) $@ -$(imgdir)/appvm/blk/root.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/rootfs.erofs +$(imgdir)/appvm/blk/root.img: ../../scripts/make-gpt.sh ../../scripts/make-gpt.bash ../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) ../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:5460386f-2203-4911-8694-91400125c604:root diff --git a/img/app/default.nix b/img/app/default.nix index 253fef08f5db29757da9d11fda67ac23fe6040c3..06764356d4126d3a2cd6a3e590accfeab6cffda4 100644 --- a/img/app/default.nix +++ b/img/app/default.nix @@ -8,7 +8,7 @@ pkgsStatic.callPackage ( { lib, stdenvNoCC, runCommand, writeClosure , erofs-utils, jq, s6-rc, util-linux , busybox, cacert, dejavu_fonts, execline, kmod, linux_latest, mdevd, s6 -, s6-linux-init, spectrum-app-tools +, s6-linux-init, spectrum-app-tools, bash }: let @@ -106,13 +106,14 @@ stdenvNoCC.mkDerivation { ./. ../../lib/common.mk ../../scripts/make-erofs.sh + ../../scripts/make-gpt.bash ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk ]); }; sourceRoot = "source/img/app"; - nativeBuildInputs = [ erofs-utils jq spectrum-build-tools s6-rc util-linux ]; + nativeBuildInputs = [ erofs-utils jq spectrum-build-tools s6-rc util-linux bash ]; env = { KERNEL = "${kernel}/${baseNameOf kernelTarget}"; diff --git a/lib/kcmdline-utils.mk b/lib/kcmdline-utils.mk index fa228552e583f15fc77a746985060ad5d04cdf2c..7f1ef7d197ccf68c17640f4fdf44c167939fca13 100644 --- a/lib/kcmdline-utils.mk +++ b/lib/kcmdline-utils.mk @@ -1,6 +1,8 @@ # SPDX-License-Identifier: EUPL-1.2+ # SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> -READ_ROOTHASH = { \ - set -eufo pipefail; \ - read -r version < ../../version; \ - LC_ALL=C expr "x$$version" : '^\(x0\|x[1-9][0-9]*\)\(\.\(0\|[1-9][0-9]*\)\)\{2\}$$' >/dev/null; } +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +READ_ROOTHASH = { set -euo pipefail; \ + read -r roothash < build/rootfs.verity.roothash; \ + LC_ALL=C expr "x$$roothash" : '^x[a-f0-9]\{64\}$$' >/dev/null; } + +LIVE_IMAGE_DEPS = ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/make-gpt.bash ../../scripts/sfdisk-field.awk build/rootfs.verity.superblock build/rootfs.verity.roothash ../../scripts/make-live-image.sh ../../lib/kcmdline-utils.mk diff --git a/lib/version.nix b/lib/version.nix new file mode 100644 index 0000000000000000000000000000000000000000..1c1568137313c37c4e1377a063992f7bf6856e57 --- /dev/null +++ b/lib/version.nix @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +let + raw_version = builtins.readFile ../version; + version_length = builtins.stringLength raw_version - 1; + version = builtins.substring 0 version_length raw_version; + number_re = "(0|[1-9][0-9]{0,2})"; +in +if version_length < 0 || builtins.substring version_length 1 raw_version != "\n" then + builtins.abort "Version file missing trailing newline (contents ${builtins.toJSON raw_version})" +else if builtins.match "^(${number_re}\\.){2}${number_re}$" version == null then + builtins.abort "Version ${builtins.toJSON version} is invalid" +else + version diff --git a/release/checks/no-roothash.nix b/release/checks/no-roothash.nix index 76d1e8b88ba74e6981775f3d4b8d10138c342d84..1d044cb04828ea221e7d1656e5eb7942669fe73c 100644 --- a/release/checks/no-roothash.nix +++ b/release/checks/no-roothash.nix @@ -28,6 +28,6 @@ in { machine = create_machine(flags) machine.start() - machine.wait_for_console_text("roothash invalid or missing") + machine.wait_for_console_text("x-spectrum-roothash not set") ''; }))) (_: {}) diff --git a/release/live/Makefile b/release/live/Makefile index 6dcbdeedda5d6ccf293f60dc62043f46c81ecf83..3072d869f13efbf5ea196d191881aeab85726d2e 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -4,22 +4,21 @@ .POSIX: include ../../lib/common.mk +include ../../lib/kcmdline-utils.mk DTBS ?= build/empty dest = build/live.img -$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat build/rootfs.verity.superblock build/rootfs.verity.roothash $(ROOT_FS) - ../../scripts/make-gpt.sh $@.tmp \ - build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - build/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=build/rootfs.verity.roothash bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 build/rootfs.verity.roothash)") - mv $@.tmp $@ +$(dest): $(LIVE_IMAGE_DEPS) build/boot.fat + ../../scripts/make-live-image.sh release $(dest) $(ROOT_FS) build/empty: mkdir -p $@ -build/spectrum.efi: build/rootfs.verity.roothash $(DTBS) $(KERNEL) $(INITRAMFS) +build/spectrum.efi: build/rootfs.verity.roothash $(DTBS) $(KERNEL) $(INITRAMFS) ../../lib/kcmdline-utils.mk + set -euo pipefail; \ + $(READ_ROOTHASH) && \ { \ printf "[UKI]\nDeviceTreeAuto=" && \ find $(DTBS) -name '*.dtb' -print0 | tr '\0' ' ' ;\ @@ -29,7 +28,7 @@ build/spectrum.efi: build/rootfs.verity.roothash $(DTBS) $(KERNEL) $(INITRAMFS) --linux $(KERNEL) \ --initrd $(INITRAMFS) \ --os-release $$'NAME="Spectrum"\n' \ - --cmdline "ro intel_iommu=on roothash=$$(cat build/rootfs.verity.roothash)" + --cmdline "ro intel_iommu=on x-spectrum-roothash=$$roothash x-spectrum-version=$$VERSION" build/boot.fat: $(SYSTEMD_BOOT_EFI) build/spectrum.efi $(TRUNCATE) -s 440401920 $@ diff --git a/release/live/default.nix b/release/live/default.nix index 2a1dc3e1dd939f21edac582bf39737eb4d46eb0c..b5c0c8df31d4c6cb7fdd2337e8169f36655dd1a8 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -1,12 +1,13 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2021-2023, 2025 Alyssa Ross <hi@alyssa.is> # SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, rootfs, src , lib, pkgsStatic, stdenvNoCC , cryptsetup, dosfstools, jq, mtools, util-linux -, systemdUkify +, systemdUkify, bash }: let @@ -32,15 +33,20 @@ stdenv.mkDerivation { fileset = lib.fileset.intersection src (lib.fileset.unions [ ./. ../../lib/common.mk + ../../lib/kcmdline-utils.mk + ../../scripts/format-uuid.awk ../../scripts/format-uuid.sh + ../../scripts/make-gpt.bash ../../scripts/make-gpt.sh + ../../scripts/make-live-image.sh ../../scripts/sfdisk-field.awk + ../../version ]); }; sourceRoot = "source/release/live"; nativeBuildInputs = [ - cryptsetup dosfstools jq spectrum-build-tools mtools systemd util-linux + bash cryptsetup dosfstools jq spectrum-build-tools mtools systemd util-linux ]; env = { @@ -49,6 +55,7 @@ stdenv.mkDerivation { ROOT_FS = rootfs; SYSTEMD_BOOT_EFI = "${systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; EFINAME = "BOOT${toUpper efiArch}.EFI"; + VERSION = import ../../lib/version.nix; } // lib.optionalAttrs stdenv.hostPlatform.linux-kernel.DTB or false { DTBS = "${rootfs.kernel}/dtbs"; }; diff --git a/release/live/shell.nix b/release/live/shell.nix index 5acaa8c5b113fd2789aaea9268487b193bab37af..e1e78214871c0e4681bff0d5a894c8ae3b8c3c02 100644 --- a/release/live/shell.nix +++ b/release/live/shell.nix @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> -import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm }: +import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm, rootfs }: (callSpectrumPackage ./. {}).overrideAttrs ( { nativeBuildInputs ? [], env ? {}, ... }: @@ -9,7 +9,9 @@ import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm }: nativeBuildInputs = nativeBuildInputs ++ [ qemu_kvm ]; env = env // { + ROOT_FS = rootfs; OVMF_CODE = "${qemu_kvm}/share/qemu/edk2-${stdenv.hostPlatform.qemuArch}-code.fd"; + VERSION = import ../../lib/version.nix; }; } )) (_: {}) diff --git a/scripts/format-uuid.awk b/scripts/format-uuid.awk new file mode 100644 index 0000000000000000000000000000000000000000..a5349d68a4d29be5f750650236420c9b5a7257eb --- /dev/null +++ b/scripts/format-uuid.awk @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +function format_uuid(arg) { + if (arg in found_so_far) { + fail("Duplicate UUID, try changing the image (by even 1 bit)"); + } + found_so_far[arg] = 1; + print (substr(arg, 1, 8) "-" \ + substr(arg, 9, 4) "-" \ + substr(arg, 13, 4) "-" \ + substr(arg, 17, 4) "-" \ + substr(arg, 21, 12)); +} + +function fail(msg) { + print msg > "/dev/stderr"; + exit 1; +} + +BEGIN { + FS = ""; + RS = "\n"; + if ((getline) != 1) + fail("Empty input file"); + roothash = $0; + if (roothash !~ /^[a-f0-9]{64}$/) + fail("Invalid root hash"); + if (getline) + fail("Junk after root hash"); + found_so_far[""] = ""; + for (i = 1; i != 49; i += 16) { + format_uuid(substr($0, i, 32)); + } + format_uuid(substr($0, 49, 16) substr($0, 1, 16)); +} diff --git a/scripts/format-uuid.sh b/scripts/format-uuid.sh index 497a5f2daeef88e0143f5021cd64fa2181ffe163..f589b3340252c653df97a82ce429528beee43b1a 100755 --- a/scripts/format-uuid.sh +++ b/scripts/format-uuid.sh @@ -4,6 +4,7 @@ # SPDX-FileCopyrightText: 2022 Unikie # SPDX-License-Identifier: EUPL-1.2+ +set -o pipefail substr () { str=$1 beg=$2 diff --git a/scripts/make-gpt.bash b/scripts/make-gpt.bash new file mode 100644 index 0000000000000000000000000000000000000000..f9d53817e3cc4342cac5d4c832cf4aa129880399 --- /dev/null +++ b/scripts/make-gpt.bash @@ -0,0 +1,72 @@ +#!/usr/bin/bash -- +# SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2022 Unikie +# SPDX-License-Identifier: EUPL-1.2+ +# +# usage: make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... + +set -xeuo pipefail +ONE_MiB=1048576 + +# Prints the number of 1MiB blocks required to store the file named +# $1. We use 1MiB blocks because that's what sfdisk uses for +# alignment. It would be possible to get a slightly smaller image +# using actual normal-sized 512-byte blocks, but it's probably not +# worth it to configure sfdisk to do that. +sizeMiB() { + wc -c "$1" | awk -v ONE_MiB=$ONE_MiB \ + '{printf "%d\n", ($1 + ONE_MiB - 1) / ONE_MiB}' +} + +# Copies from path $3 into partition number $2 in partition table $1. +fillPartition() { + start="$(sfdisk -J "$1" | jq -r --argjson index "$2" \ + '.partitiontable.partitions[$index].start * 512')" + + # GNU cat will use copy_file_range(2) if possible, whereas dd + # will always do a userspace copy, which is significantly slower. + lseek -S 1 "$start" cat "$3" 1<>"$1" +} + +# Prints the partition path from a PATH:PARTTYPE[:PARTUUID[:PARTLABEL]] string. +partitionPath() { + awk -F: '{print $1}' <<EOF +$1 +EOF +} + +scriptsDir="$(dirname "$0")" + +out="$1" +shift + +table="label: gpt" + +# Keep 1MiB free at the start, and 1MiB free at the end. +gptBytes=$((ONE_MiB * 2)) +for partition; do + if [[ "$partition" =~ :([1-9][0-9]*)MiB$ ]]; then + sizeMiB=${BASH_REMATCH[1]} + partition=${partition%:*} + else + partitionPath=$(partitionPath "$partition") + sizeMiB=$(sizeMiB "$partitionPath") + fi + table=$table' +size='${sizeMiB}MiB$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition") + gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) +done + +rm -f "$out" +truncate -s "$gptBytes" "$out" +printf %s\\n "$table" +sfdisk --no-reread --no-tell-kernel "$out" <<EOF +$table +EOF + +n=0 +for partition; do + partitionPath=$(partitionPath "$partition") + fillPartition "$out" "$n" "$partitionPath" + n=$((n + 1)) +done diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh index 96f0d2c8494c093558c0e32e7e920b569bb078ef..665057da8281d2b5282081e4999098fbaa29e6ca 100755 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -1,65 +1,4 @@ -#!/bin/sh -eu -# -# SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> -# SPDX-FileCopyrightText: 2022 Unikie +#!/bin/sh -- # SPDX-License-Identifier: EUPL-1.2+ -# -# usage: make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... - -ONE_MiB=1048576 - -# Prints the number of 1MiB blocks required to store the file named -# $1. We use 1MiB blocks because that's what sfdisk uses for -# alignment. It would be possible to get a slightly smaller image -# using actual normal-sized 512-byte blocks, but it's probably not -# worth it to configure sfdisk to do that. -sizeMiB() { - wc -c "$1" | awk -v ONE_MiB=$ONE_MiB \ - '{printf "%d\n", ($1 + ONE_MiB - 1) / ONE_MiB}' -} - -# Copies from path $3 into partition number $2 in partition table $1. -fillPartition() { - start="$(sfdisk -J "$1" | jq -r --argjson index "$2" \ - '.partitiontable.partitions[$index].start * 512')" - - # GNU cat will use copy_file_range(2) if possible, whereas dd - # will always do a userspace copy, which is significantly slower. - lseek -S 1 "$start" cat "$3" 1<>"$1" -} - -# Prints the partition path from a PATH:PARTTYPE[:PARTUUID[:PARTLABEL]] string. -partitionPath() { - awk -F: '{print $1}' <<EOF -$1 -EOF -} - -scriptsDir="$(dirname "$0")" - -out="$1" -shift - -nl=' -' -table="label: gpt" - -# Keep 1MiB free at the start, and 1MiB free at the end. -gptBytes=$((ONE_MiB * 2)) -for partition; do - sizeMiB="$(sizeMiB "$(partitionPath "$partition")")" - table="$table${nl}size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" - gptBytes="$((gptBytes + sizeMiB * ONE_MiB))" -done - -rm -f "$out" -truncate -s "$gptBytes" "$out" -sfdisk --no-reread --no-tell-kernel "$out" <<EOF -$table -EOF - -n=0 -for partition; do - fillPartition "$out" "$n" "$(partitionPath "$partition")" - n="$((n + 1))" -done +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +exec bash -- "${0%.sh}.bash" "$@" diff --git a/scripts/make-live-image.sh b/scripts/make-live-image.sh new file mode 100755 index 0000000000000000000000000000000000000000..2d8f5140fd23280d0f8ff2c0cb1640875dab4e8e --- /dev/null +++ b/scripts/make-live-image.sh @@ -0,0 +1,41 @@ +#!/bin/sh -- +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +set -euo pipefail +if [ ! -f build/rootfs.verity.superblock ]; then + echo 'No superblock found' >&2 + exit 1 +fi +case $0 in +(/*) dir=${0%/*}/;; +(*/*) dir=./${0%/*};; +(*) dir=.;; +esac +usage () { + echo 'Usage: make-live-image.sh [release|live] OUTPUT_FILE ROOT_FILESYSTEM' >&2 + exit 1 +} +if [ "$#" != 3 ]; then usage; fi +file_type=$1 output_file=$2 root_filesystem=$3 +root_hashes=$(LC_ALL=C awk -f "${dir}/format-uuid.awk" < build/rootfs.verity.roothash) +# The awk script produces output that is meant for field splitting +# and has no characters special for globbing. +# shellcheck disable=SC2086 +set -- $root_hashes +case $file_type in +(release) + "$dir/make-gpt.sh" "$output_file.tmp" \ + build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ + "build/rootfs.verity.superblock:verity:$1:Spectrum_OS_$VERSION.verity:1024MiB" \ + "$root_filesystem:root:$2:Spectrum_OS_$VERSION:20480MiB" \ + "/dev/null:verity:$3:_empty:1024MiB" \ + "/dev/null:root:$4:_empty:20480MiB" + ;; +(live) + "$dir/make-gpt.sh" "$output_file.tmp" \ + "build/rootfs.verity.superblock:verity:$1:Spectrum_OS_$VERSION.verity" \ + "$root_filesystem:root:$2:Spectrum_OS_$VERSION";; +(*) usage;; +esac +mv -- "$output_file.tmp" "$output_file" diff --git a/scripts/sfdisk-field.awk b/scripts/sfdisk-field.awk index e13c86d2fb11a066eebd043808e659b08dbd269c..72eec9a0a770563d32da14440fe2552eb2e39b68 100644 --- a/scripts/sfdisk-field.awk +++ b/scripts/sfdisk-field.awk @@ -24,6 +24,7 @@ BEGIN { arch = _arch } + comma = "" for (n in fields) { if (n <= skip) continue @@ -33,6 +34,6 @@ BEGIN { fields[n] = uuid } - printf "%s=%s,", keys[n - skip], fields[n] + printf ",%s%s=%s", comma, keys[n - skip], fields[n] } } diff --git a/version b/version new file mode 100644 index 0000000000000000000000000000000000000000..77d6f4ca23711533e724789a0a0045eab28c5ea6 --- /dev/null +++ b/version @@ -0,0 +1 @@ +0.0.0 diff --git a/version.license b/version.license new file mode 100644 index 0000000000000000000000000000000000000000..e9aa5bf149a1b426dba78c7df37b92c0a992a7dd --- /dev/null +++ b/version.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: EUPL-1.2+ +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> diff --git a/vm/sys/net/Makefile b/vm/sys/net/Makefile index b377e12bba8f062026e997de18e19c9af8e07cb5..c29a8faee4e0a7eb170325e4d1eaeeba4532df41 100644 --- a/vm/sys/net/Makefile +++ b/vm/sys/net/Makefile @@ -23,7 +23,7 @@ $(vmdir)/netvm/vmlinux: $(KERNEL) mkdir -p $$(dirname $@) cp $(KERNEL) $@ -$(vmdir)/netvm/blk/root.img: ../../../scripts/make-gpt.sh ../../../scripts/sfdisk-field.awk build/rootfs.erofs +$(vmdir)/netvm/blk/root.img: ../../../scripts/make-gpt.sh ../../../scripts/make-gpt.bash ../../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) ../../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:ea21da27-0391-48da-9235-9d2ab2ca7844:root diff --git a/vm/sys/net/default.nix b/vm/sys/net/default.nix index 9d9df0b001060085239e00ffa59f8e091f0b88bf..2f3eea176928315ac0cd6e81bb788e965613e3a9 100644 --- a/vm/sys/net/default.nix +++ b/vm/sys/net/default.nix @@ -8,7 +8,7 @@ pkgsStatic.callPackage ( { lib, stdenvNoCC, nixos, runCommand, writeClosure , erofs-utils, jq, s6-rc, util-linux, xorg , busybox, connmanMinimal, dbus, execline, kmod, linux_latest, mdevd, nftables -, s6, s6-linux-init +, s6, s6-linux-init, bash }: let @@ -106,13 +106,14 @@ stdenvNoCC.mkDerivation { ./. ../../../lib/common.mk ../../../scripts/make-erofs.sh + ../../../scripts/make-gpt.bash ../../../scripts/make-gpt.sh ../../../scripts/sfdisk-field.awk ]); }; sourceRoot = "source/vm/sys/net"; - nativeBuildInputs = [ erofs-utils jq spectrum-build-tools s6-rc util-linux ]; + nativeBuildInputs = [ erofs-utils jq spectrum-build-tools s6-rc util-linux bash ]; env = { KERNEL = "${kernel}/${baseNameOf kernelTarget}"; -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
systemd-sysupdate has strict requirements on the partition layout:
- The label of the active partition must match the template in the .transfer file. For instance, the root filesystem of Spectrum OS 0.0.0 will be in a partition with label Spectrum_OS_0.0.0. - The label of the inactive partition must either be that of the old version of the OS or "_empty". The former indicates an incomplete update. - The partition type UUID must conform to the Discoverable Partition Specification.
After installing an image to a partition, systemd-sysupdate updates the label of the partition to match the image's version. However, it does not update the partition UUID. Therefore, use the partition label, not the partition UUID, to find the root filesystem and its verity metadata.
systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, make the partitions very large so that there is plenty of room for the OS to grow. This requires rewriting the code that calculates the partition sizes.
Since the partition label includes the OS version, add an OS version number. Use 0.0.0 to indicate that Spectrum OS is still in very early development and should not be used.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/initramfs/Makefile | 17 +++++----- host/initramfs/default.nix | 2 ++ host/initramfs/etc/init | 17 +++------- host/initramfs/etc/probe | 20 ++++++++---- host/initramfs/shell.nix | 1 + host/rootfs/Makefile | 23 ++++++++------ host/rootfs/default.nix | 3 ++ host/rootfs/shell.nix | 1 + img/app/Makefile | 2 +- img/app/default.nix | 5 +-- lib/kcmdline-utils.mk | 10 +++--- lib/version.nix | 15 +++++++++ release/checks/no-roothash.nix | 2 +- release/live/Makefile | 15 ++++----- release/live/default.nix | 11 +++++-- release/live/shell.nix | 4 ++- scripts/format-uuid.awk | 35 ++++++++++++++++++++ scripts/format-uuid.sh | 1 + scripts/make-gpt.bash | 72 ++++++++++++++++++++++++++++++++++++++++++ scripts/make-gpt.sh | 67 ++------------------------------------- scripts/make-live-image.sh | 41 ++++++++++++++++++++++++ scripts/sfdisk-field.awk | 3 +- version | 1 + version.license | 2 ++ vm/sys/net/Makefile | 2 +- vm/sys/net/default.nix | 5 +-- 26 files changed, 252 insertions(+), 125 deletions(-)
I expect this will change once we figure out what to do about the installer / combined image, so I'm leaving it for now.
This step provides versioned release artifacts. Writing a detached OpenPGP signature of SHA256SUMS to SHA256SUMS.gpg is sufficient to create a directory usable by systemd-sysupdate. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 4 ++-- host/rootfs/default.nix | 6 +++--- release/checks/integration/default.nix | 2 +- release/combined/eosimages.nix | 2 +- release/live/Makefile | 14 ++++++++++++++ release/live/default.nix | 5 +---- 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 84f1b385198ecfa5905b69e4901e56150ea1b424..35adb3d972c1a30705a5b123c65abf837617eb72 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -91,7 +91,7 @@ clean: # supports one output per rule, so we combine the two outputs then # define two more rules to separate them again. build/rootfs.verity: $(dest) - $(VERITYSETUP) format $(dest) build/rootfs.verity.superblock.tmp \ + set -euo pipefail; $(VERITYSETUP) format $(dest) build/rootfs.verity.superblock.tmp \ | awk -F ':[[:blank:]]*' '$$1 == "Root hash" {print $$2; exit}' \ > build/rootfs.verity.roothash.tmp cat build/rootfs.verity.roothash.tmp build/rootfs.verity.superblock.tmp \ @@ -100,7 +100,7 @@ build/rootfs.verity: $(dest) build/rootfs.verity.roothash: build/rootfs.verity head -n 1 build/rootfs.verity > $@ build/rootfs.verity.superblock: build/rootfs.verity - tail -n +2 build/rootfs.verity > $@ + { read -r && cat; } < build/rootfs.verity > $@ build/live.img: $(LIVE_IMAGE_DEPS) $(dest) ../../scripts/make-live-image.sh live $@ $(dest) diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index bc364b930b30e00c55b17b5e4248a303392cf3a0..995b9bfd4c53edf9fa060011c128464518d15d6e 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -8,8 +8,8 @@ import ../../lib/call-package.nix ( }: pkgsStatic.callPackage ( -{ busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils, execline -, inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos +{ btrfs-progs, busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils +, execline, inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos , runCommand, s6, s6-linux-init, s6-rc, socat, spectrum-host-tools , stdenvNoCC, util-linux, virtiofsd, writeClosure , xdg-desktop-portal-spectrum-host, xorg @@ -82,7 +82,7 @@ let # Packages that should be fully linked into /usr, # (not just their bin/* files). usrPackages = [ - appvm kernel.modules firmware kmod kmod.lib + appvm btrfs-progs firmware kernel.modules kmod kmod.lib netvm mesa dejavu_fonts systemd util-linux westonLite ]; diff --git a/release/checks/integration/default.nix b/release/checks/integration/default.nix index 340fb6e11fed5971caf879d0a8a40baf395a7589..947d9cb8f2a5e1d7e93b6814581d33e342b522fc 100644 --- a/release/checks/integration/default.nix +++ b/release/checks/integration/default.nix @@ -86,7 +86,7 @@ stdenv.mkDerivation (finalAttrs: { env = { QEMU_SYSTEM = "qemu-system-${stdenv.hostPlatform.qemuArch} -nographic"; EFI_PATH = "${qemu_kvm}/share/qemu/edk2-${stdenv.hostPlatform.qemuArch}-code.fd"; - IMG_PATH = live; + IMG_PATH = "${live}/live.img"; USER_DATA_PATH = userData; }; diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index ba44d9cd82d55d491293ed36cc0402db8ebd3ffe..b168dcf61a74f96fed1d52858c0c3ebfc311873c 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -7,7 +7,7 @@ import ../../lib/call-package.nix ( runCommand "eosimages.img" { nativeBuildInputs = [ e2fsprogs tar2ext4 ]; imageName = "Spectrum-0.0-x86_64-generic.0.Live.img"; - image = callSpectrumPackage ../live {}; + image = "${callSpectrumPackage ../live {}}/live.img"; __structuredAttrs = true; unsafeDiscardReferences = { out = true; }; dontFixup = true; diff --git a/release/live/Makefile b/release/live/Makefile index 3072d869f13efbf5ea196d191881aeab85726d2e..9aa2488a57ba583ff49f0d95af4f91878a0cd5dd 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -30,6 +30,20 @@ build/spectrum.efi: build/rootfs.verity.roothash $(DTBS) $(KERNEL) $(INITRAMFS) --os-release $$'NAME="Spectrum"\n' \ --cmdline "ro intel_iommu=on x-spectrum-roothash=$$roothash x-spectrum-version=$$VERSION" +install: build/rootfs.verity.superblock $(ROOT_FS) build/spectrum.efi $(dest) + set -euo pipefail; \ + $(READ_ROOTHASH); \ + mkdir -p -- $(DESTDIR) build; \ + cp -- build/rootfs.verity.superblock $(DESTDIR)/"Spectrum_OS_$$VERSION.verity"; \ + cp -- $(ROOT_FS) $(DESTDIR)/"Spectrum_OS_$$VERSION.root"; \ + cp -- build/spectrum.efi $(DESTDIR)/"Spectrum_OS_$$VERSION.efi"; \ + cp $(dest) $(DESTDIR)/live.img; \ + cd $(DESTDIR); \ + sha256sum live.img \ + "Spectrum_OS_$$VERSION.root" \ + "Spectrum_OS_$$VERSION.verity" \ + "Spectrum_OS_$$VERSION.efi" > SHA256SUMS + build/boot.fat: $(SYSTEMD_BOOT_EFI) build/spectrum.efi $(TRUNCATE) -s 440401920 $@ $(MKFS_FAT) $@ diff --git a/release/live/default.nix b/release/live/default.nix index b5c0c8df31d4c6cb7fdd2337e8169f36655dd1a8..c6dcabd49363e113eb0783ced2a167633a6e19c3 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -56,14 +56,11 @@ stdenv.mkDerivation { SYSTEMD_BOOT_EFI = "${systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; EFINAME = "BOOT${toUpper efiArch}.EFI"; VERSION = import ../../lib/version.nix; + DESTDIR = "$(out)"; } // lib.optionalAttrs stdenv.hostPlatform.linux-kernel.DTB or false { DTBS = "${rootfs.kernel}/dtbs"; }; - buildFlags = [ "dest=$(out)" ]; - - dontInstall = true; - enableParallelBuilding = true; __structuredAttrs = true; -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
This step provides versioned release artifacts. Writing a detached OpenPGP signature of SHA256SUMS to SHA256SUMS.gpg is sufficient to create a directory usable by systemd-sysupdate.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 4 ++-- host/rootfs/default.nix | 6 +++--- release/checks/integration/default.nix | 2 +- release/combined/eosimages.nix | 2 +- release/live/Makefile | 14 ++++++++++++++ release/live/default.nix | 5 +---- 6 files changed, 22 insertions(+), 11 deletions(-)
And then on the server we'd only serve one of these at a time, so we'd use the SHA256SUMS file generated by the build?
diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 84f1b385198ecfa5905b69e4901e56150ea1b424..35adb3d972c1a30705a5b123c65abf837617eb72 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -91,7 +91,7 @@ clean: # supports one output per rule, so we combine the two outputs then # define two more rules to separate them again. build/rootfs.verity: $(dest) - $(VERITYSETUP) format $(dest) build/rootfs.verity.superblock.tmp \ + set -euo pipefail; $(VERITYSETUP) format $(dest) build/rootfs.verity.superblock.tmp \ | awk -F ':[[:blank:]]*' '$$1 == "Root hash" {print $$2; exit}' \ > build/rootfs.verity.roothash.tmp cat build/rootfs.verity.roothash.tmp build/rootfs.verity.superblock.tmp \
Correct but unrelated change.
@@ -100,7 +100,7 @@ build/rootfs.verity: $(dest) build/rootfs.verity.roothash: build/rootfs.verity head -n 1 build/rootfs.verity > $@ build/rootfs.verity.superblock: build/rootfs.verity - tail -n +2 build/rootfs.verity > $@ + { read -r && cat; } < build/rootfs.verity > $@
Why?
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index bc364b930b30e00c55b17b5e4248a303392cf3a0..995b9bfd4c53edf9fa060011c128464518d15d6e 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -8,8 +8,8 @@ import ../../lib/call-package.nix ( }: pkgsStatic.callPackage (
-{ busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils, execline -, inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos +{ btrfs-progs, busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils +, execline, inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos , runCommand, s6, s6-linux-init, s6-rc, socat, spectrum-host-tools , stdenvNoCC, util-linux, virtiofsd, writeClosure , xdg-desktop-portal-spectrum-host, xorg @@ -82,7 +82,7 @@ let # Packages that should be fully linked into /usr, # (not just their bin/* files). usrPackages = [ - appvm kernel.modules firmware kmod kmod.lib + appvm btrfs-progs firmware kernel.modules kmod kmod.lib netvm mesa dejavu_fonts systemd util-linux westonLite ];
Unrelated.
diff --git a/release/checks/integration/default.nix b/release/checks/integration/default.nix index 340fb6e11fed5971caf879d0a8a40baf395a7589..947d9cb8f2a5e1d7e93b6814581d33e342b522fc 100644 --- a/release/checks/integration/default.nix +++ b/release/checks/integration/default.nix @@ -86,7 +86,7 @@ stdenv.mkDerivation (finalAttrs: { env = { QEMU_SYSTEM = "qemu-system-${stdenv.hostPlatform.qemuArch} -nographic"; EFI_PATH = "${qemu_kvm}/share/qemu/edk2-${stdenv.hostPlatform.qemuArch}-code.fd"; - IMG_PATH = live; + IMG_PATH = "${live}/live.img"; USER_DATA_PATH = userData; };
diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index ba44d9cd82d55d491293ed36cc0402db8ebd3ffe..b168dcf61a74f96fed1d52858c0c3ebfc311873c 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -7,7 +7,7 @@ import ../../lib/call-package.nix ( runCommand "eosimages.img" { nativeBuildInputs = [ e2fsprogs tar2ext4 ]; imageName = "Spectrum-0.0-x86_64-generic.0.Live.img"; - image = callSpectrumPackage ../live {}; + image = "${callSpectrumPackage ../live {}}/live.img"; __structuredAttrs = true; unsafeDiscardReferences = { out = true; }; dontFixup = true; diff --git a/release/live/Makefile b/release/live/Makefile index 3072d869f13efbf5ea196d191881aeab85726d2e..9aa2488a57ba583ff49f0d95af4f91878a0cd5dd 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -30,6 +30,20 @@ build/spectrum.efi: build/rootfs.verity.roothash $(DTBS) $(KERNEL) $(INITRAMFS) --os-release $$'NAME="Spectrum"\n' \ --cmdline "ro intel_iommu=on x-spectrum-roothash=$$roothash x-spectrum-version=$$VERSION"
+install: build/rootfs.verity.superblock $(ROOT_FS) build/spectrum.efi $(dest) + set -euo pipefail; \
I don't think this needs to hack around the normal Make thing of having one shell per line.
+ $(READ_ROOTHASH); \ + mkdir -p -- $(DESTDIR) build; \ + cp -- build/rootfs.verity.superblock $(DESTDIR)/"Spectrum_OS_$$VERSION.verity"; \ + cp -- $(ROOT_FS) $(DESTDIR)/"Spectrum_OS_$$VERSION.root"; \ + cp -- build/spectrum.efi $(DESTDIR)/"Spectrum_OS_$$VERSION.efi"; \ + cp $(dest) $(DESTDIR)/live.img; \ + cd $(DESTDIR); \ + sha256sum live.img \ + "Spectrum_OS_$$VERSION.root" \ + "Spectrum_OS_$$VERSION.verity" \ + "Spectrum_OS_$$VERSION.efi" > SHA256SUMS +
No "OS" in the project name. Given that we won't ever want the live image and the individual partition updates at the same time, it probably makes sense to put these into separate builds. Maybe we add host/verity and host/efi to build those, assemble them into an image in release/live, and also add release/update that combines the partition images and the SHA256SUMS file.
diff --git a/release/live/default.nix b/release/live/default.nix index b5c0c8df31d4c6cb7fdd2337e8169f36655dd1a8..c6dcabd49363e113eb0783ced2a167633a6e19c3 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -56,14 +56,11 @@ stdenv.mkDerivation { SYSTEMD_BOOT_EFI = "${systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; EFINAME = "BOOT${toUpper efiArch}.EFI"; VERSION = import ../../lib/version.nix; + DESTDIR = "$(out)"; } // lib.optionalAttrs stdenv.hostPlatform.linux-kernel.DTB or false { DTBS = "${rootfs.kernel}/dtbs"; };
- buildFlags = [ "dest=$(out)" ]; -
DESTDIR should stay in buildFlags so it's not in the environment for nix-shell.
- dontInstall = true; - enableParallelBuilding = true;
__structuredAttrs = true;
-- 2.51.2
No functional change intended. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 15 +-------------- host/rootfs/default.nix | 7 ++++--- lib/verity.mk | 18 ++++++++++++++++++ release/live/Makefile | 17 +---------------- release/live/default.nix | 1 + 5 files changed, 25 insertions(+), 33 deletions(-) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 35adb3d972c1a30705a5b123c65abf837617eb72..4712d9063e9f2e3c9b8b7b4fb2a7e54d119c6840 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -87,20 +87,7 @@ clean: rm -rf build .PHONY: clean -# veritysetup format produces two files, but Make only (portably) -# supports one output per rule, so we combine the two outputs then -# define two more rules to separate them again. -build/rootfs.verity: $(dest) - set -euo pipefail; $(VERITYSETUP) format $(dest) build/rootfs.verity.superblock.tmp \ - | awk -F ':[[:blank:]]*' '$$1 == "Root hash" {print $$2; exit}' \ - > build/rootfs.verity.roothash.tmp - cat build/rootfs.verity.roothash.tmp build/rootfs.verity.superblock.tmp \ - > $@ - rm build/rootfs.verity.roothash.tmp build/rootfs.verity.superblock.tmp -build/rootfs.verity.roothash: build/rootfs.verity - head -n 1 build/rootfs.verity > $@ -build/rootfs.verity.superblock: build/rootfs.verity - { read -r && cat; } < build/rootfs.verity > $@ +include ../../lib/verity.mk build/live.img: $(LIVE_IMAGE_DEPS) $(dest) ../../scripts/make-live-image.sh live $@ $(dest) diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 995b9bfd4c53edf9fa060011c128464518d15d6e..cb39f0d77b6640198da3ab840a2c8ca7cc1c91a1 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -8,8 +8,8 @@ import ../../lib/call-package.nix ( }: pkgsStatic.callPackage ( -{ btrfs-progs, busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils -, execline, inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos +{ busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils, execline +, inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos , runCommand, s6, s6-linux-init, s6-rc, socat, spectrum-host-tools , stdenvNoCC, util-linux, virtiofsd, writeClosure , xdg-desktop-portal-spectrum-host, xorg @@ -82,7 +82,7 @@ let # Packages that should be fully linked into /usr, # (not just their bin/* files). usrPackages = [ - appvm btrfs-progs firmware kernel.modules kmod kmod.lib + appvm firmware kernel.modules kmod kmod.lib netvm mesa dejavu_fonts systemd util-linux westonLite ]; @@ -134,6 +134,7 @@ stdenvNoCC.mkDerivation { ./. ../../lib/common.mk ../../lib/kcmdline-utils.mk + ../../lib/verity.mk ../../scripts/make-erofs.sh ../../version ]); diff --git a/lib/verity.mk b/lib/verity.mk new file mode 100644 index 0000000000000000000000000000000000000000..77945d14f37d62e67274a7356613f8e7f162f809 --- /dev/null +++ b/lib/verity.mk @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> + +# veritysetup format produces two files, but Make only (portably) +# supports one output per rule, so we combine the two outputs then +# define two more rules to separate them again. +build/rootfs.verity: $(ROOT_FS) + mkdir -p build + $(VERITYSETUP) format $(ROOT_FS) build/rootfs.verity.superblock.tmp \ + | awk -F ':[[:blank:]]*' '$$1 == "Root hash" {print $$2; exit}' \ + > build/rootfs.verity.roothash.tmp + cat build/rootfs.verity.roothash.tmp build/rootfs.verity.superblock.tmp \ + > $@ + rm build/rootfs.verity.roothash.tmp build/rootfs.verity.superblock.tmp +build/rootfs.verity.roothash: build/rootfs.verity + head -n 1 build/rootfs.verity > $@ +build/rootfs.verity.superblock: build/rootfs.verity + { read -r && cat;} < build/rootfs.verity > $@ diff --git a/release/live/Makefile b/release/live/Makefile index 9aa2488a57ba583ff49f0d95af4f91878a0cd5dd..e6e91eee0f418114174e20384531788759a7db09 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -5,6 +5,7 @@ include ../../lib/common.mk include ../../lib/kcmdline-utils.mk +include ../../lib/verity.mk DTBS ?= build/empty @@ -51,22 +52,6 @@ build/boot.fat: $(SYSTEMD_BOOT_EFI) build/spectrum.efi $(MCOPY) -i $@ build/spectrum.efi ::/EFI/Linux $(MCOPY) -i $@ $(SYSTEMD_BOOT_EFI) ::/EFI/BOOT/$(EFINAME) -# veritysetup format produces two files, but Make only (portably) -# supports one output per rule, so we combine the two outputs then -# define two more rules to separate them again. -build/rootfs.verity: $(ROOT_FS) - mkdir -p build - $(VERITYSETUP) format $(ROOT_FS) build/rootfs.verity.superblock.tmp \ - | awk -F ':[[:blank:]]*' '$$1 == "Root hash" {print $$2; exit}' \ - > build/rootfs.verity.roothash.tmp - cat build/rootfs.verity.roothash.tmp build/rootfs.verity.superblock.tmp \ - > $@ - rm build/rootfs.verity.roothash.tmp build/rootfs.verity.superblock.tmp -build/rootfs.verity.roothash: build/rootfs.verity - head -n 1 build/rootfs.verity > $@ -build/rootfs.verity.superblock: build/rootfs.verity - tail -n +2 build/rootfs.verity > $@ - clean: rm -rf build .PHONY: clean diff --git a/release/live/default.nix b/release/live/default.nix index c6dcabd49363e113eb0783ced2a167633a6e19c3..08dc198afc25b6362f2aedabf9e9450dd02eb4ad 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -34,6 +34,7 @@ stdenv.mkDerivation { ./. ../../lib/common.mk ../../lib/kcmdline-utils.mk + ../../lib/verity.mk ../../scripts/format-uuid.awk ../../scripts/format-uuid.sh ../../scripts/make-gpt.bash -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
No functional change intended.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 15 +-------------- host/rootfs/default.nix | 7 ++++--- lib/verity.mk | 18 ++++++++++++++++++ release/live/Makefile | 17 +---------------- release/live/default.nix | 1 + 5 files changed, 25 insertions(+), 33 deletions(-)
Looks good. How about basing this on main, and then doing the pipefail change after this one so it only has to be done in one place?
On 10/29/25 08:22, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
No functional change intended.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 15 +-------------- host/rootfs/default.nix | 7 ++++--- lib/verity.mk | 18 ++++++++++++++++++ release/live/Makefile | 17 +---------------- release/live/default.nix | 1 + 5 files changed, 25 insertions(+), 33 deletions(-)
Looks good. How about basing this on main, and then doing the pipefail change after this one so it only has to be done in one place?
I will send this as a separate patch series. -- Sincerely, Demi Marie Obenour (she/her/hers)
Include a new 'update' command to update the system. This works as follows: 1. Take a global, system-wide lock. 2. Create a BTRFS subvolume for the sys.updates VM to write the updates. 3. Bind-mount this subvolume into the VM's shared directory. 4. Start sys.updates to get the updates. 5. Wait for the VM to shut down. 6. Take a BTRFS snapshot of the subvolume. 7. Call syncfs() to flush all of the data on the subvolume. 8. Inspect the contents of the subvolume. Check that everything is a regular file and that the names are reasonable. Check that SHA256SUMS and SHA256SUMS.gpg are present. 9. Call systemd-sysupdate to run the actual update. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 8 ++- host/rootfs/default.nix | 17 ++++--- host/rootfs/file-list.mk | 5 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 21 ++++++++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 21 ++++++++ .../image/etc/sysupdate.d/70-kernel.transfer | 25 ++++++++++ host/rootfs/image/usr/bin/run-update | 54 ++++++++++++++++++++ host/rootfs/image/usr/bin/update | 56 +++++++++++++++++++++ host/rootfs/image/usr/bin/vm-start | 25 +++++++++- host/rootfs/os-release.in | 13 +++++ host/rootfs/os-release.in.license | 2 + host/rootfs/shell.nix | 2 +- update-signing-keys.gpg | 1 + update-signing-keys.gpg.license | 2 + update-url | 1 + update-url.license | 2 + vm/app/sysupdate.d/50-verity.transfer | 18 +++++++ vm/app/sysupdate.d/60-root.transfer | 18 +++++++ vm/app/sysupdate.d/70-kernel.transfer | 18 +++++++ vm/app/updates.nix | 57 ++++++++++++++++++++++ 21 files changed, 356 insertions(+), 11 deletions(-) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 4712d9063e9f2e3c9b8b7b4fb2a7e54d119c6840..2faa1e46c1a3bbbdf31baf1e972d9b4ecb389ae5 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -10,6 +10,7 @@ include file-list.mk dest = build/rootfs.erofs DIRS = \ + boot \ dev \ etc/s6-linux-init/env \ etc/s6-linux-init/run-image/configs \ @@ -47,11 +48,13 @@ DIRS = \ FIFOS = etc/s6-linux-init/run-image/service/s6-svscan-log/fifo -BUILD_FILES = build/etc/s6-rc +BUILD_FILES = build/etc/s6-rc build/etc/os-release $(dest): ../../scripts/make-erofs.sh $(PACKAGES_FILE) $(FILES) $(BUILD_FILES) build/empty build/fifo file-list.mk + set -euo pipefail; \ { \ cat $(PACKAGES_FILE) ;\ + printf '%s\n%s\n' ../../update-signing-keys.gpg /etc/systemd/import-pubring.gpg; \ for file in $(FILES) $(LINKS); do printf '%s\n%s\n' $$file "$${file#image/}"; done ;\ for file in $(BUILD_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ printf 'build/empty\n%s\n' $(DIRS) ;\ @@ -99,6 +102,9 @@ debug: $(VMLINUX) .PHONY: debug +build/etc/os-release: os-release.in build/etc + sed 's#@VERSION@#$(VERSION)#g' < os-release.in > $@ + run: build/live.img $(EXT_FS) build/rootfs.verity.roothash ../../lib/kcmdline-utils.mk @set -x && \ ext="$$(mktemp build/spectrum-rootfs-extfs.XXXXXXXXXX.img)" && \ diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index cb39f0d77b6640198da3ab840a2c8ca7cc1c91a1..c412fe17f45bde79f1efa42cadb29cfd5fbc3991 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -8,10 +8,10 @@ import ../../lib/call-package.nix ( }: pkgsStatic.callPackage ( -{ busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils, execline -, inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos -, runCommand, s6, s6-linux-init, s6-rc, socat, spectrum-host-tools -, stdenvNoCC, util-linux, virtiofsd, writeClosure +{ btrfs-progs, bash, busybox, cloud-hypervisor, cryptsetup, dbus +, erofs-utils, execline, inkscape, inotify-tools, iproute2, jq, lib +, mdevd, nixos, runCommand, s6, s6-linux-init, s6-rc, socat +, spectrum-host-tools, stdenvNoCC, util-linux, virtiofsd, writeClosure , xdg-desktop-portal-spectrum-host, xorg }: let @@ -40,8 +40,8 @@ let no_pgo_foot = foot.override { allowPgo = false; }; packages = [ - cloud-hypervisor crosvm cryptsetup dbus execline inotify-tools - iproute2 jq mdevd s6 s6-linux-init s6-rc socat + btrfs-progs cloud-hypervisor crosvm cryptsetup dbus execline + inotify-tools iproute2 jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools virtiofsd xdg-desktop-portal-spectrum-host (busybox.override { @@ -90,6 +90,7 @@ let appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; appvm-foot = callSpectrumPackage ../../vm/app/foot.nix {}; appvm-gnome-text-editor = callSpectrumPackage ../../vm/app/gnome-text-editor.nix {}; + appvm-updates = callSpectrumPackage ../../vm/app/updates.nix {}; }; packagesSysroot = runCommand "packages-sysroot" { @@ -135,13 +136,15 @@ stdenvNoCC.mkDerivation { ../../lib/common.mk ../../lib/kcmdline-utils.mk ../../lib/verity.mk + ../../lib/kcmdline-utils.mk ../../scripts/make-erofs.sh ../../version + ../../update-signing-keys.gpg ]); }; sourceRoot = "source/host/rootfs"; - nativeBuildInputs = [ erofs-utils spectrum-build-tools s6-rc ]; + nativeBuildInputs = [ erofs-utils spectrum-build-tools s6-rc bash btrfs-progs ]; env = { PACKAGES = runCommand "packages" {} '' diff --git a/host/rootfs/file-list.mk b/host/rootfs/file-list.mk index 9acaa1d90bed674814775becf89c1c847d0ce3e3..905422ebda0f70ce32def788e0c093527af293fc 100644 --- a/host/rootfs/file-list.mk +++ b/host/rootfs/file-list.mk @@ -42,13 +42,18 @@ FILES = \ image/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/notification-fd \ image/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/run \ image/etc/s6-linux-init/scripts/rc.init \ + image/etc/sysupdate.d/50-verity.transfer \ + image/etc/sysupdate.d/60-root.transfer \ + image/etc/sysupdate.d/70-kernel.transfer \ image/etc/udev/rules.d/99-spectrum.rules \ image/etc/xdg/weston/autolaunch \ image/etc/xdg/weston/weston.ini \ image/usr/bin/assign-devices \ image/usr/bin/create-vm-dependencies \ image/usr/bin/run-appimage \ + image/usr/bin/run-update \ image/usr/bin/run-vmm \ + image/usr/bin/update \ image/usr/bin/vm-console \ image/usr/bin/vm-import \ image/usr/bin/vm-start \ diff --git a/host/rootfs/image/etc/fstab b/host/rootfs/image/etc/fstab index 6a82ecc85090a37b13603b29f74ca6e554a28c33..78cec99f29dda993ad97048771097121a0e42622 100644 --- a/host/rootfs/image/etc/fstab +++ b/host/rootfs/image/etc/fstab @@ -4,3 +4,4 @@ proc /proc proc defaults 0 0 devpts /dev/pts devpts defaults,gid=4,mode=620 0 0 tmpfs /dev/shm tmpfs defaults 0 0 sysfs /sys sysfs defaults 0 0 +tmpfs /tmp tmpfs defaults,mode=0700 0 0 diff --git a/host/rootfs/image/etc/sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..07a698f3956e19f9f55efff52db51128c16a5b56 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A +Verify=yes + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_OS_@v.verity + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_OS_@v.verity +MatchPartitionType=root-verity +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/60-root.transfer b/host/rootfs/image/etc/sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..ebc102d1d7f4341565cd452f1bd89ffe9640b361 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/60-root.transfer @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A +Verify=yes + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_OS_@v.root + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_OS_@v +MatchPartitionType=root +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..6f75dfb04abf5ae911be3ae95318685321a86f5f --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A +Verify=yes + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_OS_@v.efi + +[Target] +Type=regular-file +Path=/EFI/Linux +PathRelativeTo=boot +MatchPattern=Spectrum_OS_@v+@l-@d.efi \ + Spectrum_OS_@v+@l.efi \ + Spectrum_OS_@v.efi +Mode=0644 +TriesLeft=3 +TriesDone=0 +InstancesLeft=2 diff --git a/host/rootfs/image/usr/bin/run-update b/host/rootfs/image/usr/bin/run-update new file mode 100644 index 0000000000000000000000000000000000000000..c1938df01189c26f6c7ffd4c0010fabdc5fb3405 --- /dev/null +++ b/host/rootfs/image/usr/bin/run-update @@ -0,0 +1,54 @@ +#!/bin/execlineb -S1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2024 Alyssa Ross <hi@alyssa.is> + +backtick -E dir { mktemp -d /run/vm/by-id/XXXXXX } +backtick -E id { basename -- $dir } + +if { mkdir -p /run/configs/${id}/fs } +if { redirfd -w 1 /run/configs/${id}/fs/type echo appimage } +if { touch /run/configs/${id}/fs/run } +if { mount --rbind $1 /run/configs/${id}/fs/run } +if { + ln -s /usr/lib/spectrum/img/appvm/blk /usr/lib/spectrum/img/appvm/vmlinux + /run/configs/${id} +} + +if { ln -s /run/configs/${id} ${dir}/config } + +if { create-vm-dependencies $id } + +piperw 4 3 +background { + fdclose 3 + fdmove 0 4 + + # Wait for the VMM to be up, then start the VM. + if { redirfd -w 1 /dev/null head -1 } + vm-start $id +} +fdclose 4 + +foreground { run-vmm $id } +fdclose 3 + +if { + forx -pE service { + dbus + vhost-user-fs + vhost-user-gpu + xdg-desktop-portal-spectrum-host + } + s6-instance-delete /run/service/${service} $id +} + +if { + forx -E mount { + /run/configs/${id}/fs/run + ${dir}/fs/config + ${dir}/fs/doc + } + umount $mount +} + +rm -r $dir /run/configs/${id} diff --git a/host/rootfs/image/usr/bin/update b/host/rootfs/image/usr/bin/update new file mode 100755 index 0000000000000000000000000000000000000000..8e147929cecbef5873cd02c946adf1355da444c6 --- /dev/null +++ b/host/rootfs/image/usr/bin/update @@ -0,0 +1,56 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Steps: +# +# 1. Take a global, system-wide lock. +# 2. Create a BTRFS subvolume for the sys.updates VM to write the updates. +# 3. Bind-mount this subvolume into the VM's shared directory. +# 4. Start sys.updates to get the updates. +# 5. Wait for the VM to shut down. +# 6. Take a BTRFS snapshot of the subvolume. +# 7. Call syncfs() to flush all of the data on the subvolume. +# 8. Inspect the contents of the subvolume. +# Check that everything is a regular file and that the names are reasonable. +# Check that SHA256SUMS and SHA256SUMS.gpg are present. +# 9. Call systemd-sysupdate to run the actual update. + +if { mkdir -p -m 0700 /run/updater } +if { + case $1 { + /[0-9A-Za-z._/-]+ { true } + } + foreground { fdmove -c 1 2 echo 'Update directory path has forbidden characters or is not absolute' } + exit 1 +} +execline-cd $1 +s6-setlock /run/update-lock +foreground { + # This might fail with a "File exists" error, but that is fine. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + if { umask 0022 mkdir -p shared/etc/systemd shared/update-destination } + # TODO: use a safe copy program that is not vulnerable to symlink attacks. + # This should be okay as the directory has not been shared yet, but better + # safe than sorry. Also nosymfollow should be a mitigation, but still, + # better safe than sorry. + if { cp /etc/systemd/import-pubring.gpg shared/etc/systemd } + if { + if { + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-updates } + basename -- $id_path + } + vm-start $update_vm_id shared + } + if { btrfs subvolume snapshot -- shared private } + if { sync -- private } + if { updates-dir-check private/update-destination } + unshare --mount + if { mount --bind -o ro -- private/update-destination /run/updater } + /usr/lib/systemd/systemd-sysupdate update + } +} +importas -i sysupdate_exit_status "?" +foreground { btrfs subvolume delete -- shared private } +exit $sysupdate_exit_status diff --git a/host/rootfs/image/usr/bin/vm-start b/host/rootfs/image/usr/bin/vm-start index 67480e5215d8a8260ce3f03c67f71ba8f210c291..8ae8d94203345c4f3e8b6e46de0d139fda6c11d6 100755 --- a/host/rootfs/image/usr/bin/vm-start +++ b/host/rootfs/image/usr/bin/vm-start @@ -1,6 +1,24 @@ -#!/bin/execlineb -S1 +#!/bin/execlineb -Ws0 # SPDX-License-Identifier: EUPL-1.2+ # SPDX-FileCopyrightText: 2022-2023, 2025 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { + case "${#}" { + 1 { true } + 2 { + multisubstitute { + define sourcedir ${2} + define fsdir /run/vm/by-id/${1}/fs/user + } + if { mkdir -p ${fsdir} } + foreground { redirfd -w 2 /dev/null umount ${fsdir} } + mount --bind -- ${sourcedir} ${fsdir} + } + } + foreground { fdmove -c 1 2 echo "Bad number of arguments ${#} (expected 1 or 3)" } + exit 100 +} foreground { s6-rc -bu change vm-env } @@ -20,4 +38,7 @@ foreground { redirfd -w 2 /dev/null s6-svwait -U /run/service/vmm/instance/${1} } -ch-remote --api-socket /run/vm/by-id/${1}/vmm boot +if { ch-remote --api-socket /run/vm/by-id/${1}/vmm boot } +case "${#}" { + 2 { s6-svwait -D /run/service/vmm/instance/${1} } +} diff --git a/host/rootfs/os-release.in b/host/rootfs/os-release.in new file mode 100644 index 0000000000000000000000000000000000000000..8a167a39366dedc6ff9024efdb98383ec84618ec --- /dev/null +++ b/host/rootfs/os-release.in @@ -0,0 +1,13 @@ +NAME="Spectrum OS" +ID="spectrum" +PRETTY_NAME="Spectrum OS @VERSION@" +VERSION=@VERSION@ +VERSION_ID=@VERSION@ +IMAGE_ID=spectrum_os_@VERSION@ +IMAGE_VERSION=@VERSION@ +RELEASE_TYPE=development +HOME_URL="https://www.spectrum-os.org" +BUG_REPORT_URL="mailto:discuss@spectrum-os.org" +ANSI_COLOR="1;34" +VENDOR_NAME="Spectrum" +VENDOR_URL="https://www.spectrum-os.org" diff --git a/host/rootfs/os-release.in.license b/host/rootfs/os-release.in.license new file mode 100644 index 0000000000000000000000000000000000000000..c4a0586a407fe14c3e0855749a7524ac3871dda4 --- /dev/null +++ b/host/rootfs/os-release.in.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> diff --git a/host/rootfs/shell.nix b/host/rootfs/shell.nix index bd234e90ee19bdfa6591d29c518cb0dc393b01c8..b5ed566062da03944567dd610c88e1a58523e303 100644 --- a/host/rootfs/shell.nix +++ b/host/rootfs/shell.nix @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: 2022 Unikie import ../../lib/call-package.nix ( -{ callSpectrumPackage, rootfs, pkgsStatic, srcOnly, stdenv +{ callSpectrumPackage, rootfs, srcOnly, stdenv , btrfs-progs, cryptsetup, jq, netcat, qemu_kvm, reuse, util-linux }: diff --git a/update-signing-keys.gpg b/update-signing-keys.gpg new file mode 100644 index 0000000000000000000000000000000000000000..b4c15467614ee15deef02af05f4c6554a1f7a013 --- /dev/null +++ b/update-signing-keys.gpg @@ -0,0 +1 @@ +NOT A VALID KEY - UPDATES WILL NOT WORK diff --git a/update-signing-keys.gpg.license b/update-signing-keys.gpg.license new file mode 100644 index 0000000000000000000000000000000000000000..c4a0586a407fe14c3e0855749a7524ac3871dda4 --- /dev/null +++ b/update-signing-keys.gpg.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> diff --git a/update-url b/update-url new file mode 100644 index 0000000000000000000000000000000000000000..a5b28fe9b3bbeb5e823c9e17c4765a074a83b350 --- /dev/null +++ b/update-url @@ -0,0 +1 @@ +https://your-spectrum-os-update-server.invalid/download-directory/ diff --git a/update-url.license b/update-url.license new file mode 100644 index 0000000000000000000000000000000000000000..c4a0586a407fe14c3e0855749a7524ac3871dda4 --- /dev/null +++ b/update-url.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> diff --git a/vm/app/sysupdate.d/50-verity.transfer b/vm/app/sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..e437860426b8a651ca20ee7bddff1a9b3cf39507 --- /dev/null +++ b/vm/app/sysupdate.d/50-verity.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_OS_@v.verity + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/user/update-destination +MatchPattern=Spectrum_OS_@v.verity +Mode=0644 diff --git a/vm/app/sysupdate.d/60-root.transfer b/vm/app/sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..84ab4cd693342bb1118aa9525c797080c5b356dc --- /dev/null +++ b/vm/app/sysupdate.d/60-root.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_OS_@v.root + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/user/update-destination +MatchPattern=Spectrum_OS_@v.root +Mode=0644 diff --git a/vm/app/sysupdate.d/70-kernel.transfer b/vm/app/sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..931944ac68483d864600748e469de9cd4829ff4a --- /dev/null +++ b/vm/app/sysupdate.d/70-kernel.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_OS_@v.efi + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/user/update-destination +MatchPattern=Spectrum_OS_@v.efi +Mode=0644 diff --git a/vm/app/updates.nix b/vm/app/updates.nix new file mode 100644 index 0000000000000000000000000000000000000000..30beca30e578b5c869eaedf2fd7e8913bf616a0c --- /dev/null +++ b/vm/app/updates.nix @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +import ../../lib/call-package.nix ( +{ callSpectrumPackage, lib, pkgsMusl, pkgsStatic, src, writeScript, systemd }: + +pkgsMusl.callPackage ( +{ stdenvNoCC, curl }: + +pkgsStatic.callPackage ( +{ execline, runCommand }: + +let + raw_update_url = builtins.readFile ../../update-url; + update-url = + if builtins.match "^https?://([[:alnum:]:./?=~-]|%[[:xdigit:]]{2})+/\n$" raw_update_url == null then + builtins.abort "Bad update URL" + else + builtins.substring 0 (builtins.stringLength raw_update_url - 1) raw_update_url; + sysupdate-d = stdenvNoCC.mkDerivation { + name = "spectrum-systemd-transfer-files"; + src = ./.; + installPhase = + '' + mkdir -- "$out" + ( + cd -- "$src" && + for i in sysupdate.d/*.transfer; do + s=''${i#sysupdate.d/} && + sed 's,@UPDATE_URL@,${update-url},g' < "$i" > "$out/$s" || exit + done + printf %s\\n '${update-url}' > "$out/update-url" + ) || exit + ''; + }; + l = lib.escapeShellArgs; + mountpoint = "/run/virtiofs/virtiofs0/user"; + sysupdate-path = "${systemd}/lib/systemd/systemd-sysupdate"; + runner = writeScript "update-run-script" ( + "#!/bin/sh --\n" + + builtins.concatStringsSep " && \\\n" [ + (l ["mount" "-toverlay" "-olowerdir=${mountpoint}/etc:/etc" "--" "overlay" "/etc"]) + (l [sysupdate-path "--definitions=${sysupdate-d}" "update"]) + (l ["${curl}/bin/curl" "-L" "--proto" "=http,https" + "-o" "${mountpoint}/update-destination/SHA256SUMS.gpg" + "--" "${update-url}SHA256SUMS.gpg"]) + (l ["${curl}/bin/curl" "-L" "--proto" "=http,https" + "-o" "${mountpoint}/update-destination/SHA256SUMS" + "--" "${update-url}/SHA256SUMS"]) + ]); +in + +callSpectrumPackage ../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = "${runner}"; +}) {}) {}) (_: {}) -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
Include a new 'update' command to update the system. This works as follows:
1. Take a global, system-wide lock. 2. Create a BTRFS subvolume for the sys.updates VM to write the updates. 3. Bind-mount this subvolume into the VM's shared directory. 4. Start sys.updates to get the updates. 5. Wait for the VM to shut down. 6. Take a BTRFS snapshot of the subvolume. 7. Call syncfs() to flush all of the data on the subvolume.
Why do we need to do this?
8. Inspect the contents of the subvolume. Check that everything is a regular file and that the names are reasonable. Check that SHA256SUMS and SHA256SUMS.gpg are present. 9. Call systemd-sysupdate to run the actual update.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 8 ++- host/rootfs/default.nix | 17 ++++--- host/rootfs/file-list.mk | 5 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 21 ++++++++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 21 ++++++++ .../image/etc/sysupdate.d/70-kernel.transfer | 25 ++++++++++ host/rootfs/image/usr/bin/run-update | 54 ++++++++++++++++++++
This doesn't seem to be used anywhere, and is an exact copy of run-appimage aside from being non-executable.
host/rootfs/image/usr/bin/update | 56 +++++++++++++++++++++ host/rootfs/image/usr/bin/vm-start | 25 +++++++++- host/rootfs/os-release.in | 13 +++++ host/rootfs/os-release.in.license | 2 + host/rootfs/shell.nix | 2 +- update-signing-keys.gpg | 1 + update-signing-keys.gpg.license | 2 + update-url | 1 + update-url.license | 2 + vm/app/sysupdate.d/50-verity.transfer | 18 +++++++ vm/app/sysupdate.d/60-root.transfer | 18 +++++++ vm/app/sysupdate.d/70-kernel.transfer | 18 +++++++ vm/app/updates.nix | 57 ++++++++++++++++++++++
This should be vm/app/updates/default.nix and vm/app/updates/sysupdate.d.
21 files changed, 356 insertions(+), 11 deletions(-)
diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 4712d9063e9f2e3c9b8b7b4fb2a7e54d119c6840..2faa1e46c1a3bbbdf31baf1e972d9b4ecb389ae5 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -10,6 +10,7 @@ include file-list.mk dest = build/rootfs.erofs
DIRS = \ + boot \ dev \ etc/s6-linux-init/env \ etc/s6-linux-init/run-image/configs \ @@ -47,11 +48,13 @@ DIRS = \
FIFOS = etc/s6-linux-init/run-image/service/s6-svscan-log/fifo
-BUILD_FILES = build/etc/s6-rc +BUILD_FILES = build/etc/s6-rc build/etc/os-release
$(dest): ../../scripts/make-erofs.sh $(PACKAGES_FILE) $(FILES) $(BUILD_FILES) build/empty build/fifo file-list.mk + set -euo pipefail; \ { \ cat $(PACKAGES_FILE) ;\ + printf '%s\n%s\n' ../../update-signing-keys.gpg /etc/systemd/import-pubring.gpg; \
Probably should just be a normal file in image/etc.
for file in $(FILES) $(LINKS); do printf '%s\n%s\n' $$file "$${file#image/}"; done ;\ for file in $(BUILD_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ printf 'build/empty\n%s\n' $(DIRS) ;\ @@ -99,6 +102,9 @@ debug: $(VMLINUX) .PHONY: debug
+build/etc/os-release: os-release.in build/etc + sed 's#@VERSION@#$(VERSION)#g' < os-release.in > $@ + run: build/live.img $(EXT_FS) build/rootfs.verity.roothash ../../lib/kcmdline-utils.mk @set -x && \ ext="$$(mktemp build/spectrum-rootfs-extfs.XXXXXXXXXX.img)" && \ diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index cb39f0d77b6640198da3ab840a2c8ca7cc1c91a1..c412fe17f45bde79f1efa42cadb29cfd5fbc3991 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -8,10 +8,10 @@ import ../../lib/call-package.nix ( }: pkgsStatic.callPackage (
-{ busybox, cloud-hypervisor, cryptsetup, dbus, erofs-utils, execline -, inkscape, inotify-tools, iproute2, jq, lib, mdevd, nixos -, runCommand, s6, s6-linux-init, s6-rc, socat, spectrum-host-tools -, stdenvNoCC, util-linux, virtiofsd, writeClosure +{ btrfs-progs, bash, busybox, cloud-hypervisor, cryptsetup, dbus +, erofs-utils, execline, inkscape, inotify-tools, iproute2, jq, lib +, mdevd, nixos, runCommand, s6, s6-linux-init, s6-rc, socat +, spectrum-host-tools, stdenvNoCC, util-linux, virtiofsd, writeClosure , xdg-desktop-portal-spectrum-host, xorg }: let @@ -40,8 +40,8 @@ let no_pgo_foot = foot.override { allowPgo = false; };
packages = [ - cloud-hypervisor crosvm cryptsetup dbus execline inotify-tools - iproute2 jq mdevd s6 s6-linux-init s6-rc socat + btrfs-progs cloud-hypervisor crosvm cryptsetup dbus execline + inotify-tools iproute2 jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools virtiofsd xdg-desktop-portal-spectrum-host
(busybox.override { @@ -90,6 +90,7 @@ let appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; appvm-foot = callSpectrumPackage ../../vm/app/foot.nix {}; appvm-gnome-text-editor = callSpectrumPackage ../../vm/app/gnome-text-editor.nix {}; + appvm-updates = callSpectrumPackage ../../vm/app/updates.nix {}; };
packagesSysroot = runCommand "packages-sysroot" { @@ -135,13 +136,15 @@ stdenvNoCC.mkDerivation { ../../lib/common.mk ../../lib/kcmdline-utils.mk ../../lib/verity.mk + ../../lib/kcmdline-utils.mk
Duplicate.
../../scripts/make-erofs.sh ../../version + ../../update-signing-keys.gpg ]); }; sourceRoot = "source/host/rootfs";
- nativeBuildInputs = [ erofs-utils spectrum-build-tools s6-rc ]; + nativeBuildInputs = [ erofs-utils spectrum-build-tools s6-rc bash btrfs-progs ];
What are these used for at build time?
diff --git a/host/rootfs/image/etc/sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..07a698f3956e19f9f55efff52db51128c16a5b56 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A +Verify=yes
Defaults to yes, and is unlikely to change.
+ +[Source] +Type=url-file +Path=file:///run/updater
The docs for url-file say "referenced via a HTTP or HTTPS URL". Should we submit a PR to clarify file:// is also supported? AIUI Lennart is basically on board with our approach here.
+MatchPattern=Spectrum_OS_@v.verity + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_OS_@v.verity +MatchPartitionType=root-verity +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..6f75dfb04abf5ae911be3ae95318685321a86f5f --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A +Verify=yes + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_OS_@v.efi + +[Target] +Type=regular-file +Path=/EFI/Linux +PathRelativeTo=boot +MatchPattern=Spectrum_OS_@v+@l-@d.efi \ + Spectrum_OS_@v+@l.efi \ + Spectrum_OS_@v.efi +Mode=0644 +TriesLeft=3 +TriesDone=0
Boot counting would be cool, but maybe we should leave it out for now and implement it all at once later? (Or is this a complete implementation?)
diff --git a/host/rootfs/image/usr/bin/update b/host/rootfs/image/usr/bin/update new file mode 100755 index 0000000000000000000000000000000000000000..8e147929cecbef5873cd02c946adf1355da444c6 --- /dev/null +++ b/host/rootfs/image/usr/bin/update @@ -0,0 +1,56 @@ +#!/bin/execlineb -WS1
Oh TIL execlineb -W. We should probably set that in more places.
+# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Steps: +# +# 1. Take a global, system-wide lock. +# 2. Create a BTRFS subvolume for the sys.updates VM to write the updates. +# 3. Bind-mount this subvolume into the VM's shared directory. +# 4. Start sys.updates to get the updates. +# 5. Wait for the VM to shut down. +# 6. Take a BTRFS snapshot of the subvolume. +# 7. Call syncfs() to flush all of the data on the subvolume. +# 8. Inspect the contents of the subvolume. +# Check that everything is a regular file and that the names are reasonable. +# Check that SHA256SUMS and SHA256SUMS.gpg are present. +# 9. Call systemd-sysupdate to run the actual update. + +if { mkdir -p -m 0700 /run/updater } +if { + case $1 { + /[0-9A-Za-z._/-]+ { true } + } + foreground { fdmove -c 1 2 echo 'Update directory path has forbidden characters or is not absolute' } + exit 1 +}
Why are we trying to protect against the user here?
+execline-cd $1
Just cd is fine.
+s6-setlock /run/update-lock +foreground { + # This might fail with a "File exists" error, but that is fine. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + if { umask 0022 mkdir -p shared/etc/systemd shared/update-destination } + # TODO: use a safe copy program that is not vulnerable to symlink attacks. + # This should be okay as the directory has not been shared yet, but better + # safe than sorry. Also nosymfollow should be a mitigation, but still, + # better safe than sorry.
If we think it's currently safe, I'd rather not have the comment. I don't like TODOs that end up sticking around forever.
+ if { cp /etc/systemd/import-pubring.gpg shared/etc/systemd }
Might as well bind mount this and save the copy? If we run in a namespace it'll be cleaned up when we exit. (Maybe I'm over-enthusiastic about bind mounts…)
+ if {
Unnecessary if I think?
+ if { + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-updates } + basename -- $id_path + } + vm-start $update_vm_id shared
Okay, so I guess run-update is unused and included by default?
+ } + if { btrfs subvolume snapshot -- shared private }
Can I suggest "snapshot" as the name? And a comment about why we do it would be nice.
+ if { sync -- private } + if { updates-dir-check private/update-destination } + unshare --mount + if { mount --bind -o ro -- private/update-destination /run/updater } + /usr/lib/systemd/systemd-sysupdate update + } +} +importas -i sysupdate_exit_status "?"
No need to quote ? in execline.
diff --git a/host/rootfs/image/usr/bin/vm-start b/host/rootfs/image/usr/bin/vm-start index 67480e5215d8a8260ce3f03c67f71ba8f210c291..8ae8d94203345c4f3e8b6e46de0d139fda6c11d6 100755 --- a/host/rootfs/image/usr/bin/vm-start +++ b/host/rootfs/image/usr/bin/vm-start @@ -1,6 +1,24 @@ -#!/bin/execlineb -S1 +#!/bin/execlineb -Ws0 # SPDX-License-Identifier: EUPL-1.2+ # SPDX-FileCopyrightText: 2022-2023, 2025 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { + case "${#}" { + 1 { true } + 2 { + multisubstitute { + define sourcedir ${2} + define fsdir /run/vm/by-id/${1}/fs/user + } + if { mkdir -p ${fsdir} } + foreground { redirfd -w 2 /dev/null umount ${fsdir} } + mount --bind -- ${sourcedir} ${fsdir} + } + } + foreground { fdmove -c 1 2 echo "Bad number of arguments ${#} (expected 1 or 3)" } + exit 100 +}
foreground { s6-rc -bu change vm-env }
@@ -20,4 +38,7 @@ foreground { redirfd -w 2 /dev/null s6-svwait -U /run/service/vmm/instance/${1} } -ch-remote --api-socket /run/vm/by-id/${1}/vmm boot +if { ch-remote --api-socket /run/vm/by-id/${1}/vmm boot } +case "${#}" { + 2 { s6-svwait -D /run/service/vmm/instance/${1} } +}
Hmm. I don't think this belongs in vm-start. You should be able to interact with the fs directory before starting the VM, outside of vm-start.
diff --git a/host/rootfs/os-release.in b/host/rootfs/os-release.in new file mode 100644 index 0000000000000000000000000000000000000000..8a167a39366dedc6ff9024efdb98383ec84618ec --- /dev/null +++ b/host/rootfs/os-release.in @@ -0,0 +1,13 @@ +NAME="Spectrum OS" +ID="spectrum"
The os-release(5) examples don't quote where unnecessary so let's follow that convention.
+PRETTY_NAME="Spectrum OS @VERSION@" +VERSION=@VERSION@ +VERSION_ID=@VERSION@ +IMAGE_ID=spectrum_os_@VERSION@
Reading the documentation it doesn't look like a version is supposed to be included here, and that's what IMAGE_VERSION is for?
+IMAGE_VERSION=@VERSION@ +RELEASE_TYPE=development +HOME_URL="https://www.spectrum-os.org"
No www is canonical.
diff --git a/host/rootfs/shell.nix b/host/rootfs/shell.nix index bd234e90ee19bdfa6591d29c518cb0dc393b01c8..b5ed566062da03944567dd610c88e1a58523e303 100644 --- a/host/rootfs/shell.nix +++ b/host/rootfs/shell.nix @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: 2022 Unikie
import ../../lib/call-package.nix ( -{ callSpectrumPackage, rootfs, pkgsStatic, srcOnly, stdenv +{ callSpectrumPackage, rootfs, srcOnly, stdenv , btrfs-progs, cryptsetup, jq, netcat, qemu_kvm, reuse, util-linux }:
Good catch but doesn't belong in this change.
diff --git a/vm/app/sysupdate.d/50-verity.transfer b/vm/app/sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..e437860426b8a651ca20ee7bddff1a9b3cf39507 --- /dev/null +++ b/vm/app/sysupdate.d/50-verity.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_OS_@v.verity + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/user/update-destination
What does "user" mean here?
diff --git a/vm/app/updates.nix b/vm/app/updates.nix new file mode 100644 index 0000000000000000000000000000000000000000..30beca30e578b5c869eaedf2fd7e8913bf616a0c --- /dev/null +++ b/vm/app/updates.nix @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +import ../../lib/call-package.nix ( +{ callSpectrumPackage, lib, pkgsMusl, pkgsStatic, src, writeScript, systemd }: + +pkgsMusl.callPackage ( +{ stdenvNoCC, curl }:
App VMs are Glibc, so no need for this callPackage.
+ +pkgsStatic.callPackage ( +{ execline, runCommand }:
This looks vestigial.
+ +let + raw_update_url = builtins.readFile ../../update-url; + update-url = + if builtins.match "^https?://([[:alnum:]:./?=~-]|%[[:xdigit:]]{2})+/\n$" raw_update_url == null then + builtins.abort "Bad update URL" + else + builtins.substring 0 (builtins.stringLength raw_update_url - 1) raw_update_url;
Let's just inline the URL here. We definitely do not need validations in Nix code.
+ sysupdate-d = stdenvNoCC.mkDerivation { + name = "spectrum-systemd-transfer-files"; + src = ./.; + installPhase = + '' + mkdir -- "$out" + ( + cd -- "$src" && + for i in sysupdate.d/*.transfer; do + s=''${i#sysupdate.d/} && + sed 's,@UPDATE_URL@,${update-url},g' < "$i" > "$out/$s" || exit + done + printf %s\\n '${update-url}' > "$out/update-url" + ) || exit + ''; + };
We have pkgs.substitute and friends for this.
+ l = lib.escapeShellArgs; + mountpoint = "/run/virtiofs/virtiofs0/user"; + sysupdate-path = "${systemd}/lib/systemd/systemd-sysupdate"; + runner = writeScript "update-run-script" ( + "#!/bin/sh --\n" + + builtins.concatStringsSep " && \\\n" [ + (l ["mount" "-toverlay" "-olowerdir=${mountpoint}/etc:/etc" "--" "overlay" "/etc"]) + (l [sysupdate-path "--definitions=${sysupdate-d}" "update"]) + (l ["${curl}/bin/curl" "-L" "--proto" "=http,https" + "-o" "${mountpoint}/update-destination/SHA256SUMS.gpg" + "--" "${update-url}SHA256SUMS.gpg"]) + (l ["${curl}/bin/curl" "-L" "--proto" "=http,https" + "-o" "${mountpoint}/update-destination/SHA256SUMS" + "--" "${update-url}/SHA256SUMS"])
Why is this a shell script?
+ ]); +in + +callSpectrumPackage ../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = "${runner}"; +}) {}) {}) (_: {})
-- 2.51.2
This implements updates via systemd-sysupdate. See individual commit messages for details. There are major changes to the image build process. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes in v2: - updates-dir-check: - Do not check that there is a SHA256SUMS or SHA256SUMS.gpg file in the update directory. systemd-sysupdate will fail if it cannot find a manifest or its signature. - Follow symlinks in opening the directory. The path is from a trusted source and will always point to a BTRFS snapshot, never a symlink. The only exception is the last component, which is still checked to not be a symlink. - VM: - Link SHA256SUMS.sha256.asc to SHA256SUMS.gpg. Recent systemd-sysupdate seems to use the former name. - Get update URL from host. - Use an execline script instead of a shell script. - Update script: - Unmount shared directory if already mounted. This avoids errors when mounting it again. - Delete old snapshot if present. - Provide the VM information with a different directory layout. - Do not bind-mount the information passed into the VM into the shared VM folder. Instead rely on this folder being read-only to the guest. This is enforced by a read-only bind mount in virtiofs's mount namespace. - Testing: - Lots of manual update testing. - Disable the test for the live image as it doesn't work anymore. - Nix: - Move validation to a separate low-priority patch. - Documentation: - Document that updating the system is now possible. - Installer: - Remove the "Try Spectrum" button. - - Link to v1: https://spectrum-os.org/lists/archives/spectrum-devel/20251029-updates-v1-0-... --- Demi Marie Obenour (8): host/rootfs: Install all programs from util-linuxMinimal host/rootfs: Install systemd-pull tools: Add directory checker for updates Adjust partition layout to support updates release: Create directory with system update Support updates via systemd-sysupdate Documentation: Update support lib/config.nix: Validate configuration parameters Documentation/development/build-configuration.adoc | 11 ++ Documentation/installation/index.adoc | 3 +- Documentation/using-spectrum/index.adoc | 2 + Documentation/using-spectrum/updates.adoc | 29 +++++ host/efi.nix | 5 +- host/initramfs/Makefile | 12 +- host/initramfs/default.nix | 1 + host/initramfs/etc/init | 17 +-- host/initramfs/etc/probe | 20 +-- host/initramfs/shell.nix | 2 + host/rootfs/Makefile | 23 ++-- host/rootfs/busybox-config | 134 +++++++++++++++++++++ host/rootfs/busybox-config.license | 4 + host/rootfs/default.nix | 82 ++++++++----- host/rootfs/file-list.mk | 4 + host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++ host/rootfs/image/usr/bin/update | 89 ++++++++++++++ host/rootfs/os-release.in | 13 ++ host/rootfs/os-release.in.license | 2 + host/rootfs/shell.nix | 2 + host/rootfs/updatevm-url-env | 3 + host/rootfs/vm-sysupdate.d/50-verity.transfer | 18 +++ host/rootfs/vm-sysupdate.d/60-root.transfer | 18 +++ host/rootfs/vm-sysupdate.d/70-kernel.transfer | 18 +++ img/app/Makefile | 2 +- img/app/default.nix | 1 + lib/config.default.nix | 3 + lib/config.nix | 41 ++++++- lib/fake-update-signing-key.gpg | 1 + lib/fake-update-signing-key.gpg.license | 2 + lib/kcmdline-utils.mk | 5 + release.nix | 2 + release/checks/integration/try.c | 4 + release/checks/no-roothash.nix | 2 +- release/combined/eosimages.nix | 14 ++- release/combined/grub.cfg.in | 5 - release/live/Makefile | 9 +- release/live/default.nix | 8 +- release/live/shell.nix | 4 +- release/update.nix | 30 +++++ scripts/format-uuid.awk | 35 ++++++ scripts/make-gpt.bash | 72 +++++++++++ scripts/make-gpt.sh | 67 +---------- scripts/make-live-image.sh | 43 +++++++ scripts/sfdisk-field.awk | 3 +- tools/default.nix | 1 + tools/meson.build | 4 + tools/updates-dir-check.c | 78 ++++++++++++ vm/app/updates.nix | 37 ++++++ vm/sys/net/Makefile | 2 +- vm/sys/net/default.nix | 1 + 54 files changed, 895 insertions(+), 154 deletions(-) --- base-commit: 001037d8841613f2858e79daee83a930799d2f6c change-id: 20250928-updates-92e99849e231 prerequisite-patch-id: a756e9643d106f5ce5ab1bb713875f9551ba6e2a prerequisite-patch-id: 33a6c22a2130e94e6d09bafcab75b8263f66ee75 -- Sincerely, Demi Marie Obenour (she/her/hers)
Busybox fdisk doesn't support GPT, only MBR. Busybox programs are also often buggy, so use the util-linux version where possible. This requires disabling a lot of Busybox applets, so move the Busybox config to a separate file that Nix loads via builtins.readFile. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/busybox-config | 134 +++++++++++++++++++++++++++++++++++++ host/rootfs/busybox-config.license | 4 ++ host/rootfs/default.nix | 36 +++------- 3 files changed, 147 insertions(+), 27 deletions(-) diff --git a/host/rootfs/busybox-config b/host/rootfs/busybox-config new file mode 100644 index 0000000000000000000000000000000000000000..f2fd5fcab4141ea63c663e433412a253d5235aab --- /dev/null +++ b/host/rootfs/busybox-config @@ -0,0 +1,134 @@ +CONFIG_ADDPART n +CONFIG_AGETTY n +CONFIG_BITS n +CONFIG_BLKDISCARD n +CONFIG_BLKID n +CONFIG_BLKPR n +CONFIG_BLKZONE n +CONFIG_BLOCKDEV n +CONFIG_CAL n +CONFIG_CHATTR n +CONFIG_CHCPU n +CONFIG_CHMEM n +CONFIG_CHOOM n +CONFIG_CHRT n +CONFIG_COLCRT n +CONFIG_COLRM n +CONFIG_COLUMN n +CONFIG_CORESCHED n +CONFIG_CTRLALTDEL n +CONFIG_DELPART n +CONFIG_DEPMOD n +CONFIG_DMESG n +CONFIG_EJECT n +CONFIG_ENOSYS n +CONFIG_EXCH n +CONFIG_FADVISE n +CONFIG_FALLOCATE n +CONFIG_FDISK n +CONFIG_FINCORE n +CONFIG_FINDFS n +CONFIG_FINDMNT n +CONFIG_FLOCK n +CONFIG_FSCK n +CONFIG_FSCK_CRAMFS n +CONFIG_FSCK_MINIX n +CONFIG_FSFREEZE n +CONFIG_FSTRIM n +CONFIG_GETOPT n +CONFIG_HALT n +CONFIG_HARDLINK n +CONFIG_HD n +CONFIG_HEXDUMP n +CONFIG_HWCLOCK n +CONFIG_I386 n +CONFIG_INIT n +CONFIG_INSMOD n +CONFIG_IONICE n +CONFIG_IP n +CONFIG_IPCMK n +CONFIG_IPCRM n +CONFIG_IPCS n +CONFIG_ISOSIZE n +CONFIG_KILL n +CONFIG_LAST n +CONFIG_LASTB n +CONFIG_LDATTACH n +CONFIG_LINUX32 n +CONFIG_LINUX64 n +CONFIG_LOGGER n +CONFIG_LOOK n +CONFIG_LOSETUP n +CONFIG_LSATTR n +CONFIG_LSBLK n +CONFIG_LSCLOCKS n +CONFIG_LSCPU n +CONFIG_LSFD n +CONFIG_LSIPC n +CONFIG_LSIRQ n +CONFIG_LSLOCKS n +CONFIG_LSLOGINS n +CONFIG_LSMEM n +CONFIG_LSMOD n +CONFIG_LSNS n +CONFIG_MCOOKIE n +CONFIG_MESG n +CONFIG_MKE2FS n +CONFIG_MKFS n +CONFIG_MKFS_BFS n +CONFIG_MKFS_CRAMFS n +CONFIG_MKFS_EXT2 n +CONFIG_MKFS_MINIX n +CONFIG_MKSWAP n +CONFIG_MODINFO n +CONFIG_MODPROBE n +CONFIG_MOUNT n +CONFIG_MOUNTPOINT n +CONFIG_NAMEI n +CONFIG_NOLOGIN n +CONFIG_NSENTER n +CONFIG_PARTX n +CONFIG_PIPESZ n +CONFIG_PIVOT_ROOT n +CONFIG_POWEROFF n +CONFIG_PRLIMIT n +CONFIG_READPROFILE n +CONFIG_REBOOT n +CONFIG_RENAME n +CONFIG_RENICE n +CONFIG_RESIZEPART n +CONFIG_REV n +CONFIG_RFKILL n +CONFIG_RMMOD n +CONFIG_RTCWAKE n +CONFIG_SCRIPT n +CONFIG_SCRIPTLIVE n +CONFIG_SCRIPTREPLAY n +CONFIG_SETARCH n +CONFIG_SETPGID n +CONFIG_SETPRIV n +CONFIG_SETSID n +CONFIG_SFDISK n +CONFIG_SHUTDOWN n +CONFIG_SULOGIN n +CONFIG_SWAPLABEL n +CONFIG_SWAPOFF n +CONFIG_SWAPON n +CONFIG_SWITCH_ROOT n +CONFIG_TASKSET n +CONFIG_UCLAMPSET n +CONFIG_UMOUNT n +CONFIG_UNAME26 n +CONFIG_UNSHARE n +CONFIG_UTMPDUMP n +CONFIG_UUIDD n +CONFIG_UUIDGEN n +CONFIG_UUIDPARSE n +CONFIG_WAITPID n +CONFIG_WALL n +CONFIG_WDCTL n +CONFIG_WHEREIS n +CONFIG_WIPEFS n +CONFIG_WRITE n +CONFIG_X86_64 n +CONFIG_ZRAMCTL n diff --git a/host/rootfs/busybox-config.license b/host/rootfs/busybox-config.license new file mode 100644 index 0000000000000000000000000000000000000000..ba50b647fbfac9b79ecb29f33a36c07d3e332ba2 --- /dev/null +++ b/host/rootfs/busybox-config.license @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2025 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index eb6c64067091ef3802596ce581f82f322f5bfe34..aea2e46bb5998176eb6d9b8aef802ae270fdd28c 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -38,25 +38,8 @@ let virtiofsd xdg-desktop-portal-spectrum-host (busybox.override { - extraConfig = '' - CONFIG_CHATTR n - CONFIG_DEPMOD n - CONFIG_FINDFS n - CONFIG_HALT n - CONFIG_INIT n - CONFIG_INSMOD n - CONFIG_IP n - CONFIG_LSATTR n - CONFIG_LSMOD n - CONFIG_MKE2FS n - CONFIG_MKFS_EXT2 n - CONFIG_MODINFO n - CONFIG_MODPROBE n - CONFIG_MOUNT n - CONFIG_POWEROFF n - CONFIG_REBOOT n - CONFIG_RMMOD n - ''; + # Use a separate file as it is a bit too big. + extraConfig = builtins.readFile ./busybox-config; }) # Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. @@ -96,6 +79,12 @@ let mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps + # lndir silently ignores existing links, so run it before ln + # so that ln catches any duplicates. + for pkg in ${escapeShellArgs usrPackages}; do + lndir -ignorelinks -silent "$pkg" "$out/usr" + done + # Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \ @@ -110,18 +99,11 @@ let ln -st $out/usr/share/dbus-1/services \ ${pkgsGui.xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service - for pkg in ${escapeShellArgs usrPackages}; do - lndir -ignorelinks -silent "$pkg" "$out/usr" - done + ln -st "$out/usr/bin" ${util-linuxMinimal}/bin/* ${concatStrings (mapAttrsToList (name: path: '' ln -s ${path} $out/usr/lib/spectrum/vm/${name} '') appvms)} - - # TODO: this is a hack and we should just build the util-linux - # programs we want. - # https://lore.kernel.org/util-linux/87zgrl6ufb.fsf@alyssa.is/ - ln -s ${util-linuxMinimal}/bin/{findfs,uuidgen,lsblk,mount} $out/usr/bin ''; in -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
Busybox fdisk doesn't support GPT, only MBR. Busybox programs are also often buggy, so use the util-linux version where possible. This requires disabling a lot of Busybox applets, so move the Busybox config to a separate file that Nix loads via builtins.readFile.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/busybox-config | 134 +++++++++++++++++++++++++++++++++++++ host/rootfs/busybox-config.license | 4 ++ host/rootfs/default.nix | 36 +++------- 3 files changed, 147 insertions(+), 27 deletions(-)
diff --git a/host/rootfs/busybox-config.license b/host/rootfs/busybox-config.license new file mode 100644 index 0000000000000000000000000000000000000000..ba50b647fbfac9b79ecb29f33a36c07d3e332ba2 --- /dev/null +++ b/host/rootfs/busybox-config.license @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2025 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index eb6c64067091ef3802596ce581f82f322f5bfe34..aea2e46bb5998176eb6d9b8aef802ae270fdd28c 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -38,25 +38,8 @@ let virtiofsd xdg-desktop-portal-spectrum-host
(busybox.override { - extraConfig = '' - CONFIG_CHATTR n - CONFIG_DEPMOD n - CONFIG_FINDFS n - CONFIG_HALT n - CONFIG_INIT n - CONFIG_INSMOD n - CONFIG_IP n - CONFIG_LSATTR n - CONFIG_LSMOD n - CONFIG_MKE2FS n - CONFIG_MKFS_EXT2 n - CONFIG_MODINFO n - CONFIG_MODPROBE n - CONFIG_MOUNT n - CONFIG_POWEROFF n - CONFIG_REBOOT n - CONFIG_RMMOD n - ''; + # Use a separate file as it is a bit too big. + extraConfig = builtins.readFile ./busybox-config; })
# Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. @@ -96,6 +79,12 @@ let mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps
+ # lndir silently ignores existing links, so run it before ln + # so that ln catches any duplicates. + for pkg in ${escapeShellArgs usrPackages}; do + lndir -ignorelinks -silent "$pkg" "$out/usr" + done + # Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \ @@ -110,18 +99,11 @@ let ln -st $out/usr/share/dbus-1/services \ ${pkgsGui.xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service
- for pkg in ${escapeShellArgs usrPackages}; do - lndir -ignorelinks -silent "$pkg" "$out/usr" - done + ln -st "$out/usr/bin" ${util-linuxMinimal}/bin/*
Why not just add it to packages?
${concatStrings (mapAttrsToList (name: path: '' ln -s ${path} $out/usr/lib/spectrum/vm/${name} '') appvms)} - - # TODO: this is a hack and we should just build the util-linux - # programs we want. - # https://lore.kernel.org/util-linux/87zgrl6ufb.fsf@alyssa.is/ - ln -s ${util-linuxMinimal}/bin/{findfs,uuidgen,lsblk,mount} $out/usr/bin ''; in
-- 2.51.2
Due to a systemd bug [1], building systemd-sysupdate does not require that systemd-pull is built as well. However, systemd-sysupdate has a run-time dependency on systemd-pull. Therefore, override the systemd derivation so that systemd-pull is built. Confusingly, this requires enabling systemd-importd. If systemd-pull or systemd-sysupdate is not built, the resulting image will be broken and users will not be able to recover without either a reinstall or reverting to the previous version. Therefore, add a check to ensure that both are in fact built. Use 'cat' rather than just 'stat' to catch broken symlinks and the like. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/default.nix | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index aea2e46bb5998176eb6d9b8aef802ae270fdd28c..c2045ad96cca37a1bf1a7b82aa35a583cc5aee93 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -43,7 +43,8 @@ let }) # Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. - ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod systemd ]); + ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod ]); + nixosAllHardware = nixos ({ modulesPath, ... }: { imports = [ (modulesPath + "/profiles/all-hardware.nix") ]; @@ -64,7 +65,15 @@ let # https://inbox.vuxu.org/musl/20251017-dlopen-use-rpath-of-caller-dso-v1-1-46c... usrPackages = [ appvm kernel.modules firmware netvm - ] ++ (with pkgsGui; [ dejavu_fonts kmod.lib mesa westonLite ]); + ] ++ (with pkgsGui; [ + dejavu_fonts kmod.lib mesa westonLite + # Work around NixOS/nixpkgs#459020: without "withImportd = true" + # systemd-pull doesn't get built, so systemd-sysupdate doesn't work. + (systemd.override { + withImportd = true; + withSysupdate = true; + }) + ]); appvms = { appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; @@ -85,6 +94,16 @@ let lndir -ignorelinks -silent "$pkg" "$out/usr" done + # If systemd-pull is missing systemd-sysupdate will fail with a + # very confusing error message. If systemd-sysupdate doesn't work, + # users will not be able to receive an update that fixes the problem. + for i in sysupdate pull; do + if ! cat -- "$out/usr/lib/systemd/systemd-$i" > /dev/null; then + echo "link to systemd-$i didn't get installed" >&2 + exit 1 + fi + done + # Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \ -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
Due to a systemd bug [1], building systemd-sysupdate does not require that systemd-pull is built as well. However, systemd-sysupdate has a run-time dependency on systemd-pull. Therefore, override the systemd derivation so that systemd-pull is built. Confusingly, this requires enabling systemd-importd.
If systemd-pull or systemd-sysupdate is not built, the resulting image will be broken and users will not be able to recover without either a reinstall or reverting to the previous version. Therefore, add a check to ensure that both are in fact built. Use 'cat' rather than just 'stat' to catch broken symlinks and the like.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/default.nix | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-)
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index aea2e46bb5998176eb6d9b8aef802ae270fdd28c..c2045ad96cca37a1bf1a7b82aa35a583cc5aee93 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -43,7 +43,8 @@ let })
# Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. - ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod systemd ]); + ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod ]); +
nixosAllHardware = nixos ({ modulesPath, ... }: { imports = [ (modulesPath + "/profiles/all-hardware.nix") ]; @@ -64,7 +65,15 @@ let # https://inbox.vuxu.org/musl/20251017-dlopen-use-rpath-of-caller-dso-v1-1-46c... usrPackages = [ appvm kernel.modules firmware netvm - ] ++ (with pkgsGui; [ dejavu_fonts kmod.lib mesa westonLite ]); + ] ++ (with pkgsGui; [ + dejavu_fonts kmod.lib mesa westonLite + # Work around NixOS/nixpkgs#459020: without "withImportd = true" + # systemd-pull doesn't get built, so systemd-sysupdate doesn't work. + (systemd.override { + withImportd = true; + withSysupdate = true; + }) + ]);
Let's fix this upstream instead: https://github.com/NixOS/nixpkgs/pull/461277
appvms = { appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; @@ -85,6 +94,16 @@ let lndir -ignorelinks -silent "$pkg" "$out/usr" done
+ # If systemd-pull is missing systemd-sysupdate will fail with a + # very confusing error message. If systemd-sysupdate doesn't work, + # users will not be able to receive an update that fixes the problem. + for i in sysupdate pull; do + if ! cat -- "$out/usr/lib/systemd/systemd-$i" > /dev/null; then + echo "link to systemd-$i didn't get installed" >&2 + exit 1 + fi + done + # Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \
Looks like this will be fixed upstream (as a build error) in the next systemd release.
On 11/13/25 10:22, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Due to a systemd bug [1], building systemd-sysupdate does not require that systemd-pull is built as well. However, systemd-sysupdate has a run-time dependency on systemd-pull. Therefore, override the systemd derivation so that systemd-pull is built. Confusingly, this requires enabling systemd-importd.
If systemd-pull or systemd-sysupdate is not built, the resulting image will be broken and users will not be able to recover without either a reinstall or reverting to the previous version. Therefore, add a check to ensure that both are in fact built. Use 'cat' rather than just 'stat' to catch broken symlinks and the like.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/default.nix | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-)
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index aea2e46bb5998176eb6d9b8aef802ae270fdd28c..c2045ad96cca37a1bf1a7b82aa35a583cc5aee93 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -43,7 +43,8 @@ let })
# Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. - ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod systemd ]); + ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod ]); +
nixosAllHardware = nixos ({ modulesPath, ... }: { imports = [ (modulesPath + "/profiles/all-hardware.nix") ]; @@ -64,7 +65,15 @@ let # https://inbox.vuxu.org/musl/20251017-dlopen-use-rpath-of-caller-dso-v1-1-46c... usrPackages = [ appvm kernel.modules firmware netvm - ] ++ (with pkgsGui; [ dejavu_fonts kmod.lib mesa westonLite ]); + ] ++ (with pkgsGui; [ + dejavu_fonts kmod.lib mesa westonLite + # Work around NixOS/nixpkgs#459020: without "withImportd = true" + # systemd-pull doesn't get built, so systemd-sysupdate doesn't work. + (systemd.override { + withImportd = true; + withSysupdate = true; + }) + ]);
Let's fix this upstream instead:
Is it okay if I keep this until that PR is merged and Spectrum uses a nixpkgs that has it? I'd prefer to not send patches that I can't test, and without that PR the override is still needed. Even after the PR is merged, the override is harmless. I'll add a link to the PR in a comment.
appvms = { appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; @@ -85,6 +94,16 @@ let lndir -ignorelinks -silent "$pkg" "$out/usr" done
+ # If systemd-pull is missing systemd-sysupdate will fail with a + # very confusing error message. If systemd-sysupdate doesn't work, + # users will not be able to receive an update that fixes the problem. + for i in sysupdate pull; do + if ! cat -- "$out/usr/lib/systemd/systemd-$i" > /dev/null; then + echo "link to systemd-$i didn't get installed" >&2 + exit 1 + fi + done + # Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \
Looks like this will be fixed upstream (as a build error) in the next systemd release.
Indeed so. -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 10:22, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Due to a systemd bug [1], building systemd-sysupdate does not require that systemd-pull is built as well. However, systemd-sysupdate has a run-time dependency on systemd-pull. Therefore, override the systemd derivation so that systemd-pull is built. Confusingly, this requires enabling systemd-importd.
If systemd-pull or systemd-sysupdate is not built, the resulting image will be broken and users will not be able to recover without either a reinstall or reverting to the previous version. Therefore, add a check to ensure that both are in fact built. Use 'cat' rather than just 'stat' to catch broken symlinks and the like.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/default.nix | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-)
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index aea2e46bb5998176eb6d9b8aef802ae270fdd28c..c2045ad96cca37a1bf1a7b82aa35a583cc5aee93 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -43,7 +43,8 @@ let })
# Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. - ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod systemd ]); + ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod ]); +
nixosAllHardware = nixos ({ modulesPath, ... }: { imports = [ (modulesPath + "/profiles/all-hardware.nix") ]; @@ -64,7 +65,15 @@ let # https://inbox.vuxu.org/musl/20251017-dlopen-use-rpath-of-caller-dso-v1-1-46c... usrPackages = [ appvm kernel.modules firmware netvm - ] ++ (with pkgsGui; [ dejavu_fonts kmod.lib mesa westonLite ]); + ] ++ (with pkgsGui; [ + dejavu_fonts kmod.lib mesa westonLite + # Work around NixOS/nixpkgs#459020: without "withImportd = true" + # systemd-pull doesn't get built, so systemd-sysupdate doesn't work. + (systemd.override { + withImportd = true; + withSysupdate = true; + }) + ]);
Let's fix this upstream instead:
Is it okay if I keep this until that PR is merged and Spectrum uses a nixpkgs that has it? I'd prefer to not send patches that I can't test, and without that PR the override is still needed. Even after the PR is merged, the override is harmless. I'll add a link to the PR in a comment.
Yeah that's fine.
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either. Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. It also ensures that both SHA256SUMS and SHA256SUMS.gpg are present. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- tools/default.nix | 1 + tools/meson.build | 4 +++ tools/updates-dir-check.c | 78 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/tools/default.nix b/tools/default.nix index 18d4dd6353edf5c128213fa5c1716717f90edf07..a1b352e660f02a53e97ed1e3420a4de90bb24ce3 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -77,6 +77,7 @@ stdenv.mkDerivation (finalAttrs: { ./sd-notify-adapter.c ./start-vmm ./subprojects + ./updates-dir-check.c ] ++ lib.optionals driverSupport [ ./xdp-forwarder ])); diff --git a/tools/meson.build b/tools/meson.build index d465e99c2ef597fdf7e818748d08db3d96f4ec6b..a7c21684dd64ad9e87c85bcdf31792e81b55faa4 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -29,6 +29,10 @@ if get_option('host') c_args : '-D_GNU_SOURCE', install: true) + executable('updates-dir-check', 'updates-dir-check.c', + c_args : '-D_GNU_SOURCE', + install: true) + subdir('lsvm') subdir('start-vmm') endif diff --git a/tools/updates-dir-check.c b/tools/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..15b58204476299d8e7fe7ffdbac5245e04332e7d --- /dev/null +++ b/tools/updates-dir-check.c @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> + +#include <linux/openat2.h> +#include <sys/syscall.h> +#include <unistd.h> + +#include <err.h> + +static void checkdir(int fd) +{ + DIR *d = fdopendir(fd); + if (d == NULL) + err(EXIT_FAILURE, "fdopendir"); + // If there is an I/O error while there are dirty pages outstanding, + // the dirty pages are silently discarded. This means that the contents + // of the filesystem can change behind userspace's back. Flush all + // dirty pages in the filesystem with the directory to prevent this. + if (syncfs(fd) != 0) + err(EXIT_FAILURE, "syncfs"); + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(EXIT_FAILURE, "readdir"); + break; + } + assert(entry->d_reclen > offsetof(struct dirent, d_name)); + size_t len = strnlen(entry->d_name, entry->d_reclen - offsetof(struct dirent, d_name)); + if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(EXIT_FAILURE, "Filename must begin with an ASCII letter"); + for (size_t i = 1; i < len; ++i) { + c = (unsigned char)entry->d_name[i]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) { + if (c >= 0x20 && c <= 0x7E) + errx(EXIT_FAILURE, "Forbidden subsequent character in filename: '%c'", (int)c); + else + errx(EXIT_FAILURE, "Forbidden subsequent character in filename: byte %d", (int)c); + } + } + if (entry->d_name[len - 1] == '.') + errx(EXIT_FAILURE, "Filename must not end with a '.'"); + if (entry->d_type != DT_REG) + errx(EXIT_FAILURE, "Entry contains non-regular file %s", entry->d_name); + } + closedir(d); +} + +int main(int argc, char **argv) +{ + for (int i = 1; i < argc; ++i) { + int fd = open(argv[i], O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_NOFOLLOW); + if (fd < 0) + err(EXIT_FAILURE, "open(%s)", argv[i]); + checkdir(fd); + } + return 0; +} -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either.
Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. It also ensures that both SHA256SUMS and SHA256SUMS.gpg are present.
This needs updated, because it doesn't any more.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- tools/default.nix | 1 + tools/meson.build | 4 +++ tools/updates-dir-check.c | 78 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+)
diff --git a/tools/default.nix b/tools/default.nix index 18d4dd6353edf5c128213fa5c1716717f90edf07..a1b352e660f02a53e97ed1e3420a4de90bb24ce3 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -77,6 +77,7 @@ stdenv.mkDerivation (finalAttrs: { ./sd-notify-adapter.c ./start-vmm ./subprojects + ./updates-dir-check.c ] ++ lib.optionals driverSupport [ ./xdp-forwarder ])); diff --git a/tools/meson.build b/tools/meson.build index d465e99c2ef597fdf7e818748d08db3d96f4ec6b..a7c21684dd64ad9e87c85bcdf31792e81b55faa4 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -29,6 +29,10 @@ if get_option('host') c_args : '-D_GNU_SOURCE', install: true)
+ executable('updates-dir-check', 'updates-dir-check.c', + c_args : '-D_GNU_SOURCE', + install: true) + subdir('lsvm') subdir('start-vmm') endif diff --git a/tools/updates-dir-check.c b/tools/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..15b58204476299d8e7fe7ffdbac5245e04332e7d --- /dev/null +++ b/tools/updates-dir-check.c @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> + +#include <linux/openat2.h> +#include <sys/syscall.h> +#include <unistd.h> + +#include <err.h>
Kernel headers should always follow libc headers. Sometimes they rely on defines from libc (especially with musl). Although openat2 doesn't actually seem to be used any more, so these headers just need a prune.
+ +static void checkdir(int fd) +{ + DIR *d = fdopendir(fd); + if (d == NULL) + err(EXIT_FAILURE, "fdopendir"); + // If there is an I/O error while there are dirty pages outstanding, + // the dirty pages are silently discarded. This means that the contents + // of the filesystem can change behind userspace's back. Flush all + // dirty pages in the filesystem with the directory to prevent this. + if (syncfs(fd) != 0) + err(EXIT_FAILURE, "syncfs"); + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(EXIT_FAILURE, "readdir"); + break; + } + assert(entry->d_reclen > offsetof(struct dirent, d_name));
Would be a POSIX violation for d_name not to be valid I think.
+ size_t len = strnlen(entry->d_name, entry->d_reclen - offsetof(struct dirent, d_name));
POSIX also guarantees a terminating null byte, so strlen() would be fine here.
+ if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(EXIT_FAILURE, "Filename must begin with an ASCII letter");
Would the comparison not be valid without the cast?
+ for (size_t i = 1; i < len; ++i) { + c = (unsigned char)entry->d_name[i]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) { + if (c >= 0x20 && c <= 0x7E) + errx(EXIT_FAILURE, "Forbidden subsequent character in filename: '%c'", (int)c); + else + errx(EXIT_FAILURE, "Forbidden subsequent character in filename: byte %d", (int)c); + } + } + if (entry->d_name[len - 1] == '.') + errx(EXIT_FAILURE, "Filename must not end with a '.'");
I'm still not sold on this validation, but as long as it doesn't cause problems I guess it's fine.
+ if (entry->d_type != DT_REG) + errx(EXIT_FAILURE, "Entry contains non-regular file %s", entry->d_name); + } + closedir(d); +} + +int main(int argc, char **argv) +{ + for (int i = 1; i < argc; ++i) {
+ int fd = open(argv[i], O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_NOFOLLOW);
Wasn't O_NOFOLLOW going to be removed here?
+ if (fd < 0) + err(EXIT_FAILURE, "open(%s)", argv[i]); + checkdir(fd); + } + return 0; +}
-- 2.51.2
On 11/13/25 08:21, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either.
Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. It also ensures that both SHA256SUMS and SHA256SUMS.gpg are present.
This needs updated, because it doesn't any more.
Will fix in v3.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- tools/default.nix | 1 + tools/meson.build | 4 +++ tools/updates-dir-check.c | 78 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+)
diff --git a/tools/default.nix b/tools/default.nix index 18d4dd6353edf5c128213fa5c1716717f90edf07..a1b352e660f02a53e97ed1e3420a4de90bb24ce3 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -77,6 +77,7 @@ stdenv.mkDerivation (finalAttrs: { ./sd-notify-adapter.c ./start-vmm ./subprojects + ./updates-dir-check.c ] ++ lib.optionals driverSupport [ ./xdp-forwarder ])); diff --git a/tools/meson.build b/tools/meson.build index d465e99c2ef597fdf7e818748d08db3d96f4ec6b..a7c21684dd64ad9e87c85bcdf31792e81b55faa4 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -29,6 +29,10 @@ if get_option('host') c_args : '-D_GNU_SOURCE', install: true)
+ executable('updates-dir-check', 'updates-dir-check.c', + c_args : '-D_GNU_SOURCE', + install: true) + subdir('lsvm') subdir('start-vmm') endif diff --git a/tools/updates-dir-check.c b/tools/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..15b58204476299d8e7fe7ffdbac5245e04332e7d --- /dev/null +++ b/tools/updates-dir-check.c @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> + +#include <linux/openat2.h> +#include <sys/syscall.h> +#include <unistd.h> + +#include <err.h>
Kernel headers should always follow libc headers. Sometimes they rely on defines from libc (especially with musl). Although openat2 doesn't actually seem to be used any more, so these headers just need a prune.
Will fix.
+static void checkdir(int fd) +{ + DIR *d = fdopendir(fd); + if (d == NULL) + err(EXIT_FAILURE, "fdopendir"); + // If there is an I/O error while there are dirty pages outstanding, + // the dirty pages are silently discarded. This means that the contents + // of the filesystem can change behind userspace's back. Flush all + // dirty pages in the filesystem with the directory to prevent this. + if (syncfs(fd) != 0) + err(EXIT_FAILURE, "syncfs"); + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(EXIT_FAILURE, "readdir"); + break; + } + assert(entry->d_reclen > offsetof(struct dirent, d_name));
Would be a POSIX violation for d_name not to be valid I think.
Indeed it would be, but I preferred to check that explicitly.
+ size_t len = strnlen(entry->d_name, entry->d_reclen - offsetof(struct dirent, d_name));
POSIX also guarantees a terminating null byte, so strlen() would be fine here.
I preferred to double-check libc, but if you prefer to get rid of those assertions I'm okay with that.
+ if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(EXIT_FAILURE, "Filename must begin with an ASCII letter");
Would the comparison not be valid without the cast?
It would be in this case, but the subsequent cast to int in the error path assumes that the cast was done. Signed characters are much trickier and casting to unsigned makes the code easier to reason about.
+ for (size_t i = 1; i < len; ++i) { + c = (unsigned char)entry->d_name[i]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) { + if (c >= 0x20 && c <= 0x7E) + errx(EXIT_FAILURE, "Forbidden subsequent character in filename: '%c'", (int)c); + else + errx(EXIT_FAILURE, "Forbidden subsequent character in filename: byte %d", (int)c); + } + } + if (entry->d_name[len - 1] == '.') + errx(EXIT_FAILURE, "Filename must not end with a '.'");
I'm still not sold on this validation, but as long as it doesn't cause problems I guess it's fine.
+ if (entry->d_type != DT_REG) + errx(EXIT_FAILURE, "Entry contains non-regular file %s", entry->d_name); + } + closedir(d); +} + +int main(int argc, char **argv) +{ + for (int i = 1; i < argc; ++i) {
+ int fd = open(argv[i], O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_NOFOLLOW);
Wasn't O_NOFOLLOW going to be removed here?
It should never be called on a symlink, so if it does that is a bug in the caller. I can remove it if you prefer, though.
+ if (fd < 0) + err(EXIT_FAILURE, "open(%s)", argv[i]); + checkdir(fd); + } + return 0; +}
-- 2.51.2
-- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 08:21, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
+static void checkdir(int fd) +{ + DIR *d = fdopendir(fd); + if (d == NULL) + err(EXIT_FAILURE, "fdopendir"); + // If there is an I/O error while there are dirty pages outstanding, + // the dirty pages are silently discarded. This means that the contents + // of the filesystem can change behind userspace's back. Flush all + // dirty pages in the filesystem with the directory to prevent this. + if (syncfs(fd) != 0) + err(EXIT_FAILURE, "syncfs"); + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(EXIT_FAILURE, "readdir"); + break; + } + assert(entry->d_reclen > offsetof(struct dirent, d_name));
Would be a POSIX violation for d_name not to be valid I think.
Indeed it would be, but I preferred to check that explicitly.
+ size_t len = strnlen(entry->d_name, entry->d_reclen - offsetof(struct dirent, d_name));
POSIX also guarantees a terminating null byte, so strlen() would be fine here.
I preferred to double-check libc, but if you prefer to get rid of those assertions I'm okay with that.
I don't think it's Spectrum's place to confirm libc is POSIX compliant. Adding these checks isn't free, because it's more stuff to get past to understand what's going on.
+ if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(EXIT_FAILURE, "Filename must begin with an ASCII letter");
Would the comparison not be valid without the cast?
It would be in this case, but the subsequent cast to int in the error path assumes that the cast was done. Signed characters are much trickier and casting to unsigned makes the code easier to reason about.
Is it safe to assume 'A' etc. are representable and comparable as unsigned values? (I'm sure it is in practice, but I'd like it if I could be confident this is being done strictly correctly.)
+ if (entry->d_type != DT_REG) + errx(EXIT_FAILURE, "Entry contains non-regular file %s", entry->d_name); + } + closedir(d); +} + +int main(int argc, char **argv) +{ + for (int i = 1; i < argc; ++i) {
+ int fd = open(argv[i], O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_NOFOLLOW);
Wasn't O_NOFOLLOW going to be removed here?
It should never be called on a symlink, so if it does that is a bug in the caller. I can remove it if you prefer, though.
I don't think it's the place of this program to put constraints on its caller like that.
On 11/13/25 13:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 08:21, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
+static void checkdir(int fd) +{ + DIR *d = fdopendir(fd); + if (d == NULL) + err(EXIT_FAILURE, "fdopendir"); + // If there is an I/O error while there are dirty pages outstanding, + // the dirty pages are silently discarded. This means that the contents + // of the filesystem can change behind userspace's back. Flush all + // dirty pages in the filesystem with the directory to prevent this. + if (syncfs(fd) != 0) + err(EXIT_FAILURE, "syncfs"); + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(EXIT_FAILURE, "readdir"); + break; + } + assert(entry->d_reclen > offsetof(struct dirent, d_name));
Would be a POSIX violation for d_name not to be valid I think.
Indeed it would be, but I preferred to check that explicitly.
+ size_t len = strnlen(entry->d_name, entry->d_reclen - offsetof(struct dirent, d_name));
POSIX also guarantees a terminating null byte, so strlen() would be fine here.
I preferred to double-check libc, but if you prefer to get rid of those assertions I'm okay with that.
I don't think it's Spectrum's place to confirm libc is POSIX compliant. Adding these checks isn't free, because it's more stuff to get past to understand what's going on.
Oh, that makes sense.
+ if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(EXIT_FAILURE, "Filename must begin with an ASCII letter");
Would the comparison not be valid without the cast?
It would be in this case, but the subsequent cast to int in the error path assumes that the cast was done. Signed characters are much trickier and casting to unsigned makes the code easier to reason about.
Is it safe to assume 'A' etc. are representable and comparable as unsigned values? (I'm sure it is in practice, but I'd like it if I could be confident this is being done strictly correctly.)
I don't know if the C standard requires it, but we assume it.
+ if (entry->d_type != DT_REG) + errx(EXIT_FAILURE, "Entry contains non-regular file %s", entry->d_name); + } + closedir(d); +} + +int main(int argc, char **argv) +{ + for (int i = 1; i < argc; ++i) {
+ int fd = open(argv[i], O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_NOFOLLOW);
Wasn't O_NOFOLLOW going to be removed here?
It should never be called on a symlink, so if it does that is a bug in the caller. I can remove it if you prefer, though.
I don't think it's the place of this program to put constraints on its caller like that.
Makes sense. -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 13:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 08:21, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
+ if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(EXIT_FAILURE, "Filename must begin with an ASCII letter");
Would the comparison not be valid without the cast?
It would be in this case, but the subsequent cast to int in the error path assumes that the cast was done. Signed characters are much trickier and casting to unsigned makes the code easier to reason about.
Is it safe to assume 'A' etc. are representable and comparable as unsigned values? (I'm sure it is in practice, but I'd like it if I could be confident this is being done strictly correctly.)
I don't know if the C standard requires it, but we assume it.
Is it a safe assumption, at least on all Linux architectures?
On 11/14/25 08:08, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 13:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 08:21, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
+ if (entry->d_name[0] == '.') + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) + continue; + unsigned char c = (unsigned char)entry->d_name[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + errx(EXIT_FAILURE, "Filename must begin with an ASCII letter");
Would the comparison not be valid without the cast?
It would be in this case, but the subsequent cast to int in the error path assumes that the cast was done. Signed characters are much trickier and casting to unsigned makes the code easier to reason about.
Is it safe to assume 'A' etc. are representable and comparable as unsigned values? (I'm sure it is in practice, but I'd like it if I could be confident this is being done strictly correctly.)
I don't know if the C standard requires it, but we assume it.
Is it a safe assumption, at least on all Linux architectures?
I believe this assumption is hard-coded into GCC and Clang, so it's pretty safe. -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/14/25 08:08, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 13:01, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 08:21, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
> + if (entry->d_name[0] == '.') > + if (len == 1 || (len == 2 && entry->d_name[1] == '.')) > + continue; > + unsigned char c = (unsigned char)entry->d_name[0]; > + if (!((c >= 'A' && c <= 'Z') || > + (c >= 'a' && c <= 'z'))) > + errx(EXIT_FAILURE, "Filename must begin with an ASCII letter");
Would the comparison not be valid without the cast?
It would be in this case, but the subsequent cast to int in the error path assumes that the cast was done. Signed characters are much trickier and casting to unsigned makes the code easier to reason about.
Is it safe to assume 'A' etc. are representable and comparable as unsigned values? (I'm sure it is in practice, but I'd like it if I could be confident this is being done strictly correctly.)
I don't know if the C standard requires it, but we assume it.
Is it a safe assumption, at least on all Linux architectures?
I believe this assumption is hard-coded into GCC and Clang, so it's pretty safe.
Cool, okay.
systemd-sysupdate has strict requirements on the partition layout: - The label of the active partition must match the template in the .transfer file. For instance, the root filesystem of Spectrum 0.0.0 will be in a partition with label Spectrum_0.0.0. - The label of the inactive partition must either be that of the old version of Spectrum or "_empty". The former indicates an incomplete update. - The partition type UUID must conform to the Discoverable Partition Specification. After installing an image to a partition, systemd-sysupdate updates the label of the partition to match the image's version. However, it does not update the partition UUID. Therefore, use the partition label, not the partition UUID, to find the root filesystem and its verity metadata. systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, make the partitions very large so that there is plenty of room for the OS to grow. This requires rewriting the code that calculates the partition sizes. Since the partition label includes the OS version, add an OS version number. Use 0.0.0 to indicate that Spectrum OS is still in very early development and should not be used. The version number can be overridden in the build configuration file. mkfs.ext4 is not able to produce images with files large enough to hold both the primary and backup copy of the root partition [1]. Reducing the sizes of partitions to be little greater than the size of the root filesystem image does not help. The produced file is still too large. Therefore, compress the image, which causes it to be small enough that mkfs.ext4 can handle it. This breaks the live image, so remove it. The live image will return once Spectrum switches to the GNOME OS installer [2]. [1]: https://github.com/tytso/e2fsprogs/issues/254 [2]: https://spectrum-os.org/lists/archives/spectrum-devel/87wm4dlkhz.fsf@alyssa.... Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/development/build-configuration.adoc | 11 ++++ host/efi.nix | 5 +- host/initramfs/Makefile | 12 ++-- host/initramfs/default.nix | 1 + host/initramfs/etc/init | 17 ++--- host/initramfs/etc/probe | 20 +++--- host/initramfs/shell.nix | 2 + host/rootfs/Makefile | 21 ++++--- host/rootfs/default.nix | 1 + host/rootfs/shell.nix | 2 + img/app/Makefile | 2 +- img/app/default.nix | 1 + lib/config.default.nix | 1 + lib/config.nix | 3 +- lib/kcmdline-utils.mk | 5 ++ release/checks/integration/try.c | 4 ++ release/checks/no-roothash.nix | 2 +- release/combined/eosimages.nix | 14 +++-- release/combined/grub.cfg.in | 5 -- release/live/Makefile | 9 +-- release/live/default.nix | 8 ++- release/live/shell.nix | 3 +- scripts/format-uuid.awk | 35 +++++++++++ scripts/make-gpt.bash | 72 ++++++++++++++++++++++ scripts/make-gpt.sh | 67 +------------------- scripts/make-live-image.sh | 43 +++++++++++++ scripts/sfdisk-field.awk | 3 +- vm/sys/net/Makefile | 2 +- vm/sys/net/default.nix | 1 + 29 files changed, 248 insertions(+), 124 deletions(-) diff --git a/Documentation/development/build-configuration.adoc b/Documentation/development/build-configuration.adoc index 545aa8c05ac40a101b5ee280015cde7ec4f3a66f..0659d104efeeb8f483c24d8ea8d38a5d928d9358 100644 --- a/Documentation/development/build-configuration.adoc +++ b/Documentation/development/build-configuration.adoc @@ -40,3 +40,14 @@ for supported configuration attributes and their default values. }; } ---- + +.config.nix to adjust the version of the OS +[example] +[source,nix] +---- +{ default, ... }: + +{ + version = "0.0.1"; +} +---- diff --git a/host/efi.nix b/host/efi.nix index a2b47fd050fbf00050473a0d5a1373eb96c341b5..7ac3782fd15de1d1313f53d53239f46bf5dcc949 100644 --- a/host/efi.nix +++ b/host/efi.nix @@ -4,7 +4,7 @@ import ../lib/call-package.nix ( { bash, callSpectrumPackage, cryptsetup, runCommand -, stdenv, systemdUkify, rootfs +, stdenv, systemdUkify, rootfs, config }: let initramfs = callSpectrumPackage ./initramfs {}; @@ -27,6 +27,7 @@ runCommand "spectrum-efi" { KERNEL = kernel; INITRAMFS = initramfs; ROOTFS = rootfs; + VERSION = config.version; }; } '' read -r roothash < "$ROOTFS/rootfs.verity.roothash" @@ -41,6 +42,6 @@ runCommand "spectrum-efi" { --linux "$KERNEL" \ --initrd "$INITRAMFS" \ --os-release $'NAME="Spectrum"\n' \ - --cmdline "ro intel_iommu=on roothash=$roothash" + --cmdline "ro intel_iommu=on roothash=$roothash x-spectrum-version=$VERSION" '' ) (_: {}) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 102870ecba4456303414e2531ea592473ddfc1cf..bb3cdf49407c847832a849990ba1fdca79c60c30 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -4,6 +4,7 @@ .POSIX: include ../../lib/common.mk +include ../../lib/kcmdline-utils.mk dest = build/initramfs @@ -35,16 +36,13 @@ build/mountpoints: cd build/mountpoints && mkdir -p $(MOUNTPOINTS) find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';' -build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_VERITY) $(ROOT_FS_VERITY_ROOTHASH) $(ROOT_FS) - ../../scripts/make-gpt.sh $@.tmp \ - "$$ROOT_FS_VERITY:verity:$$(../../scripts/format-uuid.sh "$$(dd "if=$$ROOT_FS_VERITY_ROOTHASH" bs=32 skip=1 count=1 status=none)")" \ - $(dest):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 "$$ROOT_FS_VERITY_ROOTHASH")") - mv $@.tmp $@ +build/live.img: $(LIVE_IMAGE_DEPS) $(ROOT_FS) $(ROOT_FS_VERITY) $(ROOT_FS_VERITY_ROOTHASH) + ../../scripts/make-live-image.sh live $@ build/loop.tar: build/live.img $(TAR) -cf $@ build/live.img -build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 +build/loop.img: ../../scripts/make-gpt.sh ../../scripts/make-gpt.bash ../../scripts/sfdisk-field.awk build/loop.ext4 ../../scripts/make-gpt.sh $@.tmp \ build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e mv $@.tmp $@ @@ -58,7 +56,7 @@ run: $(dest) $(RUN_IMAGE) $(ROOT_FS_VERITY_ROOTHASH) -machine virtualization=on \ -kernel $(KERNEL) \ -initrd $(dest) \ - -append "ro earlycon console=hvc0 intel_iommu=on roothash=$$(< "$$ROOT_FS_VERITY_ROOTHASH") nokaslr" \ + -append "ro earlycon console=hvc0 intel_iommu=on roothash=$$(< "$$ROOT_FS_VERITY_ROOTHASH") x-spectrum-version=$$VERSION nokaslr" \ -cpu max \ -gdb unix:build/gdb.sock,server,nowait \ -parallel none \ diff --git a/host/initramfs/default.nix b/host/initramfs/default.nix index d35e1b514ec48015f5110e65e5ae944b28244c4f..dbbe3c5489a23b454c1a27e17aa7d431713e71fa 100644 --- a/host/initramfs/default.nix +++ b/host/initramfs/default.nix @@ -99,6 +99,7 @@ stdenvNoCC.mkDerivation { fileset = lib.fileset.intersection src (lib.fileset.unions [ ./. ../../lib/common.mk + ../../lib/kcmdline-utils.mk ]); }; sourceRoot = "source/host/initramfs"; diff --git a/host/initramfs/etc/init b/host/initramfs/etc/init index 719488741b6d31564c2c17c0e41f15d16b1c0a08..1488916120ad4042273e328c10686601a3abfcf1 100755 --- a/host/initramfs/etc/init +++ b/host/initramfs/etc/init @@ -6,22 +6,16 @@ export PATH /bin if { mount -a } -piperw 3 4 -if { fdmove 1 4 /etc/getuuids } -fdclose 4 -# head -1 would be clearer, but it might use buffered I/O and consume -# too much from the fifo. Ideally we'd have line(1) from illumos. -backtick ROOTFS_UUID { fdmove 0 3 dd count=1 bs=37 status=none } -backtick VERITY_UUID { fdmove 0 3 dd count=1 bs=37 status=none } -fdclose 3 - if { mkfifo /dev/rootfs.poll } background { - fdclose 3 mdevd -C -b134217728 } -importas -iu mdevd_pid ! + +multisubstitute { + importas -iu mdevd_pid ! + importas -i roothash roothash +} if { modprobe erofs } @@ -36,7 +30,6 @@ background { kill $mdevd_pid } background { rm /dev/rootfs.poll } if { - importas -Si roothash veritysetup open /dev/rootfs root-verity /dev/verity $roothash } diff --git a/host/initramfs/etc/probe b/host/initramfs/etc/probe index 4cbd00db52c1a7128b5c619a43d415675feaee0b..11a81c9be8f1adaef3cee17efdba1eb80e9fe3c7 100755 --- a/host/initramfs/etc/probe +++ b/host/initramfs/etc/probe @@ -14,9 +14,13 @@ if -n { forx -pE module { ext4 loop } modprobe $module } - backtick -E uuid { lsblk -lnpo PARTUUID $mdev } + backtick uuid { lsblk -lnpo PARTUUID $mdev } + multisubstiute { + define mdev_ $mdev + importas -Si uuid + } if { mkdir -p /mnt/${uuid} } - if { mount $mdev /mnt/${uuid} } + if { mount $mdev_ /mnt/${uuid} } find /mnt/${uuid} -name *.img -exec losetup -Pf {} ; @@ -24,11 +28,13 @@ if -n { # Check whether we now have all the partitions we need to boot. -importas -i rootfs_uuid ROOTFS_UUID -importas -i verity_uuid VERITY_UUID - -backtick -E rootfs_dev { findfs PARTUUID=${rootfs_uuid} } -backtick -E verity_dev { findfs PARTUUID=${verity_uuid} } +importas -i version x-spectrum-version +backtick rootfs_dev { findfs PARTLABEL=Spectrum_${version} } +backtick verity_dev { findfs PARTLABEL=Spectrum_${version}.verity } +multisubstitute { + importas -iS rootfs_dev + importas -iS verity_dev +} if { ln -s $rootfs_dev /dev/rootfs } if { ln -s $verity_dev /dev/verity } diff --git a/host/initramfs/shell.nix b/host/initramfs/shell.nix index fef8198685564bef2d8d673e0dc9403ee9c9a444..192b1c1de870c9049d2c32717b85706601999064 100644 --- a/host/initramfs/shell.nix +++ b/host/initramfs/shell.nix @@ -4,6 +4,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, rootfs, pkgsStatic, stdenv , cryptsetup, jq, qemu_kvm, tar2ext4, util-linux +, config }: let @@ -20,5 +21,6 @@ initramfs.overrideAttrs ({ nativeBuildInputs ? [], env ? {}, ... }: { ROOT_FS = "${rootfs}/rootfs"; ROOT_FS_VERITY = "${rootfs}/rootfs.verity.superblock"; ROOT_FS_VERITY_ROOTHASH = "${rootfs}/rootfs.verity.roothash"; + VERSION = config.version; }; })) (_: {}) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index d7764d9b796f1773b4bebd0d50eec52b9be29e42..76c8ff1628454d769e09e0bc915d198fece080e0 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -4,6 +4,7 @@ .POSIX: include ../../lib/common.mk +include ../../lib/kcmdline-utils.mk include file-list.mk dest = build @@ -38,9 +39,11 @@ DIRS = \ etc/s6-linux-init/run-image/vm/by-id \ etc/s6-linux-init/run-image/vm/by-name \ ext \ + home \ proc \ run \ - sys + sys \ + tmp FIFOS = etc/s6-linux-init/run-image/service/s6-svscan-log/fifo @@ -95,11 +98,10 @@ clean: rm -rf build .PHONY: clean -build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(dest)/timestamp - ../../scripts/make-gpt.sh $@.tmp \ - $(dest)/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(dest)/rootfs.verity.roothash bs=32 skip=1 count=1 status=none)") \ - $(dest)/rootfs:root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(dest)/rootfs.verity.roothash)") - mv $@.tmp $@ +build/live.img: $(LIVE_IMAGE_DEPS) $(dest)/timestamp + ROOT_FS=$(dest)/rootfs ROOT_FS_VERITY=$(dest)/rootfs.verity.superblock \ + ROOT_FS_VERITY_ROOTHASH=$(dest)/rootfs.verity.roothash \ + ../../scripts/make-live-image.sh live $@ debug: $(GDB) -q \ @@ -112,7 +114,10 @@ run: build/live.img @set -x && \ ext="$$(mktemp build/spectrum-rootfs-extfs.XXXXXXXXXX.img)" && \ truncate -s 10G "$$ext" && \ - mkfs.btrfs "$$ext" && \ + dir=$$(mktemp -d) && \ + mkdir -- "$$dir/tmp" "$$dir/home" && \ + mkfs.btrfs --rootdir "$$dir" --subvol tmp --subvol home -- "$$ext" && \ + rm -rf -- "$$dir" && \ exec 3<>"$$ext" && \ rm -f "$$ext" && \ set +x && \ @@ -129,7 +134,7 @@ run: build/live.img -device virtconsole,chardev=virtiocon0 \ -drive file=build/live.img,if=virtio,format=raw,readonly=on \ -drive file=/proc/self/fd/3,if=virtio,format=raw \ - -append "earlycon console=hvc0 roothash=$$(< $(dest)/rootfs.verity.roothash) intel_iommu=on nokaslr" \ + -append "earlycon console=hvc0 roothash=$$(< $(dest)/rootfs.verity.roothash) intel_iommu=on nokaslr x-spectrum-version=$$VERSION" \ -device virtio-keyboard \ -device virtio-mouse \ -device virtio-gpu \ diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index c2045ad96cca37a1bf1a7b82aa35a583cc5aee93..b574b8ddf5858867156507429a55b7f537e3c485 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -134,6 +134,7 @@ stdenvNoCC.mkDerivation { fileset = fileset.intersection src (fileset.unions [ ./. ../../lib/common.mk + ../../lib/kcmdline-utils.mk ../../scripts/make-erofs.sh ]); }; diff --git a/host/rootfs/shell.nix b/host/rootfs/shell.nix index 6df2f575fdfc7cdf8067ccfdb5fecaad9f6ea5e6..27f93e05fce036257d27cf9992fee8c925073f80 100644 --- a/host/rootfs/shell.nix +++ b/host/rootfs/shell.nix @@ -5,6 +5,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, rootfs, pkgsStatic, srcOnly, stdenv , btrfs-progs, cryptsetup, jq, netcat, qemu_kvm, reuse, util-linux +, config }: rootfs.overrideAttrs ( @@ -20,5 +21,6 @@ rootfs.overrideAttrs ( KERNEL = "${passthru.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; LINUX_SRC = srcOnly passthru.kernel.configfile; VMLINUX = "${passthru.kernel.dev}/vmlinux"; + VERSION = config.version; }; })) (_: {}) diff --git a/img/app/Makefile b/img/app/Makefile index 48eba871339d314479f730101246ace3fa39e2db..547b46c92661a900d02d2c5c8cae60f4008a7b7d 100644 --- a/img/app/Makefile +++ b/img/app/Makefile @@ -24,7 +24,7 @@ $(imgdir)/appvm/vmlinux: $(KERNEL) mkdir -p $$(dirname $@) cp $(KERNEL) $@ -$(imgdir)/appvm/blk/root.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/rootfs.erofs +$(imgdir)/appvm/blk/root.img: ../../scripts/make-gpt.sh ../../scripts/make-gpt.bash ../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) ../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:5460386f-2203-4911-8694-91400125c604:root diff --git a/img/app/default.nix b/img/app/default.nix index d9c4389d196ab19910849f012631461626775ef8..332569315db5a6be6101e6df127f6478d9302b17 100644 --- a/img/app/default.nix +++ b/img/app/default.nix @@ -98,6 +98,7 @@ stdenvNoCC.mkDerivation { ./. ../../lib/common.mk ../../scripts/make-erofs.sh + ../../scripts/make-gpt.bash ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk ]); diff --git a/lib/config.default.nix b/lib/config.default.nix index a8422345cc00f9413bb19ec968fd89c82fed801b..489c231490a8b66aa01f50053b25646060f7f963 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -4,4 +4,5 @@ { pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; + version = "0.0.0"; } diff --git a/lib/config.nix b/lib/config.nix index e437cdbe9aa22dd0f9c8d7052ac331c8fccf6ce6..01bcfa2bb2d5c412e212f5a60d9032e89c8a7442 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -18,5 +18,4 @@ let inherit default; } else config; in - -default // callConfig config + default // callConfig config; diff --git a/lib/kcmdline-utils.mk b/lib/kcmdline-utils.mk new file mode 100644 index 0000000000000000000000000000000000000000..5ed97c1a4b0c93d427fbb67f58736eee7fe09259 --- /dev/null +++ b/lib/kcmdline-utils.mk @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +LIVE_IMAGE_DEPS = ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/make-gpt.bash ../../scripts/sfdisk-field.awk ../../scripts/make-live-image.sh ../../lib/kcmdline-utils.mk diff --git a/release/checks/integration/try.c b/release/checks/integration/try.c index 4b874c0a7e9b48324497450fb5488e04576fd43b..c34b582230f75ff3374446468d2461a78c0099a6 100644 --- a/release/checks/integration/try.c +++ b/release/checks/integration/try.c @@ -10,6 +10,10 @@ void test(struct config c) { struct vm *vm; + // Spectrum's live image doesn't work right now. + // Mark the test as skipped. + exit(77); + c.drives.img = getenv_or_die("COMBINED_PATH"); vm = start_qemu(c); diff --git a/release/checks/no-roothash.nix b/release/checks/no-roothash.nix index 91e3beff1956265a2445d9eeaf69f9f206ec6347..aa08cf9dc4a4fc7386aa92d374300d4a0f011efc 100644 --- a/release/checks/no-roothash.nix +++ b/release/checks/no-roothash.nix @@ -28,6 +28,6 @@ in { machine = create_machine(flags) machine.start() - machine.wait_for_console_text("roothash invalid or missing") + machine.wait_for_console_text("roothash not set") ''; }))) (_: {}) diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..ba44d9cd82d55d491293ed36cc0402db8ebd3ffe 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -12,11 +12,15 @@ runCommand "eosimages.img" { unsafeDiscardReferences = { out = true; }; dontFixup = true; } '' + set -o pipefail mkdir dir cd dir - ln -s $image $imageName - sha256sum $imageName > $imageName.sha256 - tar -chf $NIX_BUILD_TOP/eosimages.tar * - tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out - e2label $out eosimages + ln -s -- "$image" "$imageName" + sha256sum -- "$imageName" > "$imageName.sha256" & + pid=$! + gzip -9 < "$image" > "$imageName.gz" + sha256sum -- "$imageName.gz" > "$imageName.gz.sha256" + wait "$pid" + tar -ch -- "$imageName.gz" "$imageName.gz.sha256" "$imageName.sha256" | tar2ext4 -o "$out" + e2label "$out" eosimages '') (_: {}) diff --git a/release/combined/grub.cfg.in b/release/combined/grub.cfg.in index a8e73a3b4dc0d643cf575e3cc545ec9ff72380cb..a22f5fc96ba6451d44c0f9768a15a1f48c5dce1c 100644 --- a/release/combined/grub.cfg.in +++ b/release/combined/grub.cfg.in @@ -15,11 +15,6 @@ set gfxpayload=keep terminal_output gfxterm terminal_output console -menuentry "Try Spectrum" { - loopback live (hd0,gpt3)/Spectrum-0.0-x86_64-generic.0.Live.img - chainloader (live,gpt1)/EFI/Linux/spectrum.efi -} - menuentry "Install Spectrum" { set root=(hd0,gpt2) linux @linux@ @kernelParams@ diff --git a/release/live/Makefile b/release/live/Makefile index 4de8743f42dec65aa863c3020cd70124316a6118..367010c86c5e64272a404a480ed8a43213b3a875 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -4,17 +4,14 @@ .POSIX: include ../../lib/common.mk +include ../../lib/kcmdline-utils.mk DTBS ?= build/empty dest = build/live.img -$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_VERITY) $(ROOT_FS_VERITY_ROOTHASH) $(ROOT_FS) - ../../scripts/make-gpt.sh $@.tmp \ - build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - "$$ROOT_FS_VERITY":verity:$$(../../scripts/format-uuid.sh "$$(dd if="$$ROOT_FS_VERITY_ROOTHASH" bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 "$$ROOT_FS_VERITY_ROOTHASH")") - mv $@.tmp $@ +$(dest): build/boot.fat $(LIVE_IMAGE_DEPS) $(ROOT_FS) $(ROOT_FS_VERITY) $(ROOT_FS_VERITY_ROOTHASH) + ../../scripts/make-live-image.sh release $@ build/empty: mkdir -p $@ diff --git a/release/live/default.nix b/release/live/default.nix index c234d87e62cc9ae65ba60f94bab6e58b43beddbc..dc649732ffa46a998a4a66360aa8ff7ef6bccae0 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -1,12 +1,13 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2021-2023, 2025 Alyssa Ross <hi@alyssa.is> # SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, rootfs, src , lib, pkgsStatic, stdenvNoCC , cryptsetup, dosfstools, jq, mtools, util-linux -, systemdUkify, efi +, systemdUkify, version, efi }: let @@ -25,8 +26,12 @@ stdenv.mkDerivation { fileset = lib.fileset.intersection src (lib.fileset.unions [ ./. ../../lib/common.mk + ../../lib/kcmdline-utils.mk + ../../scripts/format-uuid.awk ../../scripts/format-uuid.sh + ../../scripts/make-gpt.bash ../../scripts/make-gpt.sh + ../../scripts/make-live-image.sh ../../scripts/sfdisk-field.awk ]); }; @@ -44,6 +49,7 @@ stdenv.mkDerivation { SYSTEMD_BOOT_EFI = "${efi.systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; EFI_IMAGE = efi; EFINAME = "BOOT${toUpper efiArch}.EFI"; + VERSION = version; }; buildFlags = [ "dest=$(out)" ]; diff --git a/release/live/shell.nix b/release/live/shell.nix index 5acaa8c5b113fd2789aaea9268487b193bab37af..05250525defa0e8a10cde45b5e49f878fcec599f 100644 --- a/release/live/shell.nix +++ b/release/live/shell.nix @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> -import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm }: +import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm, rootfs }: (callSpectrumPackage ./. {}).overrideAttrs ( { nativeBuildInputs ? [], env ? {}, ... }: @@ -9,6 +9,7 @@ import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm }: nativeBuildInputs = nativeBuildInputs ++ [ qemu_kvm ]; env = env // { + ROOT_FS = rootfs; OVMF_CODE = "${qemu_kvm}/share/qemu/edk2-${stdenv.hostPlatform.qemuArch}-code.fd"; }; } diff --git a/scripts/format-uuid.awk b/scripts/format-uuid.awk new file mode 100644 index 0000000000000000000000000000000000000000..a5349d68a4d29be5f750650236420c9b5a7257eb --- /dev/null +++ b/scripts/format-uuid.awk @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +function format_uuid(arg) { + if (arg in found_so_far) { + fail("Duplicate UUID, try changing the image (by even 1 bit)"); + } + found_so_far[arg] = 1; + print (substr(arg, 1, 8) "-" \ + substr(arg, 9, 4) "-" \ + substr(arg, 13, 4) "-" \ + substr(arg, 17, 4) "-" \ + substr(arg, 21, 12)); +} + +function fail(msg) { + print msg > "/dev/stderr"; + exit 1; +} + +BEGIN { + FS = ""; + RS = "\n"; + if ((getline) != 1) + fail("Empty input file"); + roothash = $0; + if (roothash !~ /^[a-f0-9]{64}$/) + fail("Invalid root hash"); + if (getline) + fail("Junk after root hash"); + found_so_far[""] = ""; + for (i = 1; i != 49; i += 16) { + format_uuid(substr($0, i, 32)); + } + format_uuid(substr($0, 49, 16) substr($0, 1, 16)); +} diff --git a/scripts/make-gpt.bash b/scripts/make-gpt.bash new file mode 100644 index 0000000000000000000000000000000000000000..f9d53817e3cc4342cac5d4c832cf4aa129880399 --- /dev/null +++ b/scripts/make-gpt.bash @@ -0,0 +1,72 @@ +#!/usr/bin/bash -- +# SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2022 Unikie +# SPDX-License-Identifier: EUPL-1.2+ +# +# usage: make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... + +set -xeuo pipefail +ONE_MiB=1048576 + +# Prints the number of 1MiB blocks required to store the file named +# $1. We use 1MiB blocks because that's what sfdisk uses for +# alignment. It would be possible to get a slightly smaller image +# using actual normal-sized 512-byte blocks, but it's probably not +# worth it to configure sfdisk to do that. +sizeMiB() { + wc -c "$1" | awk -v ONE_MiB=$ONE_MiB \ + '{printf "%d\n", ($1 + ONE_MiB - 1) / ONE_MiB}' +} + +# Copies from path $3 into partition number $2 in partition table $1. +fillPartition() { + start="$(sfdisk -J "$1" | jq -r --argjson index "$2" \ + '.partitiontable.partitions[$index].start * 512')" + + # GNU cat will use copy_file_range(2) if possible, whereas dd + # will always do a userspace copy, which is significantly slower. + lseek -S 1 "$start" cat "$3" 1<>"$1" +} + +# Prints the partition path from a PATH:PARTTYPE[:PARTUUID[:PARTLABEL]] string. +partitionPath() { + awk -F: '{print $1}' <<EOF +$1 +EOF +} + +scriptsDir="$(dirname "$0")" + +out="$1" +shift + +table="label: gpt" + +# Keep 1MiB free at the start, and 1MiB free at the end. +gptBytes=$((ONE_MiB * 2)) +for partition; do + if [[ "$partition" =~ :([1-9][0-9]*)MiB$ ]]; then + sizeMiB=${BASH_REMATCH[1]} + partition=${partition%:*} + else + partitionPath=$(partitionPath "$partition") + sizeMiB=$(sizeMiB "$partitionPath") + fi + table=$table' +size='${sizeMiB}MiB$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition") + gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) +done + +rm -f "$out" +truncate -s "$gptBytes" "$out" +printf %s\\n "$table" +sfdisk --no-reread --no-tell-kernel "$out" <<EOF +$table +EOF + +n=0 +for partition; do + partitionPath=$(partitionPath "$partition") + fillPartition "$out" "$n" "$partitionPath" + n=$((n + 1)) +done diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh index 96f0d2c8494c093558c0e32e7e920b569bb078ef..665057da8281d2b5282081e4999098fbaa29e6ca 100755 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -1,65 +1,4 @@ -#!/bin/sh -eu -# -# SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> -# SPDX-FileCopyrightText: 2022 Unikie +#!/bin/sh -- # SPDX-License-Identifier: EUPL-1.2+ -# -# usage: make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... - -ONE_MiB=1048576 - -# Prints the number of 1MiB blocks required to store the file named -# $1. We use 1MiB blocks because that's what sfdisk uses for -# alignment. It would be possible to get a slightly smaller image -# using actual normal-sized 512-byte blocks, but it's probably not -# worth it to configure sfdisk to do that. -sizeMiB() { - wc -c "$1" | awk -v ONE_MiB=$ONE_MiB \ - '{printf "%d\n", ($1 + ONE_MiB - 1) / ONE_MiB}' -} - -# Copies from path $3 into partition number $2 in partition table $1. -fillPartition() { - start="$(sfdisk -J "$1" | jq -r --argjson index "$2" \ - '.partitiontable.partitions[$index].start * 512')" - - # GNU cat will use copy_file_range(2) if possible, whereas dd - # will always do a userspace copy, which is significantly slower. - lseek -S 1 "$start" cat "$3" 1<>"$1" -} - -# Prints the partition path from a PATH:PARTTYPE[:PARTUUID[:PARTLABEL]] string. -partitionPath() { - awk -F: '{print $1}' <<EOF -$1 -EOF -} - -scriptsDir="$(dirname "$0")" - -out="$1" -shift - -nl=' -' -table="label: gpt" - -# Keep 1MiB free at the start, and 1MiB free at the end. -gptBytes=$((ONE_MiB * 2)) -for partition; do - sizeMiB="$(sizeMiB "$(partitionPath "$partition")")" - table="$table${nl}size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" - gptBytes="$((gptBytes + sizeMiB * ONE_MiB))" -done - -rm -f "$out" -truncate -s "$gptBytes" "$out" -sfdisk --no-reread --no-tell-kernel "$out" <<EOF -$table -EOF - -n=0 -for partition; do - fillPartition "$out" "$n" "$(partitionPath "$partition")" - n="$((n + 1))" -done +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +exec bash -- "${0%.sh}.bash" "$@" diff --git a/scripts/make-live-image.sh b/scripts/make-live-image.sh new file mode 100755 index 0000000000000000000000000000000000000000..6608cc35b7a15178adf5ff3d3917b5243c5da6cd --- /dev/null +++ b/scripts/make-live-image.sh @@ -0,0 +1,43 @@ +#!/bin/sh -- +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +set -euo pipefail +case $0 in +(/*) dir=${0%/*}/;; +(*/*) dir=./${0%/*};; +(*) dir=.;; +esac +usage () { + echo 'Usage: make-live-image.sh [release|live] OUTPUT_FILE' >&2 + exit 1 +} +if [ "$#" != 2 ]; then usage; fi +file_type=$1 output_file=$2 +for i in "$ROOT_FS" "$ROOT_FS_VERITY" "$ROOT_FS_VERITY_ROOTHASH" "$VERSION"; do + # Some characters not special to the shell can't be handled by this code. + case $i in + (-*|*[!A-Za-z0-9._/+@-]*) printf 'Forbidden characters in "%s"\n' "$i" >&2; exit 1;; + esac +done +root_hashes=$(LC_ALL=C awk -f "${dir}/format-uuid.awk" < "$ROOT_FS_VERITY_ROOTHASH") +# The awk script produces output that is meant for field splitting +# and has no characters special for globbing. +# shellcheck disable=SC2086 +set -- $root_hashes +case $file_type in +(release) + "$dir/make-gpt.sh" "$output_file.tmp" \ + build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ + "$ROOT_FS_VERITY:verity:$1:Spectrum_$VERSION.verity:1024MiB" \ + "$ROOT_FS:root:$2:Spectrum_$VERSION:20480MiB" \ + "/dev/null:verity:$3:_empty:1024MiB" \ + "/dev/null:root:$4:_empty:20480MiB" + ;; +(live) + "$dir/make-gpt.sh" "$output_file.tmp" \ + "$ROOT_FS_VERITY:verity:$1:Spectrum_$VERSION.verity" \ + "$ROOT_FS:root:$2:Spectrum_$VERSION";; +(*) usage;; +esac +mv -- "$output_file.tmp" "$output_file" diff --git a/scripts/sfdisk-field.awk b/scripts/sfdisk-field.awk index e13c86d2fb11a066eebd043808e659b08dbd269c..72eec9a0a770563d32da14440fe2552eb2e39b68 100644 --- a/scripts/sfdisk-field.awk +++ b/scripts/sfdisk-field.awk @@ -24,6 +24,7 @@ BEGIN { arch = _arch } + comma = "" for (n in fields) { if (n <= skip) continue @@ -33,6 +34,6 @@ BEGIN { fields[n] = uuid } - printf "%s=%s,", keys[n - skip], fields[n] + printf ",%s%s=%s", comma, keys[n - skip], fields[n] } } diff --git a/vm/sys/net/Makefile b/vm/sys/net/Makefile index d71c2325eff3bae921f33c61f799846d35e401c2..20675dda7436394ebbf08e685323bdf8532618f3 100644 --- a/vm/sys/net/Makefile +++ b/vm/sys/net/Makefile @@ -23,7 +23,7 @@ $(vmdir)/netvm/vmlinux: $(KERNEL) mkdir -p $$(dirname $@) cp $(KERNEL) $@ -$(vmdir)/netvm/blk/root.img: ../../../scripts/make-gpt.sh ../../../scripts/sfdisk-field.awk build/rootfs.erofs +$(vmdir)/netvm/blk/root.img: ../../../scripts/make-gpt.sh ../../../scripts/make-gpt.bash ../../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) ../../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:ea21da27-0391-48da-9235-9d2ab2ca7844:root diff --git a/vm/sys/net/default.nix b/vm/sys/net/default.nix index de273e55360c29614e61aa4d1646019bbbfd2c25..994c19a11e37420c111b979d3dece0ad2037a5a1 100644 --- a/vm/sys/net/default.nix +++ b/vm/sys/net/default.nix @@ -106,6 +106,7 @@ stdenvNoCC.mkDerivation { ./. ../../../lib/common.mk ../../../scripts/make-erofs.sh + ../../../scripts/make-gpt.bash ../../../scripts/make-gpt.sh ../../../scripts/sfdisk-field.awk ]); -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
systemd-sysupdate has strict requirements on the partition layout:
- The label of the active partition must match the template in the .transfer file. For instance, the root filesystem of Spectrum 0.0.0 will be in a partition with label Spectrum_0.0.0. - The label of the inactive partition must either be that of the old version of Spectrum or "_empty". The former indicates an incomplete update.
Do you mean "the latter"?
- The partition type UUID must conform to the Discoverable Partition Specification.
After installing an image to a partition, systemd-sysupdate updates the label of the partition to match the image's version. However, it does not update the partition UUID. Therefore, use the partition label, not the partition UUID, to find the root filesystem and its verity metadata.
Seems a bit odd, considering I got this trick from systemd-veritysetup-generator. Is that not compatible with systemd-sysupdate?
systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, make the partitions very large so that there is plenty of room for the OS to grow. This requires rewriting the code that calculates the partition sizes.
Since the partition label includes the OS version, add an OS version number. Use 0.0.0 to indicate that Spectrum OS is still in very early development and should not be used. The version number can be overridden in the build configuration file.
mkfs.ext4 is not able to produce images with files large enough to hold both the primary and backup copy of the root partition [1]. Reducing the sizes of partitions to be little greater than the size of the root filesystem image does not help. The produced file is still too large. Therefore, compress the image, which causes it to be small enough that mkfs.ext4 can handle it. This breaks the live image, so remove it. The live image will return once Spectrum switches to the GNOME OS installer [2].
[1]: https://github.com/tytso/e2fsprogs/issues/254 [2]: https://spectrum-os.org/lists/archives/spectrum-devel/87wm4dlkhz.fsf@alyssa....
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/development/build-configuration.adoc | 11 ++++ host/efi.nix | 5 +- host/initramfs/Makefile | 12 ++-- host/initramfs/default.nix | 1 + host/initramfs/etc/init | 17 ++--- host/initramfs/etc/probe | 20 +++--- host/initramfs/shell.nix | 2 + host/rootfs/Makefile | 21 ++++--- host/rootfs/default.nix | 1 + host/rootfs/shell.nix | 2 + img/app/Makefile | 2 +- img/app/default.nix | 1 + lib/config.default.nix | 1 + lib/config.nix | 3 +- lib/kcmdline-utils.mk | 5 ++ release/checks/integration/try.c | 4 ++ release/checks/no-roothash.nix | 2 +- release/combined/eosimages.nix | 14 +++-- release/combined/grub.cfg.in | 5 -- release/live/Makefile | 9 +-- release/live/default.nix | 8 ++- release/live/shell.nix | 3 +- scripts/format-uuid.awk | 35 +++++++++++ scripts/make-gpt.bash | 72 ++++++++++++++++++++++ scripts/make-gpt.sh | 67 +------------------- scripts/make-live-image.sh | 43 +++++++++++++ scripts/sfdisk-field.awk | 3 +- vm/sys/net/Makefile | 2 +- vm/sys/net/default.nix | 1 + 29 files changed, 248 insertions(+), 124 deletions(-)
Would you mind splitting up this patch in your next submission? This is a lot to review all at once, and I doubt finding the rootfs from label is very difficult to separate from changing the size of the partitions, for example. I don't think I'm going to be able to do a full, thorough review with there being this many different things going on at once.
diff --git a/Documentation/development/build-configuration.adoc b/Documentation/development/build-configuration.adoc index 545aa8c05ac40a101b5ee280015cde7ec4f3a66f..0659d104efeeb8f483c24d8ea8d38a5d928d9358 100644 --- a/Documentation/development/build-configuration.adoc +++ b/Documentation/development/build-configuration.adoc @@ -40,3 +40,14 @@ for supported configuration attributes and their default values. }; } ---- + +.config.nix to adjust the version of the OS +[example] +[source,nix] +---- +{ default, ... }: + +{ + version = "0.0.1"; +} +----
Not sure this is necessary. The paragraph above already says to refer to lib/config.default.nix to see supported configuration options, so readers can learn about version there. It's not like pkgsArgs where it's quite complex to use so warrants additional handholding.
diff --git a/host/initramfs/etc/probe b/host/initramfs/etc/probe index 4cbd00db52c1a7128b5c619a43d415675feaee0b..11a81c9be8f1adaef3cee17efdba1eb80e9fe3c7 100755 --- a/host/initramfs/etc/probe +++ b/host/initramfs/etc/probe @@ -14,9 +14,13 @@ if -n { forx -pE module { ext4 loop } modprobe $module } - backtick -E uuid { lsblk -lnpo PARTUUID $mdev } + backtick uuid { lsblk -lnpo PARTUUID $mdev } + multisubstiute { + define mdev_ $mdev + importas -Si uuid + } if { mkdir -p /mnt/${uuid} } - if { mount $mdev /mnt/${uuid} } + if { mount $mdev_ /mnt/${uuid} } find /mnt/${uuid} -name *.img -exec losetup -Pf {} ;
I don't understand what this change does.
@@ -24,11 +28,13 @@ if -n {
# Check whether we now have all the partitions we need to boot.
-importas -i rootfs_uuid ROOTFS_UUID -importas -i verity_uuid VERITY_UUID - -backtick -E rootfs_dev { findfs PARTUUID=${rootfs_uuid} } -backtick -E verity_dev { findfs PARTUUID=${verity_uuid} } +importas -i version x-spectrum-version +backtick rootfs_dev { findfs PARTLABEL=Spectrum_${version} } +backtick verity_dev { findfs PARTLABEL=Spectrum_${version}.verity } +multisubstitute { + importas -iS rootfs_dev + importas -iS verity_dev +}
if { ln -s $rootfs_dev /dev/rootfs } if { ln -s $verity_dev /dev/verity }
Using multisubstitute is a good change but should be a separate patch since it's equally applicable to the current code as it is to yours.
diff --git a/lib/config.nix b/lib/config.nix index e437cdbe9aa22dd0f9c8d7052ac331c8fccf6ce6..01bcfa2bb2d5c412e212f5a60d9032e89c8a7442 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -18,5 +18,4 @@ let inherit default; } else config; in - -default // callConfig config + default // callConfig config;
Looks like an accidental inclusion.
diff --git a/lib/kcmdline-utils.mk b/lib/kcmdline-utils.mk new file mode 100644 index 0000000000000000000000000000000000000000..5ed97c1a4b0c93d427fbb67f58736eee7fe09259 --- /dev/null +++ b/lib/kcmdline-utils.mk @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +LIVE_IMAGE_DEPS = ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/make-gpt.bash ../../scripts/sfdisk-field.awk ../../scripts/make-live-image.sh ../../lib/kcmdline-utils.mk
Why is this called kcmdline-utils.mk? It doesn't seem to have anything to do with a kernel command line.
diff --git a/release/checks/integration/try.c b/release/checks/integration/try.c index 4b874c0a7e9b48324497450fb5488e04576fd43b..c34b582230f75ff3374446468d2461a78c0099a6 100644 --- a/release/checks/integration/try.c +++ b/release/checks/integration/try.c @@ -10,6 +10,10 @@ void test(struct config c) { struct vm *vm;
+ // Spectrum's live image doesn't work right now. + // Mark the test as skipped. + exit(77); + c.drives.img = getenv_or_die("COMBINED_PATH");
vm = start_qemu(c);
We can just delete the test.
diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..ba44d9cd82d55d491293ed36cc0402db8ebd3ffe 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -12,11 +12,15 @@ runCommand "eosimages.img" { unsafeDiscardReferences = { out = true; }; dontFixup = true; } '' + set -o pipefail mkdir dir cd dir - ln -s $image $imageName - sha256sum $imageName > $imageName.sha256 - tar -chf $NIX_BUILD_TOP/eosimages.tar * - tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out - e2label $out eosimages + ln -s -- "$image" "$imageName" + sha256sum -- "$imageName" > "$imageName.sha256" & + pid=$! + gzip -9 < "$image" > "$imageName.gz" + sha256sum -- "$imageName.gz" > "$imageName.gz.sha256" + wait "$pid" + tar -ch -- "$imageName.gz" "$imageName.gz.sha256" "$imageName.sha256" | tar2ext4 -o "$out" + e2label "$out" eosimages '') (_: {})
My comments on this from last time still apply[1]. [1]: https://spectrum-os.org/lists/archives/spectrum-devel/87v7jyj5a3.fsf@alyssa....
diff --git a/scripts/format-uuid.awk b/scripts/format-uuid.awk new file mode 100644 index 0000000000000000000000000000000000000000..a5349d68a4d29be5f750650236420c9b5a7257eb --- /dev/null +++ b/scripts/format-uuid.awk @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +function format_uuid(arg) { + if (arg in found_so_far) { + fail("Duplicate UUID, try changing the image (by even 1 bit)"); + } + found_so_far[arg] = 1; + print (substr(arg, 1, 8) "-" \ + substr(arg, 9, 4) "-" \ + substr(arg, 13, 4) "-" \ + substr(arg, 17, 4) "-" \ + substr(arg, 21, 12)); +} + +function fail(msg) { + print msg > "/dev/stderr"; + exit 1; +} + +BEGIN { + FS = ""; + RS = "\n"; + if ((getline) != 1) + fail("Empty input file"); + roothash = $0; + if (roothash !~ /^[a-f0-9]{64}$/) + fail("Invalid root hash"); + if (getline) + fail("Junk after root hash"); + found_so_far[""] = ""; + for (i = 1; i != 49; i += 16) { + format_uuid(substr($0, i, 32)); + } + format_uuid(substr($0, 49, 16) substr($0, 1, 16)); +}
So now we have two format-uuid scripts, one in sh and one in awk? Why? What was wrong with the sh one?
diff --git a/scripts/make-gpt.bash b/scripts/make-gpt.bash new file mode 100644 index 0000000000000000000000000000000000000000..f9d53817e3cc4342cac5d4c832cf4aa129880399 --- /dev/null +++ b/scripts/make-gpt.bash @@ -0,0 +1,72 @@ +#!/usr/bin/bash -- +# SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2022 Unikie +# SPDX-License-Identifier: EUPL-1.2+ +# +# usage: make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... + +set -xeuo pipefail +ONE_MiB=1048576 + +# Prints the number of 1MiB blocks required to store the file named +# $1. We use 1MiB blocks because that's what sfdisk uses for +# alignment. It would be possible to get a slightly smaller image +# using actual normal-sized 512-byte blocks, but it's probably not +# worth it to configure sfdisk to do that. +sizeMiB() { + wc -c "$1" | awk -v ONE_MiB=$ONE_MiB \ + '{printf "%d\n", ($1 + ONE_MiB - 1) / ONE_MiB}' +} + +# Copies from path $3 into partition number $2 in partition table $1. +fillPartition() { + start="$(sfdisk -J "$1" | jq -r --argjson index "$2" \ + '.partitiontable.partitions[$index].start * 512')" + + # GNU cat will use copy_file_range(2) if possible, whereas dd + # will always do a userspace copy, which is significantly slower. + lseek -S 1 "$start" cat "$3" 1<>"$1" +} + +# Prints the partition path from a PATH:PARTTYPE[:PARTUUID[:PARTLABEL]] string. +partitionPath() { + awk -F: '{print $1}' <<EOF +$1 +EOF +} + +scriptsDir="$(dirname "$0")" + +out="$1" +shift + +table="label: gpt" + +# Keep 1MiB free at the start, and 1MiB free at the end. +gptBytes=$((ONE_MiB * 2)) +for partition; do + if [[ "$partition" =~ :([1-9][0-9]*)MiB$ ]]; then + sizeMiB=${BASH_REMATCH[1]} + partition=${partition%:*} + else + partitionPath=$(partitionPath "$partition") + sizeMiB=$(sizeMiB "$partitionPath") + fi
Would be a lot simpler to just multiply by 1024 * 1024 in whatever runs this script, wouldn't it?
diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh index 96f0d2c8494c093558c0e32e7e920b569bb078ef..665057da8281d2b5282081e4999098fbaa29e6ca 100755 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -1,65 +1,4 @@ -#!/bin/sh -eu -# -# SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> -# SPDX-FileCopyrightText: 2022 Unikie +#!/bin/sh -- # SPDX-License-Identifier: EUPL-1.2+ -# -# usage: make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... - -ONE_MiB=1048576 - -# Prints the number of 1MiB blocks required to store the file named -# $1. We use 1MiB blocks because that's what sfdisk uses for -# alignment. It would be possible to get a slightly smaller image -# using actual normal-sized 512-byte blocks, but it's probably not -# worth it to configure sfdisk to do that. -sizeMiB() { - wc -c "$1" | awk -v ONE_MiB=$ONE_MiB \ - '{printf "%d\n", ($1 + ONE_MiB - 1) / ONE_MiB}' -} - -# Copies from path $3 into partition number $2 in partition table $1. -fillPartition() { - start="$(sfdisk -J "$1" | jq -r --argjson index "$2" \ - '.partitiontable.partitions[$index].start * 512')" - - # GNU cat will use copy_file_range(2) if possible, whereas dd - # will always do a userspace copy, which is significantly slower. - lseek -S 1 "$start" cat "$3" 1<>"$1" -} - -# Prints the partition path from a PATH:PARTTYPE[:PARTUUID[:PARTLABEL]] string. -partitionPath() { - awk -F: '{print $1}' <<EOF -$1 -EOF -} - -scriptsDir="$(dirname "$0")" - -out="$1" -shift - -nl=' -' -table="label: gpt" - -# Keep 1MiB free at the start, and 1MiB free at the end. -gptBytes=$((ONE_MiB * 2)) -for partition; do - sizeMiB="$(sizeMiB "$(partitionPath "$partition")")" - table="$table${nl}size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" - gptBytes="$((gptBytes + sizeMiB * ONE_MiB))" -done - -rm -f "$out" -truncate -s "$gptBytes" "$out" -sfdisk --no-reread --no-tell-kernel "$out" <<EOF -$table -EOF - -n=0 -for partition; do - fillPartition "$out" "$n" "$(partitionPath "$partition")" - n="$((n + 1))" -done +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +exec bash -- "${0%.sh}.bash" "$@"
Not sure what the purpose of this is. We don't need to keep old paths working.
diff --git a/scripts/make-live-image.sh b/scripts/make-live-image.sh new file mode 100755 index 0000000000000000000000000000000000000000..6608cc35b7a15178adf5ff3d3917b5243c5da6cd --- /dev/null +++ b/scripts/make-live-image.sh @@ -0,0 +1,43 @@ +#!/bin/sh -- +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +set -euo pipefail +case $0 in +(/*) dir=${0%/*}/;; +(*/*) dir=./${0%/*};; +(*) dir=.;; +esac +usage () { + echo 'Usage: make-live-image.sh [release|live] OUTPUT_FILE' >&2 + exit 1 +} +if [ "$#" != 2 ]; then usage; fi +file_type=$1 output_file=$2 +for i in "$ROOT_FS" "$ROOT_FS_VERITY" "$ROOT_FS_VERITY_ROOTHASH" "$VERSION"; do + # Some characters not special to the shell can't be handled by this code. + case $i in + (-*|*[!A-Za-z0-9._/+@-]*) printf 'Forbidden characters in "%s"\n' "$i" >&2; exit 1;; + esac +done +root_hashes=$(LC_ALL=C awk -f "${dir}/format-uuid.awk" < "$ROOT_FS_VERITY_ROOTHASH") +# The awk script produces output that is meant for field splitting +# and has no characters special for globbing. +# shellcheck disable=SC2086 +set -- $root_hashes +case $file_type in +(release) + "$dir/make-gpt.sh" "$output_file.tmp" \ + build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ + "$ROOT_FS_VERITY:verity:$1:Spectrum_$VERSION.verity:1024MiB" \ + "$ROOT_FS:root:$2:Spectrum_$VERSION:20480MiB" \ + "/dev/null:verity:$3:_empty:1024MiB" \ + "/dev/null:root:$4:_empty:20480MiB" + ;; +(live) + "$dir/make-gpt.sh" "$output_file.tmp" \ + "$ROOT_FS_VERITY:verity:$1:Spectrum_$VERSION.verity" \ + "$ROOT_FS:root:$2:Spectrum_$VERSION";; +(*) usage;; +esac +mv -- "$output_file.tmp" "$output_file"
If we need separate modes for each caller anyway, I don't think there's having them in a shared script is a win over having them local to the place they're used.
diff --git a/scripts/sfdisk-field.awk b/scripts/sfdisk-field.awk index e13c86d2fb11a066eebd043808e659b08dbd269c..72eec9a0a770563d32da14440fe2552eb2e39b68 100644 --- a/scripts/sfdisk-field.awk +++ b/scripts/sfdisk-field.awk @@ -24,6 +24,7 @@ BEGIN { arch = _arch }
+ comma = "" for (n in fields) { if (n <= skip) continue @@ -33,6 +34,6 @@ BEGIN { fields[n] = uuid }
- printf "%s=%s,", keys[n - skip], fields[n] + printf ",%s%s=%s", comma, keys[n - skip], fields[n] } }
I don't understand. The comma variable is always empty?
Whenever a release is made, create a directory with the release files to be used for an update. After its SHA256SSUMS file is signed, the file is ready to be uploaded to a webserver for users to update from. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release.nix | 2 ++ release/update.nix | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/release.nix b/release.nix index a4fe66ee5925aeee3a1f5f1fac249c595cee0885..704abb39a3d01152eac3dfe313066834c3cd0a66 100644 --- a/release.nix +++ b/release.nix @@ -8,5 +8,7 @@ import lib/call-package.nix ({ callSpectrumPackage }: { checks = callSpectrumPackage release/checks {}; + updates = callSpectrumPackage release/update.nix {}; + combined = callSpectrumPackage release/combined/run-vm.nix {}; }) (_: {}) diff --git a/release/update.nix b/release/update.nix new file mode 100644 index 0000000000000000000000000000000000000000..ec51eb12d33030255b7b4a7e74e14416f1f0659d --- /dev/null +++ b/release/update.nix @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../lib/call-package.nix ( +{ callSpectrumPackage, config, efi +, runCommand, stdenv, rootfs +}: + +runCommand "spectrum-update-directory" { + __structuredAttrs = true; + unsafeDiscardReferences = { out = true; }; + dontFixup = true; + env = { + VERSION = config.version; + ROOTHASH = "${rootfs}/rootfs.verity.roothash"; + VERITY = "${rootfs}/rootfs.verity.superblock"; + ROOT_FS = "${rootfs}/rootfs"; + EFI = efi; + }; +} '' + read -r roothash < "$ROOTHASH" + mkdir -- "$out" + cp -- "$VERITY" "$out/Spectrum_$VERSION.verity" + cp -- "$ROOT_FS" "$out/Spectrum_$VERSION.root" + cp -- "$EFI" "$out/Spectrum_$VERSION.efi" + cd -- "$out" + sha256sum -b "Spectrum_$VERSION.root" "Spectrum_$VERSION.verity" "Spectrum_$VERSION.efi" > SHA256SUMS + '' +) (_: {}) -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
Whenever a release is made, create a directory with the release files to be used for an update. After its SHA256SSUMS file is signed, the file is ready to be uploaded to a webserver for users to update from.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release.nix | 2 ++ release/update.nix | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+)
diff --git a/release.nix b/release.nix index a4fe66ee5925aeee3a1f5f1fac249c595cee0885..704abb39a3d01152eac3dfe313066834c3cd0a66 100644 --- a/release.nix +++ b/release.nix @@ -8,5 +8,7 @@ import lib/call-package.nix ({ callSpectrumPackage }: {
checks = callSpectrumPackage release/checks {};
+ updates = callSpectrumPackage release/update.nix {}; +
Should this just be called "update" (singular)?
combined = callSpectrumPackage release/combined/run-vm.nix {}; }) (_: {}) diff --git a/release/update.nix b/release/update.nix new file mode 100644 index 0000000000000000000000000000000000000000..ec51eb12d33030255b7b4a7e74e14416f1f0659d --- /dev/null +++ b/release/update.nix @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../lib/call-package.nix ( +{ callSpectrumPackage, config, efi +, runCommand, stdenv, rootfs +}: + +runCommand "spectrum-update-directory" { + __structuredAttrs = true; + unsafeDiscardReferences = { out = true; }; + dontFixup = true; + env = { + VERSION = config.version; + ROOTHASH = "${rootfs}/rootfs.verity.roothash"; + VERITY = "${rootfs}/rootfs.verity.superblock"; + ROOT_FS = "${rootfs}/rootfs"; + EFI = efi; + };
I'd just inline these as string interpolations rather than passing them as environment variables (except maybe VERSION).
+} '' + read -r roothash < "$ROOTHASH" + mkdir -- "$out" + cp -- "$VERITY" "$out/Spectrum_$VERSION.verity" + cp -- "$ROOT_FS" "$out/Spectrum_$VERSION.root" + cp -- "$EFI" "$out/Spectrum_$VERSION.efi" + cd -- "$out" + sha256sum -b "Spectrum_$VERSION.root" "Spectrum_$VERSION.verity" "Spectrum_$VERSION.efi" > SHA256SUMS + '' +) (_: {})
-- 2.51.2
On 11/13/25 11:04, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Whenever a release is made, create a directory with the release files to be used for an update. After its SHA256SSUMS file is signed, the file is ready to be uploaded to a webserver for users to update from.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release.nix | 2 ++ release/update.nix | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+)
diff --git a/release.nix b/release.nix index a4fe66ee5925aeee3a1f5f1fac249c595cee0885..704abb39a3d01152eac3dfe313066834c3cd0a66 100644 --- a/release.nix +++ b/release.nix @@ -8,5 +8,7 @@ import lib/call-package.nix ({ callSpectrumPackage }: {
checks = callSpectrumPackage release/checks {};
+ updates = callSpectrumPackage release/update.nix {}; +
Should this just be called "update" (singular)?
Sure!
combined = callSpectrumPackage release/combined/run-vm.nix {}; }) (_: {}) diff --git a/release/update.nix b/release/update.nix new file mode 100644 index 0000000000000000000000000000000000000000..ec51eb12d33030255b7b4a7e74e14416f1f0659d --- /dev/null +++ b/release/update.nix @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../lib/call-package.nix ( +{ callSpectrumPackage, config, efi +, runCommand, stdenv, rootfs +}: + +runCommand "spectrum-update-directory" { + __structuredAttrs = true; + unsafeDiscardReferences = { out = true; }; + dontFixup = true; + env = { + VERSION = config.version; + ROOTHASH = "${rootfs}/rootfs.verity.roothash"; + VERITY = "${rootfs}/rootfs.verity.superblock"; + ROOT_FS = "${rootfs}/rootfs"; + EFI = efi; + };
I'd just inline these as string interpolations rather than passing them as environment variables (except maybe VERSION).
In general, this is very bad practice and has caused security vulnerabilities in GitHub Actions. These have even been exploited in the wild. However, this is *not* a vulnerability in this context as the input is trusted and known not to contain shell metacharacters. I mostly wanted to avoid bad habits that are fine in the Nix context, but not in others.
+} '' + read -r roothash < "$ROOTHASH" + mkdir -- "$out" + cp -- "$VERITY" "$out/Spectrum_$VERSION.verity" + cp -- "$ROOT_FS" "$out/Spectrum_$VERSION.root" + cp -- "$EFI" "$out/Spectrum_$VERSION.efi" + cd -- "$out" + sha256sum -b "Spectrum_$VERSION.root" "Spectrum_$VERSION.verity" "Spectrum_$VERSION.efi" > SHA256SUMS + '' +) (_: {})
-- 2.51.2
-- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 11:04, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
combined = callSpectrumPackage release/combined/run-vm.nix {}; }) (_: {}) diff --git a/release/update.nix b/release/update.nix new file mode 100644 index 0000000000000000000000000000000000000000..ec51eb12d33030255b7b4a7e74e14416f1f0659d --- /dev/null +++ b/release/update.nix @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../lib/call-package.nix ( +{ callSpectrumPackage, config, efi +, runCommand, stdenv, rootfs +}: + +runCommand "spectrum-update-directory" { + __structuredAttrs = true; + unsafeDiscardReferences = { out = true; }; + dontFixup = true; + env = { + VERSION = config.version; + ROOTHASH = "${rootfs}/rootfs.verity.roothash"; + VERITY = "${rootfs}/rootfs.verity.superblock"; + ROOT_FS = "${rootfs}/rootfs"; + EFI = efi; + };
I'd just inline these as string interpolations rather than passing them as environment variables (except maybe VERSION).
In general, this is very bad practice and has caused security vulnerabilities in GitHub Actions. These have even been exploited in the wild. However, this is *not* a vulnerability in this context as the input is trusted and known not to contain shell metacharacters. I mostly wanted to avoid bad habits that are fine in the Nix context, but not in others.
I think that will result in overly unidiomatic Nix code. Bad practices are always contextual.
Include a new 'update' command to update the system. This works as follows: 1. Take a global, system-wide lock. 2. Create a BTRFS subvolume for the sys.updates VM to write the updates. 3. Bind-mount this subvolume into the VM's shared directory. 4. Start sys.appvm-updates to get the updates. 5. Wait for the VM to shut down. 6. Take a BTRFS snapshot of the subvolume. 7. Call syncfs() to flush all of the data on the subvolume. 8. Inspect the contents of the subvolume. Check that everything is a regular file and that the names are reasonable. Check that SHA256SUMS and SHA256SUMS.gpg are present. 9. Call systemd-sysupdate to run the actual update. sys.appvm-updates uses host-provided information to fetch the update. This allows editing files on the host to change the update URL and signing key. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 2 + host/rootfs/default.nix | 28 ++++++- host/rootfs/file-list.mk | 4 + host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++++ host/rootfs/image/usr/bin/update | 89 ++++++++++++++++++++++ host/rootfs/os-release.in | 13 ++++ host/rootfs/os-release.in.license | 2 + host/rootfs/updatevm-url-env | 3 + host/rootfs/vm-sysupdate.d/50-verity.transfer | 18 +++++ host/rootfs/vm-sysupdate.d/60-root.transfer | 18 +++++ host/rootfs/vm-sysupdate.d/70-kernel.transfer | 18 +++++ lib/config.default.nix | 2 + lib/config.nix | 11 ++- lib/fake-update-signing-key.gpg | 1 + lib/fake-update-signing-key.gpg.license | 2 + release/live/default.nix | 4 +- release/live/shell.nix | 3 +- vm/app/updates.nix | 37 +++++++++ 21 files changed, 309 insertions(+), 7 deletions(-) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 76c8ff1628454d769e09e0bc915d198fece080e0..86f48d4aa196ff35fb0b3e4224201e9a2566626b 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -10,6 +10,7 @@ include file-list.mk dest = build DIRS = \ + boot \ dev \ etc/s6-linux-init/env \ etc/s6-linux-init/run-image/configs \ @@ -56,6 +57,7 @@ BUILD_FILES = build/etc/s6-rc $(dest)/timestamp: ../../scripts/make-erofs.sh $(PACKAGES_FILE) $(FILES) $(BUILD_FILES) build/empty build/fifo file-list.mk $(dest) { \ cat $(PACKAGES_FILE) ;\ + printf '%s\n%s\n' "$$UPDATE_SIGNING_KEY" /etc/systemd/import-pubring.gpg; \ for file in $(FILES) $(LINKS); do printf '%s\n%s\n' $$file "$${file#image/}"; done ;\ for file in $(BUILD_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ printf 'build/empty\n%s\n' $(DIRS) ;\ diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index b574b8ddf5858867156507429a55b7f537e3c485..0a7638f8d78cf36592c2721d059bc867b04f233c 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -5,6 +5,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src , pkgsMusl, pkgsStatic, linux_latest +, config }: pkgsStatic.callPackage ( @@ -13,6 +14,7 @@ pkgsStatic.callPackage ( , busybox, cloud-hypervisor, cryptsetup, dbus, execline, inkscape , iproute2, inotify-tools, jq, mdevd, s6, s6-linux-init, socat , util-linuxMinimal, virtiofsd, xorg, xdg-desktop-portal-spectrum-host +, btrfs-progs }: let @@ -36,6 +38,7 @@ let cloud-hypervisor cryptsetup dbus execline inotify-tools iproute2 jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools virtiofsd xdg-desktop-portal-spectrum-host + btrfs-progs (busybox.override { # Use a separate file as it is a bit too big. @@ -79,11 +82,24 @@ let appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; appvm-foot = callSpectrumPackage ../../vm/app/foot.nix {}; appvm-gnome-text-editor = callSpectrumPackage ../../vm/app/gnome-text-editor.nix {}; + appvm-updates = callSpectrumPackage ../../vm/app/updates.nix {}; }; packagesSysroot = runCommand "packages-sysroot" { depsBuildBuild = [ inkscape ]; nativeBuildInputs = [ xorg.lndir ]; + env = { + VERSION = config.version; + UPDATE_URL = config.update-url; + }; + src = fileset.toSource { + root = ./.; + fileset = fileset.intersection src (fileset.unions [ + ./vm-sysupdate.d + ./os-release.in + ./updatevm-url-env + ]); + }; } '' mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps @@ -95,8 +111,7 @@ let done # If systemd-pull is missing systemd-sysupdate will fail with a - # very confusing error message. If systemd-sysupdate doesn't work, - # users will not be able to receive an update that fixes the problem. + # very confusing error message. for i in sysupdate pull; do if ! cat -- "$out/usr/lib/systemd/systemd-$i" > /dev/null; then echo "link to systemd-$i didn't get installed" >&2 @@ -118,6 +133,14 @@ let ln -st $out/usr/share/dbus-1/services \ ${pkgsGui.xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service + mkdir -p -- "$out/etc/updatevm/sysupdate.d" + substitute "$src/os-release.in" "$out/etc/os-release" --subst-var VERSION + for d in "$src/vm-sysupdate.d"/*.transfer; do + result_file=''${d#"$src/vm-sysupdate.d/"} + substitute "$d" "$out/etc/updatevm/sysupdate.d/$result_file" --subst-var UPDATE_URL + done + substitute "$src/updatevm-url-env" "$out/etc/updatevm/url-env" --subst-var UPDATE_URL + ln -st "$out/usr/bin" ${util-linuxMinimal}/bin/* ${concatStrings (mapAttrsToList (name: path: '' @@ -147,6 +170,7 @@ stdenvNoCC.mkDerivation { printf "%s\n/\n" ${packagesSysroot} >$out sed p ${writeClosure [ packagesSysroot] } >>$out ''; + UPDATE_SIGNING_KEY = config.update-signing-key; }; makeFlags = [ "dest=$(out)" ]; diff --git a/host/rootfs/file-list.mk b/host/rootfs/file-list.mk index 9acaa1d90bed674814775becf89c1c847d0ce3e3..e69dc4fb5ead88ed9ed16848b3c6cba9bbad89a6 100644 --- a/host/rootfs/file-list.mk +++ b/host/rootfs/file-list.mk @@ -42,6 +42,9 @@ FILES = \ image/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/notification-fd \ image/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/run \ image/etc/s6-linux-init/scripts/rc.init \ + image/etc/sysupdate.d/50-verity.transfer \ + image/etc/sysupdate.d/60-root.transfer \ + image/etc/sysupdate.d/70-kernel.transfer \ image/etc/udev/rules.d/99-spectrum.rules \ image/etc/xdg/weston/autolaunch \ image/etc/xdg/weston/weston.ini \ @@ -49,6 +52,7 @@ FILES = \ image/usr/bin/create-vm-dependencies \ image/usr/bin/run-appimage \ image/usr/bin/run-vmm \ + image/usr/bin/update \ image/usr/bin/vm-console \ image/usr/bin/vm-import \ image/usr/bin/vm-start \ diff --git a/host/rootfs/image/etc/fstab b/host/rootfs/image/etc/fstab index 6a82ecc85090a37b13603b29f74ca6e554a28c33..78cec99f29dda993ad97048771097121a0e42622 100644 --- a/host/rootfs/image/etc/fstab +++ b/host/rootfs/image/etc/fstab @@ -4,3 +4,4 @@ proc /proc proc defaults 0 0 devpts /dev/pts devpts defaults,gid=4,mode=620 0 0 tmpfs /dev/shm tmpfs defaults 0 0 sysfs /sys sysfs defaults 0 0 +tmpfs /tmp tmpfs defaults,mode=0700 0 0 diff --git a/host/rootfs/image/etc/sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..120713218eb37399af41bcff004dc640cd58fec2 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v.verity + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_@v.verity +MatchPartitionType=root-verity +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/60-root.transfer b/host/rootfs/image/etc/sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..e71e1ca263401c5f65ac0ed4d90ef7d22987667e --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/60-root.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v.root + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_@v +MatchPartitionType=root +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..e4190587a6bb127cb7315f38d59e48cf279318a4 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/EFI/Linux +PathRelativeTo=boot +MatchPattern=Spectrum_@v.efi +Mode=0644 +InstancesMax=2 diff --git a/host/rootfs/image/usr/bin/update b/host/rootfs/image/usr/bin/update new file mode 100755 index 0000000000000000000000000000000000000000..cbbf8ad8634a7771a0a5f7d6586ee88cdc0672a8 --- /dev/null +++ b/host/rootfs/image/usr/bin/update @@ -0,0 +1,89 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Steps: +# +# 1. Take a global, system-wide lock. +# 2. Create a BTRFS subvolume for the sys.updates VM to write the updates. +# 3. Bind-mount this subvolume into the VM's shared directory. +# 4. Start sys.updates to get the updates. +# 5. Wait for the VM to shut down. +# 6. Take a BTRFS snapshot of the subvolume. +# 7. Call syncfs() to flush all of the data on the subvolume. +# 8. Inspect the contents of the subvolume. +# Check that everything is a regular file and that the names are reasonable. +# Check that SHA256SUMS and SHA256SUMS.gpg are present. +# 9. Call systemd-sysupdate to run the actual update. + +if { mkdir -p -m 0700 /run/updater } +s6-setlock /run/update-lock +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Snapshot directory may have files or directories with untrusted names. + # Redirect its output to /dev/null to avoid printing them to the console. + ifelse -n { redirfd -w 2 /dev/null rm -rf -- snapshot } { + foreground { redirfd -w 2 echo "Cannot remove snapshot directory" } + exit 1 + } + + backtick -E update_vm_id_ { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-updates } + basename -- $id_path + } + + multisubstitute { + define fsdir /run/vm/by-id/${update_vm_id_}/fs + define update_vm_id ${update_vm_id_} + define svcdir /run/service/vmm/instance/${update_vm_id_} + } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. + + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + if { rm -rf -- ${fsdir}/etc } + if { umask 022 mkdir -p -- ${fsdir}/updates ${fsdir}/etc/systemd } + if { cp -R -- /etc/updatevm/sysupdate.d /etc/updatevm/url-env ${fsdir}/etc } + if { cp -- /etc/systemd/import-pubring.gpg ${fsdir}/etc/systemd } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- ${fsdir}/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared ${fsdir}/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + if { s6-svwait -D ${svcdir} } + + # Remove the bind mount. + if { umount -- ${fsdir}/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Perform the update in a separate mount namespace. + unshare --mount + if { mount --bind -o ro -- snapshot /run/updater } + + # Validate the update directory. + if { updates-dir-check /run/updater } + /usr/lib/systemd/systemd-sysupdate update +} +importas -i sysupdate_exit_status ? +# Clean up. +foreground { btrfs subvolume delete -- snapshot } +exit $sysupdate_exit_status diff --git a/host/rootfs/os-release.in b/host/rootfs/os-release.in new file mode 100644 index 0000000000000000000000000000000000000000..078e8f15ea73555b606e7f23ed34a3e0e3299f0a --- /dev/null +++ b/host/rootfs/os-release.in @@ -0,0 +1,13 @@ +NAME="Spectrum OS" +ID=spectrum +PRETTY_NAME="Spectrum @VERSION@" +VERSION=@VERSION@ +VERSION_ID=@VERSION@ +IMAGE_ID=spectrum-root +IMAGE_VERSION=@VERSION@ +RELEASE_TYPE=development +HOME_URL="https://spectrum-os.org" +BUG_REPORT_URL="mailto:discuss@spectrum-os.org" +ANSI_COLOR="1;34" +VENDOR_NAME=Spectrum +VENDOR_URL="https://spectrum-os.org" diff --git a/host/rootfs/os-release.in.license b/host/rootfs/os-release.in.license new file mode 100644 index 0000000000000000000000000000000000000000..c4a0586a407fe14c3e0855749a7524ac3871dda4 --- /dev/null +++ b/host/rootfs/os-release.in.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> diff --git a/host/rootfs/updatevm-url-env b/host/rootfs/updatevm-url-env new file mode 100644 index 0000000000000000000000000000000000000000..a1a9f6f86509d4c8bab2d5eef3653f732b887ad5 --- /dev/null +++ b/host/rootfs/updatevm-url-env @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +UPDATE_URL="@UPDATE_URL@" diff --git a/host/rootfs/vm-sysupdate.d/50-verity.transfer b/host/rootfs/vm-sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..ae715dcc11a9711d8f3cab3801cd95ecc0fad11b --- /dev/null +++ b/host/rootfs/vm-sysupdate.d/50-verity.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v.verity + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v.verity +Mode=0644 diff --git a/host/rootfs/vm-sysupdate.d/60-root.transfer b/host/rootfs/vm-sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..b2488dfd21197c72f9c15467e293d335c5b85ee4 --- /dev/null +++ b/host/rootfs/vm-sysupdate.d/60-root.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v.root + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v.root +Mode=0644 diff --git a/host/rootfs/vm-sysupdate.d/70-kernel.transfer b/host/rootfs/vm-sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..cb181239d71c5a6d0a5b3652d5534a23eda64183 --- /dev/null +++ b/host/rootfs/vm-sysupdate.d/70-kernel.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v.efi +Mode=0644 diff --git a/lib/config.default.nix b/lib/config.default.nix index 489c231490a8b66aa01f50053b25646060f7f963..e53b01f1259543b988458a14b3014eb8ca29e90d 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -5,4 +5,6 @@ pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; version = "0.0.0"; + update-url = "https://your-spectrum-os-update-server.invalid/download-directory"; + update-signing-key = ./fake-update-signing-key.gpg; } diff --git a/lib/config.nix b/lib/config.nix index 01bcfa2bb2d5c412e212f5a60d9032e89c8a7442..5b6b95013734202b7e2e01d5ffce313080658006 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -1,5 +1,6 @@ -# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> # SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> let customConfigPath = builtins.tryEval <spectrum-config>; @@ -17,5 +18,11 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; in - default // callConfig config; + finalConfig // { + update-signing-key = builtins.path { + name = "signing-key"; + path = finalConfig.update-signing-key; + }; + } diff --git a/lib/fake-update-signing-key.gpg b/lib/fake-update-signing-key.gpg new file mode 100644 index 0000000000000000000000000000000000000000..b4c15467614ee15deef02af05f4c6554a1f7a013 --- /dev/null +++ b/lib/fake-update-signing-key.gpg @@ -0,0 +1 @@ +NOT A VALID KEY - UPDATES WILL NOT WORK diff --git a/lib/fake-update-signing-key.gpg.license b/lib/fake-update-signing-key.gpg.license new file mode 100644 index 0000000000000000000000000000000000000000..c4a0586a407fe14c3e0855749a7524ac3871dda4 --- /dev/null +++ b/lib/fake-update-signing-key.gpg.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> diff --git a/release/live/default.nix b/release/live/default.nix index dc649732ffa46a998a4a66360aa8ff7ef6bccae0..581420da9acf855d4b3d9ececc1ef406f742fd75 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -7,7 +7,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, rootfs, src , lib, pkgsStatic, stdenvNoCC , cryptsetup, dosfstools, jq, mtools, util-linux -, systemdUkify, version, efi +, systemdUkify, config, efi }: let @@ -49,7 +49,7 @@ stdenv.mkDerivation { SYSTEMD_BOOT_EFI = "${efi.systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; EFI_IMAGE = efi; EFINAME = "BOOT${toUpper efiArch}.EFI"; - VERSION = version; + VERSION = config.version; }; buildFlags = [ "dest=$(out)" ]; diff --git a/release/live/shell.nix b/release/live/shell.nix index 05250525defa0e8a10cde45b5e49f878fcec599f..4ca8f53fdbbc11072fe226b9036d69de8a870249 100644 --- a/release/live/shell.nix +++ b/release/live/shell.nix @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> -import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm, rootfs }: +import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm, rootfs, config }: (callSpectrumPackage ./. {}).overrideAttrs ( { nativeBuildInputs ? [], env ? {}, ... }: @@ -11,6 +11,7 @@ import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm, root env = env // { ROOT_FS = rootfs; OVMF_CODE = "${qemu_kvm}/share/qemu/edk2-${stdenv.hostPlatform.qemuArch}-code.fd"; + VERSION = config.version; }; } )) (_: {}) diff --git a/vm/app/updates.nix b/vm/app/updates.nix new file mode 100644 index 0000000000000000000000000000000000000000..d2c1e5fcb35b37c7ed8a173f19b97894a36a7f0c --- /dev/null +++ b/vm/app/updates.nix @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../lib/call-package.nix ( +{ callSpectrumPackage, config, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + update-url = config.update-url; + mountpoint = "/run/virtiofs/virtiofs0"; + sysupdate-path = "${systemd}/lib/systemd/systemd-sysupdate"; + runner = writeScript "update-run-script" + '' + #!/usr/bin/execlineb -P + if { mount -toverlay -olowerdir=${mountpoint}/etc:/etc -- overlay /etc } + envfile ${mountpoint}/etc/url-env + importas -i update_url UPDATE_URL + if { ${sysupdate-path} update } + if { ${curl}/bin/curl -L --proto =http,https + -o ${mountpoint}/updates/SHA256SUMS.gpg ''${update_url}/SHA256SUMS.gpg } + # systemd-sysupdate recently went from needing SHA256SUMS.gpg to SHA256SUMS.sha256.asc. + # I (Demi) have no need if this is intentional or a bug. I also have no idea if this + # behavior will stay unchanged in the future. Therefore, create both files and let + # systemd-sysupdate ignore the one it isn't interested in. + if { ln -f ${mountpoint}/updates/SHA256SUMS.gpg ${mountpoint}/updates/SHA256SUMS.sha256.asc } + ${curl}/bin/curl -L --proto =http,https + -o ${mountpoint}/updates/SHA256SUMS ''${update_url}/SHA256SUMS + ''; +in + +callSpectrumPackage ../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = "${runner}"; +}) (_: {}) -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
Include a new 'update' command to update the system. This works as follows:
1. Take a global, system-wide lock. 2. Create a BTRFS subvolume for the sys.updates VM to write the updates. 3. Bind-mount this subvolume into the VM's shared directory. 4. Start sys.appvm-updates to get the updates. 5. Wait for the VM to shut down. 6. Take a BTRFS snapshot of the subvolume. 7. Call syncfs() to flush all of the data on the subvolume. 8. Inspect the contents of the subvolume. Check that everything is a regular file and that the names are reasonable. Check that SHA256SUMS and SHA256SUMS.gpg are present.
Not any more.
9. Call systemd-sysupdate to run the actual update.
sys.appvm-updates uses host-provided information to fetch the update. This allows editing files on the host to change the update URL and signing key.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 2 + host/rootfs/default.nix | 28 ++++++- host/rootfs/file-list.mk | 4 + host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++++ host/rootfs/image/usr/bin/update | 89 ++++++++++++++++++++++ host/rootfs/os-release.in | 13 ++++ host/rootfs/os-release.in.license | 2 + host/rootfs/updatevm-url-env | 3 + host/rootfs/vm-sysupdate.d/50-verity.transfer | 18 +++++ host/rootfs/vm-sysupdate.d/60-root.transfer | 18 +++++ host/rootfs/vm-sysupdate.d/70-kernel.transfer | 18 +++++ lib/config.default.nix | 2 + lib/config.nix | 11 ++- lib/fake-update-signing-key.gpg | 1 + lib/fake-update-signing-key.gpg.license | 2 + release/live/default.nix | 4 +- release/live/shell.nix | 3 +- vm/app/updates.nix | 37 +++++++++ 21 files changed, 309 insertions(+), 7 deletions(-)
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index b574b8ddf5858867156507429a55b7f537e3c485..0a7638f8d78cf36592c2721d059bc867b04f233c 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -5,6 +5,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src , pkgsMusl, pkgsStatic, linux_latest +, config }: pkgsStatic.callPackage (
@@ -13,6 +14,7 @@ pkgsStatic.callPackage ( , busybox, cloud-hypervisor, cryptsetup, dbus, execline, inkscape , iproute2, inotify-tools, jq, mdevd, s6, s6-linux-init, socat , util-linuxMinimal, virtiofsd, xorg, xdg-desktop-portal-spectrum-host +, btrfs-progs }:
let @@ -36,6 +38,7 @@ let cloud-hypervisor cryptsetup dbus execline inotify-tools iproute2 jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools virtiofsd xdg-desktop-portal-spectrum-host + btrfs-progs
Let's keep this sorted.
@@ -79,11 +82,24 @@ let appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; appvm-foot = callSpectrumPackage ../../vm/app/foot.nix {}; appvm-gnome-text-editor = callSpectrumPackage ../../vm/app/gnome-text-editor.nix {}; + appvm-updates = callSpectrumPackage ../../vm/app/updates.nix {};
I think appvm-sysupdate or appvm-systemd-sysupdate would be clearer.
};
packagesSysroot = runCommand "packages-sysroot" { depsBuildBuild = [ inkscape ]; nativeBuildInputs = [ xorg.lndir ]; + env = { + VERSION = config.version; + UPDATE_URL = config.update-url; + }; + src = fileset.toSource { + root = ./.; + fileset = fileset.intersection src (fileset.unions [ + ./vm-sysupdate.d + ./os-release.in + ./updatevm-url-env + ]); + }; } '' mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps @@ -95,8 +111,7 @@ let done
# If systemd-pull is missing systemd-sysupdate will fail with a - # very confusing error message. If systemd-sysupdate doesn't work, - # users will not be able to receive an update that fixes the problem. + # very confusing error message. for i in sysupdate pull; do if ! cat -- "$out/usr/lib/systemd/systemd-$i" > /dev/null; then echo "link to systemd-$i didn't get installed" >&2 @@ -118,6 +133,14 @@ let ln -st $out/usr/share/dbus-1/services \ ${pkgsGui.xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service
+ mkdir -p -- "$out/etc/updatevm/sysupdate.d" + substitute "$src/os-release.in" "$out/etc/os-release" --subst-var VERSION + for d in "$src/vm-sysupdate.d"/*.transfer; do + result_file=''${d#"$src/vm-sysupdate.d/"} + substitute "$d" "$out/etc/updatevm/sysupdate.d/$result_file" --subst-var UPDATE_URL + done + substitute "$src/updatevm-url-env" "$out/etc/updatevm/url-env" --subst-var UPDATE_URL +
I think it would make more sense to do these at the Make layer. It handles other generated files, so I don't see why it can't handle these too, and then if I add something to os-release I don't have to rebuild any Nix stuff.
diff --git a/host/rootfs/image/etc/fstab b/host/rootfs/image/etc/fstab index 6a82ecc85090a37b13603b29f74ca6e554a28c33..78cec99f29dda993ad97048771097121a0e42622 100644 --- a/host/rootfs/image/etc/fstab +++ b/host/rootfs/image/etc/fstab @@ -4,3 +4,4 @@ proc /proc proc defaults 0 0 devpts /dev/pts devpts defaults,gid=4,mode=620 0 0 tmpfs /dev/shm tmpfs defaults 0 0 sysfs /sys sysfs defaults 0 0 +tmpfs /tmp tmpfs defaults,mode=0700 0 0
Is this used?
diff --git a/host/rootfs/image/usr/bin/update b/host/rootfs/image/usr/bin/update new file mode 100755 index 0000000000000000000000000000000000000000..cbbf8ad8634a7771a0a5f7d6586ee88cdc0672a8 --- /dev/null +++ b/host/rootfs/image/usr/bin/update @@ -0,0 +1,89 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Steps: +# +# 1. Take a global, system-wide lock. +# 2. Create a BTRFS subvolume for the sys.updates VM to write the updates. +# 3. Bind-mount this subvolume into the VM's shared directory. +# 4. Start sys.updates to get the updates. +# 5. Wait for the VM to shut down. +# 6. Take a BTRFS snapshot of the subvolume. +# 7. Call syncfs() to flush all of the data on the subvolume. +# 8. Inspect the contents of the subvolume. +# Check that everything is a regular file and that the names are reasonable. +# Check that SHA256SUMS and SHA256SUMS.gpg are present.
Not any more.
+# 9. Call systemd-sysupdate to run the actual update. + +if { mkdir -p -m 0700 /run/updater } +s6-setlock /run/update-lock +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } +
Wouldn't it break if there's already stuff in it? I'd do foreground { redirfd -w 2 /dev/null btrfs subvolume delete -- shared } if { btrfs subvolume create -- shared } and then you know you've got an empty subvolume.
+ # Snapshot directory may have files or directories with untrusted names. + # Redirect its output to /dev/null to avoid printing them to the console. + ifelse -n { redirfd -w 2 /dev/null rm -rf -- snapshot } { + foreground { redirfd -w 2 echo "Cannot remove snapshot directory" } + exit 1 + }
Why not btrfs subvolume delete? It's faster and won't print names.
+ + backtick -E update_vm_id_ { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-updates } + basename -- $id_path + } + + multisubstitute { + define fsdir /run/vm/by-id/${update_vm_id_}/fs + define update_vm_id ${update_vm_id_}
Why?
+ define svcdir /run/service/vmm/instance/${update_vm_id_}
Can also use /run/vm/by-name/sys.appvm-updates/fs and /run/vm/by-name/sys.appvm-updates/service if you prefer, although you need to look up the ID for vm-start anyway currently.
+ } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. + + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + if { rm -rf -- ${fsdir}/etc } + if { umask 022 mkdir -p -- ${fsdir}/updates ${fsdir}/etc/systemd } + if { cp -R -- /etc/updatevm/sysupdate.d /etc/updatevm/url-env ${fsdir}/etc } + if { cp -- /etc/systemd/import-pubring.gpg ${fsdir}/etc/systemd }
Why copy rather than bind mount?
+ + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- ${fsdir}/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared ${fsdir}/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + if { s6-svwait -D ${svcdir} } +
It might be more robust to use a transient VM, like we use for AppImages, so that nothing can restart it. Transient VMs are still developing though, so it's also fine to say we'll do it this way for now and adapt it later. This would also save all the filesystem resetting you're needing to do here.
diff --git a/host/rootfs/os-release.in.license b/host/rootfs/os-release.in.license new file mode 100644 index 0000000000000000000000000000000000000000..c4a0586a407fe14c3e0855749a7524ac3871dda4 --- /dev/null +++ b/host/rootfs/os-release.in.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
os-release files can have comments, so no need for a separate license file here.
diff --git a/lib/config.nix b/lib/config.nix index 01bcfa2bb2d5c412e212f5a60d9032e89c8a7442..5b6b95013734202b7e2e01d5ffce313080658006 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -1,5 +1,6 @@ -# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> # SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
Why have I changed from 2023 to 2024?
let customConfigPath = builtins.tryEval <spectrum-config>; @@ -17,5 +18,11 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; in - default // callConfig config; + finalConfig // { + update-signing-key = builtins.path { + name = "signing-key"; + path = finalConfig.update-signing-key; + }; + }
What does this do?
diff --git a/lib/fake-update-signing-key.gpg b/lib/fake-update-signing-key.gpg new file mode 100644 index 0000000000000000000000000000000000000000..b4c15467614ee15deef02af05f4c6554a1f7a013 --- /dev/null +++ b/lib/fake-update-signing-key.gpg @@ -0,0 +1 @@ +NOT A VALID KEY - UPDATES WILL NOT WORK diff --git a/lib/fake-update-signing-key.gpg.license b/lib/fake-update-signing-key.gpg.license new file mode 100644 index 0000000000000000000000000000000000000000..c4a0586a407fe14c3e0855749a7524ac3871dda4 --- /dev/null +++ b/lib/fake-update-signing-key.gpg.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
Given it's not a valid key anyway might as well just put this in the file.
diff --git a/release/live/default.nix b/release/live/default.nix index dc649732ffa46a998a4a66360aa8ff7ef6bccae0..581420da9acf855d4b3d9ececc1ef406f742fd75 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -7,7 +7,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, rootfs, src , lib, pkgsStatic, stdenvNoCC , cryptsetup, dosfstools, jq, mtools, util-linux -, systemdUkify, version, efi +, systemdUkify, config, efi }:
let @@ -49,7 +49,7 @@ stdenv.mkDerivation { SYSTEMD_BOOT_EFI = "${efi.systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; EFI_IMAGE = efi; EFINAME = "BOOT${toUpper efiArch}.EFI"; - VERSION = version; + VERSION = config.version; };
buildFlags = [ "dest=$(out)" ];
Maybe this should be squashed into an earlier patch?
diff --git a/vm/app/updates.nix b/vm/app/updates.nix new file mode 100644 index 0000000000000000000000000000000000000000..d2c1e5fcb35b37c7ed8a173f19b97894a36a7f0c --- /dev/null +++ b/vm/app/updates.nix @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../lib/call-package.nix ( +{ callSpectrumPackage, config, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + update-url = config.update-url; + mountpoint = "/run/virtiofs/virtiofs0"; + sysupdate-path = "${systemd}/lib/systemd/systemd-sysupdate"; + runner = writeScript "update-run-script" + '' + #!/usr/bin/execlineb -P + if { mount -toverlay -olowerdir=${mountpoint}/etc:/etc -- overlay /etc } + envfile ${mountpoint}/etc/url-env
Seems like overkill to use an envfile for a single URL?
+ importas -i update_url UPDATE_URL + if { ${sysupdate-path} update } + if { ${curl}/bin/curl -L --proto =http,https + -o ${mountpoint}/updates/SHA256SUMS.gpg ''${update_url}/SHA256SUMS.gpg } + # systemd-sysupdate recently went from needing SHA256SUMS.gpg to SHA256SUMS.sha256.asc. + # I (Demi) have no need if this is intentional or a bug. I also have no idea if this + # behavior will stay unchanged in the future. Therefore, create both files and let + # systemd-sysupdate ignore the one it isn't interested in. + if { ln -f ${mountpoint}/updates/SHA256SUMS.gpg ${mountpoint}/updates/SHA256SUMS.sha256.asc }
Would be good to figure out why that happened. If we add a comment like this it's very unlikely to ever get cleaned up.
+ ${curl}/bin/curl -L --proto =http,https + -o ${mountpoint}/updates/SHA256SUMS ''${update_url}/SHA256SUMS + ''; +in + +callSpectrumPackage ../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = "${runner}";
Might as well inline this.
+}) (_: {})
-- 2.51.2
On 11/13/25 11:44, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Include a new 'update' command to update the system. This works as follows:
1. Take a global, system-wide lock. 2. Create a BTRFS subvolume for the sys.updates VM to write the updates. 3. Bind-mount this subvolume into the VM's shared directory. 4. Start sys.appvm-updates to get the updates. 5. Wait for the VM to shut down. 6. Take a BTRFS snapshot of the subvolume. 7. Call syncfs() to flush all of the data on the subvolume. 8. Inspect the contents of the subvolume. Check that everything is a regular file and that the names are reasonable. Check that SHA256SUMS and SHA256SUMS.gpg are present.
Not any more.
Will fix.
9. Call systemd-sysupdate to run the actual update.
sys.appvm-updates uses host-provided information to fetch the update. This allows editing files on the host to change the update URL and signing key.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 2 + host/rootfs/default.nix | 28 ++++++- host/rootfs/file-list.mk | 4 + host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++++ host/rootfs/image/usr/bin/update | 89 ++++++++++++++++++++++ host/rootfs/os-release.in | 13 ++++ host/rootfs/os-release.in.license | 2 + host/rootfs/updatevm-url-env | 3 + host/rootfs/vm-sysupdate.d/50-verity.transfer | 18 +++++ host/rootfs/vm-sysupdate.d/60-root.transfer | 18 +++++ host/rootfs/vm-sysupdate.d/70-kernel.transfer | 18 +++++ lib/config.default.nix | 2 + lib/config.nix | 11 ++- lib/fake-update-signing-key.gpg | 1 + lib/fake-update-signing-key.gpg.license | 2 + release/live/default.nix | 4 +- release/live/shell.nix | 3 +- vm/app/updates.nix | 37 +++++++++ 21 files changed, 309 insertions(+), 7 deletions(-)
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index b574b8ddf5858867156507429a55b7f537e3c485..0a7638f8d78cf36592c2721d059bc867b04f233c 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -5,6 +5,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src , pkgsMusl, pkgsStatic, linux_latest +, config }: pkgsStatic.callPackage (
@@ -13,6 +14,7 @@ pkgsStatic.callPackage ( , busybox, cloud-hypervisor, cryptsetup, dbus, execline, inkscape , iproute2, inotify-tools, jq, mdevd, s6, s6-linux-init, socat , util-linuxMinimal, virtiofsd, xorg, xdg-desktop-portal-spectrum-host +, btrfs-progs }:
let @@ -36,6 +38,7 @@ let cloud-hypervisor cryptsetup dbus execline inotify-tools iproute2 jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools virtiofsd xdg-desktop-portal-spectrum-host + btrfs-progs
Let's keep this sorted.
Will fix.
@@ -79,11 +82,24 @@ let appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; appvm-foot = callSpectrumPackage ../../vm/app/foot.nix {}; appvm-gnome-text-editor = callSpectrumPackage ../../vm/app/gnome-text-editor.nix {}; + appvm-updates = callSpectrumPackage ../../vm/app/updates.nix {};
I think appvm-sysupdate or appvm-systemd-sysupdate would be clearer.
Will fix.
};
packagesSysroot = runCommand "packages-sysroot" { depsBuildBuild = [ inkscape ]; nativeBuildInputs = [ xorg.lndir ]; + env = { + VERSION = config.version; + UPDATE_URL = config.update-url; + }; + src = fileset.toSource { + root = ./.; + fileset = fileset.intersection src (fileset.unions [ + ./vm-sysupdate.d + ./os-release.in + ./updatevm-url-env + ]); + }; } '' mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps @@ -95,8 +111,7 @@ let done
# If systemd-pull is missing systemd-sysupdate will fail with a - # very confusing error message. If systemd-sysupdate doesn't work, - # users will not be able to receive an update that fixes the problem. + # very confusing error message. for i in sysupdate pull; do if ! cat -- "$out/usr/lib/systemd/systemd-$i" > /dev/null; then echo "link to systemd-$i didn't get installed" >&2 @@ -118,6 +133,14 @@ let ln -st $out/usr/share/dbus-1/services \ ${pkgsGui.xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service
+ mkdir -p -- "$out/etc/updatevm/sysupdate.d" + substitute "$src/os-release.in" "$out/etc/os-release" --subst-var VERSION + for d in "$src/vm-sysupdate.d"/*.transfer; do + result_file=''${d#"$src/vm-sysupdate.d/"} + substitute "$d" "$out/etc/updatevm/sysupdate.d/$result_file" --subst-var UPDATE_URL + done + substitute "$src/updatevm-url-env" "$out/etc/updatevm/url-env" --subst-var UPDATE_URL +
I think it would make more sense to do these at the Make layer. It handles other generated files, so I don't see why it can't handle these too, and then if I add something to os-release I don't have to rebuild any Nix stuff.
Will fix.
diff --git a/host/rootfs/image/etc/fstab b/host/rootfs/image/etc/fstab index 6a82ecc85090a37b13603b29f74ca6e554a28c33..78cec99f29dda993ad97048771097121a0e42622 100644 --- a/host/rootfs/image/etc/fstab +++ b/host/rootfs/image/etc/fstab @@ -4,3 +4,4 @@ proc /proc proc defaults 0 0 devpts /dev/pts devpts defaults,gid=4,mode=620 0 0 tmpfs /dev/shm tmpfs defaults 0 0 sysfs /sys sysfs defaults 0 0 +tmpfs /tmp tmpfs defaults,mode=0700 0 0
Is this used?
No.
diff --git a/host/rootfs/image/usr/bin/update b/host/rootfs/image/usr/bin/update new file mode 100755 index 0000000000000000000000000000000000000000..cbbf8ad8634a7771a0a5f7d6586ee88cdc0672a8 --- /dev/null +++ b/host/rootfs/image/usr/bin/update @@ -0,0 +1,89 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Steps: +# +# 1. Take a global, system-wide lock. +# 2. Create a BTRFS subvolume for the sys.updates VM to write the updates. +# 3. Bind-mount this subvolume into the VM's shared directory. +# 4. Start sys.updates to get the updates. +# 5. Wait for the VM to shut down. +# 6. Take a BTRFS snapshot of the subvolume. +# 7. Call syncfs() to flush all of the data on the subvolume. +# 8. Inspect the contents of the subvolume. +# Check that everything is a regular file and that the names are reasonable. +# Check that SHA256SUMS and SHA256SUMS.gpg are present.
Not any more.
Will fix in v3.
+# 9. Call systemd-sysupdate to run the actual update. + +if { mkdir -p -m 0700 /run/updater } +s6-setlock /run/update-lock +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } +
Wouldn't it break if there's already stuff in it?
No, it works fine in this case. I checked :).
I'd do
foreground { redirfd -w 2 /dev/null btrfs subvolume delete -- shared } if { btrfs subvolume create -- shared }
and then you know you've got an empty subvolume.
An empty subvolume isn't good: it means that systemd-sysupdate will redownload an update even when it isn't needed.
+ # Snapshot directory may have files or directories with untrusted names. + # Redirect its output to /dev/null to avoid printing them to the console. + ifelse -n { redirfd -w 2 /dev/null rm -rf -- snapshot } { + foreground { redirfd -w 2 echo "Cannot remove snapshot directory" } + exit 1 + }
Why not btrfs subvolume delete? It's faster and won't print names.
It doesn't distinguish "subvolume doesn't exist" from "problem deleting subvolume". A better solution is to call `rm -f` if `btrfs subvolume delete` failed. That ignores "does not exist" errors, but not other errors.
+ + backtick -E update_vm_id_ { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-updates } + basename -- $id_path + } + + multisubstitute { + define fsdir /run/vm/by-id/${update_vm_id_}/fs + define update_vm_id ${update_vm_id_}
Why?
Avoiding serial substitution.
+ define svcdir /run/service/vmm/instance/${update_vm_id_}
Can also use /run/vm/by-name/sys.appvm-updates/fs and /run/vm/by-name/sys.appvm-updates/service if you prefer, although you need to look up the ID for vm-start anyway currently.
I have a patch for that coming up.
+ } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. + + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + if { rm -rf -- ${fsdir}/etc } + if { umask 022 mkdir -p -- ${fsdir}/updates ${fsdir}/etc/systemd } + if { cp -R -- /etc/updatevm/sysupdate.d /etc/updatevm/url-env ${fsdir}/etc } + if { cp -- /etc/systemd/import-pubring.gpg ${fsdir}/etc/systemd }
Why copy rather than bind mount?
Target does not exist and I didn't want to bind-mount all of /etc/systemd.
+ + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- ${fsdir}/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared ${fsdir}/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + if { s6-svwait -D ${svcdir} } +
It might be more robust to use a transient VM, like we use for AppImages, so that nothing can restart it. Transient VMs are still developing though, so it's also fine to say we'll do it this way for now and adapt it later. This would also save all the filesystem resetting you're needing to do here.
The path to the update directory is user-provided. It's not from the VM's persistent storage.
diff --git a/host/rootfs/os-release.in.license b/host/rootfs/os-release.in.license new file mode 100644 index 0000000000000000000000000000000000000000..c4a0586a407fe14c3e0855749a7524ac3871dda4 --- /dev/null +++ b/host/rootfs/os-release.in.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
os-release files can have comments, so no need for a separate license file here.
diff --git a/lib/config.nix b/lib/config.nix index 01bcfa2bb2d5c412e212f5a60d9032e89c8a7442..5b6b95013734202b7e2e01d5ffce313080658006 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -1,5 +1,6 @@ -# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> # SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
Why have I changed from 2023 to 2024? Mistake 🙂
let customConfigPath = builtins.tryEval <spectrum-config>; @@ -17,5 +18,11 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; in - default // callConfig config; + finalConfig // { + update-signing-key = builtins.path { + name = "signing-key"; + path = finalConfig.update-signing-key; + }; + }
What does this do?
This ensures that the Nix store path doesn't depend on the name of the update signing key, only its contents.
diff --git a/lib/fake-update-signing-key.gpg b/lib/fake-update-signing-key.gpg new file mode 100644 index 0000000000000000000000000000000000000000..b4c15467614ee15deef02af05f4c6554a1f7a013 --- /dev/null +++ b/lib/fake-update-signing-key.gpg @@ -0,0 +1 @@ +NOT A VALID KEY - UPDATES WILL NOT WORK diff --git a/lib/fake-update-signing-key.gpg.license b/lib/fake-update-signing-key.gpg.license new file mode 100644 index 0000000000000000000000000000000000000000..c4a0586a407fe14c3e0855749a7524ac3871dda4 --- /dev/null +++ b/lib/fake-update-signing-key.gpg.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
Given it's not a valid key anyway might as well just put this in the file.
diff --git a/release/live/default.nix b/release/live/default.nix index dc649732ffa46a998a4a66360aa8ff7ef6bccae0..581420da9acf855d4b3d9ececc1ef406f742fd75 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -7,7 +7,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, rootfs, src , lib, pkgsStatic, stdenvNoCC , cryptsetup, dosfstools, jq, mtools, util-linux -, systemdUkify, version, efi +, systemdUkify, config, efi }:
let @@ -49,7 +49,7 @@ stdenv.mkDerivation { SYSTEMD_BOOT_EFI = "${efi.systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; EFI_IMAGE = efi; EFINAME = "BOOT${toUpper efiArch}.EFI"; - VERSION = version; + VERSION = config.version; };
buildFlags = [ "dest=$(out)" ];
Maybe this should be squashed into an earlier patch?
Correct.
diff --git a/vm/app/updates.nix b/vm/app/updates.nix new file mode 100644 index 0000000000000000000000000000000000000000..d2c1e5fcb35b37c7ed8a173f19b97894a36a7f0c --- /dev/null +++ b/vm/app/updates.nix @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../lib/call-package.nix ( +{ callSpectrumPackage, config, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + update-url = config.update-url; + mountpoint = "/run/virtiofs/virtiofs0"; + sysupdate-path = "${systemd}/lib/systemd/systemd-sysupdate"; + runner = writeScript "update-run-script" + '' + #!/usr/bin/execlineb -P + if { mount -toverlay -olowerdir=${mountpoint}/etc:/etc -- overlay /etc } + envfile ${mountpoint}/etc/url-env
Seems like overkill to use an envfile for a single URL?
It is indeed overkill, but I'm not aware of a simpler option. There is backtick + cat but that's two programs rather than one.
+ importas -i update_url UPDATE_URL + if { ${sysupdate-path} update } + if { ${curl}/bin/curl -L --proto =http,https + -o ${mountpoint}/updates/SHA256SUMS.gpg ''${update_url}/SHA256SUMS.gpg } + # systemd-sysupdate recently went from needing SHA256SUMS.gpg to SHA256SUMS.sha256.asc. + # I (Demi) have no need if this is intentional or a bug. I also have no idea if this + # behavior will stay unchanged in the future. Therefore, create both files and let + # systemd-sysupdate ignore the one it isn't interested in. + if { ln -f ${mountpoint}/updates/SHA256SUMS.gpg ${mountpoint}/updates/SHA256SUMS.sha256.asc }
Would be good to figure out why that happened. If we add a comment like this it's very unlikely to ever get cleaned up.
https://github.com/systemd/systemd/issues/39273
+ ${curl}/bin/curl -L --proto =http,https + -o ${mountpoint}/updates/SHA256SUMS ''${update_url}/SHA256SUMS + ''; +in + +callSpectrumPackage ../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = "${runner}";
Might as well inline this.
I chose to keep it separate to improve readability.
+}) (_: {})
-- 2.51.2 -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 11:44, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
+# 9. Call systemd-sysupdate to run the actual update. + +if { mkdir -p -m 0700 /run/updater } +s6-setlock /run/update-lock +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } +
Wouldn't it break if there's already stuff in it?
No, it works fine in this case. I checked :).
I'd do
foreground { redirfd -w 2 /dev/null btrfs subvolume delete -- shared } if { btrfs subvolume create -- shared }
and then you know you've got an empty subvolume.
An empty subvolume isn't good: it means that systemd-sysupdate will redownload an update even when it isn't needed.
When would the update have already been downloaded but not applied? Only if there's an error in actually installing it? I'd feel happier knowing we were always starting with a clean state.
+ # Snapshot directory may have files or directories with untrusted names. + # Redirect its output to /dev/null to avoid printing them to the console. + ifelse -n { redirfd -w 2 /dev/null rm -rf -- snapshot } { + foreground { redirfd -w 2 echo "Cannot remove snapshot directory" } + exit 1 + }
Why not btrfs subvolume delete? It's faster and won't print names.
It doesn't distinguish "subvolume doesn't exist" from "problem deleting subvolume". A better solution is to call `rm -f` if `btrfs subvolume delete` failed. That ignores "does not exist" errors, but not other errors.
Right. I had assumed btrfs subvolume create would cause an error if it already existed, but you've said it doesn't.
+ + backtick -E update_vm_id_ { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-updates } + basename -- $id_path + } + + multisubstitute { + define fsdir /run/vm/by-id/${update_vm_id_}/fs + define update_vm_id ${update_vm_id_}
Why?
Avoiding serial substitution.
But this is serial substitution. You're substituting update_vm_id_, and then you're doing another substitution of update_vm_id without the underscore. Why? Why not the following? backtick update_vm_id { ... } multisubstitute { define fsdir ... importas -Siu update_vm_id }
+ } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. + + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + if { rm -rf -- ${fsdir}/etc } + if { umask 022 mkdir -p -- ${fsdir}/updates ${fsdir}/etc/systemd } + if { cp -R -- /etc/updatevm/sysupdate.d /etc/updatevm/url-env ${fsdir}/etc } + if { cp -- /etc/systemd/import-pubring.gpg ${fsdir}/etc/systemd }
Why copy rather than bind mount?
Target does not exist and I didn't want to bind-mount all of /etc/systemd.
You can touch a file and then bind mount, and still save a copy.
+ + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- ${fsdir}/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared ${fsdir}/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + if { s6-svwait -D ${svcdir} } +
It might be more robust to use a transient VM, like we use for AppImages, so that nothing can restart it. Transient VMs are still developing though, so it's also fine to say we'll do it this way for now and adapt it later. This would also save all the filesystem resetting you're needing to do here.
The path to the update directory is user-provided. It's not from the VM's persistent storage.
Ideally (at some point) it isn't user provided, and is the VM's transient disk-backed storage, IMO.
@@ -17,5 +18,11 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; in - default // callConfig config; + finalConfig // { + update-signing-key = builtins.path { + name = "signing-key"; + path = finalConfig.update-signing-key; + }; + }
What does this do?
This ensures that the Nix store path doesn't depend on the name of the update signing key, only its contents.
Interesting. Does that matter, though? It ends up being called /etc/systemd/import-pubring.gpg in the image regardless.
diff --git a/vm/app/updates.nix b/vm/app/updates.nix new file mode 100644 index 0000000000000000000000000000000000000000..d2c1e5fcb35b37c7ed8a173f19b97894a36a7f0c --- /dev/null +++ b/vm/app/updates.nix @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../lib/call-package.nix ( +{ callSpectrumPackage, config, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + update-url = config.update-url; + mountpoint = "/run/virtiofs/virtiofs0"; + sysupdate-path = "${systemd}/lib/systemd/systemd-sysupdate"; + runner = writeScript "update-run-script" + '' + #!/usr/bin/execlineb -P + if { mount -toverlay -olowerdir=${mountpoint}/etc:/etc -- overlay /etc } + envfile ${mountpoint}/etc/url-env
Seems like overkill to use an envfile for a single URL?
It is indeed overkill, but I'm not aware of a simpler option. There is backtick + cat but that's two programs rather than one.
I think the canonical way would be redirfd + withstdinas, but that's also two programs, so if you want to avoid that, perhaps s6-envdir? Reading it isn't any simpler but writing it at least doesn't require a special tool.
+ importas -i update_url UPDATE_URL + if { ${sysupdate-path} update } + if { ${curl}/bin/curl -L --proto =http,https + -o ${mountpoint}/updates/SHA256SUMS.gpg ''${update_url}/SHA256SUMS.gpg } + # systemd-sysupdate recently went from needing SHA256SUMS.gpg to SHA256SUMS.sha256.asc. + # I (Demi) have no need if this is intentional or a bug. I also have no idea if this + # behavior will stay unchanged in the future. Therefore, create both files and let + # systemd-sysupdate ignore the one it isn't interested in. + if { ln -f ${mountpoint}/updates/SHA256SUMS.gpg ${mountpoint}/updates/SHA256SUMS.sha256.asc }
Would be good to figure out why that happened. If we add a comment like this it's very unlikely to ever get cleaned up.
"hwdb: drop trailing whitespace"?
+ ${curl}/bin/curl -L --proto =http,https + -o ${mountpoint}/updates/SHA256SUMS ''${update_url}/SHA256SUMS + ''; +in + +callSpectrumPackage ../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = "${runner}";
Might as well inline this.
I chose to keep it separate to improve readability.
Okay. I'd find it more readable inlined but it's your code. :)
On 11/14/25 07:14, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 11:44, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
+# 9. Call systemd-sysupdate to run the actual update. + +if { mkdir -p -m 0700 /run/updater } +s6-setlock /run/update-lock +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } +
Wouldn't it break if there's already stuff in it?
No, it works fine in this case. I checked :).
I'd do
foreground { redirfd -w 2 /dev/null btrfs subvolume delete -- shared } if { btrfs subvolume create -- shared }
and then you know you've got an empty subvolume.
An empty subvolume isn't good: it means that systemd-sysupdate will redownload an update even when it isn't needed.
When would the update have already been downloaded but not applied? Only if there's an error in actually installing it? I'd feel happier knowing we were always starting with a clean state.
The existence of the downloaded files is how the update VM knows "we already have the latest version installed, don't try to download anything new". The VM doesn't know the actual version installed on the host, so it can't query this by other means.
+ # Snapshot directory may have files or directories with untrusted names. + # Redirect its output to /dev/null to avoid printing them to the console. + ifelse -n { redirfd -w 2 /dev/null rm -rf -- snapshot } { + foreground { redirfd -w 2 echo "Cannot remove snapshot directory" } + exit 1 + }
Why not btrfs subvolume delete? It's faster and won't print names.
It doesn't distinguish "subvolume doesn't exist" from "problem deleting subvolume". A better solution is to call `rm -f` if `btrfs subvolume delete` failed. That ignores "does not exist" errors, but not other errors.
Right. I had assumed btrfs subvolume create would cause an error if it already existed, but you've said it doesn't.
I don't want to create the subvolume over and over, because of the re
+ + backtick -E update_vm_id_ { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-updates } + basename -- $id_path + } + + multisubstitute { + define fsdir /run/vm/by-id/${update_vm_id_}/fs + define update_vm_id ${update_vm_id_}
Why?
Avoiding serial substitution.
But this is serial substitution. You're substituting update_vm_id_, and then you're doing another substitution of update_vm_id without the underscore. Why? Why not the following?
backtick update_vm_id { ... } multisubstitute { define fsdir ... importas -Siu update_vm_id }
"serial substitution" means that one substitutes into a string that has already been substituted into. See the multisubstitute documentation for why this is bad. I avoided the route you describe because I wanted to define fsdir and update_vm_id in the same call to multisubstitute.
+ } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. + + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + if { rm -rf -- ${fsdir}/etc } + if { umask 022 mkdir -p -- ${fsdir}/updates ${fsdir}/etc/systemd } + if { cp -R -- /etc/updatevm/sysupdate.d /etc/updatevm/url-env ${fsdir}/etc } + if { cp -- /etc/systemd/import-pubring.gpg ${fsdir}/etc/systemd }
Why copy rather than bind mount?
Target does not exist and I didn't want to bind-mount all of /etc/systemd.
You can touch a file and then bind mount, and still save a copy.
This is a tiny file. I suspect the extra exec is more expensive than the copy.
+ + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- ${fsdir}/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared ${fsdir}/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + if { s6-svwait -D ${svcdir} } +
It might be more robust to use a transient VM, like we use for AppImages, so that nothing can restart it. Transient VMs are still developing though, so it's also fine to say we'll do it this way for now and adapt it later. This would also save all the filesystem resetting you're needing to do here.
The path to the update directory is user-provided. It's not from the VM's persistent storage.
Ideally (at some point) it isn't user provided, and is the VM's transient disk-backed storage, IMO.
I'd prefer that too, but right now this would cause repeated calls to the updater to download the current version over and over. That definitely isn't good. I'd prefer to get a basic updater working first and then improve it over time.
@@ -17,5 +18,11 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; in - default // callConfig config; + finalConfig // { + update-signing-key = builtins.path { + name = "signing-key"; + path = finalConfig.update-signing-key; + }; + }
What does this do?
This ensures that the Nix store path doesn't depend on the name of the update signing key, only its contents.
Interesting. Does that matter, though? It ends up being called /etc/systemd/import-pubring.gpg in the image regardless.
Otherwise renaming it would cause a pointless rebuild of a bunchof stuff.
diff --git a/vm/app/updates.nix b/vm/app/updates.nix new file mode 100644 index 0000000000000000000000000000000000000000..d2c1e5fcb35b37c7ed8a173f19b97894a36a7f0c --- /dev/null +++ b/vm/app/updates.nix @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../lib/call-package.nix ( +{ callSpectrumPackage, config, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + update-url = config.update-url; + mountpoint = "/run/virtiofs/virtiofs0"; + sysupdate-path = "${systemd}/lib/systemd/systemd-sysupdate"; + runner = writeScript "update-run-script" + '' + #!/usr/bin/execlineb -P + if { mount -toverlay -olowerdir=${mountpoint}/etc:/etc -- overlay /etc } + envfile ${mountpoint}/etc/url-env
Seems like overkill to use an envfile for a single URL?
It is indeed overkill, but I'm not aware of a simpler option. There is backtick + cat but that's two programs rather than one.
I think the canonical way would be redirfd + withstdinas, but that's also two programs, so if you want to avoid that, perhaps s6-envdir? Reading it isn't any simpler but writing it at least doesn't require a special tool.
We need sed to generate the .transfer files anyway.
+ importas -i update_url UPDATE_URL + if { ${sysupdate-path} update } + if { ${curl}/bin/curl -L --proto =http,https + -o ${mountpoint}/updates/SHA256SUMS.gpg ''${update_url}/SHA256SUMS.gpg } + # systemd-sysupdate recently went from needing SHA256SUMS.gpg to SHA256SUMS.sha256.asc. + # I (Demi) have no need if this is intentional or a bug. I also have no idea if this + # behavior will stay unchanged in the future. Therefore, create both files and let + # systemd-sysupdate ignore the one it isn't interested in. + if { ln -f ${mountpoint}/updates/SHA256SUMS.gpg ${mountpoint}/updates/SHA256SUMS.sha256.asc }
Would be good to figure out why that happened. If we add a comment like this it's very unlikely to ever get cleaned up.
"hwdb: drop trailing whitespace"?
https://github.com/systemd/systemd/issues/39723 ("systemd-sysupdate checks for SHA256SUMS.sha256.asc when fetching from file:///"), which was fixed in <https://github.com/systemd/systemd/commit/aa7574417b86ac0bb7ed492b7cfc872e9ace15d7> ("pull: fix SHA256SUMS fallback for file:// URLs"). systemd v258.1 should have the fix. Does Spectrum use an older nixpkgs?
+ ${curl}/bin/curl -L --proto =http,https + -o ${mountpoint}/updates/SHA256SUMS ''${update_url}/SHA256SUMS + ''; +in + +callSpectrumPackage ../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = "${runner}";
Might as well inline this.
I chose to keep it separate to improve readability.
Okay. I'd find it more readable inlined but it's your code. :) I changed my mind :). -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/14/25 07:14, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 11:44, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
+ + backtick -E update_vm_id_ { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-updates } + basename -- $id_path + } + + multisubstitute { + define fsdir /run/vm/by-id/${update_vm_id_}/fs + define update_vm_id ${update_vm_id_}
Why?
Avoiding serial substitution.
But this is serial substitution. You're substituting update_vm_id_, and then you're doing another substitution of update_vm_id without the underscore. Why? Why not the following?
backtick update_vm_id { ... } multisubstitute { define fsdir ... importas -Siu update_vm_id }
"serial substitution" means that one substitutes into a string that has already been substituted into. See the multisubstitute documentation for why this is bad. I avoided the route you describe because I wanted to define fsdir and update_vm_id in the same call to multisubstitute.
Right — I now understand. I think going forward we might want to consider having to substitute more than once as an indication that the script is too complicated for execline, which I'm sure you'll be happy to here. But we can do this way for now and do a full audit of our execline scripts later when we get a chance.
+ } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. + + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + if { rm -rf -- ${fsdir}/etc } + if { umask 022 mkdir -p -- ${fsdir}/updates ${fsdir}/etc/systemd } + if { cp -R -- /etc/updatevm/sysupdate.d /etc/updatevm/url-env ${fsdir}/etc } + if { cp -- /etc/systemd/import-pubring.gpg ${fsdir}/etc/systemd }
Why copy rather than bind mount?
Target does not exist and I didn't want to bind-mount all of /etc/systemd.
You can touch a file and then bind mount, and still save a copy.
This is a tiny file. I suspect the extra exec is more expensive than the copy.
My concern is the tmpfs space utilization — I know it's small but I'd like to avoid files being stored on tmpfs at all unless there's a good reason.
@@ -17,5 +18,11 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; in - default // callConfig config; + finalConfig // { + update-signing-key = builtins.path { + name = "signing-key"; + path = finalConfig.update-signing-key; + }; + }
What does this do?
This ensures that the Nix store path doesn't depend on the name of the update signing key, only its contents.
Interesting. Does that matter, though? It ends up being called /etc/systemd/import-pubring.gpg in the image regardless.
Otherwise renaming it would cause a pointless rebuild of a bunchof stuff.
Okay. Can we do this at the point of use? I'd rather not have a special override for this one config value — it's another place to have to remember to read to understand how config works.
diff --git a/vm/app/updates.nix b/vm/app/updates.nix new file mode 100644 index 0000000000000000000000000000000000000000..d2c1e5fcb35b37c7ed8a173f19b97894a36a7f0c --- /dev/null +++ b/vm/app/updates.nix @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../lib/call-package.nix ( +{ callSpectrumPackage, config, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + update-url = config.update-url; + mountpoint = "/run/virtiofs/virtiofs0"; + sysupdate-path = "${systemd}/lib/systemd/systemd-sysupdate"; + runner = writeScript "update-run-script" + '' + #!/usr/bin/execlineb -P + if { mount -toverlay -olowerdir=${mountpoint}/etc:/etc -- overlay /etc } + envfile ${mountpoint}/etc/url-env
Seems like overkill to use an envfile for a single URL?
It is indeed overkill, but I'm not aware of a simpler option. There is backtick + cat but that's two programs rather than one.
I think the canonical way would be redirfd + withstdinas, but that's also two programs, so if you want to avoid that, perhaps s6-envdir? Reading it isn't any simpler but writing it at least doesn't require a special tool.
We need sed to generate the .transfer files anyway.
What does sed have to do with envfile/s6-envdir/etc?
+ importas -i update_url UPDATE_URL + if { ${sysupdate-path} update } + if { ${curl}/bin/curl -L --proto =http,https + -o ${mountpoint}/updates/SHA256SUMS.gpg ''${update_url}/SHA256SUMS.gpg } + # systemd-sysupdate recently went from needing SHA256SUMS.gpg to SHA256SUMS.sha256.asc. + # I (Demi) have no need if this is intentional or a bug. I also have no idea if this + # behavior will stay unchanged in the future. Therefore, create both files and let + # systemd-sysupdate ignore the one it isn't interested in. + if { ln -f ${mountpoint}/updates/SHA256SUMS.gpg ${mountpoint}/updates/SHA256SUMS.sha256.asc }
Would be good to figure out why that happened. If we add a comment like this it's very unlikely to ever get cleaned up.
"hwdb: drop trailing whitespace"?
https://github.com/systemd/systemd/issues/39723 ("systemd-sysupdate checks for SHA256SUMS.sha256.asc when fetching from file:///"), which was fixed in <https://github.com/systemd/systemd/commit/aa7574417b86ac0bb7ed492b7cfc872e9ace15d7> ("pull: fix SHA256SUMS fallback for file:// URLs"). systemd v258.1 should have the fix. Does Spectrum use an older nixpkgs?
Yes, we're still on 258. I expect an update before this series is applied though, because then we'll also pick up the systemd musl fix.
On 11/20/25 09:56, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/14/25 07:14, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 11:44, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
+ + backtick -E update_vm_id_ { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-updates } + basename -- $id_path + } + + multisubstitute { + define fsdir /run/vm/by-id/${update_vm_id_}/fs + define update_vm_id ${update_vm_id_}
Why?
Avoiding serial substitution.
But this is serial substitution. You're substituting update_vm_id_, and then you're doing another substitution of update_vm_id without the underscore. Why? Why not the following?
backtick update_vm_id { ... } multisubstitute { define fsdir ... importas -Siu update_vm_id }
"serial substitution" means that one substitutes into a string that has already been substituted into. See the multisubstitute documentation for why this is bad. I avoided the route you describe because I wanted to define fsdir and update_vm_id in the same call to multisubstitute.
Right — I now understand. I think going forward we might want to consider having to substitute more than once as an indication that the script is too complicated for execline, which I'm sure you'll be happy to here. But we can do this way for now and do a full audit of our execline scripts later when we get a chance.
That seems like a good idea. The multisubstitute command is there for a reason, though :).
+ } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. + + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + if { rm -rf -- ${fsdir}/etc } + if { umask 022 mkdir -p -- ${fsdir}/updates ${fsdir}/etc/systemd } + if { cp -R -- /etc/updatevm/sysupdate.d /etc/updatevm/url-env ${fsdir}/etc } + if { cp -- /etc/systemd/import-pubring.gpg ${fsdir}/etc/systemd }
Why copy rather than bind mount?
Target does not exist and I didn't want to bind-mount all of /etc/systemd.
You can touch a file and then bind mount, and still save a copy.
This is a tiny file. I suspect the extra exec is more expensive than the copy.
My concern is the tmpfs space utilization — I know it's small but I'd like to avoid files being stored on tmpfs at all unless there's a good reason.
@@ -17,5 +18,11 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; in - default // callConfig config; + finalConfig // { + update-signing-key = builtins.path { + name = "signing-key"; + path = finalConfig.update-signing-key; + }; + }
What does this do?
This ensures that the Nix store path doesn't depend on the name of the update signing key, only its contents.
Interesting. Does that matter, though? It ends up being called /etc/systemd/import-pubring.gpg in the image regardless.
Otherwise renaming it would cause a pointless rebuild of a bunchof stuff.
Okay. Can we do this at the point of use? I'd rather not have a special override for this one config value — it's another place to have to remember to read to understand how config works.
diff --git a/vm/app/updates.nix b/vm/app/updates.nix new file mode 100644 index 0000000000000000000000000000000000000000..d2c1e5fcb35b37c7ed8a173f19b97894a36a7f0c --- /dev/null +++ b/vm/app/updates.nix @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../lib/call-package.nix ( +{ callSpectrumPackage, config, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + update-url = config.update-url; + mountpoint = "/run/virtiofs/virtiofs0"; + sysupdate-path = "${systemd}/lib/systemd/systemd-sysupdate"; + runner = writeScript "update-run-script" + '' + #!/usr/bin/execlineb -P + if { mount -toverlay -olowerdir=${mountpoint}/etc:/etc -- overlay /etc } + envfile ${mountpoint}/etc/url-env
Seems like overkill to use an envfile for a single URL?
It is indeed overkill, but I'm not aware of a simpler option. There is backtick + cat but that's two programs rather than one.
I think the canonical way would be redirfd + withstdinas, but that's also two programs, so if you want to avoid that, perhaps s6-envdir? Reading it isn't any simpler but writing it at least doesn't require a special tool.
We need sed to generate the .transfer files anyway.
What does sed have to do with envfile/s6-envdir/etc?
The envfile and the .transfer files were both generated with sed, so using an envfile didn't require any extra tools. However, v3 doesn't use an envfile anymore, so this is moot.
+ importas -i update_url UPDATE_URL + if { ${sysupdate-path} update } + if { ${curl}/bin/curl -L --proto =http,https + -o ${mountpoint}/updates/SHA256SUMS.gpg ''${update_url}/SHA256SUMS.gpg } + # systemd-sysupdate recently went from needing SHA256SUMS.gpg to SHA256SUMS.sha256.asc. + # I (Demi) have no need if this is intentional or a bug. I also have no idea if this + # behavior will stay unchanged in the future. Therefore, create both files and let + # systemd-sysupdate ignore the one it isn't interested in. + if { ln -f ${mountpoint}/updates/SHA256SUMS.gpg ${mountpoint}/updates/SHA256SUMS.sha256.asc }
Would be good to figure out why that happened. If we add a comment like this it's very unlikely to ever get cleaned up.
"hwdb: drop trailing whitespace"?
https://github.com/systemd/systemd/issues/39723 ("systemd-sysupdate checks for SHA256SUMS.sha256.asc when fetching from file:///"), which was fixed in <https://github.com/systemd/systemd/commit/aa7574417b86ac0bb7ed492b7cfc872e9ace15d7> ("pull: fix SHA256SUMS fallback for file:// URLs"). systemd v258.1 should have the fix. Does Spectrum use an older nixpkgs?
Yes, we're still on 258. I expect an update before this series is applied though, because then we'll also pick up the systemd musl fix.
That's good, thanks! v3 switches to SHA256SUMS.sha256.asc, as it seems to be the "proper" filename. -- Sincerely, Demi Marie Obenour (she/her/hers)
There is now a way to update the OS, so the previous documentation is now stale! Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/installation/index.adoc | 3 ++- Documentation/using-spectrum/index.adoc | 2 ++ Documentation/using-spectrum/updates.adoc | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Documentation/installation/index.adoc b/Documentation/installation/index.adoc index d67c88dda062066c19c3b21e699f074cc18a6dbc..536c3dd9f78faa2ecad4127dc9ccc2058a230b1a 100644 --- a/Documentation/installation/index.adoc +++ b/Documentation/installation/index.adoc @@ -18,6 +18,7 @@ development. == Uninstalling and Updating -Currently, there is no implementation for a software update. +See xref:../using-spectrum/updates.adoc[Updating the OS] for how to enable +updates. You can replace Spectrum by installing another OS. diff --git a/Documentation/using-spectrum/index.adoc b/Documentation/using-spectrum/index.adoc index 25347a4ed7bb1f899ee0a3b85aa51da94bb954b4..5d9ea657f7c6f8c21edbf8637d2d2d0bf52f931d 100644 --- a/Documentation/using-spectrum/index.adoc +++ b/Documentation/using-spectrum/index.adoc @@ -11,3 +11,5 @@ Ready to get started with Spectrum? Here is what you can do next: * xref:running-vms.adoc[Start some applications]. * xref:creating-custom-vms.adoc[Create your own VM] to use other applications. +* xref:updates.adoc[Enable updates] so you can use newer versions of Spectrum + without reinstalling the OS. diff --git a/Documentation/using-spectrum/updates.adoc b/Documentation/using-spectrum/updates.adoc new file mode 100644 index 0000000000000000000000000000000000000000..ffd6fda269617768d486e58e30661bbefc8b2bbd --- /dev/null +++ b/Documentation/using-spectrum/updates.adoc @@ -0,0 +1,29 @@ += Updating the OS +:page-parent: Using Spectrum + +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +// SPDX-License-Identifier: GFDL-1.3-no-invariants-or-later OR CC-BY-SA-4.0 + +Spectrum supports updates via the `update` command. This +takes the path to a staging directory as argument. `update` +will create the directory, use it for the update, and then +delete it. The parent directory must exist. + +Updates are atomic and take effect after the system reboots. +If the system is rebooted, crashes, or loses power during an +update, the update will automatically be rolled back. Updates +are digitally signed and Spectrum will refuse to install an +update that does not have a trusted signature. + +Currently, Spectrum does not provide an update server, so +you must provide your own. You can do this via +xref:../development/build-configuration.adoc[build configuration]. +The default sets the signing key to `/dev/null` and the server +URL to an invalid value, so updates won't work. To enable updates, +set `update-url` to the URL of your server and `update-signing-key` +to a binary GnuPG keyring to verify the updates with. Not all possible +URLs will work, but most invalid URLs will cause an error during the +build rather than runtime misbehavior. + +Right now, it is not possible to change the update URL or signing key +except via an update or by reinstalling the OS. -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
There is now a way to update the OS, so the previous documentation is now stale!
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/installation/index.adoc | 3 ++- Documentation/using-spectrum/index.adoc | 2 ++ Documentation/using-spectrum/updates.adoc | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/Documentation/installation/index.adoc b/Documentation/installation/index.adoc index d67c88dda062066c19c3b21e699f074cc18a6dbc..536c3dd9f78faa2ecad4127dc9ccc2058a230b1a 100644 --- a/Documentation/installation/index.adoc +++ b/Documentation/installation/index.adoc @@ -18,6 +18,7 @@ development.
== Uninstalling and Updating
-Currently, there is no implementation for a software update. +See xref:../using-spectrum/updates.adoc[Updating the OS] for how to enable +updates.
Let's phrase this so it says that there's work going on to enable updates but it's not all set up yet. User-focused documentation shouldn't really be suggesting that people will have to build their own images and run their own update servers.
You can replace Spectrum by installing another OS. diff --git a/Documentation/using-spectrum/index.adoc b/Documentation/using-spectrum/index.adoc index 25347a4ed7bb1f899ee0a3b85aa51da94bb954b4..5d9ea657f7c6f8c21edbf8637d2d2d0bf52f931d 100644 --- a/Documentation/using-spectrum/index.adoc +++ b/Documentation/using-spectrum/index.adoc @@ -11,3 +11,5 @@ Ready to get started with Spectrum? Here is what you can do next:
* xref:running-vms.adoc[Start some applications]. * xref:creating-custom-vms.adoc[Create your own VM] to use other applications. +* xref:updates.adoc[Enable updates] so you can use newer versions of Spectrum + without reinstalling the OS.
This doesn't really belong in the "Using Spectrum" section, because people who're only using Spectrum should have working updates out of the box. It would make more sense to be documented alongside the configuration mechanism — that's the audience for this.
diff --git a/Documentation/using-spectrum/updates.adoc b/Documentation/using-spectrum/updates.adoc new file mode 100644 index 0000000000000000000000000000000000000000..ffd6fda269617768d486e58e30661bbefc8b2bbd --- /dev/null +++ b/Documentation/using-spectrum/updates.adoc @@ -0,0 +1,29 @@ += Updating the OS +:page-parent: Using Spectrum + +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +// SPDX-License-Identifier: GFDL-1.3-no-invariants-or-later OR CC-BY-SA-4.0 + +Spectrum supports updates via the `update` command. This +takes the path to a staging directory as argument. `update` +will create the directory, use it for the update, and then +delete it. The parent directory must exist.
And be on btrfs?
+ +Updates are atomic and take effect after the system reboots. +If the system is rebooted, crashes, or loses power during an +update, the update will automatically be rolled back. Updates
Is this currently true?
+are digitally signed and Spectrum will refuse to install an +update that does not have a trusted signature. + +Currently, Spectrum does not provide an update server, so +you must provide your own. You can do this via +xref:../development/build-configuration.adoc[build configuration]. +The default sets the signing key to `/dev/null` and the server +URL to an invalid value, so updates won't work. To enable updates, +set `update-url` to the URL of your server and `update-signing-key` +to a binary GnuPG keyring to verify the updates with. Not all possible +URLs will work, but most invalid URLs will cause an error during the +build rather than runtime misbehavior.
We should probably reference systemd-sysupdate so people can understand what their update server is supposed to serve, without us having to duplicate that information in our own documentation.
+ +Right now, it is not possible to change the update URL or signing key +except via an update or by reinstalling the OS.
On 11/13/25 11:49, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
There is now a way to update the OS, so the previous documentation is now stale!
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/installation/index.adoc | 3 ++- Documentation/using-spectrum/index.adoc | 2 ++ Documentation/using-spectrum/updates.adoc | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/Documentation/installation/index.adoc b/Documentation/installation/index.adoc index d67c88dda062066c19c3b21e699f074cc18a6dbc..536c3dd9f78faa2ecad4127dc9ccc2058a230b1a 100644 --- a/Documentation/installation/index.adoc +++ b/Documentation/installation/index.adoc @@ -18,6 +18,7 @@ development.
== Uninstalling and Updating
-Currently, there is no implementation for a software update. +See xref:../using-spectrum/updates.adoc[Updating the OS] for how to enable +updates.
Let's phrase this so it says that there's work going on to enable updates but it's not all set up yet. User-focused documentation shouldn't really be suggesting that people will have to build their own images and run their own update servers.
Would it be okay to mention that it is WIP, and also add a link to the build configuration options for those who *have* built their own images? That will continue to be relevant even after official binary releases are available. Developers are users too, and they might be a bit confused when their image either doesn't update at all or updates to an official build without any of their changes.
You can replace Spectrum by installing another OS. diff --git a/Documentation/using-spectrum/index.adoc b/Documentation/using-spectrum/index.adoc index 25347a4ed7bb1f899ee0a3b85aa51da94bb954b4..5d9ea657f7c6f8c21edbf8637d2d2d0bf52f931d 100644 --- a/Documentation/using-spectrum/index.adoc +++ b/Documentation/using-spectrum/index.adoc @@ -11,3 +11,5 @@ Ready to get started with Spectrum? Here is what you can do next:
* xref:running-vms.adoc[Start some applications]. * xref:creating-custom-vms.adoc[Create your own VM] to use other applications. +* xref:updates.adoc[Enable updates] so you can use newer versions of Spectrum + without reinstalling the OS.
This doesn't really belong in the "Using Spectrum" section, because people who're only using Spectrum should have working updates out of the box. It would make more sense to be documented alongside the configuration mechanism — that's the audience for this.
I'll add the info to the build configuration mechanism.
diff --git a/Documentation/using-spectrum/updates.adoc b/Documentation/using-spectrum/updates.adoc new file mode 100644 index 0000000000000000000000000000000000000000..ffd6fda269617768d486e58e30661bbefc8b2bbd --- /dev/null +++ b/Documentation/using-spectrum/updates.adoc @@ -0,0 +1,29 @@ += Updating the OS +:page-parent: Using Spectrum + +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +// SPDX-License-Identifier: GFDL-1.3-no-invariants-or-later OR CC-BY-SA-4.0 + +Spectrum supports updates via the `update` command. This +takes the path to a staging directory as argument. `update` +will create the directory, use it for the update, and then +delete it. The parent directory must exist.
And be on btrfs?
+ +Updates are atomic and take effect after the system reboots. +If the system is rebooted, crashes, or loses power during an +update, the update will automatically be rolled back. Updates
Is this currently true?
If not, that's a systemd-sysupdate bug.
+are digitally signed and Spectrum will refuse to install an +update that does not have a trusted signature. + +Currently, Spectrum does not provide an update server, so +you must provide your own. You can do this via +xref:../development/build-configuration.adoc[build configuration]. +The default sets the signing key to `/dev/null` and the server +URL to an invalid value, so updates won't work. To enable updates, +set `update-url` to the URL of your server and `update-signing-key` +to a binary GnuPG keyring to verify the updates with. Not all possible +URLs will work, but most invalid URLs will cause an error during the +build rather than runtime misbehavior.
We should probably reference systemd-sysupdate so people can understand what their update server is supposed to serve, without us having to duplicate that information in our own documentation.
Good idea.
+ +Right now, it is not possible to change the update URL or signing key +except via an update or by reinstalling the OS. This is actually stale. It was true in v1 because some of this information was hard-coded into the update VM, but now all of that information comes from the host. -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/13/25 11:49, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
There is now a way to update the OS, so the previous documentation is now stale!
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/installation/index.adoc | 3 ++- Documentation/using-spectrum/index.adoc | 2 ++ Documentation/using-spectrum/updates.adoc | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/Documentation/installation/index.adoc b/Documentation/installation/index.adoc index d67c88dda062066c19c3b21e699f074cc18a6dbc..536c3dd9f78faa2ecad4127dc9ccc2058a230b1a 100644 --- a/Documentation/installation/index.adoc +++ b/Documentation/installation/index.adoc @@ -18,6 +18,7 @@ development.
== Uninstalling and Updating
-Currently, there is no implementation for a software update. +See xref:../using-spectrum/updates.adoc[Updating the OS] for how to enable +updates.
Let's phrase this so it says that there's work going on to enable updates but it's not all set up yet. User-focused documentation shouldn't really be suggesting that people will have to build their own images and run their own update servers.
Would it be okay to mention that it is WIP, and also add a link to the build configuration options for those who *have* built their own images? That will continue to be relevant even after official binary releases are available. Developers are users too, and they might be a bit confused when their image either doesn't update at all or updates to an official build without any of their changes.
I think it would make more sense to cover that in the section about building your own images, because ideally you find this out just before you build your image, whereas here you might end up only finding it after and have to rebuild.
+Updates are atomic and take effect after the system reboots. +If the system is rebooted, crashes, or loses power during an +update, the update will automatically be rolled back. Updates
Is this currently true?
If not, that's a systemd-sysupdate bug.
I thought you took out the boot counting stuff?
Wrong values for the version or update URL will cause very confusing build-time or runtime errors. Provide a better user experience by validating them up-front. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- lib/config.nix | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/lib/config.nix b/lib/config.nix index 5b6b95013734202b7e2e01d5ffce313080658006..660a2427447fd9851e60e955da6bd1a5d71cfdac 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -19,10 +19,39 @@ let inherit default; } else config; finalConfig = default // callConfig config; + + # Only allow unreserved characters, : (for port numbers), /, and %-encoding. + # The rest of the code is allowed to assume that these are the only characters + # in the update URL. + # Do not use [:alnum:] or [:hexdigit:] as they depend on the locale in POSIX. + # Query strings and fragment identifiers break appending + # /SHA256SUMS and /SHA256SUMS.gpg to a URL. + # [, ], {, and } would cause globbing in curl. + url-regex = "^https?://([ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:./~-]|%[ABCDEFabcdef0123456789]{2})+$"; + update-url = finalConfig.update-url; + + # Only allow a numeric version for now. + number_re = "(0|[1-9][0-9]{0,2})"; + version_re = "^(${number_re}\\.){2}${number_re}$"; in - finalConfig // { - update-signing-key = builtins.path { - name = "signing-key"; - path = finalConfig.update-signing-key; - }; - } + if !builtins.isString update-url then + builtins.abort "Update URL must be a string, not ${builtins.typeOf update-url}" + else if builtins.match "^https?://.*" update-url == null then + builtins.abort "Update URL ${builtins.toJSON update-url} has unsupported scheme (not https:// or http://) or is invalid" + else if builtins.match url-regex update-url == null then + builtins.abort "Update URL ${builtins.toJSON update-url} has forbidden characters" + else if builtins.substring (builtins.stringLength update-url - 1) 1 update-url == "/" then + builtins.abort "Update URL ${builtins.toJSON update-url} must not end with /" + else if !builtins.isString finalConfig.version then + builtins.abort "Version must be a string, not ${builtins.typeOf finalConfig.version}" + else if builtins.match version_re finalConfig.version == null then + builtins.abort "Version ${builtins.toJSON finalConfig.version} is invalid" + else if !builtins.isPath finalConfig.update-signing-key then + builtins.abort "Update verification key file is of type ${builtins.typeOf finalConfig.update-signing-key}, not path" + else + finalConfig // { + update-signing-key = builtins.path { + name = "signing-key"; + path = finalConfig.update-signing-key; + }; + } -- 2.51.2
Demi Marie Obenour <demiobenour@gmail.com> writes:
Wrong values for the version or update URL will cause very confusing build-time or runtime errors. Provide a better user experience by validating them up-front.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- lib/config.nix | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-)
diff --git a/lib/config.nix b/lib/config.nix index 5b6b95013734202b7e2e01d5ffce313080658006..660a2427447fd9851e60e955da6bd1a5d71cfdac 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -19,10 +19,39 @@ let inherit default; } else config; finalConfig = default // callConfig config; + + # Only allow unreserved characters, : (for port numbers), /, and %-encoding. + # The rest of the code is allowed to assume that these are the only characters + # in the update URL. + # Do not use [:alnum:] or [:hexdigit:] as they depend on the locale in POSIX.
Did we confirm that's true for Nix? It would be a big problem if it was.
+ # Query strings and fragment identifiers break appending + # /SHA256SUMS and /SHA256SUMS.gpg to a URL. + # [, ], {, and } would cause globbing in curl. + url-regex = "^https?://([ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:./~-]|%[ABCDEFabcdef0123456789]{2})+$"; + update-url = finalConfig.update-url; + + # Only allow a numeric version for now. + number_re = "(0|[1-9][0-9]{0,2})"; + version_re = "^(${number_re}\\.){2}${number_re}$";
Why? Since this is only to make developers' lives a bit easier, we should err on the side of allowing stuff through rather than restricting what might be a valid use case.
in - finalConfig // { - update-signing-key = builtins.path { - name = "signing-key"; - path = finalConfig.update-signing-key; - }; - } + if !builtins.isString update-url then + builtins.abort "Update URL must be a string, not ${builtins.typeOf update-url}" + else if builtins.match "^https?://.*" update-url == null then + builtins.abort "Update URL ${builtins.toJSON update-url} has unsupported scheme (not https:// or http://) or is invalid" + else if builtins.match url-regex update-url == null then + builtins.abort "Update URL ${builtins.toJSON update-url} has forbidden characters" + else if builtins.substring (builtins.stringLength update-url - 1) 1 update-url == "/" then + builtins.abort "Update URL ${builtins.toJSON update-url} must not end with /" + else if !builtins.isString finalConfig.version then + builtins.abort "Version must be a string, not ${builtins.typeOf finalConfig.version}" + else if builtins.match version_re finalConfig.version == null then + builtins.abort "Version ${builtins.toJSON finalConfig.version} is invalid" + else if !builtins.isPath finalConfig.update-signing-key then + builtins.abort "Update verification key file is of type ${builtins.typeOf finalConfig.update-signing-key}, not path"
I wouldn't bother checking the types. There'll be a decent error message if the wrong type is used anyway, and I don't want to get into checking the type of the other config values. I wonder if it would be more maintainable to have this more local, where the values are actually used. That way, it's more likely they'll be kept up to date.
+ else + finalConfig // { + update-signing-key = builtins.path { + name = "signing-key"; + path = finalConfig.update-signing-key; + }; + }
-- 2.51.2
This implements updates via systemd-sysupdate. See individual commit messages for details. There are major changes to the image build process. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes in v3: - See individual commits for details. There are too many to mention here. - Link to v2: https://spectrum-os.org/lists/archives/spectrum-devel/20251112-updates-v2-0-... Changes in v2: - updates-dir-check: - Do not check that there is a SHA256SUMS or SHA256SUMS.gpg file in the update directory. systemd-sysupdate will fail if it cannot find a manifest or its signature. - Follow symlinks in opening the directory. The path is from a trusted source and will always point to a BTRFS snapshot, never a symlink. The only exception is the last component, which is still checked to not be a symlink. - VM: - Link SHA256SUMS.sha256.asc to SHA256SUMS.gpg. Recent systemd-sysupdate seems to use the former name. - Get update URL from host. - Use an execline script instead of a shell script. - Update script: - Unmount shared directory if already mounted. This avoids errors when mounting it again. - Delete old snapshot if present. - Provide the VM information with a different directory layout. - Do not bind-mount the information passed into the VM into the shared VM folder. Instead rely on this folder being read-only to the guest. This is enforced by a read-only bind mount in virtiofs's mount namespace. - Testing: - Lots of manual update testing. - Disable the test for the live image as it doesn't work anymore. - Nix: - Move validation to a separate low-priority patch. - Documentation: - Document that updating the system is now possible. - Installer: - Remove the "Try Spectrum" button. - Link to v1: https://spectrum-os.org/lists/archives/spectrum-devel/20251029-updates-v1-0-... --- Demi Marie Obenour (14): host/rootfs: Install all programs from util-linuxMinimal host/rootfs: Install systemd-pull tools: Add directory checker for updates scripts: port make-gpt.sh to bash scripts/make-gpt.sh: Allow specifying partition size Support generating multiple partition UUIDs scripts: Use shell expansion to get partition path Use OS version to set partition labels and UKI name release: Compress installation images and remove live image Add B partitions to installation images release: Create directory with system update Support updates via systemd-sysupdate Documentation: Update support Validate configuration parameters Documentation/development/build-configuration.adoc | 13 ++ Documentation/installation/getting-spectrum.adoc | 56 +++++++-- Documentation/installation/index.adoc | 4 +- Documentation/using-spectrum/index.adoc | 2 + Documentation/using-spectrum/updates.adoc | 30 +++++ host/efi.nix | 2 +- host/initramfs/Makefile | 18 +-- host/initramfs/etc/probe | 20 --- host/initramfs/shell.nix | 2 + host/rootfs/Makefile | 27 ++++- host/rootfs/busybox-config | 134 +++++++++++++++++++++ host/rootfs/busybox-config.license | 4 + host/rootfs/default.nix | 87 ++++++++----- host/rootfs/file-list.mk | 7 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++ .../image/etc/vm-sysupdate.d/50-verity.transfer | 18 +++ .../image/etc/vm-sysupdate.d/60-root.transfer | 18 +++ .../image/etc/vm-sysupdate.d/70-kernel.transfer | 18 +++ host/rootfs/image/usr/bin/spectrum-update | 83 +++++++++++++ host/rootfs/os-release.in | 15 +++ host/rootfs/shell.nix | 2 + img/app/Makefile | 2 +- lib/config.default.nix | 3 + lib/config.nix | 19 ++- lib/fake-update-signing-key.gpg | 3 + release.nix | 2 + release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ----- release/combined/eosimages.nix | 11 +- release/combined/grub.cfg.in | 5 - release/live/Makefile | 15 ++- release/live/default.nix | 5 +- release/live/shell.nix | 3 +- release/update.nix | 33 +++++ scripts/format-uuid.awk | 35 ++++++ scripts/format-uuid.sh | 19 --- scripts/make-gpt.sh | 30 ++--- tools/default.nix | 1 + tools/meson.build | 4 + tools/updates-dir-check.c | 133 ++++++++++++++++++++ vm/app/systemd-sysupdate/default.nix | 57 +++++++++ vm/app/systemd-sysupdate/escape-url.awk | 31 +++++ .../systemd-sysupdate/populate-transfer-directory | 26 ++++ vm/sys/net/Makefile | 2 +- 47 files changed, 920 insertions(+), 171 deletions(-) --- base-commit: e079244d16aa0b37dd6a08b6dfe55a6a16ed6d95 change-id: 20250928-updates-92e99849e231 prerequisite-patch-id: 0ed2b2073c0ab6d422aa642fd238b15428c6f7d1 prerequisite-patch-id: c518b0e42e0c87755ef725ace8e961cdfb862285 -- Sincerely, Demi Marie Obenour (she/her/hers)
Busybox fdisk doesn't support GPT, only MBR. Busybox programs are also often buggy, so use the util-linux version where possible. This requires disabling a lot of Busybox applets, so move the Busybox config to a separate file that Nix loads via builtins.readFile. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Split installation of util-linux and of systemd to separate patches. --- host/rootfs/busybox-config | 134 +++++++++++++++++++++++++++++++++++++ host/rootfs/busybox-config.license | 4 ++ host/rootfs/default.nix | 36 +++------- 3 files changed, 147 insertions(+), 27 deletions(-) diff --git a/host/rootfs/busybox-config b/host/rootfs/busybox-config new file mode 100644 index 0000000000000000000000000000000000000000..f2fd5fcab4141ea63c663e433412a253d5235aab --- /dev/null +++ b/host/rootfs/busybox-config @@ -0,0 +1,134 @@ +CONFIG_ADDPART n +CONFIG_AGETTY n +CONFIG_BITS n +CONFIG_BLKDISCARD n +CONFIG_BLKID n +CONFIG_BLKPR n +CONFIG_BLKZONE n +CONFIG_BLOCKDEV n +CONFIG_CAL n +CONFIG_CHATTR n +CONFIG_CHCPU n +CONFIG_CHMEM n +CONFIG_CHOOM n +CONFIG_CHRT n +CONFIG_COLCRT n +CONFIG_COLRM n +CONFIG_COLUMN n +CONFIG_CORESCHED n +CONFIG_CTRLALTDEL n +CONFIG_DELPART n +CONFIG_DEPMOD n +CONFIG_DMESG n +CONFIG_EJECT n +CONFIG_ENOSYS n +CONFIG_EXCH n +CONFIG_FADVISE n +CONFIG_FALLOCATE n +CONFIG_FDISK n +CONFIG_FINCORE n +CONFIG_FINDFS n +CONFIG_FINDMNT n +CONFIG_FLOCK n +CONFIG_FSCK n +CONFIG_FSCK_CRAMFS n +CONFIG_FSCK_MINIX n +CONFIG_FSFREEZE n +CONFIG_FSTRIM n +CONFIG_GETOPT n +CONFIG_HALT n +CONFIG_HARDLINK n +CONFIG_HD n +CONFIG_HEXDUMP n +CONFIG_HWCLOCK n +CONFIG_I386 n +CONFIG_INIT n +CONFIG_INSMOD n +CONFIG_IONICE n +CONFIG_IP n +CONFIG_IPCMK n +CONFIG_IPCRM n +CONFIG_IPCS n +CONFIG_ISOSIZE n +CONFIG_KILL n +CONFIG_LAST n +CONFIG_LASTB n +CONFIG_LDATTACH n +CONFIG_LINUX32 n +CONFIG_LINUX64 n +CONFIG_LOGGER n +CONFIG_LOOK n +CONFIG_LOSETUP n +CONFIG_LSATTR n +CONFIG_LSBLK n +CONFIG_LSCLOCKS n +CONFIG_LSCPU n +CONFIG_LSFD n +CONFIG_LSIPC n +CONFIG_LSIRQ n +CONFIG_LSLOCKS n +CONFIG_LSLOGINS n +CONFIG_LSMEM n +CONFIG_LSMOD n +CONFIG_LSNS n +CONFIG_MCOOKIE n +CONFIG_MESG n +CONFIG_MKE2FS n +CONFIG_MKFS n +CONFIG_MKFS_BFS n +CONFIG_MKFS_CRAMFS n +CONFIG_MKFS_EXT2 n +CONFIG_MKFS_MINIX n +CONFIG_MKSWAP n +CONFIG_MODINFO n +CONFIG_MODPROBE n +CONFIG_MOUNT n +CONFIG_MOUNTPOINT n +CONFIG_NAMEI n +CONFIG_NOLOGIN n +CONFIG_NSENTER n +CONFIG_PARTX n +CONFIG_PIPESZ n +CONFIG_PIVOT_ROOT n +CONFIG_POWEROFF n +CONFIG_PRLIMIT n +CONFIG_READPROFILE n +CONFIG_REBOOT n +CONFIG_RENAME n +CONFIG_RENICE n +CONFIG_RESIZEPART n +CONFIG_REV n +CONFIG_RFKILL n +CONFIG_RMMOD n +CONFIG_RTCWAKE n +CONFIG_SCRIPT n +CONFIG_SCRIPTLIVE n +CONFIG_SCRIPTREPLAY n +CONFIG_SETARCH n +CONFIG_SETPGID n +CONFIG_SETPRIV n +CONFIG_SETSID n +CONFIG_SFDISK n +CONFIG_SHUTDOWN n +CONFIG_SULOGIN n +CONFIG_SWAPLABEL n +CONFIG_SWAPOFF n +CONFIG_SWAPON n +CONFIG_SWITCH_ROOT n +CONFIG_TASKSET n +CONFIG_UCLAMPSET n +CONFIG_UMOUNT n +CONFIG_UNAME26 n +CONFIG_UNSHARE n +CONFIG_UTMPDUMP n +CONFIG_UUIDD n +CONFIG_UUIDGEN n +CONFIG_UUIDPARSE n +CONFIG_WAITPID n +CONFIG_WALL n +CONFIG_WDCTL n +CONFIG_WHEREIS n +CONFIG_WIPEFS n +CONFIG_WRITE n +CONFIG_X86_64 n +CONFIG_ZRAMCTL n diff --git a/host/rootfs/busybox-config.license b/host/rootfs/busybox-config.license new file mode 100644 index 0000000000000000000000000000000000000000..ba50b647fbfac9b79ecb29f33a36c07d3e332ba2 --- /dev/null +++ b/host/rootfs/busybox-config.license @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2025 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 1578155fa0fb9a4df3fb4884e21ed7d8d8f821dc..84b536eda397adfab0fbb0122a5765571d7d678e 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -38,25 +38,8 @@ let virtiofsd xdg-desktop-portal-spectrum-host (busybox.override { - extraConfig = '' - CONFIG_CHATTR n - CONFIG_DEPMOD n - CONFIG_FINDFS n - CONFIG_HALT n - CONFIG_INIT n - CONFIG_INSMOD n - CONFIG_IP n - CONFIG_LSATTR n - CONFIG_LSMOD n - CONFIG_MKE2FS n - CONFIG_MKFS_EXT2 n - CONFIG_MODINFO n - CONFIG_MODPROBE n - CONFIG_MOUNT n - CONFIG_POWEROFF n - CONFIG_REBOOT n - CONFIG_RMMOD n - ''; + # Use a separate file as it is a bit too big. + extraConfig = builtins.readFile ./busybox-config; }) # Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. @@ -96,6 +79,12 @@ let mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps + # lndir silently ignores existing links, so run it before ln + # so that ln catches any duplicates. + for pkg in ${escapeShellArgs usrPackages}; do + lndir -ignorelinks -silent "$pkg" "$out/usr" + done + # Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \ @@ -110,18 +99,11 @@ let ln -st $out/usr/share/dbus-1/services \ ${pkgsGui.xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service - for pkg in ${escapeShellArgs usrPackages}; do - lndir -ignorelinks -silent "$pkg" "$out/usr" - done + ln -st "$out/usr/bin" ${util-linuxMinimal}/bin/* ${concatStrings (mapAttrsToList (name: path: '' ln -s ${path} $out/usr/lib/spectrum/vm/${name} '') appvms)} - - # TODO: this is a hack and we should just build the util-linux - # programs we want. - # https://lore.kernel.org/util-linux/87zgrl6ufb.fsf@alyssa.is/ - ln -s ${util-linuxMinimal}/bin/{findfs,uuidgen,lsblk,mount} $out/usr/bin ''; in -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
Busybox fdisk doesn't support GPT, only MBR. Busybox programs are also often buggy, so use the util-linux version where possible. This requires disabling a lot of Busybox applets, so move the Busybox config to a separate file that Nix loads via builtins.readFile.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Split installation of util-linux and of systemd to separate patches. --- host/rootfs/busybox-config | 134 +++++++++++++++++++++++++++++++++++++ host/rootfs/busybox-config.license | 4 ++ host/rootfs/default.nix | 36 +++------- 3 files changed, 147 insertions(+), 27 deletions(-)
diff --git a/host/rootfs/busybox-config b/host/rootfs/busybox-config new file mode 100644 index 0000000000000000000000000000000000000000..f2fd5fcab4141ea63c663e433412a253d5235aab --- /dev/null +++ b/host/rootfs/busybox-config @@ -0,0 +1,134 @@ +CONFIG_ADDPART n +CONFIG_AGETTY n +CONFIG_BITS n +CONFIG_BLKDISCARD n +CONFIG_BLKID n +CONFIG_BLKPR n +CONFIG_BLKZONE n +CONFIG_BLOCKDEV n +CONFIG_CAL n +CONFIG_CHATTR n +CONFIG_CHCPU n +CONFIG_CHMEM n +CONFIG_CHOOM n +CONFIG_CHRT n +CONFIG_COLCRT n +CONFIG_COLRM n +CONFIG_COLUMN n +CONFIG_CORESCHED n +CONFIG_CTRLALTDEL n +CONFIG_DELPART n +CONFIG_DEPMOD n +CONFIG_DMESG n +CONFIG_EJECT n +CONFIG_ENOSYS n +CONFIG_EXCH n +CONFIG_FADVISE n +CONFIG_FALLOCATE n +CONFIG_FDISK n +CONFIG_FINCORE n +CONFIG_FINDFS n +CONFIG_FINDMNT n +CONFIG_FLOCK n +CONFIG_FSCK n +CONFIG_FSCK_CRAMFS n +CONFIG_FSCK_MINIX n +CONFIG_FSFREEZE n +CONFIG_FSTRIM n +CONFIG_GETOPT n +CONFIG_HALT n +CONFIG_HARDLINK n +CONFIG_HD n +CONFIG_HEXDUMP n +CONFIG_HWCLOCK n +CONFIG_I386 n +CONFIG_INIT n +CONFIG_INSMOD n +CONFIG_IONICE n +CONFIG_IP n +CONFIG_IPCMK n +CONFIG_IPCRM n +CONFIG_IPCS n +CONFIG_ISOSIZE n +CONFIG_KILL n +CONFIG_LAST n +CONFIG_LASTB n +CONFIG_LDATTACH n +CONFIG_LINUX32 n +CONFIG_LINUX64 n +CONFIG_LOGGER n +CONFIG_LOOK n +CONFIG_LOSETUP n +CONFIG_LSATTR n +CONFIG_LSBLK n +CONFIG_LSCLOCKS n +CONFIG_LSCPU n +CONFIG_LSFD n +CONFIG_LSIPC n +CONFIG_LSIRQ n +CONFIG_LSLOCKS n +CONFIG_LSLOGINS n +CONFIG_LSMEM n +CONFIG_LSMOD n +CONFIG_LSNS n +CONFIG_MCOOKIE n +CONFIG_MESG n +CONFIG_MKE2FS n +CONFIG_MKFS n +CONFIG_MKFS_BFS n +CONFIG_MKFS_CRAMFS n +CONFIG_MKFS_EXT2 n +CONFIG_MKFS_MINIX n +CONFIG_MKSWAP n +CONFIG_MODINFO n +CONFIG_MODPROBE n +CONFIG_MOUNT n +CONFIG_MOUNTPOINT n +CONFIG_NAMEI n +CONFIG_NOLOGIN n +CONFIG_NSENTER n +CONFIG_PARTX n +CONFIG_PIPESZ n +CONFIG_PIVOT_ROOT n +CONFIG_POWEROFF n +CONFIG_PRLIMIT n +CONFIG_READPROFILE n +CONFIG_REBOOT n +CONFIG_RENAME n +CONFIG_RENICE n +CONFIG_RESIZEPART n +CONFIG_REV n +CONFIG_RFKILL n +CONFIG_RMMOD n +CONFIG_RTCWAKE n +CONFIG_SCRIPT n +CONFIG_SCRIPTLIVE n +CONFIG_SCRIPTREPLAY n +CONFIG_SETARCH n +CONFIG_SETPGID n +CONFIG_SETPRIV n +CONFIG_SETSID n +CONFIG_SFDISK n +CONFIG_SHUTDOWN n +CONFIG_SULOGIN n +CONFIG_SWAPLABEL n +CONFIG_SWAPOFF n +CONFIG_SWAPON n +CONFIG_SWITCH_ROOT n +CONFIG_TASKSET n +CONFIG_UCLAMPSET n +CONFIG_UMOUNT n +CONFIG_UNAME26 n +CONFIG_UNSHARE n +CONFIG_UTMPDUMP n +CONFIG_UUIDD n +CONFIG_UUIDGEN n +CONFIG_UUIDPARSE n +CONFIG_WAITPID n +CONFIG_WALL n +CONFIG_WDCTL n +CONFIG_WHEREIS n +CONFIG_WIPEFS n +CONFIG_WRITE n +CONFIG_X86_64 n +CONFIG_ZRAMCTL n diff --git a/host/rootfs/busybox-config.license b/host/rootfs/busybox-config.license new file mode 100644 index 0000000000000000000000000000000000000000..ba50b647fbfac9b79ecb29f33a36c07d3e332ba2 --- /dev/null +++ b/host/rootfs/busybox-config.license @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2025 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 1578155fa0fb9a4df3fb4884e21ed7d8d8f821dc..84b536eda397adfab0fbb0122a5765571d7d678e 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -38,25 +38,8 @@ let virtiofsd xdg-desktop-portal-spectrum-host
(busybox.override { - extraConfig = '' - CONFIG_CHATTR n - CONFIG_DEPMOD n - CONFIG_FINDFS n - CONFIG_HALT n - CONFIG_INIT n - CONFIG_INSMOD n - CONFIG_IP n - CONFIG_LSATTR n - CONFIG_LSMOD n - CONFIG_MKE2FS n - CONFIG_MKFS_EXT2 n - CONFIG_MODINFO n - CONFIG_MODPROBE n - CONFIG_MOUNT n - CONFIG_POWEROFF n - CONFIG_REBOOT n - CONFIG_RMMOD n - ''; + # Use a separate file as it is a bit too big. + extraConfig = builtins.readFile ./busybox-config; })
# Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. @@ -96,6 +79,12 @@ let mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps
+ # lndir silently ignores existing links, so run it before ln + # so that ln catches any duplicates. + for pkg in ${escapeShellArgs usrPackages}; do + lndir -ignorelinks -silent "$pkg" "$out/usr" + done + # Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \ @@ -110,18 +99,11 @@ let ln -st $out/usr/share/dbus-1/services \ ${pkgsGui.xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service
- for pkg in ${escapeShellArgs usrPackages}; do - lndir -ignorelinks -silent "$pkg" "$out/usr" - done + ln -st "$out/usr/bin" ${util-linuxMinimal}/bin/*
Last time[1] I asked why this couldn't just be listed in the packages list like everything else. Would still like to know. [1]: https://spectrum-os.org/lists/archives/spectrum-devel/87ldkaglfc.fsf@alyssa....
${concatStrings (mapAttrsToList (name: path: '' ln -s ${path} $out/usr/lib/spectrum/vm/${name} '') appvms)} - - # TODO: this is a hack and we should just build the util-linux - # programs we want. - # https://lore.kernel.org/util-linux/87zgrl6ufb.fsf@alyssa.is/ - ln -s ${util-linuxMinimal}/bin/{findfs,uuidgen,lsblk,mount} $out/usr/bin ''; in
-- 2.52.0
On 11/19/25 09:14, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Busybox fdisk doesn't support GPT, only MBR. Busybox programs are also often buggy, so use the util-linux version where possible. This requires disabling a lot of Busybox applets, so move the Busybox config to a separate file that Nix loads via builtins.readFile.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Split installation of util-linux and of systemd to separate patches. --- host/rootfs/busybox-config | 134 +++++++++++++++++++++++++++++++++++++ host/rootfs/busybox-config.license | 4 ++ host/rootfs/default.nix | 36 +++------- 3 files changed, 147 insertions(+), 27 deletions(-)
diff --git a/host/rootfs/busybox-config b/host/rootfs/busybox-config new file mode 100644 index 0000000000000000000000000000000000000000..f2fd5fcab4141ea63c663e433412a253d5235aab --- /dev/null +++ b/host/rootfs/busybox-config @@ -0,0 +1,134 @@ +CONFIG_ADDPART n +CONFIG_AGETTY n +CONFIG_BITS n +CONFIG_BLKDISCARD n +CONFIG_BLKID n +CONFIG_BLKPR n +CONFIG_BLKZONE n +CONFIG_BLOCKDEV n +CONFIG_CAL n +CONFIG_CHATTR n +CONFIG_CHCPU n +CONFIG_CHMEM n +CONFIG_CHOOM n +CONFIG_CHRT n +CONFIG_COLCRT n +CONFIG_COLRM n +CONFIG_COLUMN n +CONFIG_CORESCHED n +CONFIG_CTRLALTDEL n +CONFIG_DELPART n +CONFIG_DEPMOD n +CONFIG_DMESG n +CONFIG_EJECT n +CONFIG_ENOSYS n +CONFIG_EXCH n +CONFIG_FADVISE n +CONFIG_FALLOCATE n +CONFIG_FDISK n +CONFIG_FINCORE n +CONFIG_FINDFS n +CONFIG_FINDMNT n +CONFIG_FLOCK n +CONFIG_FSCK n +CONFIG_FSCK_CRAMFS n +CONFIG_FSCK_MINIX n +CONFIG_FSFREEZE n +CONFIG_FSTRIM n +CONFIG_GETOPT n +CONFIG_HALT n +CONFIG_HARDLINK n +CONFIG_HD n +CONFIG_HEXDUMP n +CONFIG_HWCLOCK n +CONFIG_I386 n +CONFIG_INIT n +CONFIG_INSMOD n +CONFIG_IONICE n +CONFIG_IP n +CONFIG_IPCMK n +CONFIG_IPCRM n +CONFIG_IPCS n +CONFIG_ISOSIZE n +CONFIG_KILL n +CONFIG_LAST n +CONFIG_LASTB n +CONFIG_LDATTACH n +CONFIG_LINUX32 n +CONFIG_LINUX64 n +CONFIG_LOGGER n +CONFIG_LOOK n +CONFIG_LOSETUP n +CONFIG_LSATTR n +CONFIG_LSBLK n +CONFIG_LSCLOCKS n +CONFIG_LSCPU n +CONFIG_LSFD n +CONFIG_LSIPC n +CONFIG_LSIRQ n +CONFIG_LSLOCKS n +CONFIG_LSLOGINS n +CONFIG_LSMEM n +CONFIG_LSMOD n +CONFIG_LSNS n +CONFIG_MCOOKIE n +CONFIG_MESG n +CONFIG_MKE2FS n +CONFIG_MKFS n +CONFIG_MKFS_BFS n +CONFIG_MKFS_CRAMFS n +CONFIG_MKFS_EXT2 n +CONFIG_MKFS_MINIX n +CONFIG_MKSWAP n +CONFIG_MODINFO n +CONFIG_MODPROBE n +CONFIG_MOUNT n +CONFIG_MOUNTPOINT n +CONFIG_NAMEI n +CONFIG_NOLOGIN n +CONFIG_NSENTER n +CONFIG_PARTX n +CONFIG_PIPESZ n +CONFIG_PIVOT_ROOT n +CONFIG_POWEROFF n +CONFIG_PRLIMIT n +CONFIG_READPROFILE n +CONFIG_REBOOT n +CONFIG_RENAME n +CONFIG_RENICE n +CONFIG_RESIZEPART n +CONFIG_REV n +CONFIG_RFKILL n +CONFIG_RMMOD n +CONFIG_RTCWAKE n +CONFIG_SCRIPT n +CONFIG_SCRIPTLIVE n +CONFIG_SCRIPTREPLAY n +CONFIG_SETARCH n +CONFIG_SETPGID n +CONFIG_SETPRIV n +CONFIG_SETSID n +CONFIG_SFDISK n +CONFIG_SHUTDOWN n +CONFIG_SULOGIN n +CONFIG_SWAPLABEL n +CONFIG_SWAPOFF n +CONFIG_SWAPON n +CONFIG_SWITCH_ROOT n +CONFIG_TASKSET n +CONFIG_UCLAMPSET n +CONFIG_UMOUNT n +CONFIG_UNAME26 n +CONFIG_UNSHARE n +CONFIG_UTMPDUMP n +CONFIG_UUIDD n +CONFIG_UUIDGEN n +CONFIG_UUIDPARSE n +CONFIG_WAITPID n +CONFIG_WALL n +CONFIG_WDCTL n +CONFIG_WHEREIS n +CONFIG_WIPEFS n +CONFIG_WRITE n +CONFIG_X86_64 n +CONFIG_ZRAMCTL n diff --git a/host/rootfs/busybox-config.license b/host/rootfs/busybox-config.license new file mode 100644 index 0000000000000000000000000000000000000000..ba50b647fbfac9b79ecb29f33a36c07d3e332ba2 --- /dev/null +++ b/host/rootfs/busybox-config.license @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2025 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 1578155fa0fb9a4df3fb4884e21ed7d8d8f821dc..84b536eda397adfab0fbb0122a5765571d7d678e 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -38,25 +38,8 @@ let virtiofsd xdg-desktop-portal-spectrum-host
(busybox.override { - extraConfig = '' - CONFIG_CHATTR n - CONFIG_DEPMOD n - CONFIG_FINDFS n - CONFIG_HALT n - CONFIG_INIT n - CONFIG_INSMOD n - CONFIG_IP n - CONFIG_LSATTR n - CONFIG_LSMOD n - CONFIG_MKE2FS n - CONFIG_MKFS_EXT2 n - CONFIG_MODINFO n - CONFIG_MODPROBE n - CONFIG_MOUNT n - CONFIG_POWEROFF n - CONFIG_REBOOT n - CONFIG_RMMOD n - ''; + # Use a separate file as it is a bit too big. + extraConfig = builtins.readFile ./busybox-config; })
# Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. @@ -96,6 +79,12 @@ let mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps
+ # lndir silently ignores existing links, so run it before ln + # so that ln catches any duplicates. + for pkg in ${escapeShellArgs usrPackages}; do + lndir -ignorelinks -silent "$pkg" "$out/usr" + done + # Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \ @@ -110,18 +99,11 @@ let ln -st $out/usr/share/dbus-1/services \ ${pkgsGui.xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service
- for pkg in ${escapeShellArgs usrPackages}; do - lndir -ignorelinks -silent "$pkg" "$out/usr" - done + ln -st "$out/usr/bin" ${util-linuxMinimal}/bin/*
Last time[1] I asked why this couldn't just be listed in the packages list like everything else. Would still like to know.
[1]: https://spectrum-os.org/lists/archives/spectrum-devel/87ldkaglfc.fsf@alyssa....
It can and should be listed there. I just forgot to make the change.
${concatStrings (mapAttrsToList (name: path: '' ln -s ${path} $out/usr/lib/spectrum/vm/${name} '') appvms)} - - # TODO: this is a hack and we should just build the util-linux - # programs we want. - # https://lore.kernel.org/util-linux/87zgrl6ufb.fsf@alyssa.is/ - ln -s ${util-linuxMinimal}/bin/{findfs,uuidgen,lsblk,mount} $out/usr/bin ''; in
-- 2.52.0
-- Sincerely, Demi Marie Obenour (she/her/hers)
Due to a systemd bug [1], building systemd-sysupdate does not require that systemd-pull is built as well. However, systemd-sysupdate has a run-time dependency on systemd-pull. Therefore, override the systemd derivation so that systemd-pull is built. Confusingly, this requires enabling systemd-importd. If systemd-pull or systemd-sysupdate is not built, the resulting image will be broken and users will not be able to recover without either a reinstall or reverting to the previous version. Therefore, add a check to ensure that both are in fact built. Use 'cat' rather than just 'stat' to catch broken symlinks and the like. The override can be removed once https://github.com/NixOS/nixpkgs/pull/461277 is merged, which builds systemd-importd by default on musl. The tests will be preserved to catch any regressions. [1]: https://github.com/systemd/systemd/issues/39635 Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Add link to upstream Nixpkgs issue. --- host/rootfs/default.nix | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 84b536eda397adfab0fbb0122a5765571d7d678e..26d6dc3f9a63680bf1867c9769f807dc562c81c9 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -43,7 +43,8 @@ let }) # Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. - ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod systemd ]); + ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod ]); + nixosAllHardware = nixos ({ modulesPath, ... }: { imports = [ (modulesPath + "/profiles/all-hardware.nix") ]; @@ -64,7 +65,16 @@ let # https://inbox.vuxu.org/musl/20251017-dlopen-use-rpath-of-caller-dso-v1-1-46c... usrPackages = [ appvm kernel.modules firmware netvm - ] ++ (with pkgsGui; [ dejavu_fonts kmod.lib mesa westonLite ]); + ] ++ (with pkgsGui; [ + dejavu_fonts kmod.lib mesa westonLite + # Work around NixOS/nixpkgs#459020: without "withImportd = true" + # systemd-pull doesn't get built, so systemd-sysupdate doesn't work. + # TODO: remove this when NixOS/nixpkgs#461277 is merged. + (systemd.override { + withImportd = true; + withSysupdate = true; + }) + ]); appvms = { appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; @@ -85,6 +95,16 @@ let lndir -ignorelinks -silent "$pkg" "$out/usr" done + # If systemd-pull is missing systemd-sysupdate will fail with a + # very confusing error message. If systemd-sysupdate doesn't work, + # users will not be able to receive an update that fixes the problem. + for i in sysupdate pull; do + if ! cat -- "$out/usr/lib/systemd/systemd-$i" > /dev/null; then + echo "link to systemd-$i didn't get installed" >&2 + exit 1 + fi + done + # Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \ -- 2.52.0
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either. Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. systemd-sysupdate can leave behind temporary files with names starting with '.', so delete them instead of failing. Linux can lose cache coherency if there is an I/O error, so call syncfs() on the directory before checking anything. For the same reason, fsync() the directory if any hidden files were deleted. The directory checker also serves another critical function: it checks if the VM actually downloaded anything. Otherwise, network problems could cause updates to silently do nothing. Specifically, it checks that the VM provided a file starting with the prefix "SHA256SUMS.". These will be the last ones the in-VM updater downloads. An additional mode is provided to clean out all such files. This will be used to ensure that before the in-VM updater runs, no such files are present. Hence, if the VM didn't actually download anything, the user will get a clear error instead of a false success message or a confusing error. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Purge leftover temporary files rather than returning an error. - Split into two modes: one that deletes signature files, and one that checks that at least one signature file exists. This allows checking that the VM actually sent something. --- tools/default.nix | 1 + tools/meson.build | 4 ++ tools/updates-dir-check.c | 133 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+) diff --git a/tools/default.nix b/tools/default.nix index 27d4e152c8ef2ac16c791e57d70e68959789e446..1aaa5b35471892df1a35313f46e5489f8e10d755 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -77,6 +77,7 @@ stdenv.mkDerivation (finalAttrs: { ./sd-notify-adapter.c ./start-vmm ./subprojects + ./updates-dir-check.c ] ++ lib.optionals driverSupport [ ./xdp-forwarder ])); diff --git a/tools/meson.build b/tools/meson.build index d465e99c2ef597fdf7e818748d08db3d96f4ec6b..a7c21684dd64ad9e87c85bcdf31792e81b55faa4 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -29,6 +29,10 @@ if get_option('host') c_args : '-D_GNU_SOURCE', install: true) + executable('updates-dir-check', 'updates-dir-check.c', + c_args : '-D_GNU_SOURCE', + install: true) + subdir('lsvm') subdir('start-vmm') endif diff --git a/tools/updates-dir-check.c b/tools/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..07eb059f2718e1ad8ab087fe6509c1437ea3e96c --- /dev/null +++ b/tools/updates-dir-check.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> +#include <unistd.h> + +#include <err.h> + +[[noreturn]] static void bad_char(char c, char *msg_component) +{ + if (c >= 0x20 && c <= 0x7E) + errx(EXIT_FAILURE, "Forbidden %s character in filename: '%c'", + msg_component, (int)c); + errx(EXIT_FAILURE, + "Forbidden %s character in filename: byte %d", + msg_component, (int)(unsigned char)c); +} + +[[noreturn]] static void usage(void) +{ + errx(EXIT_FAILURE, "Usage: updates-dir-check [cleanup|check] DIRECTORIES..."); +} + +static void checkdir(int fd, bool check_sig) +{ + bool found_sig = false; + DIR *d = fdopendir(fd); + if (d == NULL) + err(EXIT_FAILURE, "fdopendir"); + // If there is an I/O error while there are dirty pages outstanding, + // the dirty pages are silently discarded. This means that the contents + // of the filesystem can change behind userspace's back. Flush all + // dirty pages in the filesystem with the directory to prevent this. + if (syncfs(fd) != 0) + err(EXIT_FAILURE, "syncfs"); + bool changed = false; + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(EXIT_FAILURE, "readdir"); + break; + } + const char *ptr = entry->d_name; + if (ptr[0] == '.') { + if (ptr[1] == '\0') + continue; + if (ptr[1] == '.' && ptr[2] == '\0') + continue; + // systemd-sysupdate uses these for temporary files. + // It normally cleans them up itself, but if there is an error + // it does not always clean them up. I'm not sure if it is + // guaranteed to clean up temporary files from a past run, so + // delete them instead of returning an error. + if (unlinkat(fd, ptr, 0)) + err(EXIT_FAILURE, "Failed to unlink temporary file"); + changed = true; + continue; + } + char c = ptr[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + bad_char(c, "initial"); + while ((c = *++ptr)) { + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) + bad_char(c, "subsequent"); + } + // Empty filenames are rejected as having a bad initial character, + // and POSIX forbids them from being returned anyway. Therefore, + // this cannot be out of bounds. + if (ptr[-1] == '.') + errx(EXIT_FAILURE, "Filename %s ends with a '.'", entry->d_name); + if (entry->d_type == DT_UNKNOWN) + errx(EXIT_FAILURE, "Filesystem didn't report type of file %s", entry->d_name); + if (entry->d_type != DT_REG) + errx(EXIT_FAILURE, "Entry contains non-regular file %s", entry->d_name); + if (strncmp(entry->d_name, "SHA256SUMS.", sizeof("SHA256SUMS.") - 1) == 0) { + // Found a signature file! + if (check_sig) + found_sig = true; + else { + if (unlinkat(fd, entry->d_name, 0)) + err(EXIT_FAILURE, "Unlinking old signature file"); + changed = true; + } + } + } + // fsync() the directory if it was changed, to avoid the above + // cache-incoherency problem. + if (changed && fsync(fd)) + errx(EXIT_FAILURE, "fsync"); + if (check_sig && !found_sig) { + warnx("sys.appvm-systemd-sysupdate didn't send a signature file."); + warnx("There was probably a problem downloading the update."); + errx(EXIT_FAILURE, "Check its logs for more information."); + } + closedir(d); +} + +int main(int argc, char **argv) +{ + if (argc != 3) + usage(); + + bool check_sig; + if (strcmp(argv[1], "cleanup") == 0) + check_sig = false; + else if (strcmp(argv[1], "check") == 0) + check_sig = true; + else + usage(); + + for (int i = 2; i < argc; ++i) { + int fd = open(argv[i], O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + err(EXIT_FAILURE, "open(%s)", argv[i]); + checkdir(fd, check_sig); + } + return 0; +} -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing
Just one "containing" is fine. :P
terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either.
Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. systemd-sysupdate can leave behind temporary files with names starting with '.', so delete them instead of failing. Linux can lose cache coherency if there is an I/O error, so call syncfs() on the directory before checking anything. For the same reason, fsync() the directory if any hidden files were deleted.
The directory checker also serves another critical function: it checks if the VM actually downloaded anything. Otherwise, network problems could cause updates to silently do nothing. Specifically, it checks that the VM provided a file starting with the prefix "SHA256SUMS.". These will be the last ones the in-VM updater downloads. An additional mode is provided to clean out all such files. This will be used to ensure that before the in-VM updater runs, no such files are present. Hence, if the VM didn't actually download anything, the user will get a clear error instead of a false success message or a confusing error.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2:
- Purge leftover temporary files rather than returning an error.
- Split into two modes: one that deletes signature files, and one that checks that at least one signature file exists. This allows checking that the VM actually sent something. --- tools/default.nix | 1 + tools/meson.build | 4 ++ tools/updates-dir-check.c | 133 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+)
Looking good. Left some style comments, but Reviewed-by: Alyssa Ross <hi@alyssa.is>
diff --git a/tools/updates-dir-check.c b/tools/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..07eb059f2718e1ad8ab087fe6509c1437ea3e96c --- /dev/null +++ b/tools/updates-dir-check.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> +#include <unistd.h> + +#include <err.h> + +[[noreturn]] static void bad_char(char c, char *msg_component) +{ + if (c >= 0x20 && c <= 0x7E) + errx(EXIT_FAILURE, "Forbidden %s character in filename: '%c'", + msg_component, (int)c); + errx(EXIT_FAILURE, + "Forbidden %s character in filename: byte %d", + msg_component, (int)(unsigned char)c);
Why not %hhu, so you don't need two layers of casts?
+} + +[[noreturn]] static void usage(void) +{ + errx(EXIT_FAILURE, "Usage: updates-dir-check [cleanup|check] DIRECTORIES..."); +} + +static void checkdir(int fd, bool check_sig)
[[gnu::fd_arg_read (1)]] (I'm bad at remembering this too so you'll see other code missing it, but it's good to add.)
+{ + bool found_sig = false; + DIR *d = fdopendir(fd); + if (d == NULL) + err(EXIT_FAILURE, "fdopendir"); + // If there is an I/O error while there are dirty pages outstanding, + // the dirty pages are silently discarded. This means that the contents + // of the filesystem can change behind userspace's back. Flush all + // dirty pages in the filesystem with the directory to prevent this. + if (syncfs(fd) != 0) + err(EXIT_FAILURE, "syncfs"); + bool changed = false; + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(EXIT_FAILURE, "readdir"); + break; + } + const char *ptr = entry->d_name; + if (ptr[0] == '.') { + if (ptr[1] == '\0') + continue; + if (ptr[1] == '.' && ptr[2] == '\0') + continue; + // systemd-sysupdate uses these for temporary files. + // It normally cleans them up itself, but if there is an error + // it does not always clean them up. I'm not sure if it is + // guaranteed to clean up temporary files from a past run, so + // delete them instead of returning an error. + if (unlinkat(fd, ptr, 0)) + err(EXIT_FAILURE, "Failed to unlink temporary file"); + changed = true; + continue; + } + char c = ptr[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + bad_char(c, "initial"); + while ((c = *++ptr)) { + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) + bad_char(c, "subsequent"); + } + // Empty filenames are rejected as having a bad initial character, + // and POSIX forbids them from being returned anyway. Therefore, + // this cannot be out of bounds. + if (ptr[-1] == '.') + errx(EXIT_FAILURE, "Filename %s ends with a '.'", entry->d_name); + if (entry->d_type == DT_UNKNOWN) + errx(EXIT_FAILURE, "Filesystem didn't report type of file %s", entry->d_name); + if (entry->d_type != DT_REG) + errx(EXIT_FAILURE, "Entry contains non-regular file %s", entry->d_name); + if (strncmp(entry->d_name, "SHA256SUMS.", sizeof("SHA256SUMS.") - 1) == 0) { + // Found a signature file!
This comment seems a bit redundant.
+ if (check_sig) + found_sig = true; + else { + if (unlinkat(fd, entry->d_name, 0)) + err(EXIT_FAILURE, "Unlinking old signature file"); + changed = true; + } + } + } + // fsync() the directory if it was changed, to avoid the above + // cache-incoherency problem.
Above where?
+ if (changed && fsync(fd)) + errx(EXIT_FAILURE, "fsync"); + if (check_sig && !found_sig) { + warnx("sys.appvm-systemd-sysupdate didn't send a signature file."); + warnx("There was probably a problem downloading the update."); + errx(EXIT_FAILURE, "Check its logs for more information."); + } + closedir(d); +} + +int main(int argc, char **argv) +{ + if (argc != 3) + usage(); + + bool check_sig; + if (strcmp(argv[1], "cleanup") == 0) + check_sig = false; + else if (strcmp(argv[1], "check") == 0) + check_sig = true; + else + usage(); + + for (int i = 2; i < argc; ++i) { + int fd = open(argv[i], O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + err(EXIT_FAILURE, "open(%s)", argv[i]);
Maybe we could just fdopen(argv[1]) inside checkdir()? We don't need any special flags AFAICT.
+ checkdir(fd, check_sig); + } + return 0; +}
-- 2.52.0
On 11/19/25 09:45, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing
Just one "containing" is fine. :P
Nice catch :)
terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either.
Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. systemd-sysupdate can leave behind temporary files with names starting with '.', so delete them instead of failing. Linux can lose cache coherency if there is an I/O error, so call syncfs() on the directory before checking anything. For the same reason, fsync() the directory if any hidden files were deleted.
The directory checker also serves another critical function: it checks if the VM actually downloaded anything. Otherwise, network problems could cause updates to silently do nothing. Specifically, it checks that the VM provided a file starting with the prefix "SHA256SUMS.". These will be the last ones the in-VM updater downloads. An additional mode is provided to clean out all such files. This will be used to ensure that before the in-VM updater runs, no such files are present. Hence, if the VM didn't actually download anything, the user will get a clear error instead of a false success message or a confusing error.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2:
- Purge leftover temporary files rather than returning an error.
- Split into two modes: one that deletes signature files, and one that checks that at least one signature file exists. This allows checking that the VM actually sent something. --- tools/default.nix | 1 + tools/meson.build | 4 ++ tools/updates-dir-check.c | 133 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+)
Looking good. Left some style comments, but
Reviewed-by: Alyssa Ross <hi@alyssa.is>
Do you want me to send another version, or would you rather fix this up on commit? All your changes look good.
diff --git a/tools/updates-dir-check.c b/tools/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..07eb059f2718e1ad8ab087fe6509c1437ea3e96c --- /dev/null +++ b/tools/updates-dir-check.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> +#include <unistd.h> + +#include <err.h> + +[[noreturn]] static void bad_char(char c, char *msg_component) +{ + if (c >= 0x20 && c <= 0x7E) + errx(EXIT_FAILURE, "Forbidden %s character in filename: '%c'", + msg_component, (int)c); + errx(EXIT_FAILURE, + "Forbidden %s character in filename: byte %d", + msg_component, (int)(unsigned char)c);
Why not %hhu, so you don't need two layers of casts?
I totally forgot that %hhu exists!
+} + +[[noreturn]] static void usage(void) +{ + errx(EXIT_FAILURE, "Usage: updates-dir-check [cleanup|check] DIRECTORIES..."); +} + +static void checkdir(int fd, bool check_sig)
[[gnu::fd_arg_read (1)]]
(I'm bad at remembering this too so you'll see other code missing it, but it's good to add.)
Good catch!
+{ + bool found_sig = false; + DIR *d = fdopendir(fd); + if (d == NULL) + err(EXIT_FAILURE, "fdopendir"); + // If there is an I/O error while there are dirty pages outstanding, + // the dirty pages are silently discarded. This means that the contents + // of the filesystem can change behind userspace's back. Flush all + // dirty pages in the filesystem with the directory to prevent this. + if (syncfs(fd) != 0) + err(EXIT_FAILURE, "syncfs"); + bool changed = false; + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(EXIT_FAILURE, "readdir"); + break; + } + const char *ptr = entry->d_name; + if (ptr[0] == '.') { + if (ptr[1] == '\0') + continue; + if (ptr[1] == '.' && ptr[2] == '\0') + continue; + // systemd-sysupdate uses these for temporary files. + // It normally cleans them up itself, but if there is an error + // it does not always clean them up. I'm not sure if it is + // guaranteed to clean up temporary files from a past run, so + // delete them instead of returning an error. + if (unlinkat(fd, ptr, 0)) + err(EXIT_FAILURE, "Failed to unlink temporary file"); + changed = true; + continue; + } + char c = ptr[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + bad_char(c, "initial"); + while ((c = *++ptr)) { + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) + bad_char(c, "subsequent"); + } + // Empty filenames are rejected as having a bad initial character, + // and POSIX forbids them from being returned anyway. Therefore, + // this cannot be out of bounds. + if (ptr[-1] == '.') + errx(EXIT_FAILURE, "Filename %s ends with a '.'", entry->d_name); + if (entry->d_type == DT_UNKNOWN) + errx(EXIT_FAILURE, "Filesystem didn't report type of file %s", entry->d_name); + if (entry->d_type != DT_REG) + errx(EXIT_FAILURE, "Entry contains non-regular file %s", entry->d_name); + if (strncmp(entry->d_name, "SHA256SUMS.", sizeof("SHA256SUMS.") - 1) == 0) { + // Found a signature file!
This comment seems a bit redundant.
It isn't necessarily obvious that any file with this prefix is a signature.
+ if (check_sig) + found_sig = true; + else { + if (unlinkat(fd, entry->d_name, 0)) + err(EXIT_FAILURE, "Unlinking old signature file"); + changed = true; + } + } + } + // fsync() the directory if it was changed, to avoid the above + // cache-incoherency problem.
Above where?
// If there is an I/O error while there are dirty pages outstanding, // the dirty pages are silently discarded. This means that the contents // of the filesystem can change behind userspace's back. Flush all // dirty pages in the filesystem with the directory to prevent this. In this case, only the directory got changed, so I only need to flush the directory.
+ if (changed && fsync(fd)) + errx(EXIT_FAILURE, "fsync"); + if (check_sig && !found_sig) { + warnx("sys.appvm-systemd-sysupdate didn't send a signature file."); + warnx("There was probably a problem downloading the update."); + errx(EXIT_FAILURE, "Check its logs for more information."); + } + closedir(d); +} + +int main(int argc, char **argv) +{ + if (argc != 3) + usage(); + + bool check_sig; + if (strcmp(argv[1], "cleanup") == 0) + check_sig = false; + else if (strcmp(argv[1], "check") == 0) + check_sig = true; + else + usage(); + + for (int i = 2; i < argc; ++i) { + int fd = open(argv[i], O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + err(EXIT_FAILURE, "open(%s)", argv[i]);
Maybe we could just fdopen(argv[1]) inside checkdir()? We don't need any special flags AFAICT.
Do you mean opendir()? That works, thanks!
+ checkdir(fd, check_sig); + } + return 0; +}
-- 2.52.0
-- Sincerely, Demi Marie Obenour (she/her/hers)
Also add some error checks and use Bash features. No other functional change intended. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Do not use wrapper script. - Make script non-executable. - Invoke script as 'bash FILE' to work around /usr/bin/env not working during a Nix build. --- host/initramfs/Makefile | 4 ++-- host/rootfs/Makefile | 2 +- img/app/Makefile | 2 +- release/live/Makefile | 4 ++-- scripts/make-gpt.sh | 20 ++++++++++---------- vm/sys/net/Makefile | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 13bb548d6146684a25dab1e31228c0b9a4ca8db7..fd8cbb6c3e775ed27d0a524bf167cb4d3940d799 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -36,7 +36,7 @@ build/mountpoints: find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';' build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ @@ -45,7 +45,7 @@ build/loop.tar: build/live.img $(TAR) -cf $@ build/live.img build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e mv $@.tmp $@ diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 4c14d3acfa8f7ffd276fdfb684d08bf58fd80a15..533471c58591d305f386157e04e9e48da7259fd5 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -96,7 +96,7 @@ clean: .PHONY: clean build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_DIR)/verity-timestamp $(ROOT_FS) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ $(ROOT_FS)/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS)/rootfs:root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH)") mv $@.tmp $@ diff --git a/img/app/Makefile b/img/app/Makefile index 48eba871339d314479f730101246ace3fa39e2db..f16c2df1b90bbec6750f2980da4dbaf49c9cb0ea 100644 --- a/img/app/Makefile +++ b/img/app/Makefile @@ -26,7 +26,7 @@ $(imgdir)/appvm/vmlinux: $(KERNEL) $(imgdir)/appvm/blk/root.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:5460386f-2203-4911-8694-91400125c604:root mv $@.tmp $@ diff --git a/release/live/Makefile b/release/live/Makefile index d61248e94599adc5229d0ad38d54b9f649d66ca1..4dbac28a90dc6f64a253961c33c597220d0e85cf 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -9,8 +9,8 @@ DTBS ?= build/empty dest = build/live.img -$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) - ../../scripts/make-gpt.sh $@.tmp \ +$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.bash ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh old mode 100755 new mode 100644 index 96f0d2c8494c093558c0e32e7e920b569bb078ef..0ae3d0005e5ae7d6214270fedb2fafb0a7064cf5 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -1,11 +1,11 @@ -#!/bin/sh -eu -# +#!/usr/bin/env -S bash -- # SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> # SPDX-FileCopyrightText: 2022 Unikie # SPDX-License-Identifier: EUPL-1.2+ # -# usage: make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... +# usage: bash make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... +set -xeuo pipefail ONE_MiB=1048576 # Prints the number of 1MiB blocks required to store the file named @@ -40,16 +40,15 @@ scriptsDir="$(dirname "$0")" out="$1" shift -nl=' -' table="label: gpt" # Keep 1MiB free at the start, and 1MiB free at the end. gptBytes=$((ONE_MiB * 2)) for partition; do - sizeMiB="$(sizeMiB "$(partitionPath "$partition")")" - table="$table${nl}size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" - gptBytes="$((gptBytes + sizeMiB * ONE_MiB))" + partitionPath=$(partitionPath "$partition") + sizeMiB=$(sizeMiB "$partitionPath") + table+=$'\n'"size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" + gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) done rm -f "$out" @@ -60,6 +59,7 @@ EOF n=0 for partition; do - fillPartition "$out" "$n" "$(partitionPath "$partition")" - n="$((n + 1))" + partitionPath=$(partitionPath "$partition") + fillPartition "$out" "$n" "$partitionPath" + n=$((n + 1)) done diff --git a/vm/sys/net/Makefile b/vm/sys/net/Makefile index d71c2325eff3bae921f33c61f799846d35e401c2..e403d697d90c0021a7a8d1dbc1553cfbda74a117 100644 --- a/vm/sys/net/Makefile +++ b/vm/sys/net/Makefile @@ -25,7 +25,7 @@ $(vmdir)/netvm/vmlinux: $(KERNEL) $(vmdir)/netvm/blk/root.img: ../../../scripts/make-gpt.sh ../../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) - ../../../scripts/make-gpt.sh $@.tmp \ + bash ../../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:ea21da27-0391-48da-9235-9d2ab2ca7844:root mv $@.tmp $@ -- 2.52.0
On 11/19/25 03:18, Demi Marie Obenour wrote:
Also add some error checks and use Bash features.
No other functional change intended.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2:
- Do not use wrapper script. - Make script non-executable. - Invoke script as 'bash FILE' to work around /usr/bin/env not working during a Nix build. --- host/initramfs/Makefile | 4 ++-- host/rootfs/Makefile | 2 +- img/app/Makefile | 2 +- release/live/Makefile | 4 ++-- scripts/make-gpt.sh | 20 ++++++++++---------- vm/sys/net/Makefile | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 13bb548d6146684a25dab1e31228c0b9a4ca8db7..fd8cbb6c3e775ed27d0a524bf167cb4d3940d799 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -36,7 +36,7 @@ build/mountpoints: find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';'
build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ @@ -45,7 +45,7 @@ build/loop.tar: build/live.img $(TAR) -cf $@ build/live.img
build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e mv $@.tmp $@
diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 4c14d3acfa8f7ffd276fdfb684d08bf58fd80a15..533471c58591d305f386157e04e9e48da7259fd5 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -96,7 +96,7 @@ clean: .PHONY: clean
build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_DIR)/verity-timestamp $(ROOT_FS) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ $(ROOT_FS)/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS)/rootfs:root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH)") mv $@.tmp $@ diff --git a/img/app/Makefile b/img/app/Makefile index 48eba871339d314479f730101246ace3fa39e2db..f16c2df1b90bbec6750f2980da4dbaf49c9cb0ea 100644 --- a/img/app/Makefile +++ b/img/app/Makefile @@ -26,7 +26,7 @@ $(imgdir)/appvm/vmlinux: $(KERNEL)
$(imgdir)/appvm/blk/root.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:5460386f-2203-4911-8694-91400125c604:root mv $@.tmp $@
diff --git a/release/live/Makefile b/release/live/Makefile index d61248e94599adc5229d0ad38d54b9f649d66ca1..4dbac28a90dc6f64a253961c33c597220d0e85cf 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -9,8 +9,8 @@ DTBS ?= build/empty
dest = build/live.img
-$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) - ../../scripts/make-gpt.sh $@.tmp \ +$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.bash ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))")
The new dependency on ../../make-gpt.bash is wrong and causes a build failure. The dependency is removed in patch 6/14 ("Support generating multiple partition UUIDs"), which is why it wasn't caught by testing. I now have access to a powerful server that can build release.nix in a reasonable amount of time, which will allow me to do it on every commit instead of just on selected commits. In fact, this is how I found the problem. -- Sincerely, Demi Marie Obenour (she/her/hers)
systemd-sysupdate requires that partitions are large enough to hold the newly downloaded images. This requires that they be large enough to have room to grow. Allow specifying the partition size manually, overriding the default (the size of the file that will be copied into the partition). Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Split into separate commit. --- scripts/make-gpt.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh index 0ae3d0005e5ae7d6214270fedb2fafb0a7064cf5..91c6038f67d4d4906fec4a3412f2ff5fca2671d5 100644 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -45,8 +45,13 @@ table="label: gpt" # Keep 1MiB free at the start, and 1MiB free at the end. gptBytes=$((ONE_MiB * 2)) for partition; do - partitionPath=$(partitionPath "$partition") - sizeMiB=$(sizeMiB "$partitionPath") + if [[ "$partition" =~ :([1-9][0-9]*)MiB$ ]]; then + sizeMiB=${BASH_REMATCH[1]} + partition=${partition%:*} + else + partitionPath=$(partitionPath "$partition") + sizeMiB=$(sizeMiB "$partitionPath") + fi table+=$'\n'"size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) done -- 2.52.0
Generate 4 partition UUIDs instead of just 2. Port scripts/format-uuid.sh to awk to make this much easier. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Split into separate commit. --- host/initramfs/Makefile | 8 +++++--- host/rootfs/Makefile | 6 ++++-- release/live/Makefile | 8 +++++--- release/live/default.nix | 2 +- scripts/format-uuid.awk | 35 +++++++++++++++++++++++++++++++++++ scripts/format-uuid.sh | 19 ------------------- 6 files changed, 50 insertions(+), 28 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index fd8cbb6c3e775ed27d0a524bf167cb4d3940d799..27a26b46a8110d35ee02a63b12931d6b9c2742e5 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -35,10 +35,12 @@ build/mountpoints: cd build/mountpoints && mkdir -p $(MOUNTPOINTS) find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';' -build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) +build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$3 \ + $(ROOT_FS):root:$$1 mv $@.tmp $@ build/loop.tar: build/live.img diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 533471c58591d305f386157e04e9e48da7259fd5..1945afde0c1d804f9d14e9177aa812cbdc71234c 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -96,9 +96,11 @@ clean: .PHONY: clean build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_DIR)/verity-timestamp $(ROOT_FS) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS)/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS)/rootfs:root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH)") + $(ROOT_FS_VERITY):verity:$$3 \ + $(ROOT_FS):root:$$1 mv $@.tmp $@ debug: diff --git a/release/live/Makefile b/release/live/Makefile index 4dbac28a90dc6f64a253961c33c597220d0e85cf..78361a48512a37514ba0e57e0cc8b0ec3a71664b 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -9,11 +9,13 @@ DTBS ?= build/empty dest = build/live.img -$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.bash ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) +$(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$3 \ + $(ROOT_FS):root:$$1 mv $@.tmp $@ build/empty: diff --git a/release/live/default.nix b/release/live/default.nix index ac2d7a55fd4fe0c02108309ecea20e368000af0d..98cb4862e239e3ad9ddbd7b5ace5716f57df683b 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -29,7 +29,7 @@ stdenv.mkDerivation { fileset = lib.fileset.intersection src (lib.fileset.unions [ ./. ../../lib/common.mk - ../../scripts/format-uuid.sh + ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk ]); diff --git a/scripts/format-uuid.awk b/scripts/format-uuid.awk new file mode 100644 index 0000000000000000000000000000000000000000..a5349d68a4d29be5f750650236420c9b5a7257eb --- /dev/null +++ b/scripts/format-uuid.awk @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +function format_uuid(arg) { + if (arg in found_so_far) { + fail("Duplicate UUID, try changing the image (by even 1 bit)"); + } + found_so_far[arg] = 1; + print (substr(arg, 1, 8) "-" \ + substr(arg, 9, 4) "-" \ + substr(arg, 13, 4) "-" \ + substr(arg, 17, 4) "-" \ + substr(arg, 21, 12)); +} + +function fail(msg) { + print msg > "/dev/stderr"; + exit 1; +} + +BEGIN { + FS = ""; + RS = "\n"; + if ((getline) != 1) + fail("Empty input file"); + roothash = $0; + if (roothash !~ /^[a-f0-9]{64}$/) + fail("Invalid root hash"); + if (getline) + fail("Junk after root hash"); + found_so_far[""] = ""; + for (i = 1; i != 49; i += 16) { + format_uuid(substr($0, i, 32)); + } + format_uuid(substr($0, 49, 16) substr($0, 1, 16)); +} diff --git a/scripts/format-uuid.sh b/scripts/format-uuid.sh deleted file mode 100755 index 3b38278aef640b2cd540d6606b05dd62018e48a6..0000000000000000000000000000000000000000 --- a/scripts/format-uuid.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -eu -# -# SPDX-FileCopyrightText: 2021-2022 Alyssa Ross <hi@alyssa.is> -# SPDX-FileCopyrightText: 2022 Unikie -# SPDX-License-Identifier: EUPL-1.2+ - -substr() { - str=$1 - beg=$2 - end=$3 - echo "$str" | cut -c "$beg-$end" -} - -u1=$(substr "$1" 1 8) -u2=$(substr "$1" 9 12) -u3=$(substr "$1" 13 16) -u4=$(substr "$1" 17 20) -u5=$(substr "$1" 21 32) -printf "%s\n" "$u1-$u2-$u3-$u4-$u5" -- 2.52.0
Avoids a pointless call to awk. No functional change intended. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Move into separate commit. --- scripts/make-gpt.sh | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh index 91c6038f67d4d4906fec4a3412f2ff5fca2671d5..ebb19c32a0d3bc3f86bf4748460971888d73e1fc 100644 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -28,13 +28,6 @@ fillPartition() { lseek -S 1 "$start" cat "$3" 1<>"$1" } -# Prints the partition path from a PATH:PARTTYPE[:PARTUUID[:PARTLABEL]] string. -partitionPath() { - awk -F: '{print $1}' <<EOF -$1 -EOF -} - scriptsDir="$(dirname "$0")" out="$1" @@ -49,8 +42,7 @@ for partition; do sizeMiB=${BASH_REMATCH[1]} partition=${partition%:*} else - partitionPath=$(partitionPath "$partition") - sizeMiB=$(sizeMiB "$partitionPath") + sizeMiB=$(sizeMiB "${partition%%:*}") fi table+=$'\n'"size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) @@ -64,7 +56,6 @@ EOF n=0 for partition; do - partitionPath=$(partitionPath "$partition") - fillPartition "$out" "$n" "$partitionPath" + fillPartition "$out" "$n" "${partition%%:*}" n=$((n + 1)) done -- 2.52.0
systemd-sysupdate has strict requirements on the partition layout: - The label of the active partition must match the template in the .transfer file. For instance, the root filesystem of Spectrum 0.0.0 must be in a partition with label "Spectrum_0.0.0", and the verity partition must have the label "Spectrum_0.0.0.verity". - The label of the inactive partition must be that of the old version of Spectrum, or "_empty" for freshly installed systems. - The partition type UUID must conform to the Discoverable Partition Specification. Also, the UKI must have a name that includes the OS version. Otherwise, it will not be deleted during updates. Since the partition label includes the OS version, add an OS version number. Use 0.0.0 to indicate that Spectrum OS is still in very early development and should not be used. The version number can be overridden in the build configuration file. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Split off into separate commit. --- host/efi.nix | 2 +- host/initramfs/Makefile | 4 ++-- host/initramfs/shell.nix | 2 ++ host/rootfs/Makefile | 4 ++-- host/rootfs/shell.nix | 2 ++ lib/config.default.nix | 1 + release/live/Makefile | 6 +++--- release/live/default.nix | 3 +++ 8 files changed, 16 insertions(+), 8 deletions(-) diff --git a/host/efi.nix b/host/efi.nix index d0ce260bd908c186059b75a1b4f42258b0e62bff..ecedb6bea6bf29c7a7303dc9062fe12b5c7a9fbd 100644 --- a/host/efi.nix +++ b/host/efi.nix @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> import ../lib/call-package.nix ( -{ callSpectrumPackage, config, cryptsetup, rootfs +{ callSpectrumPackage, cryptsetup, rootfs , runCommand, stdenv, systemdUkify }: let diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 27a26b46a8110d35ee02a63b12931d6b9c2742e5..383aa856d0b886325f3505a7596b08dad31a4851 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -39,8 +39,8 @@ build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../sc uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$3 \ - $(ROOT_FS):root:$$1 + $(ROOT_FS_VERITY):verity:$$3:Spectrum_'$(VERSION).verity' \ + $(ROOT_FS):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ build/loop.tar: build/live.img diff --git a/host/initramfs/shell.nix b/host/initramfs/shell.nix index ff067354881b480656fae9b339a0a9068475d85f..36e3956a2ebc80fd273da226af253ebe8f7f7b24 100644 --- a/host/initramfs/shell.nix +++ b/host/initramfs/shell.nix @@ -4,6 +4,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, rootfs, pkgsStatic, stdenv , cryptsetup, jq, qemu_kvm, tar2ext4, util-linux +, config }: let @@ -18,5 +19,6 @@ initramfs.overrideAttrs ({ nativeBuildInputs ? [], env ? {}, ... }: { env = env // { KERNEL = "${rootfs.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; ROOT_FS_DIR = rootfs; + VERSION = config.version; }; })) (_: {}) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 1945afde0c1d804f9d14e9177aa812cbdc71234c..0d71dd1441077c9f30945e5063aacbfc240a9006 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -99,8 +99,8 @@ build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scr uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$3 \ - $(ROOT_FS):root:$$1 + $(ROOT_FS_VERITY):verity:$$3:Spectrum_'$(VERSION).verity' \ + $(ROOT_FS):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ debug: diff --git a/host/rootfs/shell.nix b/host/rootfs/shell.nix index 6df2f575fdfc7cdf8067ccfdb5fecaad9f6ea5e6..27f93e05fce036257d27cf9992fee8c925073f80 100644 --- a/host/rootfs/shell.nix +++ b/host/rootfs/shell.nix @@ -5,6 +5,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, rootfs, pkgsStatic, srcOnly, stdenv , btrfs-progs, cryptsetup, jq, netcat, qemu_kvm, reuse, util-linux +, config }: rootfs.overrideAttrs ( @@ -20,5 +21,6 @@ rootfs.overrideAttrs ( KERNEL = "${passthru.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; LINUX_SRC = srcOnly passthru.kernel.configfile; VMLINUX = "${passthru.kernel.dev}/vmlinux"; + VERSION = config.version; }; })) (_: {}) diff --git a/lib/config.default.nix b/lib/config.default.nix index a8422345cc00f9413bb19ec968fd89c82fed801b..489c231490a8b66aa01f50053b25646060f7f963 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -4,4 +4,5 @@ { pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; + version = "0.0.0"; } diff --git a/release/live/Makefile b/release/live/Makefile index 78361a48512a37514ba0e57e0cc8b0ec3a71664b..a85edfde2d186716656ed23fe719ca63b31bcd59 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -14,8 +14,8 @@ $(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/s set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$3 \ - $(ROOT_FS):root:$$1 + $(ROOT_FS_VERITY):verity:$$3:Spectrum_'$(VERSION).verity' \ + $(ROOT_FS):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ build/empty: @@ -25,7 +25,7 @@ build/boot.fat: $(SYSTEMD_BOOT_EFI) $(EFI_IMAGE) build/empty $(TRUNCATE) -s 440401920 $@ $(MKFS_FAT) $@ $(MMD) -i $@ ::/EFI ::/EFI/BOOT ::/EFI/Linux - $(MCOPY) -i $@ $(EFI_IMAGE) ::/EFI/Linux/spectrum.efi + $(MCOPY) -i $@ $(EFI_IMAGE) '::/EFI/Linux/Spectrum_$(VERSION).efi' $(MCOPY) -i $@ $(SYSTEMD_BOOT_EFI) ::/EFI/BOOT/$(EFINAME) clean: diff --git a/release/live/default.nix b/release/live/default.nix index 98cb4862e239e3ad9ddbd7b5ace5716f57df683b..22e5a00de08ed858522a00f00359890ef52f03e0 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -1,11 +1,13 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2021-2023, 2025 Alyssa Ross <hi@alyssa.is> # SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src , lib, pkgsStatic, stdenvNoCC , cryptsetup, dosfstools, jq, mtools, util-linux +, config }: let @@ -46,6 +48,7 @@ stdenv.mkDerivation { SYSTEMD_BOOT_EFI = "${systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; EFI_IMAGE = efi; EFINAME = "BOOT${toUpper efiArch}.EFI"; + VERSION = config.version; }; buildFlags = [ "dest=$(out)" ]; -- 2.52.0
On 11/19/25 03:18, Demi Marie Obenour wrote:
@@ -25,7 +25,7 @@ build/boot.fat: $(SYSTEMD_BOOT_EFI) $(EFI_IMAGE) build/empty $(TRUNCATE) -s 440401920 $@ $(MKFS_FAT) $@ $(MMD) -i $@ ::/EFI ::/EFI/BOOT ::/EFI/Linux - $(MCOPY) -i $@ $(EFI_IMAGE) ::/EFI/Linux/spectrum.efi + $(MCOPY) -i $@ $(EFI_IMAGE) '::/EFI/Linux/Spectrum_$(VERSION).efi' $(MCOPY) -i $@ $(SYSTEMD_BOOT_EFI) ::/EFI/BOOT/$(EFINAME)
This breaks the live image, which assumes that the UKI will have the name spectrum.efi. The simplest fix is to swap this patch and the previous one, so that the live image is deleted before it gets broken. -- Sincerely, Demi Marie Obenour (she/her/hers)
systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, the partitions need to be very large so that there is plenty of room for the OS to grow. Furthermore, systemd-sysupdate requires both A and B copies of both the root and verity partitions. mkfs.ext4 is not able to produce images with files large enough to hold both the primary and backup copy of the root partition [1]. Reducing the sizes of partitions to be little greater than the size of the root filesystem image does not help. The produced file is still too large. Therefore, compress the image, which causes it to be small enough that mkfs.ext4 can handle it. This breaks the option to use the installer as a live image. Therefore, remove it. This option will return once Spectrum switches to the GNOME OS installer [2]. However, it is still possible to build a live image that is separate from the installer. Document how to build and use it. GRUB2 does support compressed loopback images, but these presumably buffer the whole image in memory. Since the entire installer will be replaced, making it work is not considered worthwhile. [1]: https://github.com/tytso/e2fsprogs/issues/254 [2]: https://spectrum-os.org/lists/archives/spectrum-devel/87wm4dlkhz.fsf@alyssa.... Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Remove live image test instead of skipping it. - Document the change. - Document that there is still a live image available, though it is separate from the installer. - Document how to build the live image. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/installation/getting-spectrum.adoc | 31 +++++++++++++++++++----- host/initramfs/Makefile | 8 ------ host/initramfs/etc/probe | 20 --------------- release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ---------------------- release/combined/eosimages.nix | 11 +++++---- release/combined/grub.cfg.in | 5 ---- 7 files changed, 32 insertions(+), 74 deletions(-) diff --git a/Documentation/installation/getting-spectrum.adoc b/Documentation/installation/getting-spectrum.adoc index 29803aa324b196119a03b22d7f1e2d7730e2c1eb..e7806e0f92793320bf0cdcbdd11dbc4e713275c7 100644 --- a/Documentation/installation/getting-spectrum.adoc +++ b/Documentation/installation/getting-spectrum.adoc @@ -42,10 +42,30 @@ still take a very long time. == Installing Spectrum To install Spectrum on a computer, you can use a USB drive as a -bootable Spectrum installer device. When booting a system from the -installer device, you will be able to choose whether to try out -Spectrum without installing it on your system (as a live image), or to -install it to your computer's internal storage. +bootable Spectrum installer device. You will need to choose whether to +try out Spectrum without installing it on your system (as a live image), +or to install it to your computer's internal storage. + +=== Building A Live Image + +First, you need to build the Spectrum image: + +[source,shell] +---- +git clone https://spectrum-os.org/git/spectrum +nix-build spectrum/release/live +---- + +If you haven't set up the xref:binary-cache.adoc[binary cache], this +will take a very long time. When it's done, a symbolic link named +"result" will appear in the current directory, pointing to the +installer image. Write that image to a USB drive, for example using +`dd` (command line) or +https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available +in Nixpkgs). Boot your system from the USB drive, and Spectrum should +be ready for you to use. + +=== Building The Installer First, you need to build the Spectrum image: @@ -62,8 +82,7 @@ installer image. Write that image to a USB drive, for example using `dd` (command line) or https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available in Nixpkgs). Boot your system from the USB drive, and you should see -a menu allowing you to choose between "Try Spectrum" and "Install -Spectrum". +a menu allowing you to "Install Spectrum". NOTE: While it's possible to install Spectrum to your internal storage, at this point in Spectrum's development there is not much diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 383aa856d0b886325f3505a7596b08dad31a4851..db33ff86e9cd994efd4ce50acdf881d69ba79299 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -43,14 +43,6 @@ build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../sc $(ROOT_FS):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ -build/loop.tar: build/live.img - $(TAR) -cf $@ build/live.img - -build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 - bash ../../scripts/make-gpt.sh $@.tmp \ - build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e - mv $@.tmp $@ - clean: rm -rf build .PHONY: clean diff --git a/host/initramfs/etc/probe b/host/initramfs/etc/probe index 4cbd00db52c1a7128b5c619a43d415675feaee0b..013092b6dcc5b82db7302c1ae7e6d8a4f5a0b802 100755 --- a/host/initramfs/etc/probe +++ b/host/initramfs/etc/probe @@ -2,26 +2,6 @@ # SPDX-License-Identifier: EUPL-1.2+ # SPDX-FileCopyrightText: 2021 Alyssa Ross <hi@alyssa.is> -if -n { - # If this is a Spectrum installer eosimages partition, we might be - # booting from the installer, and should loopback mount the images. - importas -i mdev MDEV - if { - backtick -E type { lsblk -lnpo PARTTYPE $mdev } - test $type = 56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e - } - if { - forx -pE module { ext4 loop } - modprobe $module - } - backtick -E uuid { lsblk -lnpo PARTUUID $mdev } - if { mkdir -p /mnt/${uuid} } - if { mount $mdev /mnt/${uuid} } - find /mnt/${uuid} -name *.img -exec - losetup -Pf {} - ; -} - # Check whether we now have all the partitions we need to boot. importas -i rootfs_uuid ROOTFS_UUID diff --git a/release/checks/integration/meson.build b/release/checks/integration/meson.build index 7214e47ba1ec23c247c8b76e5c8d94aff1ce1fd6..7bf8f51e4c762d2279ed6064ae1a87cb9b07494c 100644 --- a/release/checks/integration/meson.build +++ b/release/checks/integration/meson.build @@ -11,7 +11,7 @@ run_qemu = find_program('../../../scripts/run-qemu.sh') lib = static_library('spectrum-integration-test', 'lib.c') -foreach test : ['appimage', 'late-serial', 'networking', 'portal', 'try'] +foreach test : ['appimage', 'late-serial', 'networking', 'portal'] test(test, executable(test, test + '.c', link_with : lib), timeout : 400, args : [run_qemu]) diff --git a/release/checks/integration/try.c b/release/checks/integration/try.c deleted file mode 100644 index 4b874c0a7e9b48324497450fb5488e04576fd43b..0000000000000000000000000000000000000000 --- a/release/checks/integration/try.c +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is> - -#include "lib.h" - -#include <stdlib.h> -#include <string.h> - -void test(struct config c) -{ - struct vm *vm; - - c.drives.img = getenv_or_die("COMBINED_PATH"); - - vm = start_qemu(c); - - start_console_thread(vm, "GNU GRUB "); - wait_for_prompt(vm); - - start_console_thread(vm, "~ # "); - - // Assume that Try Spectrum is the first menu entry. - if (fputc('\n', vm_console_writer(vm)) == EOF) { - fputs("error writing to console\n", stderr); - exit(EXIT_FAILURE); - } - - wait_for_prompt(vm); -} diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..9921d9bcc89cc38271f83a970815f1060b3780e0 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -12,11 +12,12 @@ runCommand "eosimages.img" { unsafeDiscardReferences = { out = true; }; dontFixup = true; } '' + set -euo pipefail mkdir dir cd dir - ln -s $image $imageName - sha256sum $imageName > $imageName.sha256 - tar -chf $NIX_BUILD_TOP/eosimages.tar * - tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out - e2label $out eosimages + ln -s -- "$image" "$imageName" + gzip -9 < "$image" > "$imageName.gz" + sha256sum -- "$imageName.gz" > "$imageName.gz.sha256" + tar -ch -- "$imageName.gz" "$imageName.gz.sha256" | tar2ext4 -o "$out" + e2label "$out" eosimages '') (_: {}) diff --git a/release/combined/grub.cfg.in b/release/combined/grub.cfg.in index a8e73a3b4dc0d643cf575e3cc545ec9ff72380cb..a22f5fc96ba6451d44c0f9768a15a1f48c5dce1c 100644 --- a/release/combined/grub.cfg.in +++ b/release/combined/grub.cfg.in @@ -15,11 +15,6 @@ set gfxpayload=keep terminal_output gfxterm terminal_output console -menuentry "Try Spectrum" { - loopback live (hd0,gpt3)/Spectrum-0.0-x86_64-generic.0.Live.img - chainloader (live,gpt1)/EFI/Linux/spectrum.efi -} - menuentry "Install Spectrum" { set root=(hd0,gpt2) linux @linux@ @kernelParams@ -- 2.52.0
On 11/19/25 03:18, Demi Marie Obenour wrote:
diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..9921d9bcc89cc38271f83a970815f1060b3780e0 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -12,11 +12,12 @@ runCommand "eosimages.img" { unsafeDiscardReferences = { out = true; }; dontFixup = true; } '' + set -euo pipefail mkdir dir cd dir - ln -s $image $imageName - sha256sum $imageName > $imageName.sha256 - tar -chf $NIX_BUILD_TOP/eosimages.tar * - tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out - e2label $out eosimages + ln -s -- "$image" "$imageName" + gzip -9 < "$image" > "$imageName.gz" + sha256sum -- "$imageName.gz" > "$imageName.gz.sha256" + tar -ch -- "$imageName.gz" "$imageName.gz.sha256" | tar2ext4 -o "$out" + e2label "$out" eosimages '') (_: {})
The use of gzip -9 makes the image build *very* slow. Compression alone took about 45 minutes on a laptop. Should this be configurable? For an actual release shipped to users, this isn't a big deal, but for development work it's really bad. XZ is also supported and is also slow. -- Sincerely, Demi Marie Obenour (she/her/hers)
systemd-sysupdate never writes to the running OS partition. Instead, it requires a separate partition to write the update into. Create a separate partition for that purpose. systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, make the partitions very large so that there is plenty of room for the OS to grow. This requires rewriting the code that calculates the partition sizes. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Make into a standalone commit - Do not rely on separate script to generate the images. - Use a smaller size for the verity partition. --- release/live/Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/release/live/Makefile b/release/live/Makefile index a85edfde2d186716656ed23fe719ca63b31bcd59..cf2ace4f5e4ba20a2c0ce9803f72acf0e23f9df3 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -10,12 +10,15 @@ DTBS ?= build/empty dest = build/live.img $(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + # 162MiB was calculated by running `veritysetup format` on 20GiB from /dev/urandom uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$3:Spectrum_'$(VERSION).verity' \ - $(ROOT_FS):root:$$1:Spectrum_'$(VERSION)' + $(ROOT_FS_VERITY):verity:$$3:Spectrum_'$(VERSION).verity:162MiB' \ + $(ROOT_FS):root:$$1:Spectrum_'$(VERSION):20000MiB' \ + $(ROOT_FS_VERITY):verity:$$4:_empty:162MiB \ + $(ROOT_FS):root:$$2:_empty:20000MiB mv $@.tmp $@ build/empty: -- 2.52.0
Whenever a release is made, create a directory with the release files to be used for an update. After its SHA256SSUMS file is signed, the file is ready to be uploaded to a server for users to update from. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Use UUIDs to name the rootfs and verity superblock. This will allow systemd-sysupdate to set the correct UUIDs on the rootfs and verity partitions, avoiding the need to use labels to find these partitions. --- release.nix | 2 ++ release/update.nix | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/release.nix b/release.nix index a4fe66ee5925aeee3a1f5f1fac249c595cee0885..704abb39a3d01152eac3dfe313066834c3cd0a66 100644 --- a/release.nix +++ b/release.nix @@ -8,5 +8,7 @@ import lib/call-package.nix ({ callSpectrumPackage }: { checks = callSpectrumPackage release/checks {}; + updates = callSpectrumPackage release/update.nix {}; + combined = callSpectrumPackage release/combined/run-vm.nix {}; }) (_: {}) diff --git a/release/update.nix b/release/update.nix new file mode 100644 index 0000000000000000000000000000000000000000..77eb5fc422baa7d13e8e3ccb823c2fe69d2c39cc --- /dev/null +++ b/release/update.nix @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../lib/call-package.nix ( +{ callSpectrumPackage, config, runCommand, stdenv }: + +let + efi = import ../host/efi.nix {}; +in +runCommand "spectrum-update-directory" { + __structuredAttrs = true; + unsafeDiscardReferences = { out = true; }; + dontFixup = true; + env = { VERSION = config.version; }; +} '' + # One would expect that this is enabled already but it is not. + set -euo pipefail + mkdir -- "$out" + cd -- "$out" + read -r roothash < ${efi.rootfs}/rootfs.verity.roothash + if ! [[ "$roothash" =~ ^[0-9a-f]{64}$ ]]; then + printf 'Internal error: bad root hash %q\n' "$roothash" + exit 1 + fi + cp -- ${efi} "Spectrum_$VERSION.efi" + cp -- ${efi.rootfs}/rootfs.verity.superblock "Spectrum_''${VERSION}_''${roothash:32:32}.verity" + cp -- ${efi.rootfs}/rootfs "Spectrum_''${VERSION}_''${roothash:0:32}.root" + sha256sum -b "Spectrum_$VERSION.efi" \ + "Spectrum_''${VERSION}_''${roothash:32:32}.verity" \ + "Spectrum_''${VERSION}_''${roothash:0:32}.root" > SHA256SUMS + '' +) (_: {}) -- 2.52.0
Include a new `spectrum-update` command to update the system. This tells the new sys.appvm-systemd-sysupdate VM to download the updates into a staging directory using systemd-sysupdate. The host then runs systemd-sysupdate to apply the updates itself. sys.appvm-systemd-sysupdate uses host-provided information to fetch the update. This allows editing files on the host to change the update URL and signing key. Updates require /boot to be mounted so that systemd-sysupdate can update the unified kernel image. They also require that /tmp is writable so that they can store temporary files, so put a tmpfs there. Furthermore, there needs to be a directory for storing downloaded updates. Create /home so that users can mount their persistent data there. The directory the VM downloads updates into is *not* reset (wiped) before or after the update. This allows the VM to know if the system is already up to date. Otherwise, it would redownload the entire multi-gigabyte update image. Updates are currently not compressed. This should be changed in the future, but it would add a small amount of additional complexity. In particular, the script generating the update directory would need to generate a SHA256SUMS containing the hash of both the compressed and uncompressed versions. More importantly, the VM must not be able to make the host use the compressed version. This would be a potential security risk because decompression happens before signature verification. GnuPG currently decompresses signatures, but in the future it will be replaced by Sequoia which does not. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Generate the transfer files in the guest, not the host. - Do not use an environment file. - Reject URLs that cannot work. - Escape sed metacharacters. - Escape backslashes for systemd-sysupdate. - Do not validate update URLs at build time, only at runtime. - Reject only update URLs that cannot possibly work. - Set the partition UUIDs according to systemd's recommendation. - Do not rely on finding partitions by label. - Strip leading and trailing whitespace from the update URL. - Rename the update command from `update` to `spectrum-update`. - Delete the list of steps. Replace it with comments in the script. The awk script in the VM rejects URLs that contain whitespace. This is because they can't work, and passing them to systemd-sysupdate would require figuring out how to escape them. Rejecting such bogus URLs is simpler than preventing them from being mangled. --- host/rootfs/Makefile | 19 ++++- host/rootfs/default.nix | 14 ++-- host/rootfs/file-list.mk | 7 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 ++++++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 ++++++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 ++++++ .../image/etc/vm-sysupdate.d/50-verity.transfer | 18 +++++ .../image/etc/vm-sysupdate.d/60-root.transfer | 18 +++++ .../image/etc/vm-sysupdate.d/70-kernel.transfer | 18 +++++ host/rootfs/image/usr/bin/spectrum-update | 83 ++++++++++++++++++++++ host/rootfs/os-release.in | 15 ++++ lib/config.default.nix | 2 + lib/config.nix | 11 ++- lib/fake-update-signing-key.gpg | 3 + release/live/shell.nix | 3 +- vm/app/systemd-sysupdate/default.nix | 57 +++++++++++++++ vm/app/systemd-sysupdate/escape-url.awk | 31 ++++++++ .../systemd-sysupdate/populate-transfer-directory | 26 +++++++ 19 files changed, 376 insertions(+), 10 deletions(-) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 0d71dd1441077c9f30945e5063aacbfc240a9006..fe728b6b385862487dab06d832e79b29d57697a8 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -9,6 +9,7 @@ include file-list.mk ROOT_FS_DIR = build DIRS = \ + boot \ dev \ etc/s6-linux-init/env \ etc/s6-linux-init/run-image/configs \ @@ -38,13 +39,15 @@ DIRS = \ etc/s6-linux-init/run-image/vm/by-id \ etc/s6-linux-init/run-image/vm/by-name \ ext \ + home \ proc \ run \ - sys + sys \ + tmp FIFOS = etc/s6-linux-init/run-image/service/s6-svscan-log/fifo -BUILD_FILES = build/etc/s6-rc +BUILD_FILES = build/etc/s6-rc build/etc/os-release build/etc/update-url # This rule produces three files but Make only (portably) # supports one output per rule. Instead of resorting to temporary @@ -62,12 +65,22 @@ $(ROOT_FS): ../../scripts/make-erofs.sh $(PACKAGES_FILE) $(FILES) $(BUILD_FILES) mkdir -p $(ROOT_FS_DIR) && \ { \ cat $(PACKAGES_FILE) ;\ + printf '%s\n%s\n' "$$UPDATE_SIGNING_KEY" /etc/systemd/import-pubring.gpg; \ for file in $(FILES) $(LINKS); do printf '%s\n%s\n' $$file "$${file#image/}"; done ;\ - for file in $(BUILD_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ + for file in $(BUILD_FILES) $(BUILD_NON_TARGET_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ printf 'build/empty\n%s\n' $(DIRS) ;\ printf 'build/fifo\n%s\n' $(FIFOS) ;\ } | ../../scripts/make-erofs.sh $(ROOT_FS) +build/etc/update-url: + mkdir -p build/etc + # might have metacharacters, so avoid interpolation + printf %s\\n "$${UPDATE_URL:?'update URL empty or missing'}" > build/etc/update-url + +build/etc/os-release: + mkdir -p build/etc + sed 's/@VERSION@/$(VERSION)/g' < os-release.in > build/etc/os-release + build/fifo: mkdir -p build mkfifo -m 0600 $@ diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 26d6dc3f9a63680bf1867c9769f807dc562c81c9..f0b7e061a1f39b3e70d337ef4fe14c98a8f022c8 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -5,6 +5,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src , pkgsMusl, pkgsStatic, linux_latest +, config }: pkgsStatic.callPackage ( @@ -13,6 +14,7 @@ pkgsStatic.callPackage ( , busybox, cloud-hypervisor, cryptsetup, dbus, execline, inkscape , iproute2, inotify-tools, jq, mdevd, s6, s6-linux-init, socat , util-linuxMinimal, virtiofsd, xorg, xdg-desktop-portal-spectrum-host +, btrfs-progs }: let @@ -33,8 +35,8 @@ let foot = pkgsGui.foot.override { allowPgo = false; }; packages = [ - cloud-hypervisor cryptsetup dbus execline inotify-tools iproute2 - jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools + btrfs-progs cloud-hypervisor cryptsetup dbus execline inotify-tools + iproute2 jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools virtiofsd xdg-desktop-portal-spectrum-host (busybox.override { @@ -80,11 +82,13 @@ let appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; appvm-foot = callSpectrumPackage ../../vm/app/foot.nix {}; appvm-gnome-text-editor = callSpectrumPackage ../../vm/app/gnome-text-editor.nix {}; + appvm-systemd-sysupdate = callSpectrumPackage ../../vm/app/systemd-sysupdate {}; }; packagesSysroot = runCommand "packages-sysroot" { depsBuildBuild = [ inkscape ]; nativeBuildInputs = [ xorg.lndir ]; + src = builtins.path { name = "os-release"; path = ./os-release.in; }; } '' mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps @@ -96,8 +100,7 @@ let done # If systemd-pull is missing systemd-sysupdate will fail with a - # very confusing error message. If systemd-sysupdate doesn't work, - # users will not be able to receive an update that fixes the problem. + # very confusing error message. for i in sysupdate pull; do if ! cat -- "$out/usr/lib/systemd/systemd-$i" > /dev/null; then echo "link to systemd-$i didn't get installed" >&2 @@ -147,6 +150,9 @@ stdenvNoCC.mkDerivation { printf "%s\n/\n" ${packagesSysroot} >$out sed p ${writeClosure [ packagesSysroot] } >>$out ''; + UPDATE_SIGNING_KEY = config.update-signing-key; + UPDATE_URL = config.update-url; + VERSION = config.version; }; # The Makefile uses $(ROOT_FS_DIR), not $(dest), so it can share code diff --git a/host/rootfs/file-list.mk b/host/rootfs/file-list.mk index 9acaa1d90bed674814775becf89c1c847d0ce3e3..dc2f3f353b62931cb046e13aa757528a4587025f 100644 --- a/host/rootfs/file-list.mk +++ b/host/rootfs/file-list.mk @@ -42,13 +42,20 @@ FILES = \ image/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/notification-fd \ image/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/run \ image/etc/s6-linux-init/scripts/rc.init \ + image/etc/sysupdate.d/50-verity.transfer \ + image/etc/sysupdate.d/60-root.transfer \ + image/etc/sysupdate.d/70-kernel.transfer \ image/etc/udev/rules.d/99-spectrum.rules \ + image/etc/vm-sysupdate.d/50-verity.transfer \ + image/etc/vm-sysupdate.d/60-root.transfer \ + image/etc/vm-sysupdate.d/70-kernel.transfer \ image/etc/xdg/weston/autolaunch \ image/etc/xdg/weston/weston.ini \ image/usr/bin/assign-devices \ image/usr/bin/create-vm-dependencies \ image/usr/bin/run-appimage \ image/usr/bin/run-vmm \ + image/usr/bin/spectrum-update \ image/usr/bin/vm-console \ image/usr/bin/vm-import \ image/usr/bin/vm-start \ diff --git a/host/rootfs/image/etc/fstab b/host/rootfs/image/etc/fstab index 6a82ecc85090a37b13603b29f74ca6e554a28c33..36e073c58b91e81d63ae40a6aa6f019a2f03e546 100644 --- a/host/rootfs/image/etc/fstab +++ b/host/rootfs/image/etc/fstab @@ -4,3 +4,4 @@ proc /proc proc defaults 0 0 devpts /dev/pts devpts defaults,gid=4,mode=620 0 0 tmpfs /dev/shm tmpfs defaults 0 0 sysfs /sys sysfs defaults 0 0 +tmpfs /tmp tmpfs defaults 0 0 diff --git a/host/rootfs/image/etc/sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..9cd64b58ae55d55d378d99f5701f1ecef867e436 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v_@u.verity + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_@v.verity +MatchPartitionType=root-verity +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/60-root.transfer b/host/rootfs/image/etc/sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..cd12d2bd2b4ecd9bb5c7d26cc7c27a4bdb74cac8 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/60-root.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v_@u.root + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_@v +MatchPartitionType=root +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..e4190587a6bb127cb7315f38d59e48cf279318a4 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/EFI/Linux +PathRelativeTo=boot +MatchPattern=Spectrum_@v.efi +Mode=0644 +InstancesMax=2 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..ab4997c83605e3820a22b0b2178dcd76dfcf787e --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v_@u.verity + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v_@u.verity +Mode=0644 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer b/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..8a3175684f1697e0eca443eb0a1a97176e4f66d4 --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v_@u.root + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v_@u.root +Mode=0644 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..cb181239d71c5a6d0a5b3652d5534a23eda64183 --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v.efi +Mode=0644 diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update new file mode 100755 index 0000000000000000000000000000000000000000..ad598b557ac1cc4e9b95ff65a53a68f04d3759ee --- /dev/null +++ b/host/rootfs/image/usr/bin/spectrum-update @@ -0,0 +1,83 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { mkdir -p -m 0700 /run/updater } + +# Take a global lock to avoid races. +s6-setlock /run/update-lock + +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Delete any stale temporary files. Delete any existing signature + # files. If the VM is still running (it should not be), the VM might + # have write access to the directory. However, updates-dir-check is + # safe against that. + if { updates-dir-check cleanup shared } + + if { + # rm -f ensures that "snapshot" does not exist afterwards. + ifte { exit 0 } { rm -f snapshot } + # TODO: suppress only "subvolume does not exist" errors. + redirfd -w 2 /dev/null btrfs subvolume delete snapshot + } + + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } + basename -- $id_path + } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. + + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + if { + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } + umask 022 + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd + } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} } + + # Remove the bind mount. + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Validate the update directory. Delete any stale temporary files. + # Check that a signature file was downloaded. + if { updates-dir-check check snapshot } + + # Perform the update in a separate mount namespace. + unshare --mount + if { mount --bind -o ro -- snapshot /run/updater } + + /usr/lib/systemd/systemd-sysupdate update +} +importas -i sysupdate_exit_status ? +# Clean up. +foreground { btrfs subvolume delete -- snapshot } +exit $sysupdate_exit_status diff --git a/host/rootfs/os-release.in b/host/rootfs/os-release.in new file mode 100644 index 0000000000000000000000000000000000000000..d6e699e82f87dcb1c4656ac19d4e9986282f14a5 --- /dev/null +++ b/host/rootfs/os-release.in @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +NAME="Spectrum OS" +ID=spectrum +PRETTY_NAME="Spectrum @VERSION@" +VERSION=@VERSION@ +VERSION_ID=@VERSION@ +IMAGE_ID=spectrum-root +IMAGE_VERSION=@VERSION@ +RELEASE_TYPE=development +HOME_URL="https://spectrum-os.org" +BUG_REPORT_URL="mailto:discuss@spectrum-os.org" +ANSI_COLOR="1;34" +VENDOR_NAME=Spectrum +VENDOR_URL="https://spectrum-os.org" diff --git a/lib/config.default.nix b/lib/config.default.nix index 489c231490a8b66aa01f50053b25646060f7f963..e53b01f1259543b988458a14b3014eb8ca29e90d 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -5,4 +5,6 @@ pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; version = "0.0.0"; + update-url = "https://your-spectrum-os-update-server.invalid/download-directory"; + update-signing-key = ./fake-update-signing-key.gpg; } diff --git a/lib/config.nix b/lib/config.nix index e437cdbe9aa22dd0f9c8d7052ac331c8fccf6ce6..d7edb967f339f2d0af97adef5c0302eb58950d19 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -1,5 +1,6 @@ -# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> # SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> let customConfigPath = builtins.tryEval <spectrum-config>; @@ -17,6 +18,12 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; in -default // callConfig config +finalConfig // { + update-signing-key = builtins.path { + name = "signing-key"; + path = finalConfig.update-signing-key; + }; +} diff --git a/lib/fake-update-signing-key.gpg b/lib/fake-update-signing-key.gpg new file mode 100644 index 0000000000000000000000000000000000000000..12e18f4c7c740e31692e1f1975282fa72ac1f2e3 --- /dev/null +++ b/lib/fake-update-signing-key.gpg @@ -0,0 +1,3 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +NOT A VALID KEY - UPDATES WILL NOT WORK diff --git a/release/live/shell.nix b/release/live/shell.nix index ffaa9a571c662810348822a5952d479d251a25e5..b263eacc4d0324191e3c7737dd90d304e477e79b 100644 --- a/release/live/shell.nix +++ b/release/live/shell.nix @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> import ../../lib/call-package.nix ( -{ callSpectrumPackage, stdenv, qemu_kvm }: +{ callSpectrumPackage, config, stdenv, qemu_kvm }: let efi = callSpectrumPackage ../../host/efi.nix {}; @@ -17,6 +17,7 @@ in OVMF_CODE = "${qemu_kvm}/share/qemu/edk2-${stdenv.hostPlatform.qemuArch}-code.fd"; ROOT_FS_DIR = efi.rootfs; EFI_IMAGE = efi; + VERSION = config.version; }; } )) (_: {}) diff --git a/vm/app/systemd-sysupdate/default.nix b/vm/app/systemd-sysupdate/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..04df283f09a1f1ece9197e275d562193af170982 --- /dev/null +++ b/vm/app/systemd-sysupdate/default.nix @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../../lib/call-package.nix ( +{ callSpectrumPackage, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + escape-url = builtins.path { + name = "escape-url"; + path = ./escape-url.awk; + }; + populate-transfer-directory = builtins.path { + name = "populate-transfer-directory"; + path = ./populate-transfer-directory; + }; +in + +callSpectrumPackage ../../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = writeScript "run-script" '' +#!/usr/bin/execlineb -P +export LC_ALL C +export LANGUAGE C +if { mount -toverlay -olowerdir=/run/virtiofs/virtiofs0/etc:/etc -- overlay /etc } +backtick tmpdir { mktemp -d /run/sysupdate-XXXXXX } +# Not a useless use of cat: if there are NUL bytes in the URL +# busybox's awk might misbehave. +backtick update_url { cat /etc/update-url } +# Leading and trailing whitespace is almost certainly user error, +# but be friendly to the user (by stripping it) rather than failing. +backtick update_url { + awk "BEGIN { + url = ENVIRON[\"update_url\"] + gsub(/^[[:space:]]+/, \"\", url) + gsub(/[[:space:]]+$/, \"\", url) + print url + }" +} +multisubstitute { + importas -iSu tmpdir + importas -iSu update_url +} +if { ${populate-transfer-directory} ${escape-url} /etc/vm-sysupdate.d ''${tmpdir} ''${update_url} } +if { ${systemd}/lib/systemd/systemd-sysupdate --definitions=''${tmpdir} update } +# [ and ] are allowed in update URLs so that IPv6 addresses work, but +# they cause globbing in the curl command-line tool by default. Use --globoff +# to disable this feature. Only allow HTTP and HTTPS protocols on redirection. +if { ${curl}/bin/curl -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS -- ''${update_url}/SHA256SUMS } +${curl}/bin/curl -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS.sha256.asc -- ''${update_url}/SHA256SUMS.sha256.asc +''; +}) (_: {}) diff --git a/vm/app/systemd-sysupdate/escape-url.awk b/vm/app/systemd-sysupdate/escape-url.awk new file mode 100644 index 0000000000000000000000000000000000000000..8edd816a20ceefa08ecc7f1bc2d1cfbe33fa8a89 --- /dev/null +++ b/vm/app/systemd-sysupdate/escape-url.awk @@ -0,0 +1,31 @@ +#!/usr/bin/awk -f +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +BEGIN { + update_url = ARGV[1]; + # Check for a GNU awk misfeature + newline = "\n"; + # Reject URLs with control characters, query parameters, or fragments. + # They *cannot* work and so are rejected to produce better error messages. + # curl rejects control characters with "Malformed input to a URL function". + # Fragment specifiers ("#") and query parameters ("?") break concatenating + # /SHA256SUMS and /SHA256SUMS.sha256.asc onto the update URL. Also, it is + # simpler to reject update URLs that contain whitespace than to try to + # escape them. + if (update_url ~ /^[^\001-\040?#\x7F]+$/) { + # Backslashes are special to systemd-sysupdate. + # Use \\\\& because without the & the result is + # not portable between GNU awk and non-GNU awk. + gsub(/\\/, "\\\\&", update_url); + # "&" and "\\" are special on the RHS of a sed substitution + # and must be escaped with another backslash. The delimiter + # ("#" in this case) and "\n" must also be escaped, but they + # were rejected above so don't bother. + gsub(/[&\\]/, "\\\\&", update_url); + printf "%s", update_url; + exit 0; + } else { + print "Bad update URL from host: control characters, whitespace, query parameters, and fragment specifiers not allowed" > "/dev/stderr"; + exit 100; + } +} diff --git a/vm/app/systemd-sysupdate/populate-transfer-directory b/vm/app/systemd-sysupdate/populate-transfer-directory new file mode 100755 index 0000000000000000000000000000000000000000..f8e515c1a69b5a6a292cc3a4d387d501f1c6a3fe --- /dev/null +++ b/vm/app/systemd-sysupdate/populate-transfer-directory @@ -0,0 +1,26 @@ +#!/usr/bin/env -S execlineb -WS4 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +# $1: awk script name +# $2: transfer directory +# $3: target directory +# $4: update URL +export LC_ALL C +export LANGUAGE C +backtick -N sed_rhs { + # Use awk to both validate the URL and to escape sed metacharacters. + awk -f $1 -- $4 +} +export tmpdir $3 +elglob -w -0 transfer_file_ ${2}/*.transfer +forx -E transfer_file { $transfer_file_ } +backtick target_basename { + basename -- $transfer_file +} +multisubstitute { + importas -iuS sed_rhs + importas -iuS target_basename + importas -iuS tmpdir + define source $transfer_file +} +redirfd -w 1 ${tmpdir}/${target_basename} sed -E -- "s#@UPDATE_URL@#${sed_rhs}#g" $source -- 2.52.0
The documentation previously stated that updates were not possible without reinstalling. This is no longer the case, so correct the outdated documentation and explain how to enable updates for images one builds. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Move the documentation on how to enable updates to the part on build configuration. - Clarify what happens if an update is interrupted. - Move details to a technical note. - Link to systemd-sysupdate. --- Documentation/development/build-configuration.adoc | 13 ++++++++++ Documentation/installation/getting-spectrum.adoc | 25 +++++++++++++----- Documentation/installation/index.adoc | 4 ++- Documentation/using-spectrum/index.adoc | 2 ++ Documentation/using-spectrum/updates.adoc | 30 ++++++++++++++++++++++ 5 files changed, 66 insertions(+), 8 deletions(-) diff --git a/Documentation/development/build-configuration.adoc b/Documentation/development/build-configuration.adoc index 545aa8c05ac40a101b5ee280015cde7ec4f3a66f..24672802d2395b9ba124baeba433bf2c4fc59193 100644 --- a/Documentation/development/build-configuration.adoc +++ b/Documentation/development/build-configuration.adoc @@ -20,6 +20,19 @@ The configuration file should contain an attribute set. See https://spectrum-os.org/git/spectrum/tree/lib/config.default.nix[lib/config.default.nix] for supported configuration attributes and their default values. +To enable updates, you need to specify a version, an update URL, and an update signing key. +By default, the update URL is set to a .invalid domain and the update signing key is +an invalid key. Therefore, updates will not work. To enable updates, provide a valid key +and update server URL. Spectrum uses +https://www.freedesktop.org/software/systemd/man/latest/systemd-sysupdate.html[systemd-sysupdate], +so see the https://www.freedesktop.org/software/systemd/man/latest/sysupdate.d.html[sysupdate.d] +documentation for what you need to put on your server. Building +https://spectrum-os.org/git/spectrum/tree/release/updates.nix[release/updates.nix] produces an +directory that is compatible with systemd-sysupdate, except that the signature (`SHA256SUMS.gpg`) +is missing. + +Updates are signed, so the worst a compromised update server can do is fill up your home directory. + .config.nix to build Spectrum with a https://nixos.org/manual/nixpkgs/unstable/#sec-overlays-definition[Nixpkgs overlay] [example] [source,nix] diff --git a/Documentation/installation/getting-spectrum.adoc b/Documentation/installation/getting-spectrum.adoc index e7806e0f92793320bf0cdcbdd11dbc4e713275c7..0abc83a9e6fc01084b3faa9b93eb38398b0aef27 100644 --- a/Documentation/installation/getting-spectrum.adoc +++ b/Documentation/installation/getting-spectrum.adoc @@ -86,13 +86,24 @@ a menu allowing you to "Install Spectrum". NOTE: While it's possible to install Spectrum to your internal storage, at this point in Spectrum's development there is not much -reason to, as OS updates are not yet implemented, and persistent -storage is not yet exposed to VMs. Using the "Try Spectrum" option to -boot Spectrum will let you try out everything in Spectrum, without -having to go through the additional step of reinstalling Spectrum -every time you want to use a newer version. +reason to, as persistent storage is not yet exposed to VMs. + +Currently, Spectrum does not provide an update server, so +you must provide your own. You can do this via +xref:../development/build-configuration.adoc[build configuration]. +The default sets the signing key to `/dev/null` and the server +URL to an invalid value, so updates won't work. To enable updates, +set `update-url` to the URL of your server and `update-signing-key` +to a binary GnuPG keyring to verify the updates with. Not all possible +URLs will work, but most invalid URLs will cause an error during the +build rather than runtime misbehavior. + +In the running system, the signing key is located at +`/etc/systemd/import-pubring.gpg`. The update URL is in various files +under `/etc/updates`. These files are read-only, but one can mount +an overlayfs on top of `/etc/systemd` and `/etc/updates` if one wants +to make changes. CAUTION: Do not use Spectrum for anything important or sensitive as it is not yet suitable for real-world use. Many important security properties are -currently missing, and there is no procedure for updating to -new versions—you have to reinstall the OS. +currently missing. diff --git a/Documentation/installation/index.adoc b/Documentation/installation/index.adoc index d67c88dda062066c19c3b21e699f074cc18a6dbc..c61092c93a3965b6c4014aeaee9090532634c9be 100644 --- a/Documentation/installation/index.adoc +++ b/Documentation/installation/index.adoc @@ -18,6 +18,8 @@ development. == Uninstalling and Updating -Currently, there is no implementation for a software update. +Software updates are a work in progress. If you built Spectrum yourself, +xref:../development/build-configuration.adoc[Build configuration] for how +to enable updates for it. You can replace Spectrum by installing another OS. diff --git a/Documentation/using-spectrum/index.adoc b/Documentation/using-spectrum/index.adoc index 25347a4ed7bb1f899ee0a3b85aa51da94bb954b4..5d9ea657f7c6f8c21edbf8637d2d2d0bf52f931d 100644 --- a/Documentation/using-spectrum/index.adoc +++ b/Documentation/using-spectrum/index.adoc @@ -11,3 +11,5 @@ Ready to get started with Spectrum? Here is what you can do next: * xref:running-vms.adoc[Start some applications]. * xref:creating-custom-vms.adoc[Create your own VM] to use other applications. +* xref:updates.adoc[Enable updates] so you can use newer versions of Spectrum + without reinstalling the OS. diff --git a/Documentation/using-spectrum/updates.adoc b/Documentation/using-spectrum/updates.adoc new file mode 100644 index 0000000000000000000000000000000000000000..64f085bf1e721b46076b86228adb8e86b3e5c57d --- /dev/null +++ b/Documentation/using-spectrum/updates.adoc @@ -0,0 +1,30 @@ += Updating the OS +:page-parent: Using Spectrum + +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +// SPDX-License-Identifier: GFDL-1.3-no-invariants-or-later OR CC-BY-SA-4.0 + +Spectrum supports updates via the `spectrum-update` command. This +takes the path to a staging directory as argument. This directory +must be on a BTRFS filesystem. + +Updates are atomic and take effect after the system reboots. +If the system is rebooted, crashes, or loses power during an +update, the update will not take effect. Updates are digitally +signed and Spectrum will refuse to install an update that does +not have a trusted signature. + +See xref:../development/build-configuration.adoc[build configuration] +for what is needed for updates to work. The actual update is done using +https://www.freedesktop.org/software/systemd/man/systemd-sysupdate.html[systemd-sysupdate]. +See its documentation for the details. + +== Technical Note + +Since Spectrum's host has no network access, the VM that does the +updates (`sys.appvm-systemd-sysupdate`) is given a BTRFS subvolume to +write the updates into. It uses `systemd-sysupdate` to download the updates +into this directory. Once it exits, the host snapshots this directory and +checks it for malicious filenames or non-regular files. If the check +passes, this directory is used as the source for `systemd-sysupdate`, +which installs the updates to the OS volume and EFI system partition. -- 2.52.0
Wrong values for the version or update URL will cause very confusing build-time or runtime errors. Provide a better user experience by validating them up-front. The update URL validator is loose. It rejects only URLs that cannot possibly work: either appending /SHA256SUMS to them doesn't append to the path, or they will definitely be rejected by curl due to being malformed. The version validator is in lib/config.nix, as the version number is used in many places. It checks that the version only uses characters that are permitted by systemd's version number specification [1] and that will not break code that uses them in shell or sed commands. [1]: https://uapi-group.org/specifications/specs/version_format_specification Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Use loose URL validation: allow anything that might work. - Only reject versions that violate the specification. --- host/rootfs/default.nix | 19 ++++++++++++++++++- lib/config.nix | 20 ++++++++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index f0b7e061a1f39b3e70d337ef4fe14c98a8f022c8..06147eb8d1b713faac9b69ffdf42138d0c3e3093 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -85,6 +85,23 @@ let appvm-systemd-sysupdate = callSpectrumPackage ../../vm/app/systemd-sysupdate {}; }; + update-url = + let update-url = config.update-url; in + # Use builtins.fromJSON because it supports \uXXXX escapes. + # This is the same check done by check-url.awk in the update VM. + # The update code is careful to escape any metacharacters, but some + # simply cannot be made to work. Concatenating the URL with /SHA256SUMS + # must append to the path portion of the URL, and the URL must be one + # that libcurl will accept. I don't know how Unicode space is handled, + # but it is a bad idea. + if builtins.match (builtins.fromJSON "\"^[^\\u0001- #?\\u007F[:space:]]+$\"" update-url) == null then + builtins.abort '' + Update URL ${builtins.toJSON update-url} has forbidden characters. + Query strings, and fragment specifiers are not supported. + ASCII control characters and whitespace must be %-encoded. + '' + else + update-url; packagesSysroot = runCommand "packages-sysroot" { depsBuildBuild = [ inkscape ]; nativeBuildInputs = [ xorg.lndir ]; @@ -151,7 +168,7 @@ stdenvNoCC.mkDerivation { sed p ${writeClosure [ packagesSysroot] } >>$out ''; UPDATE_SIGNING_KEY = config.update-signing-key; - UPDATE_URL = config.update-url; + UPDATE_URL = update-url; VERSION = config.version; }; diff --git a/lib/config.nix b/lib/config.nix index d7edb967f339f2d0af97adef5c0302eb58950d19..4db6c34635abb1419224485f1e56119569375831 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -21,9 +21,17 @@ let finalConfig = default // callConfig config; in -finalConfig // { - update-signing-key = builtins.path { - name = "signing-key"; - path = finalConfig.update-signing-key; - }; -} +# See https://uapi-group.org/specifications/specs/version_format_specification +# for allowed version strings. +if builtins.match "[[:alnum:]_.~^-]+" finalConfig.version == null then + builtins.abort '' + Version ${builtins.toJSON finalConfig.version} has forbidden characters. + Only ASCII alphanumerics, ".", "_", "~", "^", "+", and "-" are allowed. + '' +else + finalConfig // { + update-signing-key = builtins.path { + name = "signing-key"; + path = finalConfig.update-signing-key; + }; + } -- 2.52.0
This implements updates via systemd-sysupdate. See individual commit messages for details. There are major changes to the image build process. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes in v4: - Fix build errors in intermediate patches. - Apply suggestions from code review. - Link to v3: https://spectrum-os.org/lists/archives/spectrum-devel/20251119-updates-v3-0-... Changes in v3: - See individual commits for details. There are too many to mention here. - Link to v2: https://spectrum-os.org/lists/archives/spectrum-devel/20251112-updates-v2-0-... Changes in v2: - updates-dir-check: - Do not check that there is a SHA256SUMS or SHA256SUMS.gpg file in the update directory. systemd-sysupdate will fail if it cannot find a manifest or its signature. - Follow symlinks in opening the directory. The path is from a trusted source and will always point to a BTRFS snapshot, never a symlink. The only exception is the last component, which is still checked to not be a symlink. - VM: - Link SHA256SUMS.sha256.asc to SHA256SUMS.gpg. Recent systemd-sysupdate seems to use the former name. - Get update URL from host. - Use an execline script instead of a shell script. - Update script: - Unmount shared directory if already mounted. This avoids errors when mounting it again. - Delete old snapshot if present. - Provide the VM information with a different directory layout. - Do not bind-mount the information passed into the VM into the shared VM folder. Instead rely on this folder being read-only to the guest. This is enforced by a read-only bind mount in virtiofs's mount namespace. - Testing: - Lots of manual update testing. - Disable the test for the live image as it doesn't work anymore. - Nix: - Move validation to a separate low-priority patch. - Documentation: - Document that updating the system is now possible. - Installer: - Remove the "Try Spectrum" button. - Link to v1: https://spectrum-os.org/lists/archives/spectrum-devel/20251029-updates-v1-0-... --- Demi Marie Obenour (14): host/rootfs: Install all programs from util-linuxMinimal host/rootfs: Install systemd-pull tools: Add directory checker for updates scripts: port make-gpt.sh to bash scripts/make-gpt.sh: Allow specifying partition size Support generating multiple partition UUIDs scripts: Use shell expansion to get partition path release: Compress installation images and remove live image Use OS version to set partition labels and UKI name Add B partitions to installation images release: Create directory with system update Support updates via systemd-sysupdate Documentation: Update support Validate configuration parameters Documentation/development/build-configuration.adoc | 13 ++ Documentation/installation/getting-spectrum.adoc | 56 +++++++-- Documentation/installation/index.adoc | 4 +- Documentation/using-spectrum/index.adoc | 2 + Documentation/using-spectrum/updates.adoc | 30 +++++ host/efi.nix | 2 +- host/initramfs/Makefile | 18 +-- host/initramfs/etc/probe | 20 --- host/initramfs/shell.nix | 2 + host/rootfs/Makefile | 27 ++++- host/rootfs/busybox-config | 134 +++++++++++++++++++++ host/rootfs/busybox-config.license | 4 + host/rootfs/default.nix | 92 +++++++++----- host/rootfs/file-list.mk | 7 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++ .../image/etc/vm-sysupdate.d/50-verity.transfer | 18 +++ .../image/etc/vm-sysupdate.d/60-root.transfer | 18 +++ .../image/etc/vm-sysupdate.d/70-kernel.transfer | 18 +++ host/rootfs/image/usr/bin/spectrum-update | 83 +++++++++++++ host/rootfs/os-release.in | 15 +++ host/rootfs/shell.nix | 2 + img/app/Makefile | 2 +- lib/config.default.nix | 4 + lib/config.nix | 15 ++- lib/fake-update-signing-key.gpg | 3 + release.nix | 2 + release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ----- release/combined/eosimages.nix | 19 ++- release/combined/grub.cfg.in | 5 - release/live/Makefile | 15 ++- release/live/default.nix | 5 +- release/live/shell.nix | 3 +- release/update.nix | 33 +++++ scripts/format-uuid.awk | 35 ++++++ scripts/format-uuid.sh | 19 --- scripts/make-gpt.sh | 30 ++--- tools/default.nix | 1 + tools/meson.build | 4 + tools/updates-dir-check.c | 134 +++++++++++++++++++++ vm/app/systemd-sysupdate/default.nix | 57 +++++++++ vm/app/systemd-sysupdate/escape-url.awk | 31 +++++ .../systemd-sysupdate/populate-transfer-directory | 26 ++++ vm/sys/net/Makefile | 2 +- 47 files changed, 928 insertions(+), 174 deletions(-) --- base-commit: e89924f5613539e4dcd9d485a82f976c817b34c1 change-id: 20250928-updates-92e99849e231 prerequisite-patch-id: c518b0e42e0c87755ef725ace8e961cdfb862285 prerequisite-patch-id: 0ed2b2073c0ab6d422aa642fd238b15428c6f7d1 -- Sincerely, Demi Marie Obenour (she/her/hers)
Busybox fdisk doesn't support GPT, only MBR. Busybox programs are also often buggy, so use the util-linux version where possible. This requires disabling a lot of Busybox applets, so move the Busybox config to a separate file that Nix loads via builtins.readFile. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v3: - Add util-linuxMinimal to packages instead of installing it manually. Changes since v2: - Split installation of util-linux and of systemd to separate patches. --- host/rootfs/busybox-config | 134 +++++++++++++++++++++++++++++++++++++ host/rootfs/busybox-config.license | 4 ++ host/rootfs/default.nix | 38 +++-------- 3 files changed, 147 insertions(+), 29 deletions(-) diff --git a/host/rootfs/busybox-config b/host/rootfs/busybox-config new file mode 100644 index 0000000000000000000000000000000000000000..f2fd5fcab4141ea63c663e433412a253d5235aab --- /dev/null +++ b/host/rootfs/busybox-config @@ -0,0 +1,134 @@ +CONFIG_ADDPART n +CONFIG_AGETTY n +CONFIG_BITS n +CONFIG_BLKDISCARD n +CONFIG_BLKID n +CONFIG_BLKPR n +CONFIG_BLKZONE n +CONFIG_BLOCKDEV n +CONFIG_CAL n +CONFIG_CHATTR n +CONFIG_CHCPU n +CONFIG_CHMEM n +CONFIG_CHOOM n +CONFIG_CHRT n +CONFIG_COLCRT n +CONFIG_COLRM n +CONFIG_COLUMN n +CONFIG_CORESCHED n +CONFIG_CTRLALTDEL n +CONFIG_DELPART n +CONFIG_DEPMOD n +CONFIG_DMESG n +CONFIG_EJECT n +CONFIG_ENOSYS n +CONFIG_EXCH n +CONFIG_FADVISE n +CONFIG_FALLOCATE n +CONFIG_FDISK n +CONFIG_FINCORE n +CONFIG_FINDFS n +CONFIG_FINDMNT n +CONFIG_FLOCK n +CONFIG_FSCK n +CONFIG_FSCK_CRAMFS n +CONFIG_FSCK_MINIX n +CONFIG_FSFREEZE n +CONFIG_FSTRIM n +CONFIG_GETOPT n +CONFIG_HALT n +CONFIG_HARDLINK n +CONFIG_HD n +CONFIG_HEXDUMP n +CONFIG_HWCLOCK n +CONFIG_I386 n +CONFIG_INIT n +CONFIG_INSMOD n +CONFIG_IONICE n +CONFIG_IP n +CONFIG_IPCMK n +CONFIG_IPCRM n +CONFIG_IPCS n +CONFIG_ISOSIZE n +CONFIG_KILL n +CONFIG_LAST n +CONFIG_LASTB n +CONFIG_LDATTACH n +CONFIG_LINUX32 n +CONFIG_LINUX64 n +CONFIG_LOGGER n +CONFIG_LOOK n +CONFIG_LOSETUP n +CONFIG_LSATTR n +CONFIG_LSBLK n +CONFIG_LSCLOCKS n +CONFIG_LSCPU n +CONFIG_LSFD n +CONFIG_LSIPC n +CONFIG_LSIRQ n +CONFIG_LSLOCKS n +CONFIG_LSLOGINS n +CONFIG_LSMEM n +CONFIG_LSMOD n +CONFIG_LSNS n +CONFIG_MCOOKIE n +CONFIG_MESG n +CONFIG_MKE2FS n +CONFIG_MKFS n +CONFIG_MKFS_BFS n +CONFIG_MKFS_CRAMFS n +CONFIG_MKFS_EXT2 n +CONFIG_MKFS_MINIX n +CONFIG_MKSWAP n +CONFIG_MODINFO n +CONFIG_MODPROBE n +CONFIG_MOUNT n +CONFIG_MOUNTPOINT n +CONFIG_NAMEI n +CONFIG_NOLOGIN n +CONFIG_NSENTER n +CONFIG_PARTX n +CONFIG_PIPESZ n +CONFIG_PIVOT_ROOT n +CONFIG_POWEROFF n +CONFIG_PRLIMIT n +CONFIG_READPROFILE n +CONFIG_REBOOT n +CONFIG_RENAME n +CONFIG_RENICE n +CONFIG_RESIZEPART n +CONFIG_REV n +CONFIG_RFKILL n +CONFIG_RMMOD n +CONFIG_RTCWAKE n +CONFIG_SCRIPT n +CONFIG_SCRIPTLIVE n +CONFIG_SCRIPTREPLAY n +CONFIG_SETARCH n +CONFIG_SETPGID n +CONFIG_SETPRIV n +CONFIG_SETSID n +CONFIG_SFDISK n +CONFIG_SHUTDOWN n +CONFIG_SULOGIN n +CONFIG_SWAPLABEL n +CONFIG_SWAPOFF n +CONFIG_SWAPON n +CONFIG_SWITCH_ROOT n +CONFIG_TASKSET n +CONFIG_UCLAMPSET n +CONFIG_UMOUNT n +CONFIG_UNAME26 n +CONFIG_UNSHARE n +CONFIG_UTMPDUMP n +CONFIG_UUIDD n +CONFIG_UUIDGEN n +CONFIG_UUIDPARSE n +CONFIG_WAITPID n +CONFIG_WALL n +CONFIG_WDCTL n +CONFIG_WHEREIS n +CONFIG_WIPEFS n +CONFIG_WRITE n +CONFIG_X86_64 n +CONFIG_ZRAMCTL n diff --git a/host/rootfs/busybox-config.license b/host/rootfs/busybox-config.license new file mode 100644 index 0000000000000000000000000000000000000000..ba50b647fbfac9b79ecb29f33a36c07d3e332ba2 --- /dev/null +++ b/host/rootfs/busybox-config.license @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2025 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 1578155fa0fb9a4df3fb4884e21ed7d8d8f821dc..dc7786a2afa74abbe5146894f73f5860b5c0ef8f 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -35,28 +35,11 @@ let packages = [ cloud-hypervisor cryptsetup dbus execline inotify-tools iproute2 jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools - virtiofsd xdg-desktop-portal-spectrum-host + util-linuxMinimal virtiofsd xdg-desktop-portal-spectrum-host (busybox.override { - extraConfig = '' - CONFIG_CHATTR n - CONFIG_DEPMOD n - CONFIG_FINDFS n - CONFIG_HALT n - CONFIG_INIT n - CONFIG_INSMOD n - CONFIG_IP n - CONFIG_LSATTR n - CONFIG_LSMOD n - CONFIG_MKE2FS n - CONFIG_MKFS_EXT2 n - CONFIG_MODINFO n - CONFIG_MODPROBE n - CONFIG_MOUNT n - CONFIG_POWEROFF n - CONFIG_REBOOT n - CONFIG_RMMOD n - ''; + # Use a separate file as it is a bit too big. + extraConfig = builtins.readFile ./busybox-config; }) # Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. @@ -96,6 +79,12 @@ let mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps + # lndir silently ignores existing links, so run it before ln + # so that ln catches any duplicates. + for pkg in ${escapeShellArgs usrPackages}; do + lndir -ignorelinks -silent "$pkg" "$out/usr" + done + # Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \ @@ -110,18 +99,9 @@ let ln -st $out/usr/share/dbus-1/services \ ${pkgsGui.xdg-desktop-portal-gtk}/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service - for pkg in ${escapeShellArgs usrPackages}; do - lndir -ignorelinks -silent "$pkg" "$out/usr" - done - ${concatStrings (mapAttrsToList (name: path: '' ln -s ${path} $out/usr/lib/spectrum/vm/${name} '') appvms)} - - # TODO: this is a hack and we should just build the util-linux - # programs we want. - # https://lore.kernel.org/util-linux/87zgrl6ufb.fsf@alyssa.is/ - ln -s ${util-linuxMinimal}/bin/{findfs,uuidgen,lsblk,mount} $out/usr/bin ''; in -- 2.52.0
This patch has been committed as f63e2598ec681dedf3b3b29c8f603a0bf2ac1512, which can be viewed online at https://spectrum-os.org/git/spectrum/commit/?id=f63e2598ec681dedf3b3b29c8f60.... This is an automated message. Send comments/questions/requests to: Alyssa Ross <hi@alyssa.is>
Due to a systemd bug [1], building systemd-sysupdate does not require that systemd-pull is built as well. However, systemd-sysupdate has a run-time dependency on systemd-pull. Therefore, override the systemd derivation so that systemd-pull is built. Confusingly, this requires enabling systemd-importd. If systemd-pull or systemd-sysupdate is not built, the resulting image will be broken and users will not be able to recover without either a reinstall or reverting to the previous version. Therefore, add a check to ensure that both are in fact built. Use 'cat' rather than just 'stat' to catch broken symlinks and the like. The override can be removed once https://github.com/NixOS/nixpkgs/pull/461277 is merged, which builds systemd-importd by default on musl. The tests will be preserved to catch any regressions. [1]: https://github.com/systemd/systemd/issues/39635 Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Add link to upstream Nixpkgs issue. --- host/rootfs/default.nix | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index dc7786a2afa74abbe5146894f73f5860b5c0ef8f..cd61c78b1f1668e7bc9c84c638ff6e7d8b6de140 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -43,7 +43,8 @@ let }) # Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. - ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod systemd ]); + ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod ]); + nixosAllHardware = nixos ({ modulesPath, ... }: { imports = [ (modulesPath + "/profiles/all-hardware.nix") ]; @@ -64,7 +65,16 @@ let # https://inbox.vuxu.org/musl/20251017-dlopen-use-rpath-of-caller-dso-v1-1-46c... usrPackages = [ appvm kernel.modules firmware netvm - ] ++ (with pkgsGui; [ dejavu_fonts kmod.lib mesa westonLite ]); + ] ++ (with pkgsGui; [ + dejavu_fonts kmod.lib mesa westonLite + # Work around NixOS/nixpkgs#459020: without "withImportd = true" + # systemd-pull doesn't get built, so systemd-sysupdate doesn't work. + # TODO: remove this when NixOS/nixpkgs#461277 is merged. + (systemd.override { + withImportd = true; + withSysupdate = true; + }) + ]); appvms = { appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; @@ -85,6 +95,16 @@ let lndir -ignorelinks -silent "$pkg" "$out/usr" done + # If systemd-pull is missing systemd-sysupdate will fail with a + # very confusing error message. If systemd-sysupdate doesn't work, + # users will not be able to receive an update that fixes the problem. + for i in sysupdate pull; do + if ! cat -- "$out/usr/lib/systemd/systemd-$i" > /dev/null; then + echo "link to systemd-$i didn't get installed" >&2 + exit 1 + fi + done + # Weston doesn't support SVG icons. inkscape -w 20 -h 20 \ -o $out/usr/share/icons/hicolor/20x20/apps/com.system76.CosmicFiles.png \ -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
Due to a systemd bug [1], building systemd-sysupdate does not require that systemd-pull is built as well. However, systemd-sysupdate has a run-time dependency on systemd-pull. Therefore, override the systemd derivation so that systemd-pull is built. Confusingly, this requires enabling systemd-importd.
If systemd-pull or systemd-sysupdate is not built, the resulting image will be broken and users will not be able to recover without either a reinstall or reverting to the previous version. Therefore, add a check to ensure that both are in fact built. Use 'cat' rather than just 'stat' to catch broken symlinks and the like.
The override can be removed once https://github.com/NixOS/nixpkgs/pull/461277 is merged, which builds systemd-importd by default on musl. The tests will be preserved to catch any regressions.
[1]: https://github.com/systemd/systemd/issues/39635
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2:
- Add link to upstream Nixpkgs issue. --- host/rootfs/default.nix | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-)
We now have the Nixpkgs fix. Given that there's also an upstream fix in systemd, I don't think this is likely to regress, so my plan is to just skip this patch. (Hopefully we end up with an integration test for updates, which would catch this anyway.)
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either. Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. systemd-sysupdate can leave behind temporary files with names starting with '.', so delete them instead of failing. Linux can lose cache coherency if there is an I/O error, so call syncfs() on the directory before checking anything. For the same reason, fsync() the directory if any hidden files were deleted. The directory checker also serves another critical function: it checks if the VM actually downloaded anything. Otherwise, network problems could cause updates to silently do nothing. Specifically, it checks that the VM provided a file starting with the prefix "SHA256SUMS.". These will be the last ones the in-VM updater downloads. An additional mode is provided to clean out all such files. This will be used to ensure that before the in-VM updater runs, no such files are present. Hence, if the VM didn't actually download anything, the user will get a clear error instead of a false success message or a confusing error. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Purge leftover temporary files rather than returning an error. - Split into two modes: one that deletes signature files, and one that checks that at least one signature file exists. This allows checking that the VM actually sent something. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- tools/default.nix | 1 + tools/meson.build | 4 ++ tools/updates-dir-check.c | 134 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+) diff --git a/tools/default.nix b/tools/default.nix index 7cb7dc5b72b8394f5383c80ccf110fec55c44f21..da82f075fdba4655bd964ba35e819d669deff3f1 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -77,6 +77,7 @@ stdenv.mkDerivation (finalAttrs: { ./sd-notify-adapter.c ./start-vmm ./subprojects + ./updates-dir-check.c ] ++ lib.optionals driverSupport [ ./xdp-forwarder ])); diff --git a/tools/meson.build b/tools/meson.build index b40853a21048e157a1bc5b59f326ac5d45a747ef..9f5468ed8b476d8a16aec8c178d946546297ae85 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -33,6 +33,10 @@ if get_option('host') install: true) subdir('start-vmm') + + executable('updates-dir-check', 'updates-dir-check.c', + c_args : '-D_GNU_SOURCE', + install: true) endif if get_option('build') diff --git a/tools/updates-dir-check.c b/tools/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..83af806bebf36754f8c794b04933bf6021338c38 --- /dev/null +++ b/tools/updates-dir-check.c @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> +#include <unistd.h> + +#include <err.h> + +[[noreturn]] static void bad_char(char c, char *msg_component) +{ + if (c >= 0x20 && c <= 0x7E) + errx(EXIT_FAILURE, "Forbidden %s character in filename: '%c'", + msg_component, c); + errx(EXIT_FAILURE, + "Forbidden %s character in filename: byte 0x%hhx", + msg_component, c); +} + +[[noreturn]] static void usage(void) +{ + errx(EXIT_FAILURE, "Usage: updates-dir-check [cleanup|check] DIRECTORIES..."); +} + +static void checkdir(int fd, bool check_sig) +{ + bool found_sig = false; + DIR *d = fdopendir(fd); + if (d == NULL) + err(EXIT_FAILURE, "fdopendir"); + // If there is an I/O error while there are dirty pages outstanding, + // the dirty pages are silently discarded. This means that the contents + // of the filesystem can change behind userspace's back. Flush all + // dirty pages in the filesystem with the directory to prevent this. + if (syncfs(fd) != 0) + err(EXIT_FAILURE, "syncfs"); + bool changed = false; + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(EXIT_FAILURE, "readdir"); + break; + } + const char *ptr = entry->d_name; + if (ptr[0] == '.') { + if (ptr[1] == '\0') + continue; + if (ptr[1] == '.' && ptr[2] == '\0') + continue; + // systemd-sysupdate uses these for temporary files. + // It normally cleans them up itself, but if there is an error + // it does not always clean them up. I'm not sure if it is + // guaranteed to clean up temporary files from a past run, so + // delete them instead of returning an error. + if (unlinkat(fd, ptr, 0)) + err(EXIT_FAILURE, "Failed to unlink temporary file"); + changed = true; + continue; + } + char c = ptr[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + bad_char(c, "initial"); + while ((c = *++ptr)) { + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) + bad_char(c, "subsequent"); + } + // Empty filenames are rejected as having a bad initial character, + // and POSIX forbids them from being returned anyway. Therefore, + // this cannot be out of bounds. + if (ptr[-1] == '.') + errx(EXIT_FAILURE, "Filename %s ends with a '.'", entry->d_name); + if (entry->d_type == DT_UNKNOWN) + errx(EXIT_FAILURE, "Filesystem didn't report type of file %s", entry->d_name); + if (entry->d_type != DT_REG) + errx(EXIT_FAILURE, "Entry contains non-regular file %s", entry->d_name); + if (strncmp(entry->d_name, "SHA256SUMS.", sizeof("SHA256SUMS.") - 1) == 0) { + // Found a signature file! + if (check_sig) + found_sig = true; + else { + if (unlinkat(fd, entry->d_name, 0)) + err(EXIT_FAILURE, "Unlinking old signature file"); + changed = true; + } + } + } + // If a change was made, enforcing cache coherency also requires + // another fsync() call. This is again because Linux can discard + // changes if there is an I/O error. + if (changed && fsync(fd)) + errx(EXIT_FAILURE, "fsync"); + if (check_sig && !found_sig) { + warnx("sys.appvm-systemd-sysupdate didn't send a signature file."); + warnx("There was probably a problem downloading the update."); + errx(EXIT_FAILURE, "Check its logs for more information."); + } + closedir(d); +} + +int main(int argc, char **argv) +{ + if (argc != 3) + usage(); + + bool check_sig; + if (strcmp(argv[1], "cleanup") == 0) + check_sig = false; + else if (strcmp(argv[1], "check") == 0) + check_sig = true; + else + usage(); + + for (int i = 2; i < argc; ++i) { + int fd = open(argv[i], O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + err(EXIT_FAILURE, "open(%s)", argv[i]); + checkdir(fd, check_sig); + } + return 0; +} -- 2.52.0
Also add some error checks and use Bash features. No other functional change intended. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Do not use wrapper script. - Make script non-executable. - Invoke script as 'bash FILE' to work around /usr/bin/env not working during a Nix build. --- host/initramfs/Makefile | 4 ++-- host/rootfs/Makefile | 2 +- img/app/Makefile | 2 +- release/live/Makefile | 2 +- scripts/make-gpt.sh | 20 ++++++++++---------- vm/sys/net/Makefile | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 13bb548d6146684a25dab1e31228c0b9a4ca8db7..fd8cbb6c3e775ed27d0a524bf167cb4d3940d799 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -36,7 +36,7 @@ build/mountpoints: find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';' build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ @@ -45,7 +45,7 @@ build/loop.tar: build/live.img $(TAR) -cf $@ build/live.img build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e mv $@.tmp $@ diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index aac915ffb2781aee0997c169e86e3fd1983aa3b3..fcfbc3e437fdb108252ba77d4d4e8f4f636ffd78 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -90,7 +90,7 @@ clean: .PHONY: clean build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_DIR)/verity-timestamp $(ROOT_FS) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ $(ROOT_FS)/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS)/rootfs:root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH)") mv $@.tmp $@ diff --git a/img/app/Makefile b/img/app/Makefile index 48eba871339d314479f730101246ace3fa39e2db..f16c2df1b90bbec6750f2980da4dbaf49c9cb0ea 100644 --- a/img/app/Makefile +++ b/img/app/Makefile @@ -26,7 +26,7 @@ $(imgdir)/appvm/vmlinux: $(KERNEL) $(imgdir)/appvm/blk/root.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:5460386f-2203-4911-8694-91400125c604:root mv $@.tmp $@ diff --git a/release/live/Makefile b/release/live/Makefile index d61248e94599adc5229d0ad38d54b9f649d66ca1..a79947c57d562677760bc669c66320953a2b0d2d 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -10,7 +10,7 @@ DTBS ?= build/empty dest = build/live.img $(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh old mode 100755 new mode 100644 index 96f0d2c8494c093558c0e32e7e920b569bb078ef..0ae3d0005e5ae7d6214270fedb2fafb0a7064cf5 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -1,11 +1,11 @@ -#!/bin/sh -eu -# +#!/usr/bin/env -S bash -- # SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> # SPDX-FileCopyrightText: 2022 Unikie # SPDX-License-Identifier: EUPL-1.2+ # -# usage: make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... +# usage: bash make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... +set -xeuo pipefail ONE_MiB=1048576 # Prints the number of 1MiB blocks required to store the file named @@ -40,16 +40,15 @@ scriptsDir="$(dirname "$0")" out="$1" shift -nl=' -' table="label: gpt" # Keep 1MiB free at the start, and 1MiB free at the end. gptBytes=$((ONE_MiB * 2)) for partition; do - sizeMiB="$(sizeMiB "$(partitionPath "$partition")")" - table="$table${nl}size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" - gptBytes="$((gptBytes + sizeMiB * ONE_MiB))" + partitionPath=$(partitionPath "$partition") + sizeMiB=$(sizeMiB "$partitionPath") + table+=$'\n'"size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" + gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) done rm -f "$out" @@ -60,6 +59,7 @@ EOF n=0 for partition; do - fillPartition "$out" "$n" "$(partitionPath "$partition")" - n="$((n + 1))" + partitionPath=$(partitionPath "$partition") + fillPartition "$out" "$n" "$partitionPath" + n=$((n + 1)) done diff --git a/vm/sys/net/Makefile b/vm/sys/net/Makefile index d71c2325eff3bae921f33c61f799846d35e401c2..e403d697d90c0021a7a8d1dbc1553cfbda74a117 100644 --- a/vm/sys/net/Makefile +++ b/vm/sys/net/Makefile @@ -25,7 +25,7 @@ $(vmdir)/netvm/vmlinux: $(KERNEL) $(vmdir)/netvm/blk/root.img: ../../../scripts/make-gpt.sh ../../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) - ../../../scripts/make-gpt.sh $@.tmp \ + bash ../../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:ea21da27-0391-48da-9235-9d2ab2ca7844:root mv $@.tmp $@ -- 2.52.0
systemd-sysupdate requires that partitions are large enough to hold the newly downloaded images. This requires that they be large enough to have room to grow. Allow specifying the partition size manually, overriding the default (the size of the file that will be copied into the partition). Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Split into separate commit. --- scripts/make-gpt.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh index 0ae3d0005e5ae7d6214270fedb2fafb0a7064cf5..91c6038f67d4d4906fec4a3412f2ff5fca2671d5 100644 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -45,8 +45,13 @@ table="label: gpt" # Keep 1MiB free at the start, and 1MiB free at the end. gptBytes=$((ONE_MiB * 2)) for partition; do - partitionPath=$(partitionPath "$partition") - sizeMiB=$(sizeMiB "$partitionPath") + if [[ "$partition" =~ :([1-9][0-9]*)MiB$ ]]; then + sizeMiB=${BASH_REMATCH[1]} + partition=${partition%:*} + else + partitionPath=$(partitionPath "$partition") + sizeMiB=$(sizeMiB "$partitionPath") + fi table+=$'\n'"size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) done -- 2.52.0
Generate 4 partition UUIDs instead of just 2. Port scripts/format-uuid.sh to awk to make this much easier. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Split into separate commit. --- host/initramfs/Makefile | 8 +++++--- host/rootfs/Makefile | 6 ++++-- release/live/Makefile | 8 +++++--- release/live/default.nix | 2 +- scripts/format-uuid.awk | 35 +++++++++++++++++++++++++++++++++++ scripts/format-uuid.sh | 19 ------------------- 6 files changed, 50 insertions(+), 28 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index fd8cbb6c3e775ed27d0a524bf167cb4d3940d799..27a26b46a8110d35ee02a63b12931d6b9c2742e5 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -35,10 +35,12 @@ build/mountpoints: cd build/mountpoints && mkdir -p $(MOUNTPOINTS) find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';' -build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) +build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$3 \ + $(ROOT_FS):root:$$1 mv $@.tmp $@ build/loop.tar: build/live.img diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index fcfbc3e437fdb108252ba77d4d4e8f4f636ffd78..f02bb76371f000e3f65bb7c2a7f217d437845481 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -90,9 +90,11 @@ clean: .PHONY: clean build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_DIR)/verity-timestamp $(ROOT_FS) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS)/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS)/rootfs:root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH)") + $(ROOT_FS_VERITY):verity:$$3 \ + $(ROOT_FS):root:$$1 mv $@.tmp $@ debug: diff --git a/release/live/Makefile b/release/live/Makefile index a79947c57d562677760bc669c66320953a2b0d2d..78361a48512a37514ba0e57e0cc8b0ec3a71664b 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -9,11 +9,13 @@ DTBS ?= build/empty dest = build/live.img -$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) +$(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$3 \ + $(ROOT_FS):root:$$1 mv $@.tmp $@ build/empty: diff --git a/release/live/default.nix b/release/live/default.nix index ac2d7a55fd4fe0c02108309ecea20e368000af0d..98cb4862e239e3ad9ddbd7b5ace5716f57df683b 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -29,7 +29,7 @@ stdenv.mkDerivation { fileset = lib.fileset.intersection src (lib.fileset.unions [ ./. ../../lib/common.mk - ../../scripts/format-uuid.sh + ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk ]); diff --git a/scripts/format-uuid.awk b/scripts/format-uuid.awk new file mode 100644 index 0000000000000000000000000000000000000000..a5349d68a4d29be5f750650236420c9b5a7257eb --- /dev/null +++ b/scripts/format-uuid.awk @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +function format_uuid(arg) { + if (arg in found_so_far) { + fail("Duplicate UUID, try changing the image (by even 1 bit)"); + } + found_so_far[arg] = 1; + print (substr(arg, 1, 8) "-" \ + substr(arg, 9, 4) "-" \ + substr(arg, 13, 4) "-" \ + substr(arg, 17, 4) "-" \ + substr(arg, 21, 12)); +} + +function fail(msg) { + print msg > "/dev/stderr"; + exit 1; +} + +BEGIN { + FS = ""; + RS = "\n"; + if ((getline) != 1) + fail("Empty input file"); + roothash = $0; + if (roothash !~ /^[a-f0-9]{64}$/) + fail("Invalid root hash"); + if (getline) + fail("Junk after root hash"); + found_so_far[""] = ""; + for (i = 1; i != 49; i += 16) { + format_uuid(substr($0, i, 32)); + } + format_uuid(substr($0, 49, 16) substr($0, 1, 16)); +} diff --git a/scripts/format-uuid.sh b/scripts/format-uuid.sh deleted file mode 100755 index 3b38278aef640b2cd540d6606b05dd62018e48a6..0000000000000000000000000000000000000000 --- a/scripts/format-uuid.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -eu -# -# SPDX-FileCopyrightText: 2021-2022 Alyssa Ross <hi@alyssa.is> -# SPDX-FileCopyrightText: 2022 Unikie -# SPDX-License-Identifier: EUPL-1.2+ - -substr() { - str=$1 - beg=$2 - end=$3 - echo "$str" | cut -c "$beg-$end" -} - -u1=$(substr "$1" 1 8) -u2=$(substr "$1" 9 12) -u3=$(substr "$1" 13 16) -u4=$(substr "$1" 17 20) -u5=$(substr "$1" 21 32) -printf "%s\n" "$u1-$u2-$u3-$u4-$u5" -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
Generate 4 partition UUIDs instead of just 2. Port scripts/format-uuid.sh to awk to make this much easier.
Would have been nice to see the awk port first with unchanged behaviour, so it was then easier to see what behaviour was actually getting changed here.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Split into separate commit. --- host/initramfs/Makefile | 8 +++++--- host/rootfs/Makefile | 6 ++++-- release/live/Makefile | 8 +++++--- release/live/default.nix | 2 +- scripts/format-uuid.awk | 35 +++++++++++++++++++++++++++++++++++ scripts/format-uuid.sh | 19 ------------------- 6 files changed, 50 insertions(+), 28 deletions(-)
No blockers in here, but there are improvements I'd like to see made at some point, even if that's after this has been applied.
diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index fd8cbb6c3e775ed27d0a524bf167cb4d3940d799..27a26b46a8110d35ee02a63b12931d6b9c2742e5 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -35,10 +35,12 @@ build/mountpoints: cd build/mountpoints && mkdir -p $(MOUNTPOINTS) find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';'
-build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) +build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -euo pipefail -- $$uuids && \
Nice trick — I like it. There's no need to set -eo pipefail on Make commands that don't use ; or | though. If we did that consistently our Makefiles would be unreadable because it'd be hard to see past all the sets to the actual functionality, and seeing this here when it's not set on every command makes me wonder what's special about this one, which turns out to be nothing.
bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$3 \ + $(ROOT_FS):root:$$1 mv $@.tmp $@
build/loop.tar: build/live.img diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index fcfbc3e437fdb108252ba77d4d4e8f4f636ffd78..f02bb76371f000e3f65bb7c2a7f217d437845481 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -90,9 +90,11 @@ clean: .PHONY: clean
build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_DIR)/verity-timestamp $(ROOT_FS) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS)/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS)/rootfs:root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH)") + $(ROOT_FS_VERITY):verity:$$3 \ + $(ROOT_FS):root:$$1 mv $@.tmp $@
debug: diff --git a/release/live/Makefile b/release/live/Makefile index a79947c57d562677760bc669c66320953a2b0d2d..78361a48512a37514ba0e57e0cc8b0ec3a71664b 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -9,11 +9,13 @@ DTBS ?= build/empty
dest = build/live.img
-$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) +$(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$3 \ + $(ROOT_FS):root:$$1 mv $@.tmp $@
build/empty: diff --git a/release/live/default.nix b/release/live/default.nix index ac2d7a55fd4fe0c02108309ecea20e368000af0d..98cb4862e239e3ad9ddbd7b5ace5716f57df683b 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -29,7 +29,7 @@ stdenv.mkDerivation { fileset = lib.fileset.intersection src (lib.fileset.unions [ ./. ../../lib/common.mk - ../../scripts/format-uuid.sh + ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk ]); diff --git a/scripts/format-uuid.awk b/scripts/format-uuid.awk new file mode 100644 index 0000000000000000000000000000000000000000..a5349d68a4d29be5f750650236420c9b5a7257eb --- /dev/null +++ b/scripts/format-uuid.awk @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +function format_uuid(arg) { + if (arg in found_so_far) { + fail("Duplicate UUID, try changing the image (by even 1 bit)"); + }
This doesn't really feel like the right place for this check? If duplicate UUIDs are broken that should be detected when assembling the partition table.
+ found_so_far[arg] = 1; + print (substr(arg, 1, 8) "-" \ + substr(arg, 9, 4) "-" \ + substr(arg, 13, 4) "-" \ + substr(arg, 17, 4) "-" \ + substr(arg, 21, 12)); +} + +function fail(msg) { + print msg > "/dev/stderr"; + exit 1; +} + +BEGIN { + FS = ""; + RS = "\n";
RS is \n by default.
+ if ((getline) != 1)
No need for the inner parens.
+ fail("Empty input file"); + roothash = $0; + if (roothash !~ /^[a-f0-9]{64}$/) + fail("Invalid root hash"); + if (getline) + fail("Junk after root hash");
This will only ever be used by our build system. It'll be readily apparent if it's providing junk even without these validations, and without them the script would be shorter and easier to fit in my brain.
+ found_so_far[""] = "";
What does this do? Won't this just cause the script to falsely claim an empty string is a duplicate UUID if it ever ends up being passed to format_uuid()?
+ for (i = 1; i != 49; i += 16) { + format_uuid(substr($0, i, 32)); + }
Either unrolling the loop, or using a modulo inside the loop and getting rid of the format_uuid() call afterwards, would have helped me figure out what this does a lot quicker, but I see now that we're generating the two UUIDs we had before, and then two additional UUIDs offset into the string. I guess this is something to do with having multiple copies of the OS installed, but I wish I understood how the offset ones will be used without having to go hunting in later patches and come back here afterwards.
+ format_uuid(substr($0, 49, 16) substr($0, 1, 16)); +}
On 11/25/25 08:02, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
Generate 4 partition UUIDs instead of just 2. Port scripts/format-uuid.sh to awk to make this much easier.
Would have been nice to see the awk port first with unchanged behaviour, so it was then easier to see what behaviour was actually getting changed here.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Split into separate commit. --- host/initramfs/Makefile | 8 +++++--- host/rootfs/Makefile | 6 ++++-- release/live/Makefile | 8 +++++--- release/live/default.nix | 2 +- scripts/format-uuid.awk | 35 +++++++++++++++++++++++++++++++++++ scripts/format-uuid.sh | 19 ------------------- 6 files changed, 50 insertions(+), 28 deletions(-)
No blockers in here, but there are improvements I'd like to see made at some point, even if that's after this has been applied.
diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index fd8cbb6c3e775ed27d0a524bf167cb4d3940d799..27a26b46a8110d35ee02a63b12931d6b9c2742e5 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -35,10 +35,12 @@ build/mountpoints: cd build/mountpoints && mkdir -p $(MOUNTPOINTS) find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';'
-build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) +build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -euo pipefail -- $$uuids && \
Nice trick — I like it. There's no need to set -eo pipefail on Make commands that don't use ; or | though. If we did that consistently our Makefiles would be unreadable because it'd be hard to see past all the sets to the actual functionality, and seeing this here when it's not set on every command makes me wonder what's special about this one, which turns out to be nothing.
Will change in v5.
bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$3 \ + $(ROOT_FS):root:$$1 mv $@.tmp $@
build/loop.tar: build/live.img diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index fcfbc3e437fdb108252ba77d4d4e8f4f636ffd78..f02bb76371f000e3f65bb7c2a7f217d437845481 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -90,9 +90,11 @@ clean: .PHONY: clean
build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_DIR)/verity-timestamp $(ROOT_FS) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS)/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS)/rootfs:root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH)") + $(ROOT_FS_VERITY):verity:$$3 \ + $(ROOT_FS):root:$$1 mv $@.tmp $@
debug: diff --git a/release/live/Makefile b/release/live/Makefile index a79947c57d562677760bc669c66320953a2b0d2d..78361a48512a37514ba0e57e0cc8b0ec3a71664b 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -9,11 +9,13 @@ DTBS ?= build/empty
dest = build/live.img
-$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) +$(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$3 \ + $(ROOT_FS):root:$$1 mv $@.tmp $@
build/empty: diff --git a/release/live/default.nix b/release/live/default.nix index ac2d7a55fd4fe0c02108309ecea20e368000af0d..98cb4862e239e3ad9ddbd7b5ace5716f57df683b 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -29,7 +29,7 @@ stdenv.mkDerivation { fileset = lib.fileset.intersection src (lib.fileset.unions [ ./. ../../lib/common.mk - ../../scripts/format-uuid.sh + ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk ]); diff --git a/scripts/format-uuid.awk b/scripts/format-uuid.awk new file mode 100644 index 0000000000000000000000000000000000000000..a5349d68a4d29be5f750650236420c9b5a7257eb --- /dev/null +++ b/scripts/format-uuid.awk @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +function format_uuid(arg) { + if (arg in found_so_far) { + fail("Duplicate UUID, try changing the image (by even 1 bit)"); + }
This doesn't really feel like the right place for this check? If duplicate UUIDs are broken that should be detected when assembling the partition table.
Will drop in v5.
+ found_so_far[arg] = 1; + print (substr(arg, 1, 8) "-" \ + substr(arg, 9, 4) "-" \ + substr(arg, 13, 4) "-" \ + substr(arg, 17, 4) "-" \ + substr(arg, 21, 12)); +} + +function fail(msg) { + print msg > "/dev/stderr"; + exit 1; +} + +BEGIN { + FS = ""; + RS = "\n";
RS is \n by default.
Will fix in v5.
+ if ((getline) != 1)
No need for the inner parens.
Will fix in v5.
+ fail("Empty input file"); + roothash = $0; + if (roothash !~ /^[a-f0-9]{64}$/) + fail("Invalid root hash"); + if (getline) + fail("Junk after root hash");
This will only ever be used by our build system. It'll be readily apparent if it's providing junk even without these validations, and without them the script would be shorter and easier to fit in my brain.
Will fix in v5.
+ found_so_far[""] = "";
What does this do? Won't this just cause the script to falsely claim an empty string is a duplicate UUID if it ever ends up being passed to format_uuid()?
That can't happen :).
+ for (i = 1; i != 49; i += 16) { + format_uuid(substr($0, i, 32)); + }
Either unrolling the loop, or using a modulo inside the loop and getting rid of the format_uuid() call afterwards, would have helped me figure out what this does a lot quicker, but I see now that we're generating the two UUIDs we had before, and then two additional UUIDs offset into the string.
I did this because it is easy to implement.
I guess this is something to do with having multiple copies of the OS installed, but I wish I understood how the offset ones will be used without having to go hunting in later patches and come back here afterwards.
+ format_uuid(substr($0, 49, 16) substr($0, 1, 16)); +}
These GUIDs aren't booted ever. They are used for the blank backup partitions. They just need to be deterministic and not collide with anything a user might use. The verity hash was a convenient source for this. A fixed GUID would work too. -- Sincerely, Demi Marie Obenour (she/her/hers)
Avoids a pointless call to awk. No functional change intended. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Move into separate commit. --- scripts/make-gpt.sh | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh index 91c6038f67d4d4906fec4a3412f2ff5fca2671d5..ebb19c32a0d3bc3f86bf4748460971888d73e1fc 100644 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -28,13 +28,6 @@ fillPartition() { lseek -S 1 "$start" cat "$3" 1<>"$1" } -# Prints the partition path from a PATH:PARTTYPE[:PARTUUID[:PARTLABEL]] string. -partitionPath() { - awk -F: '{print $1}' <<EOF -$1 -EOF -} - scriptsDir="$(dirname "$0")" out="$1" @@ -49,8 +42,7 @@ for partition; do sizeMiB=${BASH_REMATCH[1]} partition=${partition%:*} else - partitionPath=$(partitionPath "$partition") - sizeMiB=$(sizeMiB "$partitionPath") + sizeMiB=$(sizeMiB "${partition%%:*}") fi table+=$'\n'"size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) @@ -64,7 +56,6 @@ EOF n=0 for partition; do - partitionPath=$(partitionPath "$partition") - fillPartition "$out" "$n" "$partitionPath" + fillPartition "$out" "$n" "${partition%%:*}" n=$((n + 1)) done -- 2.52.0
systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, the partitions need to be very large so that there is plenty of room for the OS to grow. Furthermore, systemd-sysupdate requires both A and B copies of both the root and verity partitions. mkfs.ext4 is not able to produce images with files large enough to hold both the primary and backup copy of the root partition [1]. Reducing the sizes of partitions to be little greater than the size of the root filesystem image does not help. The produced file is still too large. Therefore, compress the image, which causes it to be small enough that mkfs.ext4 can handle it. This breaks the option to use the installer as a live image. Therefore, remove it. This option will return once Spectrum switches to the GNOME OS installer [2]. However, it is still possible to build a live image that is separate from the installer. Document how to build and use it. GRUB2 does support compressed loopback images, but these presumably buffer the whole image in memory. Since the entire installer will be replaced, making it work is not considered worthwhile. [1]: https://github.com/tytso/e2fsprogs/issues/254 [2]: https://spectrum-os.org/lists/archives/spectrum-devel/87wm4dlkhz.fsf@alyssa.... Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v3: - Make the compression level configurable. The default is 1 so that development builds finish in a reasonable amount of time. Release builds should use compression level 9. Changes since v2: - Remove live image test instead of skipping it. - Document the change. - Document that there is still a live image available, though it is separate from the installer. - Document how to build the live image. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/installation/getting-spectrum.adoc | 31 +++++++++++++++++++----- host/initramfs/Makefile | 8 ------ host/initramfs/etc/probe | 20 --------------- lib/config.default.nix | 1 + release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ---------------------- release/combined/eosimages.nix | 13 +++++----- release/combined/grub.cfg.in | 5 ---- 8 files changed, 34 insertions(+), 75 deletions(-) diff --git a/Documentation/installation/getting-spectrum.adoc b/Documentation/installation/getting-spectrum.adoc index 29803aa324b196119a03b22d7f1e2d7730e2c1eb..e7806e0f92793320bf0cdcbdd11dbc4e713275c7 100644 --- a/Documentation/installation/getting-spectrum.adoc +++ b/Documentation/installation/getting-spectrum.adoc @@ -42,10 +42,30 @@ still take a very long time. == Installing Spectrum To install Spectrum on a computer, you can use a USB drive as a -bootable Spectrum installer device. When booting a system from the -installer device, you will be able to choose whether to try out -Spectrum without installing it on your system (as a live image), or to -install it to your computer's internal storage. +bootable Spectrum installer device. You will need to choose whether to +try out Spectrum without installing it on your system (as a live image), +or to install it to your computer's internal storage. + +=== Building A Live Image + +First, you need to build the Spectrum image: + +[source,shell] +---- +git clone https://spectrum-os.org/git/spectrum +nix-build spectrum/release/live +---- + +If you haven't set up the xref:binary-cache.adoc[binary cache], this +will take a very long time. When it's done, a symbolic link named +"result" will appear in the current directory, pointing to the +installer image. Write that image to a USB drive, for example using +`dd` (command line) or +https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available +in Nixpkgs). Boot your system from the USB drive, and Spectrum should +be ready for you to use. + +=== Building The Installer First, you need to build the Spectrum image: @@ -62,8 +82,7 @@ installer image. Write that image to a USB drive, for example using `dd` (command line) or https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available in Nixpkgs). Boot your system from the USB drive, and you should see -a menu allowing you to choose between "Try Spectrum" and "Install -Spectrum". +a menu allowing you to "Install Spectrum". NOTE: While it's possible to install Spectrum to your internal storage, at this point in Spectrum's development there is not much diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 27a26b46a8110d35ee02a63b12931d6b9c2742e5..735c12fc207f027db2b605309976a832b09335d6 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -43,14 +43,6 @@ build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../sc $(ROOT_FS):root:$$1 mv $@.tmp $@ -build/loop.tar: build/live.img - $(TAR) -cf $@ build/live.img - -build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 - bash ../../scripts/make-gpt.sh $@.tmp \ - build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e - mv $@.tmp $@ - clean: rm -rf build .PHONY: clean diff --git a/host/initramfs/etc/probe b/host/initramfs/etc/probe index 4cbd00db52c1a7128b5c619a43d415675feaee0b..013092b6dcc5b82db7302c1ae7e6d8a4f5a0b802 100755 --- a/host/initramfs/etc/probe +++ b/host/initramfs/etc/probe @@ -2,26 +2,6 @@ # SPDX-License-Identifier: EUPL-1.2+ # SPDX-FileCopyrightText: 2021 Alyssa Ross <hi@alyssa.is> -if -n { - # If this is a Spectrum installer eosimages partition, we might be - # booting from the installer, and should loopback mount the images. - importas -i mdev MDEV - if { - backtick -E type { lsblk -lnpo PARTTYPE $mdev } - test $type = 56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e - } - if { - forx -pE module { ext4 loop } - modprobe $module - } - backtick -E uuid { lsblk -lnpo PARTUUID $mdev } - if { mkdir -p /mnt/${uuid} } - if { mount $mdev /mnt/${uuid} } - find /mnt/${uuid} -name *.img -exec - losetup -Pf {} - ; -} - # Check whether we now have all the partitions we need to boot. importas -i rootfs_uuid ROOTFS_UUID diff --git a/lib/config.default.nix b/lib/config.default.nix index a8422345cc00f9413bb19ec968fd89c82fed801b..b1c4c33f1dca3d503fb8e3ef855d65c49cf5a202 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -4,4 +4,5 @@ { pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; + compressionLevel = 1; } diff --git a/release/checks/integration/meson.build b/release/checks/integration/meson.build index 7214e47ba1ec23c247c8b76e5c8d94aff1ce1fd6..7bf8f51e4c762d2279ed6064ae1a87cb9b07494c 100644 --- a/release/checks/integration/meson.build +++ b/release/checks/integration/meson.build @@ -11,7 +11,7 @@ run_qemu = find_program('../../../scripts/run-qemu.sh') lib = static_library('spectrum-integration-test', 'lib.c') -foreach test : ['appimage', 'late-serial', 'networking', 'portal', 'try'] +foreach test : ['appimage', 'late-serial', 'networking', 'portal'] test(test, executable(test, test + '.c', link_with : lib), timeout : 400, args : [run_qemu]) diff --git a/release/checks/integration/try.c b/release/checks/integration/try.c deleted file mode 100644 index 4b874c0a7e9b48324497450fb5488e04576fd43b..0000000000000000000000000000000000000000 --- a/release/checks/integration/try.c +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is> - -#include "lib.h" - -#include <stdlib.h> -#include <string.h> - -void test(struct config c) -{ - struct vm *vm; - - c.drives.img = getenv_or_die("COMBINED_PATH"); - - vm = start_qemu(c); - - start_console_thread(vm, "GNU GRUB "); - wait_for_prompt(vm); - - start_console_thread(vm, "~ # "); - - // Assume that Try Spectrum is the first menu entry. - if (fputc('\n', vm_console_writer(vm)) == EOF) { - fputs("error writing to console\n", stderr); - exit(EXIT_FAILURE); - } - - wait_for_prompt(vm); -} diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..9cb35dcecee54c17392b609c493272ec83062e9b 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> import ../../lib/call-package.nix ( -{ callSpectrumPackage, runCommand, e2fsprogs, tar2ext4 }: +{ callSpectrumPackage, runCommand, e2fsprogs, tar2ext4, config }: runCommand "eosimages.img" { nativeBuildInputs = [ e2fsprogs tar2ext4 ]; @@ -12,11 +12,12 @@ runCommand "eosimages.img" { unsafeDiscardReferences = { out = true; }; dontFixup = true; } '' + set -euo pipefail mkdir dir cd dir - ln -s $image $imageName - sha256sum $imageName > $imageName.sha256 - tar -chf $NIX_BUILD_TOP/eosimages.tar * - tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out - e2label $out eosimages + ln -s -- "$image" "$imageName" + gzip -${builtins.toString (0 + config.compressionLevel)} < "$image" > "$imageName.gz" + sha256sum -- "$imageName.gz" > "$imageName.gz.sha256" + tar -ch -- "$imageName.gz" "$imageName.gz.sha256" | tar2ext4 -o "$out" + e2label "$out" eosimages '') (_: {}) diff --git a/release/combined/grub.cfg.in b/release/combined/grub.cfg.in index a8e73a3b4dc0d643cf575e3cc545ec9ff72380cb..a22f5fc96ba6451d44c0f9768a15a1f48c5dce1c 100644 --- a/release/combined/grub.cfg.in +++ b/release/combined/grub.cfg.in @@ -15,11 +15,6 @@ set gfxpayload=keep terminal_output gfxterm terminal_output console -menuentry "Try Spectrum" { - loopback live (hd0,gpt3)/Spectrum-0.0-x86_64-generic.0.Live.img - chainloader (live,gpt1)/EFI/Linux/spectrum.efi -} - menuentry "Install Spectrum" { set root=(hd0,gpt2) linux @linux@ @kernelParams@ -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, the partitions need to be very large so that there is plenty of room for the OS to grow. Furthermore, systemd-sysupdate requires both A and B copies of both the root and verity partitions.
mkfs.ext4 is not able to produce images with files large enough to hold both the primary and backup copy of the root partition [1]. Reducing the sizes of partitions to be little greater than the size of the root filesystem image does not help. The produced file is still too large. Therefore, compress the image, which causes it to be small enough that mkfs.ext4 can handle it.
This breaks the option to use the installer as a live image. Therefore, remove it. This option will return once Spectrum switches to the GNOME OS installer [2]. However, it is still possible to build a live image that is separate from the installer. Document how to build and use it.
GRUB2 does support compressed loopback images, but these presumably buffer the whole image in memory. Since the entire installer will be replaced, making it work is not considered worthwhile.
[1]: https://github.com/tytso/e2fsprogs/issues/254 [2]: https://spectrum-os.org/lists/archives/spectrum-devel/87wm4dlkhz.fsf@alyssa....
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v3: - Make the compression level configurable. The default is 1 so that development builds finish in a reasonable amount of time. Release builds should use compression level 9. Changes since v2: - Remove live image test instead of skipping it. - Document the change. - Document that there is still a live image available, though it is separate from the installer. - Document how to build the live image.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/installation/getting-spectrum.adoc | 31 +++++++++++++++++++----- host/initramfs/Makefile | 8 ------ host/initramfs/etc/probe | 20 --------------- lib/config.default.nix | 1 + release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ---------------------- release/combined/eosimages.nix | 13 +++++----- release/combined/grub.cfg.in | 5 ---- 8 files changed, 34 insertions(+), 75 deletions(-)
Okay, I've left comments, but the only real blocker here is deciding whether we go ahead with the config option. I would rather not as outlined below.
diff --git a/Documentation/installation/getting-spectrum.adoc b/Documentation/installation/getting-spectrum.adoc index 29803aa324b196119a03b22d7f1e2d7730e2c1eb..e7806e0f92793320bf0cdcbdd11dbc4e713275c7 100644 --- a/Documentation/installation/getting-spectrum.adoc +++ b/Documentation/installation/getting-spectrum.adoc @@ -42,10 +42,30 @@ still take a very long time. == Installing Spectrum
To install Spectrum on a computer, you can use a USB drive as a -bootable Spectrum installer device. When booting a system from the -installer device, you will be able to choose whether to try out -Spectrum without installing it on your system (as a live image), or to -install it to your computer's internal storage. +bootable Spectrum installer device. You will need to choose whether to +try out Spectrum without installing it on your system (as a live image), +or to install it to your computer's internal storage. + +=== Building A Live Image + +First, you need to build the Spectrum image: + +[source,shell] +---- +git clone https://spectrum-os.org/git/spectrum +nix-build spectrum/release/live +---- + +If you haven't set up the xref:binary-cache.adoc[binary cache], this +will take a very long time. When it's done, a symbolic link named +"result" will appear in the current directory, pointing to the +installer image. Write that image to a USB drive, for example using +`dd` (command line) or +https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available +in Nixpkgs). Boot your system from the USB drive, and Spectrum should +be ready for you to use. +
This is duplicating a lot of existing text. Could we not explain the building stuff once, and then just say in the Live Image / Installer sections which paths to build?
+=== Building The Installer
First, you need to build the Spectrum image:
@@ -62,8 +82,7 @@ installer image. Write that image to a USB drive, for example using `dd` (command line) or https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available in Nixpkgs). Boot your system from the USB drive, and you should see -a menu allowing you to choose between "Try Spectrum" and "Install -Spectrum". +a menu allowing you to "Install Spectrum".
This should be updated to demonstrate release/installer rather than release/combined, since the latter is now pointless and due for removal.
diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 27a26b46a8110d35ee02a63b12931d6b9c2742e5..735c12fc207f027db2b605309976a832b09335d6 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -43,14 +43,6 @@ build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../sc $(ROOT_FS):root:$$1 mv $@.tmp $@
-build/loop.tar: build/live.img - $(TAR) -cf $@ build/live.img - -build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 - bash ../../scripts/make-gpt.sh $@.tmp \ - build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e - mv $@.tmp $@ - clean: rm -rf build .PHONY: clean
Oh, was this completely unused? I'll look into whether we can just drop this immediately.
diff --git a/lib/config.default.nix b/lib/config.default.nix index a8422345cc00f9413bb19ec968fd89c82fed801b..b1c4c33f1dca3d503fb8e3ef855d65c49cf5a202 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -4,4 +4,5 @@ { pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; + compressionLevel = 1; }
I don't love proliferating config parameters… Given this is likely going to be best set to 9 for any non-development image builds, and 1 will mostly only be desirable for people working on the image (and CI, I suppose, but it should really be testing the settings used on the real image), those people working on the image could adjust the gzip parameters when doing so. At the very least, 9 should be the default. Upstream-produced images should use the default settings so they're easy to reproduce (when they are actually reproducible.)
diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..9cb35dcecee54c17392b609c493272ec83062e9b 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is>
import ../../lib/call-package.nix ( -{ callSpectrumPackage, runCommand, e2fsprogs, tar2ext4 }: +{ callSpectrumPackage, runCommand, e2fsprogs, tar2ext4, config }:
runCommand "eosimages.img" { nativeBuildInputs = [ e2fsprogs tar2ext4 ]; @@ -12,11 +12,12 @@ runCommand "eosimages.img" { unsafeDiscardReferences = { out = true; }; dontFixup = true; } '' + set -euo pipefail mkdir dir cd dir - ln -s $image $imageName - sha256sum $imageName > $imageName.sha256 - tar -chf $NIX_BUILD_TOP/eosimages.tar * - tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out - e2label $out eosimages + ln -s -- "$image" "$imageName" + gzip -${builtins.toString (0 + config.compressionLevel)} < "$image" > "$imageName.gz" + sha256sum -- "$imageName.gz" > "$imageName.gz.sha256" + tar -ch -- "$imageName.gz" "$imageName.gz.sha256" | tar2ext4 -o "$out" + e2label "$out" eosimages '') (_: {})
I have made the same comments the last two times I have reviewed this code[1][2], and yet the set that applies options already set by stdenv is still here, and so are the unnecessary quoting changes getting in the way of seeing the actual changes. Please make sure comments are addressed before resubmitting the same thing again. https://spectrum-os.org/lists/archives/spectrum-devel/87v7jyj5a3.fsf@alyssa.... https://spectrum-os.org/lists/archives/spectrum-devel/87ikfdapmx.fsf@alyssa....
On 11/25/25 08:19, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, the partitions need to be very large so that there is plenty of room for the OS to grow. Furthermore, systemd-sysupdate requires both A and B copies of both the root and verity partitions.
mkfs.ext4 is not able to produce images with files large enough to hold both the primary and backup copy of the root partition [1]. Reducing the sizes of partitions to be little greater than the size of the root filesystem image does not help. The produced file is still too large. Therefore, compress the image, which causes it to be small enough that mkfs.ext4 can handle it.
This breaks the option to use the installer as a live image. Therefore, remove it. This option will return once Spectrum switches to the GNOME OS installer [2]. However, it is still possible to build a live image that is separate from the installer. Document how to build and use it.
GRUB2 does support compressed loopback images, but these presumably buffer the whole image in memory. Since the entire installer will be replaced, making it work is not considered worthwhile.
[1]: https://github.com/tytso/e2fsprogs/issues/254 [2]: https://spectrum-os.org/lists/archives/spectrum-devel/87wm4dlkhz.fsf@alyssa....
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v3: - Make the compression level configurable. The default is 1 so that development builds finish in a reasonable amount of time. Release builds should use compression level 9. Changes since v2: - Remove live image test instead of skipping it. - Document the change. - Document that there is still a live image available, though it is separate from the installer. - Document how to build the live image.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/installation/getting-spectrum.adoc | 31 +++++++++++++++++++----- host/initramfs/Makefile | 8 ------ host/initramfs/etc/probe | 20 --------------- lib/config.default.nix | 1 + release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ---------------------- release/combined/eosimages.nix | 13 +++++----- release/combined/grub.cfg.in | 5 ---- 8 files changed, 34 insertions(+), 75 deletions(-)
Okay, I've left comments, but the only real blocker here is deciding whether we go ahead with the config option. I would rather not as outlined below.
diff --git a/Documentation/installation/getting-spectrum.adoc b/Documentation/installation/getting-spectrum.adoc index 29803aa324b196119a03b22d7f1e2d7730e2c1eb..e7806e0f92793320bf0cdcbdd11dbc4e713275c7 100644 --- a/Documentation/installation/getting-spectrum.adoc +++ b/Documentation/installation/getting-spectrum.adoc @@ -42,10 +42,30 @@ still take a very long time. == Installing Spectrum
To install Spectrum on a computer, you can use a USB drive as a -bootable Spectrum installer device. When booting a system from the -installer device, you will be able to choose whether to try out -Spectrum without installing it on your system (as a live image), or to -install it to your computer's internal storage. +bootable Spectrum installer device. You will need to choose whether to +try out Spectrum without installing it on your system (as a live image), +or to install it to your computer's internal storage. + +=== Building A Live Image + +First, you need to build the Spectrum image: + +[source,shell] +---- +git clone https://spectrum-os.org/git/spectrum +nix-build spectrum/release/live +---- + +If you haven't set up the xref:binary-cache.adoc[binary cache], this +will take a very long time. When it's done, a symbolic link named +"result" will appear in the current directory, pointing to the +installer image. Write that image to a USB drive, for example using +`dd` (command line) or +https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available +in Nixpkgs). Boot your system from the USB drive, and Spectrum should +be ready for you to use. +
This is duplicating a lot of existing text. Could we not explain the building stuff once, and then just say in the Live Image / Installer sections which paths to build?
+=== Building The Installer
First, you need to build the Spectrum image:
@@ -62,8 +82,7 @@ installer image. Write that image to a USB drive, for example using `dd` (command line) or https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available in Nixpkgs). Boot your system from the USB drive, and you should see -a menu allowing you to choose between "Try Spectrum" and "Install -Spectrum". +a menu allowing you to "Install Spectrum".
This should be updated to demonstrate release/installer rather than release/combined, since the latter is now pointless and due for removal.
Unfortunately, release/installer is broken and is even in main.
diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 27a26b46a8110d35ee02a63b12931d6b9c2742e5..735c12fc207f027db2b605309976a832b09335d6 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -43,14 +43,6 @@ build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../sc $(ROOT_FS):root:$$1 mv $@.tmp $@
-build/loop.tar: build/live.img - $(TAR) -cf $@ build/live.img - -build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 - bash ../../scripts/make-gpt.sh $@.tmp \ - build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e - mv $@.tmp $@ - clean: rm -rf build .PHONY: clean
Oh, was this completely unused? I'll look into whether we can just drop this immediately.
Yes, it was.
diff --git a/lib/config.default.nix b/lib/config.default.nix index a8422345cc00f9413bb19ec968fd89c82fed801b..b1c4c33f1dca3d503fb8e3ef855d65c49cf5a202 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -4,4 +4,5 @@ { pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; + compressionLevel = 1; }
I don't love proliferating config parameters… Given this is likely going to be best set to 9 for any non-development image builds, and 1 will mostly only be desirable for people working on the image (and CI, I suppose, but it should really be testing the settings used on the real image), those people working on the image could adjust the gzip parameters when doing so.
At the very least, 9 should be the default. Upstream-produced images should use the default settings so they're easy to reproduce (when they are actually reproducible.)
I'd prefer to have a config option so that I don't have to maintain downstream patches when making development builds. I'm fine with making this a boolean, though. I'll remove it for now to not block committing.
diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..9cb35dcecee54c17392b609c493272ec83062e9b 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is>
import ../../lib/call-package.nix ( -{ callSpectrumPackage, runCommand, e2fsprogs, tar2ext4 }: +{ callSpectrumPackage, runCommand, e2fsprogs, tar2ext4, config }:
runCommand "eosimages.img" { nativeBuildInputs = [ e2fsprogs tar2ext4 ]; @@ -12,11 +12,12 @@ runCommand "eosimages.img" { unsafeDiscardReferences = { out = true; }; dontFixup = true; } '' + set -euo pipefail mkdir dir cd dir - ln -s $image $imageName - sha256sum $imageName > $imageName.sha256 - tar -chf $NIX_BUILD_TOP/eosimages.tar * - tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out - e2label $out eosimages + ln -s -- "$image" "$imageName" + gzip -${builtins.toString (0 + config.compressionLevel)} < "$image" > "$imageName.gz" + sha256sum -- "$imageName.gz" > "$imageName.gz.sha256" + tar -ch -- "$imageName.gz" "$imageName.gz.sha256" | tar2ext4 -o "$out" + e2label "$out" eosimages '') (_: {})
I have made the same comments the last two times I have reviewed this code[1][2], and yet the set that applies options already set by stdenv is still here, and so are the unnecessary quoting changes getting in the way of seeing the actual changes. Please make sure comments are addressed before resubmitting the same thing again.
https://spectrum-os.org/lists/archives/spectrum-devel/87v7jyj5a3.fsf@alyssa.... https://spectrum-os.org/lists/archives/spectrum-devel/87ikfdapmx.fsf@alyssa....
I'll try to do better in the future. -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/25/25 08:19, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
+=== Building The Installer
First, you need to build the Spectrum image:
@@ -62,8 +82,7 @@ installer image. Write that image to a USB drive, for example using `dd` (command line) or https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available in Nixpkgs). Boot your system from the USB drive, and you should see -a menu allowing you to choose between "Try Spectrum" and "Install -Spectrum". +a menu allowing you to "Install Spectrum".
This should be updated to demonstrate release/installer rather than release/combined, since the latter is now pointless and due for removal.
Unfortunately, release/installer is broken and is even in main.
Can you tell me how to reproduce? With $(nix-build release/installer/run-vm.nix) I can run through the installer just fine…
On 11/28/25 06:09, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/25/25 08:19, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
+=== Building The Installer
First, you need to build the Spectrum image:
@@ -62,8 +82,7 @@ installer image. Write that image to a USB drive, for example using `dd` (command line) or https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available in Nixpkgs). Boot your system from the USB drive, and you should see -a menu allowing you to choose between "Try Spectrum" and "Install -Spectrum". +a menu allowing you to "Install Spectrum".
This should be updated to demonstrate release/installer rather than release/combined, since the latter is now pointless and due for removal.
Unfortunately, release/installer is broken and is even in main.
Can you tell me how to reproduce? With $(nix-build release/installer/run-vm.nix) I can run through the installer just fine…
I was probably using the wrong Nix file. -- Sincerely, Demi Marie Obenour (she/her/hers)
systemd-sysupdate has strict requirements on the partition layout: - The label of the active partition must match the template in the .transfer file. For instance, the root filesystem of Spectrum 0.0.0 must be in a partition with label "Spectrum_0.0.0", and the verity partition must have the label "Spectrum_0.0.0.verity". - The label of the inactive partition must be that of the old version of Spectrum, or "_empty" for freshly installed systems. - The partition type UUID must conform to the Discoverable Partition Specification. Also, the UKI must have a name that includes the OS version. Otherwise, it will not be deleted during updates. Since the partition label includes the OS version, add an OS version number. Use 0.0.0 to indicate that Spectrum OS is still in very early development and should not be used. The version number can be overridden in the build configuration file. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Split off into separate commit. --- host/efi.nix | 2 +- host/initramfs/Makefile | 4 ++-- host/initramfs/shell.nix | 2 ++ host/rootfs/Makefile | 4 ++-- host/rootfs/shell.nix | 2 ++ lib/config.default.nix | 1 + release/live/Makefile | 6 +++--- release/live/default.nix | 3 +++ 8 files changed, 16 insertions(+), 8 deletions(-) diff --git a/host/efi.nix b/host/efi.nix index d0ce260bd908c186059b75a1b4f42258b0e62bff..ecedb6bea6bf29c7a7303dc9062fe12b5c7a9fbd 100644 --- a/host/efi.nix +++ b/host/efi.nix @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> import ../lib/call-package.nix ( -{ callSpectrumPackage, config, cryptsetup, rootfs +{ callSpectrumPackage, cryptsetup, rootfs , runCommand, stdenv, systemdUkify }: let diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 735c12fc207f027db2b605309976a832b09335d6..db33ff86e9cd994efd4ce50acdf881d69ba79299 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -39,8 +39,8 @@ build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../sc uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$3 \ - $(ROOT_FS):root:$$1 + $(ROOT_FS_VERITY):verity:$$3:Spectrum_'$(VERSION).verity' \ + $(ROOT_FS):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ clean: diff --git a/host/initramfs/shell.nix b/host/initramfs/shell.nix index ff067354881b480656fae9b339a0a9068475d85f..36e3956a2ebc80fd273da226af253ebe8f7f7b24 100644 --- a/host/initramfs/shell.nix +++ b/host/initramfs/shell.nix @@ -4,6 +4,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, rootfs, pkgsStatic, stdenv , cryptsetup, jq, qemu_kvm, tar2ext4, util-linux +, config }: let @@ -18,5 +19,6 @@ initramfs.overrideAttrs ({ nativeBuildInputs ? [], env ? {}, ... }: { env = env // { KERNEL = "${rootfs.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; ROOT_FS_DIR = rootfs; + VERSION = config.version; }; })) (_: {}) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index f02bb76371f000e3f65bb7c2a7f217d437845481..d64bce115cc6c306956121b4bcd7271331ba1b7e 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -93,8 +93,8 @@ build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scr uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$3 \ - $(ROOT_FS):root:$$1 + $(ROOT_FS_VERITY):verity:$$3:Spectrum_'$(VERSION).verity' \ + $(ROOT_FS):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ debug: diff --git a/host/rootfs/shell.nix b/host/rootfs/shell.nix index 6df2f575fdfc7cdf8067ccfdb5fecaad9f6ea5e6..27f93e05fce036257d27cf9992fee8c925073f80 100644 --- a/host/rootfs/shell.nix +++ b/host/rootfs/shell.nix @@ -5,6 +5,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, rootfs, pkgsStatic, srcOnly, stdenv , btrfs-progs, cryptsetup, jq, netcat, qemu_kvm, reuse, util-linux +, config }: rootfs.overrideAttrs ( @@ -20,5 +21,6 @@ rootfs.overrideAttrs ( KERNEL = "${passthru.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; LINUX_SRC = srcOnly passthru.kernel.configfile; VMLINUX = "${passthru.kernel.dev}/vmlinux"; + VERSION = config.version; }; })) (_: {}) diff --git a/lib/config.default.nix b/lib/config.default.nix index b1c4c33f1dca3d503fb8e3ef855d65c49cf5a202..4ea2fead303d9a064fc0ce3a188423d6dbcc6c08 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -5,4 +5,5 @@ pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; compressionLevel = 1; + version = "0.0.0"; } diff --git a/release/live/Makefile b/release/live/Makefile index 78361a48512a37514ba0e57e0cc8b0ec3a71664b..a85edfde2d186716656ed23fe719ca63b31bcd59 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -14,8 +14,8 @@ $(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/s set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$3 \ - $(ROOT_FS):root:$$1 + $(ROOT_FS_VERITY):verity:$$3:Spectrum_'$(VERSION).verity' \ + $(ROOT_FS):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ build/empty: @@ -25,7 +25,7 @@ build/boot.fat: $(SYSTEMD_BOOT_EFI) $(EFI_IMAGE) build/empty $(TRUNCATE) -s 440401920 $@ $(MKFS_FAT) $@ $(MMD) -i $@ ::/EFI ::/EFI/BOOT ::/EFI/Linux - $(MCOPY) -i $@ $(EFI_IMAGE) ::/EFI/Linux/spectrum.efi + $(MCOPY) -i $@ $(EFI_IMAGE) '::/EFI/Linux/Spectrum_$(VERSION).efi' $(MCOPY) -i $@ $(SYSTEMD_BOOT_EFI) ::/EFI/BOOT/$(EFINAME) clean: diff --git a/release/live/default.nix b/release/live/default.nix index 98cb4862e239e3ad9ddbd7b5ace5716f57df683b..22e5a00de08ed858522a00f00359890ef52f03e0 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -1,11 +1,13 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2021-2023, 2025 Alyssa Ross <hi@alyssa.is> # SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src , lib, pkgsStatic, stdenvNoCC , cryptsetup, dosfstools, jq, mtools, util-linux +, config }: let @@ -46,6 +48,7 @@ stdenv.mkDerivation { SYSTEMD_BOOT_EFI = "${systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; EFI_IMAGE = efi; EFINAME = "BOOT${toUpper efiArch}.EFI"; + VERSION = config.version; }; buildFlags = [ "dest=$(out)" ]; -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
systemd-sysupdate has strict requirements on the partition layout:
- The label of the active partition must match the template in the .transfer file. For instance, the root filesystem of Spectrum 0.0.0 must be in a partition with label "Spectrum_0.0.0", and the verity partition must have the label "Spectrum_0.0.0.verity".
- The label of the inactive partition must be that of the old version of Spectrum, or "_empty" for freshly installed systems.
- The partition type UUID must conform to the Discoverable Partition Specification.
Also, the UKI must have a name that includes the OS version. Otherwise, it will not be deleted during updates.
Since the partition label includes the OS version, add an OS version number. Use 0.0.0 to indicate that Spectrum OS is still in very early development and should not be used. The version number can be overridden in the build configuration file.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Split off into separate commit. --- host/efi.nix | 2 +- host/initramfs/Makefile | 4 ++-- host/initramfs/shell.nix | 2 ++ host/rootfs/Makefile | 4 ++-- host/rootfs/shell.nix | 2 ++ lib/config.default.nix | 1 + release/live/Makefile | 6 +++--- release/live/default.nix | 3 +++ 8 files changed, 16 insertions(+), 8 deletions(-)
Reviewed-by: Alyssa Ross <hi@alyssa.is>
systemd-sysupdate never writes to the running OS partition. Instead, it requires a separate partition to write the update into. Create a separate partition for that purpose. systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, make the partitions very large so that there is plenty of room for the OS to grow. This requires rewriting the code that calculates the partition sizes. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Make into a standalone commit - Do not rely on separate script to generate the images. - Use a smaller size for the verity partition. --- release/live/Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/release/live/Makefile b/release/live/Makefile index a85edfde2d186716656ed23fe719ca63b31bcd59..cf2ace4f5e4ba20a2c0ce9803f72acf0e23f9df3 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -10,12 +10,15 @@ DTBS ?= build/empty dest = build/live.img $(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + # 162MiB was calculated by running `veritysetup format` on 20GiB from /dev/urandom uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$3:Spectrum_'$(VERSION).verity' \ - $(ROOT_FS):root:$$1:Spectrum_'$(VERSION)' + $(ROOT_FS_VERITY):verity:$$3:Spectrum_'$(VERSION).verity:162MiB' \ + $(ROOT_FS):root:$$1:Spectrum_'$(VERSION):20000MiB' \ + $(ROOT_FS_VERITY):verity:$$4:_empty:162MiB \ + $(ROOT_FS):root:$$2:_empty:20000MiB mv $@.tmp $@ build/empty: -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
systemd-sysupdate never writes to the running OS partition. Instead, it requires a separate partition to write the update into. Create a separate partition for that purpose.
systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, make the partitions very large so that there is plenty of room for the OS to grow. This requires rewriting the code that calculates the partition sizes.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Make into a standalone commit - Do not rely on separate script to generate the images. - Use a smaller size for the verity partition. --- release/live/Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/release/live/Makefile b/release/live/Makefile index a85edfde2d186716656ed23fe719ca63b31bcd59..cf2ace4f5e4ba20a2c0ce9803f72acf0e23f9df3 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -10,12 +10,15 @@ DTBS ?= build/empty dest = build/live.img
$(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + # 162MiB was calculated by running `veritysetup format` on 20GiB from /dev/urandom uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -euo pipefail -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$3:Spectrum_'$(VERSION).verity' \ - $(ROOT_FS):root:$$1:Spectrum_'$(VERSION)' + $(ROOT_FS_VERITY):verity:$$3:Spectrum_'$(VERSION).verity:162MiB' \ + $(ROOT_FS):root:$$1:Spectrum_'$(VERSION):20000MiB' \ + $(ROOT_FS_VERITY):verity:$$4:_empty:162MiB \ + $(ROOT_FS):root:$$2:_empty:20000MiB
Why are we filling the B partitions rather than leaving them empty?
mv $@.tmp $@
build/empty:
-- 2.52.0
Whenever a release is made, create a directory with the release files to be used for an update. After its SHA256SSUMS file is signed, the file is ready to be uploaded to a server for users to update from. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Use UUIDs to name the rootfs and verity superblock. This will allow systemd-sysupdate to set the correct UUIDs on the rootfs and verity partitions, avoiding the need to use labels to find these partitions. --- release.nix | 2 ++ release/update.nix | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/release.nix b/release.nix index a4fe66ee5925aeee3a1f5f1fac249c595cee0885..704abb39a3d01152eac3dfe313066834c3cd0a66 100644 --- a/release.nix +++ b/release.nix @@ -8,5 +8,7 @@ import lib/call-package.nix ({ callSpectrumPackage }: { checks = callSpectrumPackage release/checks {}; + updates = callSpectrumPackage release/update.nix {}; + combined = callSpectrumPackage release/combined/run-vm.nix {}; }) (_: {}) diff --git a/release/update.nix b/release/update.nix new file mode 100644 index 0000000000000000000000000000000000000000..77eb5fc422baa7d13e8e3ccb823c2fe69d2c39cc --- /dev/null +++ b/release/update.nix @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../lib/call-package.nix ( +{ callSpectrumPackage, config, runCommand, stdenv }: + +let + efi = import ../host/efi.nix {}; +in +runCommand "spectrum-update-directory" { + __structuredAttrs = true; + unsafeDiscardReferences = { out = true; }; + dontFixup = true; + env = { VERSION = config.version; }; +} '' + # One would expect that this is enabled already but it is not. + set -euo pipefail + mkdir -- "$out" + cd -- "$out" + read -r roothash < ${efi.rootfs}/rootfs.verity.roothash + if ! [[ "$roothash" =~ ^[0-9a-f]{64}$ ]]; then + printf 'Internal error: bad root hash %q\n' "$roothash" + exit 1 + fi + cp -- ${efi} "Spectrum_$VERSION.efi" + cp -- ${efi.rootfs}/rootfs.verity.superblock "Spectrum_''${VERSION}_''${roothash:32:32}.verity" + cp -- ${efi.rootfs}/rootfs "Spectrum_''${VERSION}_''${roothash:0:32}.root" + sha256sum -b "Spectrum_$VERSION.efi" \ + "Spectrum_''${VERSION}_''${roothash:32:32}.verity" \ + "Spectrum_''${VERSION}_''${roothash:0:32}.root" > SHA256SUMS + '' +) (_: {}) -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
Whenever a release is made, create a directory with the release files to be used for an update. After its SHA256SSUMS file is signed, the file is ready to be uploaded to a server for users to update from.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Use UUIDs to name the rootfs and verity superblock. This will allow systemd-sysupdate to set the correct UUIDs on the rootfs and verity partitions, avoiding the need to use labels to find these partitions. --- release.nix | 2 ++ release/update.nix | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+)
Reviewed-by: Alyssa Ross <hi@alyssa.is> But I think we should be clearer about set, see below.
diff --git a/release.nix b/release.nix index a4fe66ee5925aeee3a1f5f1fac249c595cee0885..704abb39a3d01152eac3dfe313066834c3cd0a66 100644 --- a/release.nix +++ b/release.nix @@ -8,5 +8,7 @@ import lib/call-package.nix ({ callSpectrumPackage }: {
checks = callSpectrumPackage release/checks {};
+ updates = callSpectrumPackage release/update.nix {}; + combined = callSpectrumPackage release/combined/run-vm.nix {}; }) (_: {}) diff --git a/release/update.nix b/release/update.nix new file mode 100644 index 0000000000000000000000000000000000000000..77eb5fc422baa7d13e8e3ccb823c2fe69d2c39cc --- /dev/null +++ b/release/update.nix @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../lib/call-package.nix ( +{ callSpectrumPackage, config, runCommand, stdenv }: + +let + efi = import ../host/efi.nix {}; +in +runCommand "spectrum-update-directory" { + __structuredAttrs = true; + unsafeDiscardReferences = { out = true; }; + dontFixup = true; + env = { VERSION = config.version; }; +} '' + # One would expect that this is enabled already but it is not. + set -euo pipefail
I see. stdenv leaves -eo pipefail set, but not -u. In that case, we should just set -u so it's clearer what change is actually intended to be made to the environment.
Include a new `spectrum-update` command to update the system. This tells the new sys.appvm-systemd-sysupdate VM to download the updates into a staging directory using systemd-sysupdate. The host then runs systemd-sysupdate to apply the updates itself. sys.appvm-systemd-sysupdate uses host-provided information to fetch the update. This allows editing files on the host to change the update URL and signing key. Updates require /boot to be mounted so that systemd-sysupdate can update the unified kernel image. They also require that /tmp is writable so that they can store temporary files, so put a tmpfs there. Furthermore, there needs to be a directory for storing downloaded updates. Create /home so that users can mount their persistent data there. The directory the VM downloads updates into is *not* reset (wiped) before or after the update. This allows the VM to know if the system is already up to date. Otherwise, it would redownload the entire multi-gigabyte update image. Updates are currently not compressed. This should be changed in the future, but it would add a small amount of additional complexity. In particular, the script generating the update directory would need to generate a SHA256SUMS containing the hash of both the compressed and uncompressed versions. More importantly, the VM must not be able to make the host use the compressed version. This would be a potential security risk because decompression happens before signature verification. GnuPG currently decompresses signatures, but in the future it will be replaced by Sequoia which does not. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v3: - Move builtins.path from lib/config.nix to host/rootfs/default.nix. - Change config options from "update-url" to "updateUrl" and "update-signing-key" to "updateSigningKey". Changes since v2: - Generate the transfer files in the guest, not the host. - Do not use an environment file. - Reject URLs that cannot work. - Escape sed metacharacters. - Escape backslashes for systemd-sysupdate. - Do not validate update URLs at build time, only at runtime. - Reject only update URLs that cannot possibly work. - Set the partition UUIDs according to systemd's recommendation. - Do not rely on finding partitions by label. - Strip leading and trailing whitespace from the update URL. - Rename the update command from `update` to `spectrum-update`. - Delete the list of steps. Replace it with comments in the script. The awk script in the VM rejects URLs that contain whitespace. This is because they can't work, and passing them to systemd-sysupdate would require figuring out how to escape them. Rejecting such bogus URLs is simpler than preventing them from being mangled. --- host/rootfs/Makefile | 19 ++++- host/rootfs/default.nix | 17 +++-- host/rootfs/file-list.mk | 7 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 ++++++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 ++++++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 ++++++ .../image/etc/vm-sysupdate.d/50-verity.transfer | 18 +++++ .../image/etc/vm-sysupdate.d/60-root.transfer | 18 +++++ .../image/etc/vm-sysupdate.d/70-kernel.transfer | 18 +++++ host/rootfs/image/usr/bin/spectrum-update | 83 ++++++++++++++++++++++ host/rootfs/os-release.in | 15 ++++ lib/config.default.nix | 2 + lib/config.nix | 3 +- lib/fake-update-signing-key.gpg | 3 + release/live/shell.nix | 3 +- vm/app/systemd-sysupdate/default.nix | 57 +++++++++++++++ vm/app/systemd-sysupdate/escape-url.awk | 31 ++++++++ .../systemd-sysupdate/populate-transfer-directory | 26 +++++++ 19 files changed, 372 insertions(+), 9 deletions(-) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index d64bce115cc6c306956121b4bcd7271331ba1b7e..1abb18dbf84077af3dbd527a01c02f38c4608e58 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -9,6 +9,7 @@ include file-list.mk ROOT_FS_DIR = build DIRS = \ + boot \ dev \ etc/s6-linux-init/env \ etc/s6-linux-init/run-image/configs \ @@ -32,13 +33,15 @@ DIRS = \ etc/s6-linux-init/run-image/vm/by-id \ etc/s6-linux-init/run-image/vm/by-name \ ext \ + home \ proc \ run \ - sys + sys \ + tmp FIFOS = etc/s6-linux-init/run-image/service/s6-svscan-log/fifo -BUILD_FILES = build/etc/s6-rc +BUILD_FILES = build/etc/s6-rc build/etc/os-release build/etc/update-url # This rule produces three files but Make only (portably) # supports one output per rule. Instead of resorting to temporary @@ -56,12 +59,22 @@ $(ROOT_FS): ../../scripts/make-erofs.sh $(PACKAGES_FILE) $(FILES) $(BUILD_FILES) mkdir -p $(ROOT_FS_DIR) && \ { \ cat $(PACKAGES_FILE) ;\ + printf '%s\n%s\n' "$$UPDATE_SIGNING_KEY" /etc/systemd/import-pubring.gpg; \ for file in $(FILES) $(LINKS); do printf '%s\n%s\n' $$file "$${file#image/}"; done ;\ - for file in $(BUILD_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ + for file in $(BUILD_FILES) $(BUILD_NON_TARGET_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ printf 'build/empty\n%s\n' $(DIRS) ;\ printf 'build/fifo\n%s\n' $(FIFOS) ;\ } | ../../scripts/make-erofs.sh $(ROOT_FS) +build/etc/update-url: + mkdir -p build/etc + # might have metacharacters, so avoid interpolation + printf %s\\n "$${UPDATE_URL:?'update URL empty or missing'}" > build/etc/update-url + +build/etc/os-release: + mkdir -p build/etc + sed 's/@VERSION@/$(VERSION)/g' < os-release.in > build/etc/os-release + build/fifo: mkdir -p build mkfifo -m 0600 $@ diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index cd61c78b1f1668e7bc9c84c638ff6e7d8b6de140..1ebaf11cd7e9d61444b6524de6053a0f3cfb82c8 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -5,6 +5,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src , pkgsMusl, pkgsStatic, linux_latest +, config }: pkgsStatic.callPackage ( @@ -13,6 +14,7 @@ pkgsStatic.callPackage ( , busybox, cloud-hypervisor, cryptsetup, dbus, execline, inkscape , iproute2, inotify-tools, jq, mdevd, s6, s6-linux-init, socat , util-linuxMinimal, virtiofsd, xorg, xdg-desktop-portal-spectrum-host +, btrfs-progs }: let @@ -33,8 +35,8 @@ let foot = pkgsGui.foot.override { allowPgo = false; }; packages = [ - cloud-hypervisor cryptsetup dbus execline inotify-tools iproute2 - jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools + btrfs-progs cloud-hypervisor cryptsetup dbus execline inotify-tools + iproute2 jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools util-linuxMinimal virtiofsd xdg-desktop-portal-spectrum-host (busybox.override { @@ -80,11 +82,13 @@ let appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; appvm-foot = callSpectrumPackage ../../vm/app/foot.nix {}; appvm-gnome-text-editor = callSpectrumPackage ../../vm/app/gnome-text-editor.nix {}; + appvm-systemd-sysupdate = callSpectrumPackage ../../vm/app/systemd-sysupdate {}; }; packagesSysroot = runCommand "packages-sysroot" { depsBuildBuild = [ inkscape ]; nativeBuildInputs = [ xorg.lndir ]; + src = builtins.path { name = "os-release"; path = ./os-release.in; }; } '' mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps @@ -96,8 +100,7 @@ let done # If systemd-pull is missing systemd-sysupdate will fail with a - # very confusing error message. If systemd-sysupdate doesn't work, - # users will not be able to receive an update that fixes the problem. + # very confusing error message. for i in sysupdate pull; do if ! cat -- "$out/usr/lib/systemd/systemd-$i" > /dev/null; then echo "link to systemd-$i didn't get installed" >&2 @@ -145,6 +148,12 @@ stdenvNoCC.mkDerivation { printf "%s\n/\n" ${packagesSysroot} >$out sed p ${writeClosure [ packagesSysroot] } >>$out ''; + UPDATE_SIGNING_KEY = builtins.path { + name = "signing-key"; + path = config.updateSigningKey; + }; + UPDATE_URL = config.updateUrl; + VERSION = config.version; }; # The Makefile uses $(ROOT_FS_DIR), not $(dest), so it can share code diff --git a/host/rootfs/file-list.mk b/host/rootfs/file-list.mk index ff6fd1b1aeb10584ba2da85a72ce7ea12b069f5d..cf1ceb2b6bd9bb70fca5190846757c1a9890ef94 100644 --- a/host/rootfs/file-list.mk +++ b/host/rootfs/file-list.mk @@ -38,13 +38,20 @@ FILES = \ image/etc/s6-linux-init/run-image/service/vmm/run \ image/etc/s6-linux-init/run-image/service/vmm/template/notification-fd \ image/etc/s6-linux-init/scripts/rc.init \ + image/etc/sysupdate.d/50-verity.transfer \ + image/etc/sysupdate.d/60-root.transfer \ + image/etc/sysupdate.d/70-kernel.transfer \ image/etc/udev/rules.d/99-spectrum.rules \ + image/etc/vm-sysupdate.d/50-verity.transfer \ + image/etc/vm-sysupdate.d/60-root.transfer \ + image/etc/vm-sysupdate.d/70-kernel.transfer \ image/etc/xdg/weston/autolaunch \ image/etc/xdg/weston/weston.ini \ image/usr/bin/assign-devices \ image/usr/bin/create-vm-dependencies \ image/usr/bin/run-appimage \ image/usr/bin/run-vmm \ + image/usr/bin/spectrum-update \ image/usr/bin/vm-console \ image/usr/bin/vm-import \ image/usr/bin/vm-start \ diff --git a/host/rootfs/image/etc/fstab b/host/rootfs/image/etc/fstab index 6a82ecc85090a37b13603b29f74ca6e554a28c33..36e073c58b91e81d63ae40a6aa6f019a2f03e546 100644 --- a/host/rootfs/image/etc/fstab +++ b/host/rootfs/image/etc/fstab @@ -4,3 +4,4 @@ proc /proc proc defaults 0 0 devpts /dev/pts devpts defaults,gid=4,mode=620 0 0 tmpfs /dev/shm tmpfs defaults 0 0 sysfs /sys sysfs defaults 0 0 +tmpfs /tmp tmpfs defaults 0 0 diff --git a/host/rootfs/image/etc/sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..9cd64b58ae55d55d378d99f5701f1ecef867e436 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v_@u.verity + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_@v.verity +MatchPartitionType=root-verity +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/60-root.transfer b/host/rootfs/image/etc/sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..cd12d2bd2b4ecd9bb5c7d26cc7c27a4bdb74cac8 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/60-root.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v_@u.root + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_@v +MatchPartitionType=root +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..e4190587a6bb127cb7315f38d59e48cf279318a4 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/EFI/Linux +PathRelativeTo=boot +MatchPattern=Spectrum_@v.efi +Mode=0644 +InstancesMax=2 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..ab4997c83605e3820a22b0b2178dcd76dfcf787e --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v_@u.verity + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v_@u.verity +Mode=0644 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer b/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..8a3175684f1697e0eca443eb0a1a97176e4f66d4 --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v_@u.root + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v_@u.root +Mode=0644 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..cb181239d71c5a6d0a5b3652d5534a23eda64183 --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v.efi +Mode=0644 diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update new file mode 100755 index 0000000000000000000000000000000000000000..ad598b557ac1cc4e9b95ff65a53a68f04d3759ee --- /dev/null +++ b/host/rootfs/image/usr/bin/spectrum-update @@ -0,0 +1,83 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { mkdir -p -m 0700 /run/updater } + +# Take a global lock to avoid races. +s6-setlock /run/update-lock + +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Delete any stale temporary files. Delete any existing signature + # files. If the VM is still running (it should not be), the VM might + # have write access to the directory. However, updates-dir-check is + # safe against that. + if { updates-dir-check cleanup shared } + + if { + # rm -f ensures that "snapshot" does not exist afterwards. + ifte { exit 0 } { rm -f snapshot } + # TODO: suppress only "subvolume does not exist" errors. + redirfd -w 2 /dev/null btrfs subvolume delete snapshot + } + + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } + basename -- $id_path + } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. + + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + if { + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } + umask 022 + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd + } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} } + + # Remove the bind mount. + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Validate the update directory. Delete any stale temporary files. + # Check that a signature file was downloaded. + if { updates-dir-check check snapshot } + + # Perform the update in a separate mount namespace. + unshare --mount + if { mount --bind -o ro -- snapshot /run/updater } + + /usr/lib/systemd/systemd-sysupdate update +} +importas -i sysupdate_exit_status ? +# Clean up. +foreground { btrfs subvolume delete -- snapshot } +exit $sysupdate_exit_status diff --git a/host/rootfs/os-release.in b/host/rootfs/os-release.in new file mode 100644 index 0000000000000000000000000000000000000000..d6e699e82f87dcb1c4656ac19d4e9986282f14a5 --- /dev/null +++ b/host/rootfs/os-release.in @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +NAME="Spectrum OS" +ID=spectrum +PRETTY_NAME="Spectrum @VERSION@" +VERSION=@VERSION@ +VERSION_ID=@VERSION@ +IMAGE_ID=spectrum-root +IMAGE_VERSION=@VERSION@ +RELEASE_TYPE=development +HOME_URL="https://spectrum-os.org" +BUG_REPORT_URL="mailto:discuss@spectrum-os.org" +ANSI_COLOR="1;34" +VENDOR_NAME=Spectrum +VENDOR_URL="https://spectrum-os.org" diff --git a/lib/config.default.nix b/lib/config.default.nix index 4ea2fead303d9a064fc0ce3a188423d6dbcc6c08..99679291038b48c3d62e5514d0e820a3f858aceb 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -6,4 +6,6 @@ pkgsArgs = {}; compressionLevel = 1; version = "0.0.0"; + updateUrl = "https://your-spectrum-os-update-server.invalid/download-directory"; + updateSigningKey = ./fake-update-signing-key.gpg; } diff --git a/lib/config.nix b/lib/config.nix index e437cdbe9aa22dd0f9c8d7052ac331c8fccf6ce6..bc5b42f506b7bfd2f66db48610491809351d1a2c 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -1,5 +1,6 @@ -# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> # SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> let customConfigPath = builtins.tryEval <spectrum-config>; diff --git a/lib/fake-update-signing-key.gpg b/lib/fake-update-signing-key.gpg new file mode 100644 index 0000000000000000000000000000000000000000..12e18f4c7c740e31692e1f1975282fa72ac1f2e3 --- /dev/null +++ b/lib/fake-update-signing-key.gpg @@ -0,0 +1,3 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +NOT A VALID KEY - UPDATES WILL NOT WORK diff --git a/release/live/shell.nix b/release/live/shell.nix index ffaa9a571c662810348822a5952d479d251a25e5..b263eacc4d0324191e3c7737dd90d304e477e79b 100644 --- a/release/live/shell.nix +++ b/release/live/shell.nix @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> import ../../lib/call-package.nix ( -{ callSpectrumPackage, stdenv, qemu_kvm }: +{ callSpectrumPackage, config, stdenv, qemu_kvm }: let efi = callSpectrumPackage ../../host/efi.nix {}; @@ -17,6 +17,7 @@ in OVMF_CODE = "${qemu_kvm}/share/qemu/edk2-${stdenv.hostPlatform.qemuArch}-code.fd"; ROOT_FS_DIR = efi.rootfs; EFI_IMAGE = efi; + VERSION = config.version; }; } )) (_: {}) diff --git a/vm/app/systemd-sysupdate/default.nix b/vm/app/systemd-sysupdate/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..04df283f09a1f1ece9197e275d562193af170982 --- /dev/null +++ b/vm/app/systemd-sysupdate/default.nix @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../../lib/call-package.nix ( +{ callSpectrumPackage, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + escape-url = builtins.path { + name = "escape-url"; + path = ./escape-url.awk; + }; + populate-transfer-directory = builtins.path { + name = "populate-transfer-directory"; + path = ./populate-transfer-directory; + }; +in + +callSpectrumPackage ../../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = writeScript "run-script" '' +#!/usr/bin/execlineb -P +export LC_ALL C +export LANGUAGE C +if { mount -toverlay -olowerdir=/run/virtiofs/virtiofs0/etc:/etc -- overlay /etc } +backtick tmpdir { mktemp -d /run/sysupdate-XXXXXX } +# Not a useless use of cat: if there are NUL bytes in the URL +# busybox's awk might misbehave. +backtick update_url { cat /etc/update-url } +# Leading and trailing whitespace is almost certainly user error, +# but be friendly to the user (by stripping it) rather than failing. +backtick update_url { + awk "BEGIN { + url = ENVIRON[\"update_url\"] + gsub(/^[[:space:]]+/, \"\", url) + gsub(/[[:space:]]+$/, \"\", url) + print url + }" +} +multisubstitute { + importas -iSu tmpdir + importas -iSu update_url +} +if { ${populate-transfer-directory} ${escape-url} /etc/vm-sysupdate.d ''${tmpdir} ''${update_url} } +if { ${systemd}/lib/systemd/systemd-sysupdate --definitions=''${tmpdir} update } +# [ and ] are allowed in update URLs so that IPv6 addresses work, but +# they cause globbing in the curl command-line tool by default. Use --globoff +# to disable this feature. Only allow HTTP and HTTPS protocols on redirection. +if { ${curl}/bin/curl -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS -- ''${update_url}/SHA256SUMS } +${curl}/bin/curl -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS.sha256.asc -- ''${update_url}/SHA256SUMS.sha256.asc +''; +}) (_: {}) diff --git a/vm/app/systemd-sysupdate/escape-url.awk b/vm/app/systemd-sysupdate/escape-url.awk new file mode 100644 index 0000000000000000000000000000000000000000..8edd816a20ceefa08ecc7f1bc2d1cfbe33fa8a89 --- /dev/null +++ b/vm/app/systemd-sysupdate/escape-url.awk @@ -0,0 +1,31 @@ +#!/usr/bin/awk -f +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +BEGIN { + update_url = ARGV[1]; + # Check for a GNU awk misfeature + newline = "\n"; + # Reject URLs with control characters, query parameters, or fragments. + # They *cannot* work and so are rejected to produce better error messages. + # curl rejects control characters with "Malformed input to a URL function". + # Fragment specifiers ("#") and query parameters ("?") break concatenating + # /SHA256SUMS and /SHA256SUMS.sha256.asc onto the update URL. Also, it is + # simpler to reject update URLs that contain whitespace than to try to + # escape them. + if (update_url ~ /^[^\001-\040?#\x7F]+$/) { + # Backslashes are special to systemd-sysupdate. + # Use \\\\& because without the & the result is + # not portable between GNU awk and non-GNU awk. + gsub(/\\/, "\\\\&", update_url); + # "&" and "\\" are special on the RHS of a sed substitution + # and must be escaped with another backslash. The delimiter + # ("#" in this case) and "\n" must also be escaped, but they + # were rejected above so don't bother. + gsub(/[&\\]/, "\\\\&", update_url); + printf "%s", update_url; + exit 0; + } else { + print "Bad update URL from host: control characters, whitespace, query parameters, and fragment specifiers not allowed" > "/dev/stderr"; + exit 100; + } +} diff --git a/vm/app/systemd-sysupdate/populate-transfer-directory b/vm/app/systemd-sysupdate/populate-transfer-directory new file mode 100755 index 0000000000000000000000000000000000000000..f8e515c1a69b5a6a292cc3a4d387d501f1c6a3fe --- /dev/null +++ b/vm/app/systemd-sysupdate/populate-transfer-directory @@ -0,0 +1,26 @@ +#!/usr/bin/env -S execlineb -WS4 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +# $1: awk script name +# $2: transfer directory +# $3: target directory +# $4: update URL +export LC_ALL C +export LANGUAGE C +backtick -N sed_rhs { + # Use awk to both validate the URL and to escape sed metacharacters. + awk -f $1 -- $4 +} +export tmpdir $3 +elglob -w -0 transfer_file_ ${2}/*.transfer +forx -E transfer_file { $transfer_file_ } +backtick target_basename { + basename -- $transfer_file +} +multisubstitute { + importas -iuS sed_rhs + importas -iuS target_basename + importas -iuS tmpdir + define source $transfer_file +} +redirfd -w 1 ${tmpdir}/${target_basename} sed -E -- "s#@UPDATE_URL@#${sed_rhs}#g" $source -- 2.52.0
Okay, I suppose there aren't any firm blockers here either, but something is really going to have to be done about the script that runs in the VM ASAP, because I don't see it being maintainable with this many layers of indirection. Demi Marie Obenour <demiobenour@gmail.com> writes:
Include a new `spectrum-update` command to update the system. This tells the new sys.appvm-systemd-sysupdate VM to download the updates into a staging directory using systemd-sysupdate. The host then runs systemd-sysupdate to apply the updates itself.
sys.appvm-systemd-sysupdate uses host-provided information to fetch the update. This allows editing files on the host to change the update URL and signing key.
Updates require /boot to be mounted so that systemd-sysupdate can update the unified kernel image. They also require that /tmp is writable so
What does "They" mean? What writes to /tmp? systemd-sysupdate?
that they can store temporary files, so put a tmpfs there. Furthermore, there needs to be a directory for storing downloaded updates. Create /home so that users can mount their persistent data there. The directory the VM downloads updates into is *not* reset (wiped) before or after the update. This allows the VM to know if the system is already up to date. Otherwise, it would redownload the entire multi-gigabyte update image.
Updates are currently not compressed. This should be changed in the future, but it would add a small amount of additional complexity. In particular, the script generating the update directory would need to generate a SHA256SUMS containing the hash of both the compressed and uncompressed versions. More importantly, the VM must not be able to make the host use the compressed version. This would be a potential security risk because decompression happens before signature verification. GnuPG currently decompresses signatures, but in the future it will be replaced by Sequoia which does not.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v3: - Move builtins.path from lib/config.nix to host/rootfs/default.nix. - Change config options from "update-url" to "updateUrl" and "update-signing-key" to "updateSigningKey".
Changes since v2:
- Generate the transfer files in the guest, not the host. - Do not use an environment file. - Reject URLs that cannot work. - Escape sed metacharacters. - Escape backslashes for systemd-sysupdate. - Do not validate update URLs at build time, only at runtime. - Reject only update URLs that cannot possibly work. - Set the partition UUIDs according to systemd's recommendation. - Do not rely on finding partitions by label. - Strip leading and trailing whitespace from the update URL. - Rename the update command from `update` to `spectrum-update`. - Delete the list of steps. Replace it with comments in the script.
The awk script in the VM rejects URLs that contain whitespace. This is because they can't work, and passing them to systemd-sysupdate would require figuring out how to escape them. Rejecting such bogus URLs is simpler than preventing them from being mangled. --- host/rootfs/Makefile | 19 ++++- host/rootfs/default.nix | 17 +++-- host/rootfs/file-list.mk | 7 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 ++++++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 ++++++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 ++++++ .../image/etc/vm-sysupdate.d/50-verity.transfer | 18 +++++ .../image/etc/vm-sysupdate.d/60-root.transfer | 18 +++++ .../image/etc/vm-sysupdate.d/70-kernel.transfer | 18 +++++
I might be missing something obvious here, but why aren't these part of the VM image? In the patch body you mention editing files on the host, but that's not something you can do on the Spectrum host.
host/rootfs/image/usr/bin/spectrum-update | 83 ++++++++++++++++++++++ host/rootfs/os-release.in | 15 ++++
Putting this in the root is inconsistent with how we handle generated s6-rc files, which live under image/etc/s6-rc.
lib/config.default.nix | 2 + lib/config.nix | 3 +- lib/fake-update-signing-key.gpg | 3 + release/live/shell.nix | 3 +- vm/app/systemd-sysupdate/default.nix | 57 +++++++++++++++ vm/app/systemd-sysupdate/escape-url.awk | 31 ++++++++ .../systemd-sysupdate/populate-transfer-directory | 26 +++++++ 19 files changed, 372 insertions(+), 9 deletions(-)
diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index d64bce115cc6c306956121b4bcd7271331ba1b7e..1abb18dbf84077af3dbd527a01c02f38c4608e58 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -9,6 +9,7 @@ include file-list.mk ROOT_FS_DIR = build
DIRS = \ + boot \ dev \ etc/s6-linux-init/env \ etc/s6-linux-init/run-image/configs \ @@ -32,13 +33,15 @@ DIRS = \ etc/s6-linux-init/run-image/vm/by-id \ etc/s6-linux-init/run-image/vm/by-name \ ext \ + home \ proc \ run \ - sys + sys \ + tmp
FIFOS = etc/s6-linux-init/run-image/service/s6-svscan-log/fifo
-BUILD_FILES = build/etc/s6-rc +BUILD_FILES = build/etc/s6-rc build/etc/os-release build/etc/update-url
# This rule produces three files but Make only (portably) # supports one output per rule. Instead of resorting to temporary @@ -56,12 +59,22 @@ $(ROOT_FS): ../../scripts/make-erofs.sh $(PACKAGES_FILE) $(FILES) $(BUILD_FILES) mkdir -p $(ROOT_FS_DIR) && \ { \ cat $(PACKAGES_FILE) ;\ + printf '%s\n%s\n' "$$UPDATE_SIGNING_KEY" /etc/systemd/import-pubring.gpg; \
$(UPDATE_SIGNING_KEY), for consistency.
for file in $(FILES) $(LINKS); do printf '%s\n%s\n' $$file "$${file#image/}"; done ;\ - for file in $(BUILD_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ + for file in $(BUILD_FILES) $(BUILD_NON_TARGET_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\
I don't see this used anywhere.
printf 'build/empty\n%s\n' $(DIRS) ;\ printf 'build/fifo\n%s\n' $(FIFOS) ;\ } | ../../scripts/make-erofs.sh $(ROOT_FS)
+build/etc/update-url: + mkdir -p build/etc + # might have metacharacters, so avoid interpolation + printf %s\\n "$${UPDATE_URL:?'update URL empty or missing'}" > build/etc/update-url + +build/etc/os-release: + mkdir -p build/etc + sed 's/@VERSION@/$(VERSION)/g' < os-release.in > build/etc/os-release + build/fifo: mkdir -p build mkfifo -m 0600 $@ diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index cd61c78b1f1668e7bc9c84c638ff6e7d8b6de140..1ebaf11cd7e9d61444b6524de6053a0f3cfb82c8 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -5,6 +5,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src , pkgsMusl, pkgsStatic, linux_latest +, config }: pkgsStatic.callPackage (
@@ -13,6 +14,7 @@ pkgsStatic.callPackage ( , busybox, cloud-hypervisor, cryptsetup, dbus, execline, inkscape , iproute2, inotify-tools, jq, mdevd, s6, s6-linux-init, socat , util-linuxMinimal, virtiofsd, xorg, xdg-desktop-portal-spectrum-host +, btrfs-progs }:
let @@ -33,8 +35,8 @@ let foot = pkgsGui.foot.override { allowPgo = false; };
packages = [ - cloud-hypervisor cryptsetup dbus execline inotify-tools iproute2 - jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools + btrfs-progs cloud-hypervisor cryptsetup dbus execline inotify-tools + iproute2 jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools util-linuxMinimal virtiofsd xdg-desktop-portal-spectrum-host
(busybox.override { @@ -80,11 +82,13 @@ let appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; appvm-foot = callSpectrumPackage ../../vm/app/foot.nix {}; appvm-gnome-text-editor = callSpectrumPackage ../../vm/app/gnome-text-editor.nix {}; + appvm-systemd-sysupdate = callSpectrumPackage ../../vm/app/systemd-sysupdate {}; };
packagesSysroot = runCommand "packages-sysroot" { depsBuildBuild = [ inkscape ]; nativeBuildInputs = [ xorg.lndir ]; + src = builtins.path { name = "os-release"; path = ./os-release.in; };
What does this do?
} '' mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps @@ -96,8 +100,7 @@ let done
# If systemd-pull is missing systemd-sysupdate will fail with a - # very confusing error message. If systemd-sysupdate doesn't work, - # users will not be able to receive an update that fixes the problem. + # very confusing error message. for i in sysupdate pull; do if ! cat -- "$out/usr/lib/systemd/systemd-$i" > /dev/null; then echo "link to systemd-$i didn't get installed" >&2
Stray?
diff --git a/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..cb181239d71c5a6d0a5b3652d5534a23eda64183 --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v.efi +Mode=0644 diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update new file mode 100755 index 0000000000000000000000000000000000000000..ad598b557ac1cc4e9b95ff65a53a68f04d3759ee --- /dev/null +++ b/host/rootfs/image/usr/bin/spectrum-update @@ -0,0 +1,83 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { mkdir -p -m 0700 /run/updater } + +# Take a global lock to avoid races. +s6-setlock /run/update-lock + +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Delete any stale temporary files. Delete any existing signature + # files. If the VM is still running (it should not be), the VM might + # have write access to the directory. However, updates-dir-check is + # safe against that. + if { updates-dir-check cleanup shared } + + if { + # rm -f ensures that "snapshot" does not exist afterwards. + ifte { exit 0 } { rm -f snapshot } + # TODO: suppress only "subvolume does not exist" errors. + redirfd -w 2 /dev/null btrfs subvolume delete snapshot
Why have a redundant if-true case rather than something like this? if { foreground { redirfd -w 2 /dev/null btrfs subvolume delete snapshot } rm -f snapshot }
+ } + + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } + basename -- $id_path + } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details.
There is no fsdir variable, and references to full paths like this are likely to go stale — in fact this one already has. Probably this would belong better in a dedicated documentation page about VM filesystems?
+ + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + if { + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } + umask 022 + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd
These could all be bind mounts rather than copies into RAM.
+ } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates }
Hopefully at some point we can use mount namespaces or something to make this sort of thing impossible. (Probably easier once we use a transient VM for this.)
+ # Share the update directory with the VM. + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} }
Technically there's a race here. The whole update could finish in between these two lines, as unlikely as that is. Again avoidable by using a transient VM in future.
+ # Remove the bind mount. + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Validate the update directory. Delete any stale temporary files. + # Check that a signature file was downloaded. + if { updates-dir-check check snapshot } + + # Perform the update in a separate mount namespace.
This comment doesn't add much — I can tell that the update is being performed in a separate mount namespace from the next three lines, which switch into a separate mount namespace and run the update.
diff --git a/lib/fake-update-signing-key.gpg b/lib/fake-update-signing-key.gpg new file mode 100644 index 0000000000000000000000000000000000000000..12e18f4c7c740e31692e1f1975282fa72ac1f2e3 --- /dev/null +++ b/lib/fake-update-signing-key.gpg @@ -0,0 +1,3 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +NOT A VALID KEY - UPDATES WILL NOT WORK diff --git a/release/live/shell.nix b/release/live/shell.nix index ffaa9a571c662810348822a5952d479d251a25e5..b263eacc4d0324191e3c7737dd90d304e477e79b 100644 --- a/release/live/shell.nix +++ b/release/live/shell.nix @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is>
import ../../lib/call-package.nix ( -{ callSpectrumPackage, stdenv, qemu_kvm }: +{ callSpectrumPackage, config, stdenv, qemu_kvm }:
let efi = callSpectrumPackage ../../host/efi.nix {}; @@ -17,6 +17,7 @@ in OVMF_CODE = "${qemu_kvm}/share/qemu/edk2-${stdenv.hostPlatform.qemuArch}-code.fd"; ROOT_FS_DIR = efi.rootfs; EFI_IMAGE = efi; + VERSION = config.version; }; } )) (_: {})
Is this supposed to be in a previous patch?
diff --git a/vm/app/systemd-sysupdate/default.nix b/vm/app/systemd-sysupdate/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..04df283f09a1f1ece9197e275d562193af170982 --- /dev/null +++ b/vm/app/systemd-sysupdate/default.nix @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../../lib/call-package.nix ( +{ callSpectrumPackage, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + escape-url = builtins.path { + name = "escape-url"; + path = ./escape-url.awk; + }; + populate-transfer-directory = builtins.path { + name = "populate-transfer-directory"; + path = ./populate-transfer-directory; + }; +in + +callSpectrumPackage ../../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = writeScript "run-script" '' +#!/usr/bin/execlineb -P +export LC_ALL C +export LANGUAGE C +if { mount -toverlay -olowerdir=/run/virtiofs/virtiofs0/etc:/etc -- overlay /etc } +backtick tmpdir { mktemp -d /run/sysupdate-XXXXXX } +# Not a useless use of cat: if there are NUL bytes in the URL +# busybox's awk might misbehave. +backtick update_url { cat /etc/update-url } +# Leading and trailing whitespace is almost certainly user error, +# but be friendly to the user (by stripping it) rather than failing.
I would rather fail. It's one thing to prevent valid URLs being silently mishandled, but I don't think our configuration is the right place for Postel's law, which is an invitation to more and more complexity over time to try to handle inputs that shouldn't have been valid in the first place.
+backtick update_url { + awk "BEGIN { + url = ENVIRON[\"update_url\"] + gsub(/^[[:space:]]+/, \"\", url) + gsub(/[[:space:]]+$/, \"\", url) + print url + }" +} +multisubstitute { + importas -iSu tmpdir + importas -iSu update_url +} +if { ${populate-transfer-directory} ${escape-url} /etc/vm-sysupdate.d ''${tmpdir} ''${update_url} }
These variables don't all need to be wrapped in braces, incurring the necessary corresponding Nix escaping.
+if { ${systemd}/lib/systemd/systemd-sysupdate --definitions=''${tmpdir} update } +# [ and ] are allowed in update URLs so that IPv6 addresses work, but +# they cause globbing in the curl command-line tool by default. Use --globoff +# to disable this feature. Only allow HTTP and HTTPS protocols on redirection.
"Only allow HTTP and HTTPS protocols on redirection" is redundant as a comment because it contains no information that isn't obvious from --proto-redir =http,https.
+if { ${curl}/bin/curl -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS -- ''${update_url}/SHA256SUMS } +${curl}/bin/curl -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS.sha256.asc -- ''${update_url}/SHA256SUMS.sha256.asc +''; +}) (_: {})
Okay, this is really pushing the limits of the make-vm.nix interface. Having to pass around the paths to different scripts was a warning sign that there should be a better way here. The simplest way to make this nicer would probably be to writeScript a very simple script that sets environment variables pointing to the store paths of the other components, and any other information we want to pass in from Nix, and then execs a script that lives in its own file.
diff --git a/vm/app/systemd-sysupdate/escape-url.awk b/vm/app/systemd-sysupdate/escape-url.awk new file mode 100644 index 0000000000000000000000000000000000000000..8edd816a20ceefa08ecc7f1bc2d1cfbe33fa8a89 --- /dev/null +++ b/vm/app/systemd-sysupdate/escape-url.awk @@ -0,0 +1,31 @@ +#!/usr/bin/awk -f +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +BEGIN { + update_url = ARGV[1]; + # Check for a GNU awk misfeature + newline = "\n"; + # Reject URLs with control characters, query parameters, or fragments. + # They *cannot* work and so are rejected to produce better error messages. + # curl rejects control characters with "Malformed input to a URL function". + # Fragment specifiers ("#") and query parameters ("?") break concatenating + # /SHA256SUMS and /SHA256SUMS.sha256.asc onto the update URL. Also, it is + # simpler to reject update URLs that contain whitespace than to try to + # escape them. + if (update_url ~ /^[^\001-\040?#\x7F]+$/) { + # Backslashes are special to systemd-sysupdate. + # Use \\\\& because without the & the result is + # not portable between GNU awk and non-GNU awk. + gsub(/\\/, "\\\\&", update_url); + # "&" and "\\" are special on the RHS of a sed substitution + # and must be escaped with another backslash. The delimiter + # ("#" in this case) and "\n" must also be escaped, but they + # were rejected above so don't bother. + gsub(/[&\\]/, "\\\\&", update_url); + printf "%s", update_url; + exit 0; + } else { + print "Bad update URL from host: control characters, whitespace, query parameters, and fragment specifiers not allowed" > "/dev/stderr"; + exit 100; + } +} diff --git a/vm/app/systemd-sysupdate/populate-transfer-directory b/vm/app/systemd-sysupdate/populate-transfer-directory new file mode 100755 index 0000000000000000000000000000000000000000..f8e515c1a69b5a6a292cc3a4d387d501f1c6a3fe --- /dev/null +++ b/vm/app/systemd-sysupdate/populate-transfer-directory @@ -0,0 +1,26 @@ +#!/usr/bin/env -S execlineb -WS4 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +# $1: awk script name +# $2: transfer directory +# $3: target directory +# $4: update URL +export LC_ALL C +export LANGUAGE C +backtick -N sed_rhs { + # Use awk to both validate the URL and to escape sed metacharacters. + awk -f $1 -- $4 +} +export tmpdir $3 +elglob -w -0 transfer_file_ ${2}/*.transfer +forx -E transfer_file { $transfer_file_ } +backtick target_basename { + basename -- $transfer_file +} +multisubstitute { + importas -iuS sed_rhs + importas -iuS target_basename + importas -iuS tmpdir + define source $transfer_file +} +redirfd -w 1 ${tmpdir}/${target_basename} sed -E -- "s#@UPDATE_URL@#${sed_rhs}#g" $source
Running an awk script, passed in as a parameter, to generate a sed expression, is very, very challenging to understand. Can't we do this in Nix, and save the work at runtime anyway?
The documentation previously stated that updates were not possible without reinstalling. This is no longer the case, so correct the outdated documentation and explain how to enable updates for images one builds. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Move the documentation on how to enable updates to the part on build configuration. - Clarify what happens if an update is interrupted. - Move details to a technical note. - Link to systemd-sysupdate. --- Documentation/development/build-configuration.adoc | 13 ++++++++++ Documentation/installation/getting-spectrum.adoc | 25 +++++++++++++----- Documentation/installation/index.adoc | 4 ++- Documentation/using-spectrum/index.adoc | 2 ++ Documentation/using-spectrum/updates.adoc | 30 ++++++++++++++++++++++ 5 files changed, 66 insertions(+), 8 deletions(-) diff --git a/Documentation/development/build-configuration.adoc b/Documentation/development/build-configuration.adoc index 545aa8c05ac40a101b5ee280015cde7ec4f3a66f..24672802d2395b9ba124baeba433bf2c4fc59193 100644 --- a/Documentation/development/build-configuration.adoc +++ b/Documentation/development/build-configuration.adoc @@ -20,6 +20,19 @@ The configuration file should contain an attribute set. See https://spectrum-os.org/git/spectrum/tree/lib/config.default.nix[lib/config.default.nix] for supported configuration attributes and their default values. +To enable updates, you need to specify a version, an update URL, and an update signing key. +By default, the update URL is set to a .invalid domain and the update signing key is +an invalid key. Therefore, updates will not work. To enable updates, provide a valid key +and update server URL. Spectrum uses +https://www.freedesktop.org/software/systemd/man/latest/systemd-sysupdate.html[systemd-sysupdate], +so see the https://www.freedesktop.org/software/systemd/man/latest/sysupdate.d.html[sysupdate.d] +documentation for what you need to put on your server. Building +https://spectrum-os.org/git/spectrum/tree/release/updates.nix[release/updates.nix] produces an +directory that is compatible with systemd-sysupdate, except that the signature (`SHA256SUMS.gpg`) +is missing. + +Updates are signed, so the worst a compromised update server can do is fill up your home directory. + .config.nix to build Spectrum with a https://nixos.org/manual/nixpkgs/unstable/#sec-overlays-definition[Nixpkgs overlay] [example] [source,nix] diff --git a/Documentation/installation/getting-spectrum.adoc b/Documentation/installation/getting-spectrum.adoc index e7806e0f92793320bf0cdcbdd11dbc4e713275c7..0abc83a9e6fc01084b3faa9b93eb38398b0aef27 100644 --- a/Documentation/installation/getting-spectrum.adoc +++ b/Documentation/installation/getting-spectrum.adoc @@ -86,13 +86,24 @@ a menu allowing you to "Install Spectrum". NOTE: While it's possible to install Spectrum to your internal storage, at this point in Spectrum's development there is not much -reason to, as OS updates are not yet implemented, and persistent -storage is not yet exposed to VMs. Using the "Try Spectrum" option to -boot Spectrum will let you try out everything in Spectrum, without -having to go through the additional step of reinstalling Spectrum -every time you want to use a newer version. +reason to, as persistent storage is not yet exposed to VMs. + +Currently, Spectrum does not provide an update server, so +you must provide your own. You can do this via +xref:../development/build-configuration.adoc[build configuration]. +The default sets the signing key to `/dev/null` and the server +URL to an invalid value, so updates won't work. To enable updates, +set `update-url` to the URL of your server and `update-signing-key` +to a binary GnuPG keyring to verify the updates with. Not all possible +URLs will work, but most invalid URLs will cause an error during the +build rather than runtime misbehavior. + +In the running system, the signing key is located at +`/etc/systemd/import-pubring.gpg`. The update URL is in various files +under `/etc/updates`. These files are read-only, but one can mount +an overlayfs on top of `/etc/systemd` and `/etc/updates` if one wants +to make changes. CAUTION: Do not use Spectrum for anything important or sensitive as it is not yet suitable for real-world use. Many important security properties are -currently missing, and there is no procedure for updating to -new versions—you have to reinstall the OS. +currently missing. diff --git a/Documentation/installation/index.adoc b/Documentation/installation/index.adoc index d67c88dda062066c19c3b21e699f074cc18a6dbc..c61092c93a3965b6c4014aeaee9090532634c9be 100644 --- a/Documentation/installation/index.adoc +++ b/Documentation/installation/index.adoc @@ -18,6 +18,8 @@ development. == Uninstalling and Updating -Currently, there is no implementation for a software update. +Software updates are a work in progress. If you built Spectrum yourself, +xref:../development/build-configuration.adoc[Build configuration] for how +to enable updates for it. You can replace Spectrum by installing another OS. diff --git a/Documentation/using-spectrum/index.adoc b/Documentation/using-spectrum/index.adoc index 25347a4ed7bb1f899ee0a3b85aa51da94bb954b4..5d9ea657f7c6f8c21edbf8637d2d2d0bf52f931d 100644 --- a/Documentation/using-spectrum/index.adoc +++ b/Documentation/using-spectrum/index.adoc @@ -11,3 +11,5 @@ Ready to get started with Spectrum? Here is what you can do next: * xref:running-vms.adoc[Start some applications]. * xref:creating-custom-vms.adoc[Create your own VM] to use other applications. +* xref:updates.adoc[Enable updates] so you can use newer versions of Spectrum + without reinstalling the OS. diff --git a/Documentation/using-spectrum/updates.adoc b/Documentation/using-spectrum/updates.adoc new file mode 100644 index 0000000000000000000000000000000000000000..64f085bf1e721b46076b86228adb8e86b3e5c57d --- /dev/null +++ b/Documentation/using-spectrum/updates.adoc @@ -0,0 +1,30 @@ += Updating the OS +:page-parent: Using Spectrum + +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +// SPDX-License-Identifier: GFDL-1.3-no-invariants-or-later OR CC-BY-SA-4.0 + +Spectrum supports updates via the `spectrum-update` command. This +takes the path to a staging directory as argument. This directory +must be on a BTRFS filesystem. + +Updates are atomic and take effect after the system reboots. +If the system is rebooted, crashes, or loses power during an +update, the update will not take effect. Updates are digitally +signed and Spectrum will refuse to install an update that does +not have a trusted signature. + +See xref:../development/build-configuration.adoc[build configuration] +for what is needed for updates to work. The actual update is done using +https://www.freedesktop.org/software/systemd/man/systemd-sysupdate.html[systemd-sysupdate]. +See its documentation for the details. + +== Technical Note + +Since Spectrum's host has no network access, the VM that does the +updates (`sys.appvm-systemd-sysupdate`) is given a BTRFS subvolume to +write the updates into. It uses `systemd-sysupdate` to download the updates +into this directory. Once it exits, the host snapshots this directory and +checks it for malicious filenames or non-regular files. If the check +passes, this directory is used as the source for `systemd-sysupdate`, +which installs the updates to the OS volume and EFI system partition. -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
The documentation previously stated that updates were not possible without reinstalling. This is no longer the case, so correct the outdated documentation and explain how to enable updates for images one builds.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Move the documentation on how to enable updates to the part on build configuration. - Clarify what happens if an update is interrupted. - Move details to a technical note. - Link to systemd-sysupdate. --- Documentation/development/build-configuration.adoc | 13 ++++++++++ Documentation/installation/getting-spectrum.adoc | 25 +++++++++++++----- Documentation/installation/index.adoc | 4 ++- Documentation/using-spectrum/index.adoc | 2 ++ Documentation/using-spectrum/updates.adoc | 30 ++++++++++++++++++++++ 5 files changed, 66 insertions(+), 8 deletions(-)
diff --git a/Documentation/development/build-configuration.adoc b/Documentation/development/build-configuration.adoc index 545aa8c05ac40a101b5ee280015cde7ec4f3a66f..24672802d2395b9ba124baeba433bf2c4fc59193 100644 --- a/Documentation/development/build-configuration.adoc +++ b/Documentation/development/build-configuration.adoc @@ -20,6 +20,19 @@ The configuration file should contain an attribute set. See https://spectrum-os.org/git/spectrum/tree/lib/config.default.nix[lib/config.default.nix] for supported configuration attributes and their default values.
+To enable updates, you need to specify a version, an update URL, and an update signing key. +By default, the update URL is set to a .invalid domain and the update signing key is +an invalid key. Therefore, updates will not work. To enable updates, provide a valid key +and update server URL. Spectrum uses +https://www.freedesktop.org/software/systemd/man/latest/systemd-sysupdate.html[systemd-sysupdate], +so see the https://www.freedesktop.org/software/systemd/man/latest/sysupdate.d.html[sysupdate.d] +documentation for what you need to put on your server. Building +https://spectrum-os.org/git/spectrum/tree/release/updates.nix[release/updates.nix] produces an +directory that is compatible with systemd-sysupdate, except that the signature (`SHA256SUMS.gpg`) +is missing. + +Updates are signed, so the worst a compromised update server can do is fill up your home directory.
User data partition. No home directories in Spectrum.
+ .config.nix to build Spectrum with a https://nixos.org/manual/nixpkgs/unstable/#sec-overlays-definition[Nixpkgs overlay] [example] [source,nix] diff --git a/Documentation/installation/getting-spectrum.adoc b/Documentation/installation/getting-spectrum.adoc index e7806e0f92793320bf0cdcbdd11dbc4e713275c7..0abc83a9e6fc01084b3faa9b93eb38398b0aef27 100644 --- a/Documentation/installation/getting-spectrum.adoc +++ b/Documentation/installation/getting-spectrum.adoc @@ -86,13 +86,24 @@ a menu allowing you to "Install Spectrum".
NOTE: While it's possible to install Spectrum to your internal storage, at this point in Spectrum's development there is not much -reason to, as OS updates are not yet implemented, and persistent -storage is not yet exposed to VMs. Using the "Try Spectrum" option to -boot Spectrum will let you try out everything in Spectrum, without -having to go through the additional step of reinstalling Spectrum -every time you want to use a newer version. +reason to, as persistent storage is not yet exposed to VMs. + +Currently, Spectrum does not provide an update server, so +you must provide your own. You can do this via +xref:../development/build-configuration.adoc[build configuration]. +The default sets the signing key to `/dev/null` and the server +URL to an invalid value, so updates won't work. To enable updates, +set `update-url` to the URL of your server and `update-signing-key` +to a binary GnuPG keyring to verify the updates with. Not all possible +URLs will work, but most invalid URLs will cause an error during the +build rather than runtime misbehavior. + +In the running system, the signing key is located at +`/etc/systemd/import-pubring.gpg`. The update URL is in various files +under `/etc/updates`. These files are read-only, but one can mount +an overlayfs on top of `/etc/systemd` and `/etc/updates` if one wants +to make changes.
I don't think this is something we should be encouraging in user documentation. From a user point of view, updates are not available yet. We don't need to go any further than changing from saying "updates are not supported yet" to "updates are not available yet".
CAUTION: Do not use Spectrum for anything important or sensitive as it is not yet suitable for real-world use. Many important security properties are -currently missing, and there is no procedure for updating to -new versions—you have to reinstall the OS. +currently missing. diff --git a/Documentation/installation/index.adoc b/Documentation/installation/index.adoc index d67c88dda062066c19c3b21e699f074cc18a6dbc..c61092c93a3965b6c4014aeaee9090532634c9be 100644 --- a/Documentation/installation/index.adoc +++ b/Documentation/installation/index.adoc @@ -18,6 +18,8 @@ development.
== Uninstalling and Updating
-Currently, there is no implementation for a software update. +Software updates are a work in progress. If you built Spectrum yourself, +xref:../development/build-configuration.adoc[Build configuration] for how +to enable updates for it.
You can replace Spectrum by installing another OS. diff --git a/Documentation/using-spectrum/index.adoc b/Documentation/using-spectrum/index.adoc index 25347a4ed7bb1f899ee0a3b85aa51da94bb954b4..5d9ea657f7c6f8c21edbf8637d2d2d0bf52f931d 100644 --- a/Documentation/using-spectrum/index.adoc +++ b/Documentation/using-spectrum/index.adoc @@ -11,3 +11,5 @@ Ready to get started with Spectrum? Here is what you can do next:
* xref:running-vms.adoc[Start some applications]. * xref:creating-custom-vms.adoc[Create your own VM] to use other applications. +* xref:updates.adoc[Enable updates] so you can use newer versions of Spectrum + without reinstalling the OS. diff --git a/Documentation/using-spectrum/updates.adoc b/Documentation/using-spectrum/updates.adoc new file mode 100644 index 0000000000000000000000000000000000000000..64f085bf1e721b46076b86228adb8e86b3e5c57d --- /dev/null +++ b/Documentation/using-spectrum/updates.adoc @@ -0,0 +1,30 @@ += Updating the OS +:page-parent: Using Spectrum + +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +// SPDX-License-Identifier: GFDL-1.3-no-invariants-or-later OR CC-BY-SA-4.0 + +Spectrum supports updates via the `spectrum-update` command. This +takes the path to a staging directory as argument. This directory +must be on a BTRFS filesystem. + +Updates are atomic and take effect after the system reboots. +If the system is rebooted, crashes, or loses power during an +update, the update will not take effect. Updates are digitally +signed and Spectrum will refuse to install an update that does +not have a trusted signature. + +See xref:../development/build-configuration.adoc[build configuration] +for what is needed for updates to work. The actual update is done using +https://www.freedesktop.org/software/systemd/man/systemd-sysupdate.html[systemd-sysupdate]. +See its documentation for the details.
User documentation should primarily say that updates are not available first, and only at that point maybe link to developer documentation about how to use it with a custom image.
+== Technical Note + +Since Spectrum's host has no network access, the VM that does the +updates (`sys.appvm-systemd-sysupdate`) is given a BTRFS subvolume to +write the updates into. It uses `systemd-sysupdate` to download the updates +into this directory. Once it exits, the host snapshots this directory and +checks it for malicious filenames or non-regular files. If the check +passes, this directory is used as the source for `systemd-sysupdate`, +which installs the updates to the OS volume and EFI system partition.
This shouldn't be in user documentation either, but could be in the developer documentation.
Wrong values for the version or update URL will cause very confusing build-time or runtime errors. Provide a better user experience by validating them up-front. The update URL validator is loose. It rejects only URLs that cannot possibly work: either appending /SHA256SUMS to them doesn't append to the path, or they will definitely be rejected by curl due to being malformed. The version validator is in lib/config.nix, as the version number is used in many places. It checks that the version only uses characters that are permitted by systemd's version number specification [1] and that will not break code that uses them in shell or sed commands. [1]: https://uapi-group.org/specifications/specs/version_format_specification Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v3: - Validate compression level. Changes since v2: - Use loose URL validation: allow anything that might work. - Only reject versions that violate the specification. --- Documentation/installation/getting-spectrum.adoc | 2 +- host/rootfs/default.nix | 19 ++++++++++++++++++- lib/config.nix | 12 +++++++++++- release/combined/eosimages.nix | 8 +++++++- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/Documentation/installation/getting-spectrum.adoc b/Documentation/installation/getting-spectrum.adoc index 0abc83a9e6fc01084b3faa9b93eb38398b0aef27..919b28f86eddff1b92570d46b62a1fbddc32f2d5 100644 --- a/Documentation/installation/getting-spectrum.adoc +++ b/Documentation/installation/getting-spectrum.adoc @@ -93,7 +93,7 @@ you must provide your own. You can do this via xref:../development/build-configuration.adoc[build configuration]. The default sets the signing key to `/dev/null` and the server URL to an invalid value, so updates won't work. To enable updates, -set `update-url` to the URL of your server and `update-signing-key` +set `updateUrl` to the URL of your server and `updateSigningKey` to a binary GnuPG keyring to verify the updates with. Not all possible URLs will work, but most invalid URLs will cause an error during the build rather than runtime misbehavior. diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 1ebaf11cd7e9d61444b6524de6053a0f3cfb82c8..fed99013f960287c3be3941ca593b22c55a6f79a 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -85,6 +85,23 @@ let appvm-systemd-sysupdate = callSpectrumPackage ../../vm/app/systemd-sysupdate {}; }; + update-url = + let update-url = config.updateUrl; in + # Use builtins.fromJSON because it supports \uXXXX escapes. + # This is the same check done by check-url.awk in the update VM. + # The update code is careful to escape any metacharacters, but some + # simply cannot be made to work. Concatenating the URL with /SHA256SUMS + # must append to the path portion of the URL, and the URL must be one + # that libcurl will accept. I don't know how Unicode space is handled, + # but it is a bad idea. + if builtins.match (builtins.fromJSON "\"^[^\\u0001- #?\\u007F[:space:]]+$\"" update-url) == null then + builtins.abort '' + Update URL ${builtins.toJSON update-url} has forbidden characters. + Query strings, and fragment specifiers are not supported. + ASCII control characters and whitespace must be %-encoded. + '' + else + update-url; packagesSysroot = runCommand "packages-sysroot" { depsBuildBuild = [ inkscape ]; nativeBuildInputs = [ xorg.lndir ]; @@ -152,7 +169,7 @@ stdenvNoCC.mkDerivation { name = "signing-key"; path = config.updateSigningKey; }; - UPDATE_URL = config.updateUrl; + UPDATE_URL = update-url; VERSION = config.version; }; diff --git a/lib/config.nix b/lib/config.nix index bc5b42f506b7bfd2f66db48610491809351d1a2c..2065be83ad97f8eb011f070d8c3f3249104d07f4 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -18,6 +18,16 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; in -default // callConfig config +# Version is used in many files, so validate it here. +# See https://uapi-group.org/specifications/specs/version_format_specification +# for allowed version strings. +if builtins.match "[[:alnum:]_.~^-]+" finalConfig.version == null then + builtins.abort '' + Version ${builtins.toJSON finalConfig.version} has forbidden characters. + Only ASCII alphanumerics, ".", "_", "~", "^", "+", and "-" are allowed. + '' +else + finalConfig diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 9cb35dcecee54c17392b609c493272ec83062e9b..5d1e3a67bb81cbb737823bfa3c75d88f18b31f2a 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -4,6 +4,12 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, runCommand, e2fsprogs, tar2ext4, config }: +let + compressionLevel = config.compressionLevel; +in +if compressionLevel < 1 || compressionLevel > 9 then + builtins.abort "Compression level ${builtins.toString compressionLevel} is invalid (< 1 or > 9)" +else runCommand "eosimages.img" { nativeBuildInputs = [ e2fsprogs tar2ext4 ]; imageName = "Spectrum-0.0-x86_64-generic.0.Live.img"; @@ -16,7 +22,7 @@ runCommand "eosimages.img" { mkdir dir cd dir ln -s -- "$image" "$imageName" - gzip -${builtins.toString (0 + config.compressionLevel)} < "$image" > "$imageName.gz" + gzip -${builtins.toString compressionLevel} < "$image" > "$imageName.gz" sha256sum -- "$imageName.gz" > "$imageName.gz.sha256" tar -ch -- "$imageName.gz" "$imageName.gz.sha256" | tar2ext4 -o "$out" e2label "$out" eosimages -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
Wrong values for the version or update URL will cause very confusing build-time or runtime errors. Provide a better user experience by validating them up-front.
The update URL validator is loose. It rejects only URLs that cannot possibly work: either appending /SHA256SUMS to them doesn't append to the path, or they will definitely be rejected by curl due to being malformed.
The version validator is in lib/config.nix, as the version number is used in many places. It checks that the version only uses characters that are permitted by systemd's version number specification [1] and that will not break code that uses them in shell or sed commands.
[1]: https://uapi-group.org/specifications/specs/version_format_specification
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v3: - Validate compression level.
Changes since v2: - Use loose URL validation: allow anything that might work. - Only reject versions that violate the specification. --- Documentation/installation/getting-spectrum.adoc | 2 +- host/rootfs/default.nix | 19 ++++++++++++++++++- lib/config.nix | 12 +++++++++++- release/combined/eosimages.nix | 8 +++++++- 4 files changed, 37 insertions(+), 4 deletions(-)
diff --git a/Documentation/installation/getting-spectrum.adoc b/Documentation/installation/getting-spectrum.adoc index 0abc83a9e6fc01084b3faa9b93eb38398b0aef27..919b28f86eddff1b92570d46b62a1fbddc32f2d5 100644 --- a/Documentation/installation/getting-spectrum.adoc +++ b/Documentation/installation/getting-spectrum.adoc @@ -93,7 +93,7 @@ you must provide your own. You can do this via xref:../development/build-configuration.adoc[build configuration]. The default sets the signing key to `/dev/null` and the server URL to an invalid value, so updates won't work. To enable updates, -set `update-url` to the URL of your server and `update-signing-key` +set `updateUrl` to the URL of your server and `updateSigningKey`
Wrong patch surely?
to a binary GnuPG keyring to verify the updates with. Not all possible URLs will work, but most invalid URLs will cause an error during the build rather than runtime misbehavior. diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 1ebaf11cd7e9d61444b6524de6053a0f3cfb82c8..fed99013f960287c3be3941ca593b22c55a6f79a 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -85,6 +85,23 @@ let appvm-systemd-sysupdate = callSpectrumPackage ../../vm/app/systemd-sysupdate {}; };
+ update-url =
As a Nix variable, this should stick to camel case rather than introducing another variable naming convention.
+ let update-url = config.updateUrl; in + # Use builtins.fromJSON because it supports \uXXXX escapes. + # This is the same check done by check-url.awk in the update VM. + # The update code is careful to escape any metacharacters, but some + # simply cannot be made to work. Concatenating the URL with /SHA256SUMS + # must append to the path portion of the URL, and the URL must be one + # that libcurl will accept. I don't know how Unicode space is handled, + # but it is a bad idea. + if builtins.match (builtins.fromJSON "\"^[^\\u0001- #?\\u007F[:space:]]+$\"" update-url) == null then + builtins.abort '' + Update URL ${builtins.toJSON update-url} has forbidden characters. + Query strings, and fragment specifiers are not supported. + ASCII control characters and whitespace must be %-encoded. + '' + else + update-url; packagesSysroot = runCommand "packages-sysroot" { depsBuildBuild = [ inkscape ]; nativeBuildInputs = [ xorg.lndir ]; @@ -152,7 +169,7 @@ stdenvNoCC.mkDerivation { name = "signing-key"; path = config.updateSigningKey; }; - UPDATE_URL = config.updateUrl; + UPDATE_URL = update-url; VERSION = config.version; };
diff --git a/lib/config.nix b/lib/config.nix index bc5b42f506b7bfd2f66db48610491809351d1a2c..2065be83ad97f8eb011f070d8c3f3249104d07f4 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -18,6 +18,16 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; in
-default // callConfig config +# Version is used in many files, so validate it here. +# See https://uapi-group.org/specifications/specs/version_format_specification +# for allowed version strings. +if builtins.match "[[:alnum:]_.~^-]+" finalConfig.version == null then + builtins.abort '' + Version ${builtins.toJSON finalConfig.version} has forbidden characters. + Only ASCII alphanumerics, ".", "_", "~", "^", "+", and "-" are allowed. + '' +else + finalConfig
Okay, I suppose that makes sense. In that case all configuration options probably ought to be validated here, in one place. asserts could at least prevent rightwards drift at the expense of error messages, which I think would be fine for quickly catching trivial mistakes as is the goal here.
diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 9cb35dcecee54c17392b609c493272ec83062e9b..5d1e3a67bb81cbb737823bfa3c75d88f18b31f2a 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -4,6 +4,12 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, runCommand, e2fsprogs, tar2ext4, config }:
+let + compressionLevel = config.compressionLevel; +in +if compressionLevel < 1 || compressionLevel > 9 then + builtins.abort "Compression level ${builtins.toString compressionLevel} is invalid (< 1 or > 9)" +else
Even if we keep compressionLevel, this doesn't really have the same justification for validation as the others. A mistake here isn't going to lead to confusing runtime errors.
runCommand "eosimages.img" { nativeBuildInputs = [ e2fsprogs tar2ext4 ]; imageName = "Spectrum-0.0-x86_64-generic.0.Live.img"; @@ -16,7 +22,7 @@ runCommand "eosimages.img" { mkdir dir cd dir ln -s -- "$image" "$imageName" - gzip -${builtins.toString (0 + config.compressionLevel)} < "$image" > "$imageName.gz" + gzip -${builtins.toString compressionLevel} < "$image" > "$imageName.gz" sha256sum -- "$imageName.gz" > "$imageName.gz.sha256" tar -ch -- "$imageName.gz" "$imageName.gz.sha256" | tar2ext4 -o "$out" e2label "$out" eosimages
-- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
This implements updates via systemd-sysupdate. See individual commit messages for details.
There are major changes to the image build process.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes in v4: - Fix build errors in intermediate patches. - Apply suggestions from code review. - Link to v3: https://spectrum-os.org/lists/archives/spectrum-devel/20251119-updates-v3-0-...
Changes in v3: - See individual commits for details. There are too many to mention here. - Link to v2: https://spectrum-os.org/lists/archives/spectrum-devel/20251112-updates-v2-0-...
Changes in v2: - updates-dir-check: - Do not check that there is a SHA256SUMS or SHA256SUMS.gpg file in the update directory. systemd-sysupdate will fail if it cannot find a manifest or its signature. - Follow symlinks in opening the directory. The path is from a trusted source and will always point to a BTRFS snapshot, never a symlink. The only exception is the last component, which is still checked to not be a symlink. - VM: - Link SHA256SUMS.sha256.asc to SHA256SUMS.gpg. Recent systemd-sysupdate seems to use the former name. - Get update URL from host. - Use an execline script instead of a shell script. - Update script: - Unmount shared directory if already mounted. This avoids errors when mounting it again. - Delete old snapshot if present. - Provide the VM information with a different directory layout. - Do not bind-mount the information passed into the VM into the shared VM folder. Instead rely on this folder being read-only to the guest. This is enforced by a read-only bind mount in virtiofs's mount namespace. - Testing: - Lots of manual update testing. - Disable the test for the live image as it doesn't work anymore. - Nix: - Move validation to a separate low-priority patch. - Documentation: - Document that updating the system is now possible. - Installer: - Remove the "Try Spectrum" button.
- Link to v1: https://spectrum-os.org/lists/archives/spectrum-devel/20251029-updates-v1-0-...
--- Demi Marie Obenour (14): host/rootfs: Install all programs from util-linuxMinimal host/rootfs: Install systemd-pull tools: Add directory checker for updates scripts: port make-gpt.sh to bash scripts/make-gpt.sh: Allow specifying partition size Support generating multiple partition UUIDs scripts: Use shell expansion to get partition path release: Compress installation images and remove live image Use OS version to set partition labels and UKI name Add B partitions to installation images release: Create directory with system update Support updates via systemd-sysupdate Documentation: Update support Validate configuration parameters
Documentation/development/build-configuration.adoc | 13 ++ Documentation/installation/getting-spectrum.adoc | 56 +++++++-- Documentation/installation/index.adoc | 4 +- Documentation/using-spectrum/index.adoc | 2 + Documentation/using-spectrum/updates.adoc | 30 +++++ host/efi.nix | 2 +- host/initramfs/Makefile | 18 +-- host/initramfs/etc/probe | 20 --- host/initramfs/shell.nix | 2 + host/rootfs/Makefile | 27 ++++- host/rootfs/busybox-config | 134 +++++++++++++++++++++ host/rootfs/busybox-config.license | 4 + host/rootfs/default.nix | 92 +++++++++----- host/rootfs/file-list.mk | 7 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++ .../image/etc/vm-sysupdate.d/50-verity.transfer | 18 +++ .../image/etc/vm-sysupdate.d/60-root.transfer | 18 +++ .../image/etc/vm-sysupdate.d/70-kernel.transfer | 18 +++ host/rootfs/image/usr/bin/spectrum-update | 83 +++++++++++++ host/rootfs/os-release.in | 15 +++ host/rootfs/shell.nix | 2 + img/app/Makefile | 2 +- lib/config.default.nix | 4 + lib/config.nix | 15 ++- lib/fake-update-signing-key.gpg | 3 + release.nix | 2 + release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ----- release/combined/eosimages.nix | 19 ++- release/combined/grub.cfg.in | 5 - release/live/Makefile | 15 ++- release/live/default.nix | 5 +- release/live/shell.nix | 3 +- release/update.nix | 33 +++++ scripts/format-uuid.awk | 35 ++++++ scripts/format-uuid.sh | 19 --- scripts/make-gpt.sh | 30 ++--- tools/default.nix | 1 + tools/meson.build | 4 + tools/updates-dir-check.c | 134 +++++++++++++++++++++ vm/app/systemd-sysupdate/default.nix | 57 +++++++++ vm/app/systemd-sysupdate/escape-url.awk | 31 +++++ .../systemd-sysupdate/populate-transfer-directory | 26 ++++ vm/sys/net/Makefile | 2 +- 47 files changed, 928 insertions(+), 174 deletions(-) --- base-commit: e89924f5613539e4dcd9d485a82f976c817b34c1 change-id: 20250928-updates-92e99849e231 prerequisite-patch-id: c518b0e42e0c87755ef725ace8e961cdfb862285 prerequisite-patch-id: 0ed2b2073c0ab6d422aa642fd238b15428c6f7d1
I'm finding this series quite difficult to review, because information I need to review isn't presented when I need it; I have to go and find it. "scripts: port make-gpt.sh to bash" doesn't include its motivation. I assume that's coming later, but now I have to go through potentially 9 more patches to find out what that is, and keep this patch in my head to make sure I remember to go back to it once I find out what that is. In "scripts/make-gpt.sh: Allow specifying partition size" I see bash features being used to strip a "MiB" suffix off the end of a size, so maybe that's it? But I already asked why we need to do that in my review of v2[1], and didn't get a response, so I'm still in the dark… As I'm going through these patches, I'm encountering a lot of merge conflicts. Usually when I'm reviewing a series I apply it on top of its base commit so I don't have to worry about conflicts — they're a lot easier to fix all at once in a rebase once I've reviewed all the patches and am immediately familiar with what they do — but the base commit in this series does not exist in the upstream repository, so I can't do that. After solving enough conflicts I realise that it must be because this series depends on your other one to separate out verity data, which I now recall you saying would be needed for updates, so I'll go and review that one now, and then come back here when I'm done. All this is just to say that as a patch submitter, there's a lot that can be done relatively cheaply to save a lot of inference on the part of the reviewer. It might be helpful to go through your cover letter and patches one by one in order before submission, and imagine being a reviewer who has not read the whole series in its current version, and has probably forgotten the intricacies of previous versions — what information will help them understand what's happening? If I could just review patches one by one without having to jump back and forth, I think it could go so much faster. I'd be able to stop between any two patches as well, so I wouldn't need to wait until I have enough uninterrupted time to go through the whole thing at once. Like this it's a bit of a slog, because I'm spending a lot of time trying to remember or even guess what's going on, rather than being told. [1]: https://spectrum-os.org/lists/archives/spectrum-devel/87ikfdapmx.fsf@alyssa....
This implements updates via systemd-sysupdate. See individual commit messages for details. This depends on "Move verity and EFI creation to separate Nix derivations", at <https://spectrum-os.org/lists/archives/spectrum-devel/20251126-refactor-verity-v6-0-f09555546a85@gmail.com>. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes in v5: - Fix broken shell.nix files in intermediate patches. - See individual patches messages for more details. - Link to v4: https://spectrum-os.org/lists/archives/spectrum-devel/20251121-updates-v4-0-... Changes in v4: - Fix build errors in intermediate patches. - Apply suggestions from code review. - Link to v3: https://spectrum-os.org/lists/archives/spectrum-devel/20251119-updates-v3-0-... Changes in v3: - See individual commits for details. There are too many to mention here. - Link to v2: https://spectrum-os.org/lists/archives/spectrum-devel/20251112-updates-v2-0-... Changes in v2: - updates-dir-check: - Do not check that there is a SHA256SUMS or SHA256SUMS.gpg file in the update directory. systemd-sysupdate will fail if it cannot find a manifest or its signature. - Follow symlinks in opening the directory. The path is from a trusted source and will always point to a BTRFS snapshot, never a symlink. The only exception is the last component, which is still checked to not be a symlink. - VM: - Link SHA256SUMS.sha256.asc to SHA256SUMS.gpg. Recent systemd-sysupdate seems to use the former name. - Get update URL from host. - Use an execline script instead of a shell script. - Update script: - Unmount shared directory if already mounted. This avoids errors when mounting it again. - Delete old snapshot if present. - Provide the VM information with a different directory layout. - Do not bind-mount the information passed into the VM into the shared VM folder. Instead rely on this folder being read-only to the guest. This is enforced by a read-only bind mount in virtiofs's mount namespace. - Testing: - Lots of manual update testing. - Disable the test for the live image as it doesn't work anymore. - Nix: - Move validation to a separate low-priority patch. - Documentation: - Document that updating the system is now possible. - Installer: - Remove the "Try Spectrum" button. - Link to v1: https://spectrum-os.org/lists/archives/spectrum-devel/20251029-updates-v1-0-... --- Demi Marie Obenour (13): tools: Add directory checker for updates scripts: port make-gpt.sh to bash scripts/make-gpt.sh: Allow specifying partition size Port scripts/format-uuid.sh to awk Use set and a command substitution to set UUID variables scripts: Use shell expansion to get partition path release: Compress installation images and remove live image Use OS version to set partition labels and UKI name Add B partitions to installation images release: Create directory with system update Support updates via systemd-sysupdate Documentation: Update support Validate configuration parameters Documentation/development/build-configuration.adoc | 15 +++ Documentation/development/index.adoc | 2 + Documentation/development/updates.adoc | 42 +++++++ Documentation/development/uuid-reference.adoc | 8 ++ Documentation/installation/getting-spectrum.adoc | 44 ++++--- Documentation/installation/index.adoc | 6 +- host/initramfs/Makefile | 18 +-- host/initramfs/etc/probe | 20 --- host/initramfs/shell.nix | 2 + host/rootfs/Makefile | 27 ++++- host/rootfs/default.nix | 21 +++- host/rootfs/file-list.mk | 7 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++ .../image/etc/vm-sysupdate.d/50-verity.transfer | 18 +++ .../image/etc/vm-sysupdate.d/60-root.transfer | 18 +++ .../image/etc/vm-sysupdate.d/70-kernel.transfer | 18 +++ host/rootfs/image/usr/bin/spectrum-update | 92 ++++++++++++++ host/rootfs/os-release.in | 15 +++ host/rootfs/shell.nix | 2 + img/app/Makefile | 2 +- lib/config.default.nix | 3 + lib/config.nix | 27 ++++- lib/fake-update-signing-key.gpg | 3 + release.nix | 2 + release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ----- release/combined/eosimages.nix | 8 +- release/combined/grub.cfg.in | 5 - release/live/Makefile | 17 ++- release/live/default.nix | 5 +- release/live/shell.nix | 3 +- release/update.nix | 33 +++++ scripts/format-uuid.awk | 19 +++ scripts/format-uuid.sh | 19 --- scripts/make-gpt.sh | 30 ++--- tools/default.nix | 1 + tools/meson.build | 4 + tools/updates-dir-check.c | 134 +++++++++++++++++++++ vm/app/systemd-sysupdate/default.nix | 26 ++++ vm/app/systemd-sysupdate/download-update | 68 +++++++++++ vm/sys/net/Makefile | 2 +- 44 files changed, 733 insertions(+), 145 deletions(-) --- base-commit: 64131d7c2c0e5af7ee3a8ee45f3003ba7b71a771 change-id: 20250928-updates-92e99849e231 prerequisite-patch-id: b4c17d0046f0e413bc57eaf795fcf65825839480 prerequisite-patch-id: 08a5517294d2bc746bc555820ed44cf3d2cfe8d0 -- Sincerely, Demi Marie Obenour (she/her/hers)
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either. Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. systemd-sysupdate can leave behind temporary files with names starting with '.', so delete them instead of failing. Linux can lose cache coherency if there is an I/O error, so call syncfs() on the directory before checking anything. For the same reason, fsync() the directory if any hidden files were deleted. The directory checker also serves another critical function: it checks if the VM actually downloaded anything. Otherwise, network problems could cause updates to silently do nothing. Specifically, it checks that the VM provided a file starting with the prefix "SHA256SUMS.". These will be the last ones the in-VM updater downloads. An additional mode is provided to clean out all such files. This will be used to ensure that before the in-VM updater runs, no such files are present. Hence, if the VM didn't actually download anything, the user will get a clear error instead of a false success message or a confusing error. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> Reviewed-by: Alyssa Ross <hi@alyssa.is> --- Changes since v2: - Purge leftover temporary files rather than returning an error. - Split into two modes: one that deletes signature files, and one that checks that at least one signature file exists. This allows checking that the VM actually sent something. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- tools/default.nix | 1 + tools/meson.build | 4 ++ tools/updates-dir-check.c | 134 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+) diff --git a/tools/default.nix b/tools/default.nix index 7cb7dc5b72b8394f5383c80ccf110fec55c44f21..da82f075fdba4655bd964ba35e819d669deff3f1 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -77,6 +77,7 @@ stdenv.mkDerivation (finalAttrs: { ./sd-notify-adapter.c ./start-vmm ./subprojects + ./updates-dir-check.c ] ++ lib.optionals driverSupport [ ./xdp-forwarder ])); diff --git a/tools/meson.build b/tools/meson.build index bfa290e891fafa2d03eabb221121b5df4d83fb29..666483b3304224fce9110a2788456955a2d71305 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -33,6 +33,10 @@ if get_option('host') install: true) subdir('start-vmm') + + executable('updates-dir-check', 'updates-dir-check.c', + c_args : '-D_GNU_SOURCE', + install: true) endif if get_option('build') diff --git a/tools/updates-dir-check.c b/tools/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..83af806bebf36754f8c794b04933bf6021338c38 --- /dev/null +++ b/tools/updates-dir-check.c @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> +#include <unistd.h> + +#include <err.h> + +[[noreturn]] static void bad_char(char c, char *msg_component) +{ + if (c >= 0x20 && c <= 0x7E) + errx(EXIT_FAILURE, "Forbidden %s character in filename: '%c'", + msg_component, c); + errx(EXIT_FAILURE, + "Forbidden %s character in filename: byte 0x%hhx", + msg_component, c); +} + +[[noreturn]] static void usage(void) +{ + errx(EXIT_FAILURE, "Usage: updates-dir-check [cleanup|check] DIRECTORIES..."); +} + +static void checkdir(int fd, bool check_sig) +{ + bool found_sig = false; + DIR *d = fdopendir(fd); + if (d == NULL) + err(EXIT_FAILURE, "fdopendir"); + // If there is an I/O error while there are dirty pages outstanding, + // the dirty pages are silently discarded. This means that the contents + // of the filesystem can change behind userspace's back. Flush all + // dirty pages in the filesystem with the directory to prevent this. + if (syncfs(fd) != 0) + err(EXIT_FAILURE, "syncfs"); + bool changed = false; + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(EXIT_FAILURE, "readdir"); + break; + } + const char *ptr = entry->d_name; + if (ptr[0] == '.') { + if (ptr[1] == '\0') + continue; + if (ptr[1] == '.' && ptr[2] == '\0') + continue; + // systemd-sysupdate uses these for temporary files. + // It normally cleans them up itself, but if there is an error + // it does not always clean them up. I'm not sure if it is + // guaranteed to clean up temporary files from a past run, so + // delete them instead of returning an error. + if (unlinkat(fd, ptr, 0)) + err(EXIT_FAILURE, "Failed to unlink temporary file"); + changed = true; + continue; + } + char c = ptr[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + bad_char(c, "initial"); + while ((c = *++ptr)) { + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) + bad_char(c, "subsequent"); + } + // Empty filenames are rejected as having a bad initial character, + // and POSIX forbids them from being returned anyway. Therefore, + // this cannot be out of bounds. + if (ptr[-1] == '.') + errx(EXIT_FAILURE, "Filename %s ends with a '.'", entry->d_name); + if (entry->d_type == DT_UNKNOWN) + errx(EXIT_FAILURE, "Filesystem didn't report type of file %s", entry->d_name); + if (entry->d_type != DT_REG) + errx(EXIT_FAILURE, "Entry contains non-regular file %s", entry->d_name); + if (strncmp(entry->d_name, "SHA256SUMS.", sizeof("SHA256SUMS.") - 1) == 0) { + // Found a signature file! + if (check_sig) + found_sig = true; + else { + if (unlinkat(fd, entry->d_name, 0)) + err(EXIT_FAILURE, "Unlinking old signature file"); + changed = true; + } + } + } + // If a change was made, enforcing cache coherency also requires + // another fsync() call. This is again because Linux can discard + // changes if there is an I/O error. + if (changed && fsync(fd)) + errx(EXIT_FAILURE, "fsync"); + if (check_sig && !found_sig) { + warnx("sys.appvm-systemd-sysupdate didn't send a signature file."); + warnx("There was probably a problem downloading the update."); + errx(EXIT_FAILURE, "Check its logs for more information."); + } + closedir(d); +} + +int main(int argc, char **argv) +{ + if (argc != 3) + usage(); + + bool check_sig; + if (strcmp(argv[1], "cleanup") == 0) + check_sig = false; + else if (strcmp(argv[1], "check") == 0) + check_sig = true; + else + usage(); + + for (int i = 2; i < argc; ++i) { + int fd = open(argv[i], O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + err(EXIT_FAILURE, "open(%s)", argv[i]); + checkdir(fd, check_sig); + } + return 0; +} -- 2.52.0
Future changes will be using bash-specific features. Use bash-specific code for this. Also add some error checks. No other functional change intended. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Improve commit message. Changes since v2: - Do not use wrapper script. - Make script non-executable. - Invoke script as 'bash FILE' to work around /usr/bin/env not working during a Nix build. --- host/initramfs/Makefile | 4 ++-- host/rootfs/Makefile | 2 +- img/app/Makefile | 2 +- release/live/Makefile | 2 +- scripts/make-gpt.sh | 20 ++++++++++---------- vm/sys/net/Makefile | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index f27f5e07c4707914962197b4fea8f385729370aa..2304b0885a152d8a659dddcc58f948d096034e2d 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -36,7 +36,7 @@ build/mountpoints: find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';' build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ @@ -45,7 +45,7 @@ build/loop.tar: build/live.img $(TAR) -cf $@ build/live.img build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e mv $@.tmp $@ diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 4cb048ac08b3265b0435d6ff6fc612a58c169ce9..f45758041f2f682618cb0f9e54e5a74f0b49874e 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -93,7 +93,7 @@ clean: .PHONY: clean build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/verity-timestamp $(ROOT_FS_IMAGES) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ diff --git a/img/app/Makefile b/img/app/Makefile index 48eba871339d314479f730101246ace3fa39e2db..f16c2df1b90bbec6750f2980da4dbaf49c9cb0ea 100644 --- a/img/app/Makefile +++ b/img/app/Makefile @@ -26,7 +26,7 @@ $(imgdir)/appvm/vmlinux: $(KERNEL) $(imgdir)/appvm/blk/root.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:5460386f-2203-4911-8694-91400125c604:root mv $@.tmp $@ diff --git a/release/live/Makefile b/release/live/Makefile index b37ccce42feb3ac7e8ce4faf96a67902b55be808..c712db3727b7008105388a278552fd3d81eb3b4c 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -10,7 +10,7 @@ DTBS ?= build/empty dest = build/live.img $(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh old mode 100755 new mode 100644 index 96f0d2c8494c093558c0e32e7e920b569bb078ef..0ae3d0005e5ae7d6214270fedb2fafb0a7064cf5 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -1,11 +1,11 @@ -#!/bin/sh -eu -# +#!/usr/bin/env -S bash -- # SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> # SPDX-FileCopyrightText: 2022 Unikie # SPDX-License-Identifier: EUPL-1.2+ # -# usage: make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... +# usage: bash make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... +set -xeuo pipefail ONE_MiB=1048576 # Prints the number of 1MiB blocks required to store the file named @@ -40,16 +40,15 @@ scriptsDir="$(dirname "$0")" out="$1" shift -nl=' -' table="label: gpt" # Keep 1MiB free at the start, and 1MiB free at the end. gptBytes=$((ONE_MiB * 2)) for partition; do - sizeMiB="$(sizeMiB "$(partitionPath "$partition")")" - table="$table${nl}size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" - gptBytes="$((gptBytes + sizeMiB * ONE_MiB))" + partitionPath=$(partitionPath "$partition") + sizeMiB=$(sizeMiB "$partitionPath") + table+=$'\n'"size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" + gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) done rm -f "$out" @@ -60,6 +59,7 @@ EOF n=0 for partition; do - fillPartition "$out" "$n" "$(partitionPath "$partition")" - n="$((n + 1))" + partitionPath=$(partitionPath "$partition") + fillPartition "$out" "$n" "$partitionPath" + n=$((n + 1)) done diff --git a/vm/sys/net/Makefile b/vm/sys/net/Makefile index d71c2325eff3bae921f33c61f799846d35e401c2..e403d697d90c0021a7a8d1dbc1553cfbda74a117 100644 --- a/vm/sys/net/Makefile +++ b/vm/sys/net/Makefile @@ -25,7 +25,7 @@ $(vmdir)/netvm/vmlinux: $(KERNEL) $(vmdir)/netvm/blk/root.img: ../../../scripts/make-gpt.sh ../../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) - ../../../scripts/make-gpt.sh $@.tmp \ + bash ../../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:ea21da27-0391-48da-9235-9d2ab2ca7844:root mv $@.tmp $@ -- 2.52.0
systemd-sysupdate requires that partitions are large enough to hold the newly downloaded images. This requires that they be large enough to have room to grow. Allow specifying the partition size manually, overriding the default (the size of the file that will be copied into the partition). Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Remove the MiB suffix. Changes since v2: - Split into separate commit. --- scripts/make-gpt.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh index 0ae3d0005e5ae7d6214270fedb2fafb0a7064cf5..c3f16e6c029d1d27d3da9e05e50945a56bfad9f8 100644 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -45,8 +45,13 @@ table="label: gpt" # Keep 1MiB free at the start, and 1MiB free at the end. gptBytes=$((ONE_MiB * 2)) for partition; do - partitionPath=$(partitionPath "$partition") - sizeMiB=$(sizeMiB "$partitionPath") + if [[ "$partition" =~ :([1-9][0-9]*)$ ]]; then + sizeMiB=${BASH_REMATCH[1]} + partition=${partition%:*} + else + partitionPath=$(partitionPath "$partition") + sizeMiB=$(sizeMiB "$partitionPath") + fi table+=$'\n'"size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) done -- 2.52.0
This makes it significantly easier to extend. Future changes will require it to be able to output multiple UUIDs at once. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Convert script from sh to awk before making any functional changes. --- host/initramfs/Makefile | 6 +++--- host/rootfs/Makefile | 6 +++--- release/live/Makefile | 6 +++--- release/live/default.nix | 2 +- scripts/format-uuid.awk | 9 +++++++++ scripts/format-uuid.sh | 19 ------------------- 6 files changed, 19 insertions(+), 29 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 2304b0885a152d8a659dddcc58f948d096034e2d..e04e2ff471750410926f14099cee9786d582de86 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -35,10 +35,10 @@ build/mountpoints: cd build/mountpoints && mkdir -p $(MOUNTPOINTS) find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';' -build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) +build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$(awk -f ../../scripts/format-uuid.awk "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ + $(ROOT_FS_IMAGE):root:$$(awk -f ../../scripts/format-uuid.awk "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ build/loop.tar: build/live.img diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index f45758041f2f682618cb0f9e54e5a74f0b49874e..b2c0c6176fe0de4a99d1a3737d50054b532af598 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -92,10 +92,10 @@ clean: rm -rf build .PHONY: clean -build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/verity-timestamp $(ROOT_FS_IMAGES) +build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/verity-timestamp $(ROOT_FS_IMAGES) bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$(awk -f ../../scripts/format-uuid.awk "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ + $(ROOT_FS_IMAGE):root:$$(awk -f ../../scripts/format-uuid.awk "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ debug: diff --git a/release/live/Makefile b/release/live/Makefile index c712db3727b7008105388a278552fd3d81eb3b4c..48df3ef4ad3faab4e0ad09380bd70dbdc980109f 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -9,11 +9,11 @@ DTBS ?= build/empty dest = build/live.img -$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) +$(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$(awk -f ../../scripts/format-uuid.awk "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ + $(ROOT_FS_IMAGE):root:$$(awk -f ../../scripts/format-uuid.awk "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ build/empty: diff --git a/release/live/default.nix b/release/live/default.nix index ba9bb17e697a6ecfe81e52a4ffbc375ef443b6f3..d1e2422e9f1ba666af7ad7a5cce1c80a242d0777 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -29,7 +29,7 @@ stdenv.mkDerivation { fileset = lib.fileset.intersection src (lib.fileset.unions [ ./. ../../lib/common.mk - ../../scripts/format-uuid.sh + ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk ]); diff --git a/scripts/format-uuid.awk b/scripts/format-uuid.awk new file mode 100644 index 0000000000000000000000000000000000000000..17831221bbef2d2d038f4822b22f88939eab7437 --- /dev/null +++ b/scripts/format-uuid.awk @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +BEGIN { + print (substr(ARGV[1], 1, 8) "-" \ + substr(ARGV[1], 9, 4) "-" \ + substr(ARGV[1], 13, 4) "-" \ + substr(ARGV[1], 17, 4) "-" \ + substr(ARGV[1], 21, 12)); +} diff --git a/scripts/format-uuid.sh b/scripts/format-uuid.sh deleted file mode 100755 index 3b38278aef640b2cd540d6606b05dd62018e48a6..0000000000000000000000000000000000000000 --- a/scripts/format-uuid.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -eu -# -# SPDX-FileCopyrightText: 2021-2022 Alyssa Ross <hi@alyssa.is> -# SPDX-FileCopyrightText: 2022 Unikie -# SPDX-License-Identifier: EUPL-1.2+ - -substr() { - str=$1 - beg=$2 - end=$3 - echo "$str" | cut -c "$beg-$end" -} - -u1=$(substr "$1" 1 8) -u2=$(substr "$1" 9 12) -u3=$(substr "$1" 13 16) -u4=$(substr "$1" 17 20) -u5=$(substr "$1" 21 32) -printf "%s\n" "$u1-$u2-$u3-$u4-$u5" -- 2.52.0
No functional change intended. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/initramfs/Makefile | 6 ++++-- host/rootfs/Makefile | 6 ++++-- release/live/Makefile | 6 ++++-- scripts/format-uuid.awk | 20 +++++++++++++++----- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index e04e2ff471750410926f14099cee9786d582de86..392dcfc8af3d6924fae717025124f228a2362b94 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -36,9 +36,11 @@ build/mountpoints: find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';' build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(awk -f ../../scripts/format-uuid.awk "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(awk -f ../../scripts/format-uuid.awk "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$2 \ + $(ROOT_FS_IMAGE):root:$$1 mv $@.tmp $@ build/loop.tar: build/live.img diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index b2c0c6176fe0de4a99d1a3737d50054b532af598..ab24263c6f327e47cd1d012ca8d729b0ea5eb8f3 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -93,9 +93,11 @@ clean: .PHONY: clean build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/verity-timestamp $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(awk -f ../../scripts/format-uuid.awk "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(awk -f ../../scripts/format-uuid.awk "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$2 \ + $(ROOT_FS_IMAGE):root:$$1 mv $@.tmp $@ debug: diff --git a/release/live/Makefile b/release/live/Makefile index 48df3ef4ad3faab4e0ad09380bd70dbdc980109f..5ab93451de109949af0e7ed7f70bf6827fefbf69 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -10,10 +10,12 @@ DTBS ?= build/empty dest = build/live.img $(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$(awk -f ../../scripts/format-uuid.awk "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(awk -f ../../scripts/format-uuid.awk "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$2 \ + $(ROOT_FS_IMAGE):root:$$1 mv $@.tmp $@ build/empty: diff --git a/scripts/format-uuid.awk b/scripts/format-uuid.awk index 17831221bbef2d2d038f4822b22f88939eab7437..d4c1a75d97ed86e17a118d8b2d3252cd78c77286 100644 --- a/scripts/format-uuid.awk +++ b/scripts/format-uuid.awk @@ -1,9 +1,19 @@ # SPDX-License-Identifier: EUPL-1.2+ # SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +function format_uuid(arg) { + print (substr(arg, 1, 8) "-" \ + substr(arg, 9, 4) "-" \ + substr(arg, 13, 4) "-" \ + substr(arg, 17, 4) "-" \ + substr(arg, 21, 12)); +} + BEGIN { - print (substr(ARGV[1], 1, 8) "-" \ - substr(ARGV[1], 9, 4) "-" \ - substr(ARGV[1], 13, 4) "-" \ - substr(ARGV[1], 17, 4) "-" \ - substr(ARGV[1], 21, 12)); + FS = ""; + if (getline != 1) { + print "Empty input file" > "/dev/stderr"; + exit 1; + } + format_uuid(substr($0, 1, 32)); + format_uuid(substr($0, 33, 32)); } -- 2.52.0
Avoids a pointless call to awk. No functional change intended. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Move into separate commit. --- scripts/make-gpt.sh | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh index c3f16e6c029d1d27d3da9e05e50945a56bfad9f8..7baa3cd42ad8b0b664f56c68c1f254f3c2965623 100644 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -28,13 +28,6 @@ fillPartition() { lseek -S 1 "$start" cat "$3" 1<>"$1" } -# Prints the partition path from a PATH:PARTTYPE[:PARTUUID[:PARTLABEL]] string. -partitionPath() { - awk -F: '{print $1}' <<EOF -$1 -EOF -} - scriptsDir="$(dirname "$0")" out="$1" @@ -49,8 +42,7 @@ for partition; do sizeMiB=${BASH_REMATCH[1]} partition=${partition%:*} else - partitionPath=$(partitionPath "$partition") - sizeMiB=$(sizeMiB "$partitionPath") + sizeMiB=$(sizeMiB "${partition%%:*}") fi table+=$'\n'"size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) @@ -64,7 +56,6 @@ EOF n=0 for partition; do - partitionPath=$(partitionPath "$partition") - fillPartition "$out" "$n" "$partitionPath" + fillPartition "$out" "$n" "${partition%%:*}" n=$((n + 1)) done -- 2.52.0
This patch has been committed as 1970f432152730ec19bf97a8db4613a591ee00be, which can be viewed online at https://spectrum-os.org/git/spectrum/commit/?id=1970f432152730ec19bf97a8db46.... This is an automated message. Send comments/questions/requests to: Alyssa Ross <hi@alyssa.is>
systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, the partitions need to be very large so that there is plenty of room for the OS to grow. Furthermore, systemd-sysupdate requires both A and B copies of both the root and verity partitions. mkfs.ext4 is not able to produce images with files large enough to hold both the primary and backup copy of the root partition [1]. Reducing the sizes of partitions to be little greater than the size of the root filesystem image does not help. The produced file is still too large. Therefore, compress the image, which causes it to be small enough that mkfs.ext4 can handle it. This breaks the option to use the installer as a live image. Therefore, remove it. This option will return once Spectrum switches to the GNOME OS installer [2]. However, it is still possible to build a live image that is separate from the installer. Document how to build and use it. GRUB2 does support compressed loopback images, but these presumably buffer the whole image in memory. Since the entire installer will be replaced, making it work is not considered worthwhile. [1]: https://github.com/tytso/e2fsprogs/issues/254 [2]: https://spectrum-os.org/lists/archives/spectrum-devel/87wm4dlkhz.fsf@alyssa.... Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Replace a mention of "Try Spectrum" with a mention of the live image. - Combine instructions for building an installer and a live image. - Drop the config option for the compression level. - Drop unnecessary quoting changes. - Drop unnecessary 'set -euo pipefail'. Changes since v3: - Make the compression level configurable. The default is 1 so that development builds finish in a reasonable amount of time. Release builds should use compression level 9. Changes since v2: - Remove live image test instead of skipping it. - Document the change. - Document that there is still a live image available, though it is separate from the installer. - Document how to build the live image. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/installation/getting-spectrum.adoc | 44 ++++++++++++++++-------- host/initramfs/Makefile | 8 ----- host/initramfs/etc/probe | 20 ----------- release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ---------------- release/combined/eosimages.nix | 8 ++--- release/combined/grub.cfg.in | 5 --- 7 files changed, 35 insertions(+), 81 deletions(-) diff --git a/Documentation/installation/getting-spectrum.adoc b/Documentation/installation/getting-spectrum.adoc index 29803aa324b196119a03b22d7f1e2d7730e2c1eb..22c1fe310b56adecaac5ed7e87decbfd56881919 100644 --- a/Documentation/installation/getting-spectrum.adoc +++ b/Documentation/installation/getting-spectrum.adoc @@ -23,6 +23,17 @@ documentation for https://nix.dev/manual/nix/2.24/advanced-topics/cores-vs-jobs.html[tuning cores and jobs]. +To get Spectrum, run + +[source,shell] +---- +git clone https://spectrum-os.org/git/spectrum +---- + +Then follow one of the instructions below. If you haven't set up the +xref:binary-cache.adoc[binary cache], all of the following Nix commands +will take a very long time. + == Trying Spectrum If you want to try Spectrum out to get a feel for it, without @@ -31,27 +42,30 @@ applications. [source,shell] ---- -git clone https://spectrum-os.org/git/spectrum cd spectrum/host/rootfs nix-shell --run 'make run' ---- -This builds just enough of Spectrum to try it out in a VM, but it will -still take a very long time. +This builds just enough of Spectrum to try it out in a VM. == Installing Spectrum To install Spectrum on a computer, you can use a USB drive as a -bootable Spectrum installer device. When booting a system from the -installer device, you will be able to choose whether to try out -Spectrum without installing it on your system (as a live image), or to -install it to your computer's internal storage. +bootable Spectrum installer device. You will need to choose whether to +try out Spectrum without installing it on your system (as a live image), +or to install it to your computer's internal storage. -First, you need to build the Spectrum image: +To build a live image, run: + +[source,shell] +---- +nix-build spectrum/release/live +---- + +To build an installer, run: [source,shell] ---- -git clone https://spectrum-os.org/git/spectrum nix-build spectrum/release/combined ---- @@ -61,15 +75,17 @@ will take a very long time. When it's done, a symbolic link named installer image. Write that image to a USB drive, for example using `dd` (command line) or https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available -in Nixpkgs). Boot your system from the USB drive, and you should see -a menu allowing you to choose between "Try Spectrum" and "Install -Spectrum". +in Nixpkgs). Then boot your system from the USB drive + +If you built a live image, Spectrum should be ready for you to use. +If you built an installer, you should see a menu allowing you to +"Install Spectrum". NOTE: While it's possible to install Spectrum to your internal storage, at this point in Spectrum's development there is not much reason to, as OS updates are not yet implemented, and persistent -storage is not yet exposed to VMs. Using the "Try Spectrum" option to -boot Spectrum will let you try out everything in Spectrum, without +storage is not yet exposed to VMs. Using a live image to boot +Spectrum will let you try out everything in Spectrum, without having to go through the additional step of reinstalling Spectrum every time you want to use a newer version. diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 392dcfc8af3d6924fae717025124f228a2362b94..c3d600ad5a55d81b8ca9c7a3e182ef5f4fd90f4b 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -43,14 +43,6 @@ build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../sc $(ROOT_FS_IMAGE):root:$$1 mv $@.tmp $@ -build/loop.tar: build/live.img - $(TAR) -cf $@ build/live.img - -build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 - bash ../../scripts/make-gpt.sh $@.tmp \ - build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e - mv $@.tmp $@ - clean: rm -rf build .PHONY: clean diff --git a/host/initramfs/etc/probe b/host/initramfs/etc/probe index 4cbd00db52c1a7128b5c619a43d415675feaee0b..013092b6dcc5b82db7302c1ae7e6d8a4f5a0b802 100755 --- a/host/initramfs/etc/probe +++ b/host/initramfs/etc/probe @@ -2,26 +2,6 @@ # SPDX-License-Identifier: EUPL-1.2+ # SPDX-FileCopyrightText: 2021 Alyssa Ross <hi@alyssa.is> -if -n { - # If this is a Spectrum installer eosimages partition, we might be - # booting from the installer, and should loopback mount the images. - importas -i mdev MDEV - if { - backtick -E type { lsblk -lnpo PARTTYPE $mdev } - test $type = 56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e - } - if { - forx -pE module { ext4 loop } - modprobe $module - } - backtick -E uuid { lsblk -lnpo PARTUUID $mdev } - if { mkdir -p /mnt/${uuid} } - if { mount $mdev /mnt/${uuid} } - find /mnt/${uuid} -name *.img -exec - losetup -Pf {} - ; -} - # Check whether we now have all the partitions we need to boot. importas -i rootfs_uuid ROOTFS_UUID diff --git a/release/checks/integration/meson.build b/release/checks/integration/meson.build index 7214e47ba1ec23c247c8b76e5c8d94aff1ce1fd6..7bf8f51e4c762d2279ed6064ae1a87cb9b07494c 100644 --- a/release/checks/integration/meson.build +++ b/release/checks/integration/meson.build @@ -11,7 +11,7 @@ run_qemu = find_program('../../../scripts/run-qemu.sh') lib = static_library('spectrum-integration-test', 'lib.c') -foreach test : ['appimage', 'late-serial', 'networking', 'portal', 'try'] +foreach test : ['appimage', 'late-serial', 'networking', 'portal'] test(test, executable(test, test + '.c', link_with : lib), timeout : 400, args : [run_qemu]) diff --git a/release/checks/integration/try.c b/release/checks/integration/try.c deleted file mode 100644 index 4b874c0a7e9b48324497450fb5488e04576fd43b..0000000000000000000000000000000000000000 --- a/release/checks/integration/try.c +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is> - -#include "lib.h" - -#include <stdlib.h> -#include <string.h> - -void test(struct config c) -{ - struct vm *vm; - - c.drives.img = getenv_or_die("COMBINED_PATH"); - - vm = start_qemu(c); - - start_console_thread(vm, "GNU GRUB "); - wait_for_prompt(vm); - - start_console_thread(vm, "~ # "); - - // Assume that Try Spectrum is the first menu entry. - if (fputc('\n', vm_console_writer(vm)) == EOF) { - fputs("error writing to console\n", stderr); - exit(EXIT_FAILURE); - } - - wait_for_prompt(vm); -} diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..38210922ec165a36c33d99d08f04beabdc01ed53 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -14,9 +14,9 @@ runCommand "eosimages.img" { } '' mkdir dir cd dir - ln -s $image $imageName - sha256sum $imageName > $imageName.sha256 - tar -chf $NIX_BUILD_TOP/eosimages.tar * - tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out + ln -s -- $image $imageName + gzip -9 < $image > $imageName.gz + sha256sum -- $imageName.gz > $imageName.gz.sha256 + tar -ch -- $imageName.gz $imageName.gz.sha256 | tar2ext4 -o $out e2label $out eosimages '') (_: {}) diff --git a/release/combined/grub.cfg.in b/release/combined/grub.cfg.in index a8e73a3b4dc0d643cf575e3cc545ec9ff72380cb..a22f5fc96ba6451d44c0f9768a15a1f48c5dce1c 100644 --- a/release/combined/grub.cfg.in +++ b/release/combined/grub.cfg.in @@ -15,11 +15,6 @@ set gfxpayload=keep terminal_output gfxterm terminal_output console -menuentry "Try Spectrum" { - loopback live (hd0,gpt3)/Spectrum-0.0-x86_64-generic.0.Live.img - chainloader (live,gpt1)/EFI/Linux/spectrum.efi -} - menuentry "Install Spectrum" { set root=(hd0,gpt2) linux @linux@ @kernelParams@ -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, the partitions need to be very large so that there is plenty of room for the OS to grow. Furthermore, systemd-sysupdate requires both A and B copies of both the root and verity partitions.
mkfs.ext4 is not able to produce images with files large enough to hold both the primary and backup copy of the root partition [1]. Reducing the sizes of partitions to be little greater than the size of the root filesystem image does not help. The produced file is still too large. Therefore, compress the image, which causes it to be small enough that mkfs.ext4 can handle it.
This breaks the option to use the installer as a live image. Therefore, remove it. This option will return once Spectrum switches to the GNOME OS installer [2]. However, it is still possible to build a live image that is separate from the installer. Document how to build and use it.
GRUB2 does support compressed loopback images, but these presumably buffer the whole image in memory. Since the entire installer will be replaced, making it work is not considered worthwhile.
[1]: https://github.com/tytso/e2fsprogs/issues/254 [2]: https://spectrum-os.org/lists/archives/spectrum-devel/87wm4dlkhz.fsf@alyssa....
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Replace a mention of "Try Spectrum" with a mention of the live image. - Combine instructions for building an installer and a live image. - Drop the config option for the compression level. - Drop unnecessary quoting changes. - Drop unnecessary 'set -euo pipefail'.
Changes since v3: - Make the compression level configurable. The default is 1 so that development builds finish in a reasonable amount of time. Release builds should use compression level 9.
Changes since v2: - Remove live image test instead of skipping it. - Document the change. - Document that there is still a live image available, though it is separate from the installer. - Document how to build the live image.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/installation/getting-spectrum.adoc | 44 ++++++++++++++++-------- host/initramfs/Makefile | 8 ----- host/initramfs/etc/probe | 20 ----------- release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ---------------- release/combined/eosimages.nix | 8 ++--- release/combined/grub.cfg.in | 5 --- 7 files changed, 35 insertions(+), 81 deletions(-)
Reviewed-by: Alyssa Ross <hi@alyssa.is> But I'd still like to get to the bottom of whether release/installer works.
systemd-sysupdate has strict requirements on the partition layout: - The label of the active partition must match the template in the .transfer file. For instance, the root filesystem of Spectrum 0.0.0 must be in a partition with label "Spectrum_0.0.0", and the verity partition must have the label "Spectrum_0.0.0.verity". - The label of the inactive partition must be that of the old version of Spectrum, or "_empty" for freshly installed systems. - The partition type UUID must conform to the Discoverable Partition Specification. Also, the UKI must have a name that includes the OS version. Otherwise, it will not be deleted during updates. Since the partition label includes the OS version, add an OS version number. Use 0.0.0 to indicate that Spectrum OS is still in very early development and should not be used. The version number can be overridden in the build configuration file. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> Reviewed-by: Alyssa Ross <hi@alyssa.is> --- Changes since v4: - Rebase and address merge conflicts. - Add missing "VERSION = config.version" in Nix files. Changes since v2: - Split off into separate commit. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/initramfs/Makefile | 4 ++-- host/initramfs/shell.nix | 2 ++ host/rootfs/Makefile | 4 ++-- host/rootfs/default.nix | 5 +++-- host/rootfs/shell.nix | 2 ++ lib/config.default.nix | 1 + release/live/Makefile | 8 ++++---- release/live/default.nix | 3 +++ release/live/shell.nix | 3 ++- 9 files changed, 21 insertions(+), 11 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index c3d600ad5a55d81b8ca9c7a3e182ef5f4fd90f4b..a7f7bb22255b2cc3f845da7e85cadd7aab1efdb9 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -39,8 +39,8 @@ build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../sc uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$2 \ - $(ROOT_FS_IMAGE):root:$$1 + $(ROOT_FS_VERITY):verity:$$2:Spectrum_'$(VERSION).verity' \ + $(ROOT_FS_IMAGE):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ clean: diff --git a/host/initramfs/shell.nix b/host/initramfs/shell.nix index 8b47aa53bc19a818ebf563e281f22e82202a8ea5..44d4a985e969c1a57ad42d0666189c704aef9afd 100644 --- a/host/initramfs/shell.nix +++ b/host/initramfs/shell.nix @@ -4,6 +4,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, rootfs, pkgsStatic, stdenv , cryptsetup, jq, qemu_kvm, tar2ext4, util-linux +, config }: let @@ -18,5 +19,6 @@ initramfs.overrideAttrs ({ nativeBuildInputs ? [], env ? {}, ... }: { env = env // { KERNEL = "${rootfs.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; ROOT_FS = rootfs; + VERSION = config.version; }; })) (_: {}) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index ab24263c6f327e47cd1d012ca8d729b0ea5eb8f3..a6d9f23e9f5277b7c79a53105eb2dfe1bab1451e 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -96,8 +96,8 @@ build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../sc uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$2 \ - $(ROOT_FS_IMAGE):root:$$1 + $(ROOT_FS_VERITY):verity:$$2:Spectrum_'$(VERSION).verity' \ + $(ROOT_FS_IMAGE):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ debug: diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 941c04e619baa7652d1812f4eb50445c607d5884..16a151971715f9a9d987dc92a1d06eb169de1144 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -3,8 +3,8 @@ # SPDX-FileCopyrightText: 2022 Unikie import ../../lib/call-package.nix ( -{ callSpectrumPackage, spectrum-build-tools, src -, pkgsMusl, pkgsStatic, linux_latest +{ callSpectrumPackage, config, spectrum-build-tools +, src, pkgsMusl, pkgsStatic, linux_latest }: pkgsStatic.callPackage ( @@ -125,6 +125,7 @@ stdenvNoCC.mkDerivation { printf "%s\n/\n" ${packagesSysroot} >$out sed p ${writeClosure [ packagesSysroot] } >>$out ''; + VERSION = config.version; }; # The Makefile uses $(ROOT_FS), not $(dest), so it can share code diff --git a/host/rootfs/shell.nix b/host/rootfs/shell.nix index 6df2f575fdfc7cdf8067ccfdb5fecaad9f6ea5e6..27f93e05fce036257d27cf9992fee8c925073f80 100644 --- a/host/rootfs/shell.nix +++ b/host/rootfs/shell.nix @@ -5,6 +5,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, rootfs, pkgsStatic, srcOnly, stdenv , btrfs-progs, cryptsetup, jq, netcat, qemu_kvm, reuse, util-linux +, config }: rootfs.overrideAttrs ( @@ -20,5 +21,6 @@ rootfs.overrideAttrs ( KERNEL = "${passthru.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; LINUX_SRC = srcOnly passthru.kernel.configfile; VMLINUX = "${passthru.kernel.dev}/vmlinux"; + VERSION = config.version; }; })) (_: {}) diff --git a/lib/config.default.nix b/lib/config.default.nix index a8422345cc00f9413bb19ec968fd89c82fed801b..489c231490a8b66aa01f50053b25646060f7f963 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -4,4 +4,5 @@ { pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; + version = "0.0.0"; } diff --git a/release/live/Makefile b/release/live/Makefile index 5ab93451de109949af0e7ed7f70bf6827fefbf69..46628bdaa5b4a02aca3dd15be4477c3b2c194993 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -14,8 +14,8 @@ $(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/s set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$2 \ - $(ROOT_FS_IMAGE):root:$$1 + $(ROOT_FS_VERITY):verity:$$2:Spectrum_'$(VERSION).verity' \ + $(ROOT_FS_IMAGE):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ build/empty: @@ -27,8 +27,8 @@ build/boot.fat: $(SYSTEMD_BOOT_EFI) $(EFI_IMAGE) build/empty $(MMD) -i $@ ::/EFI ::/EFI/BOOT ::/EFI/Linux # This symlink is necessary. Copying $(EFI_IMAGE) directly # results in an unbootable image. TODO: figure out why. - ln -s $(EFI_IMAGE) build/spectrum.efi - $(MCOPY) -i $@ build/spectrum.efi ::/EFI/Linux + ln -s $(EFI_IMAGE) 'build/Spectrum_$(VERSION).efi' + $(MCOPY) -i $@ 'build/Spectrum_$(VERSION).efi' ::/EFI/Linux $(MCOPY) -i $@ $(SYSTEMD_BOOT_EFI) ::/EFI/BOOT/$(EFINAME) clean: diff --git a/release/live/default.nix b/release/live/default.nix index d1e2422e9f1ba666af7ad7a5cce1c80a242d0777..aa5c5869b9c82ce3722fc39029f6aabd7d8c874d 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -1,11 +1,13 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2021-2023, 2025 Alyssa Ross <hi@alyssa.is> # SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src , lib, pkgsStatic, stdenvNoCC , cryptsetup, dosfstools, jq, mtools, util-linux +, config }: let @@ -46,6 +48,7 @@ stdenv.mkDerivation { SYSTEMD_BOOT_EFI = "${systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; EFI_IMAGE = efi; EFINAME = "BOOT${toUpper efiArch}.EFI"; + VERSION = config.version; }; buildFlags = [ "dest=$(out)" ]; diff --git a/release/live/shell.nix b/release/live/shell.nix index b0bf957c085d1581a24d8916925611da0a60ec8b..e542793a66fb972cfde90f6be2204986442b7d4b 100644 --- a/release/live/shell.nix +++ b/release/live/shell.nix @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> import ../../lib/call-package.nix ( -{ callSpectrumPackage, stdenv, qemu_kvm }: +{ callSpectrumPackage, config, stdenv, qemu_kvm }: let efi = callSpectrumPackage ../../host/efi.nix {}; @@ -17,6 +17,7 @@ in OVMF_CODE = "${qemu_kvm}/share/qemu/edk2-${stdenv.hostPlatform.qemuArch}-code.fd"; ROOT_FS = efi.rootfs; EFI_IMAGE = efi; + VERSION = config.version; }; } )) (_: {}) -- 2.52.0
systemd-sysupdate never writes to the running OS partition. Instead, it requires a separate partition to write the update into. Create a separate partition for that purpose. systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, make the partitions very large so that there is plenty of room for the OS to grow. This requires rewriting the code that calculates the partition sizes. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Leave B partitions empty. - Leave MiB unit implicit. - Use fixed GUIDs for blank partitions. Changes since v2: - Make into a standalone commit - Do not rely on separate script to generate the images. - Use a smaller size for the verity partition. --- Documentation/development/uuid-reference.adoc | 8 ++++++++ release/live/Makefile | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Documentation/development/uuid-reference.adoc b/Documentation/development/uuid-reference.adoc index 146615896104d5ab20c2e9353e5ed8f7a3dc54a6..16279c8a7e690bbaafdc3e0194f3130ba65c281c 100644 --- a/Documentation/development/uuid-reference.adoc +++ b/Documentation/development/uuid-reference.adoc @@ -59,6 +59,14 @@ Spectrum combined live system / installer image. The Spectrum installer system. +=== `18f2ccff-92f1-4bb1-a80e-24f76ecda90c` + +The not-yet-used B verity partition. + +=== `ec0c5ff3-f6b1-4adf-82b4-61336c4d135f` + +The not-yet-used B root filesystem partition. + ''' == Finding Undocumented UUIDs diff --git a/release/live/Makefile b/release/live/Makefile index 46628bdaa5b4a02aca3dd15be4477c3b2c194993..12b13d6e730c494086d1e7f763495ffbd4bd4b88 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -10,12 +10,15 @@ DTBS ?= build/empty dest = build/live.img $(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) +# 162MiB was calculated by running `veritysetup format` on 20GiB from /dev/urandom uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$2:Spectrum_'$(VERSION).verity' \ - $(ROOT_FS_IMAGE):root:$$1:Spectrum_'$(VERSION)' + $(ROOT_FS_VERITY):verity:$$2:Spectrum_'$(VERSION).verity:162' \ + $(ROOT_FS_IMAGE):root:$$1:Spectrum_'$(VERSION):20000' \ + /dev/null:verity:18f2ccff-92f1-4bb1-a80e-24f76ecda90c:_empty:162 \ + /dev/null:root:ec0c5ff3-f6b1-4adf-82b4-61336c4d135f:_empty:20000 mv $@.tmp $@ build/empty: -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
systemd-sysupdate never writes to the running OS partition. Instead, it requires a separate partition to write the update into. Create a separate partition for that purpose.
systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, make the partitions very large so that there is plenty of room for the OS to grow. This requires rewriting the code that calculates the partition sizes.
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Leave B partitions empty. - Leave MiB unit implicit. - Use fixed GUIDs for blank partitions.
Changes since v2: - Make into a standalone commit - Do not rely on separate script to generate the images. - Use a smaller size for the verity partition. --- Documentation/development/uuid-reference.adoc | 8 ++++++++ release/live/Makefile | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-)
Reviewed-by: Alyssa Ross <hi@alyssa.is>
Whenever a release is made, create a directory with the release files to be used for an update. After its SHA256SSUMS file is signed, the file is ready to be uploaded to a server for users to update from. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> Reviewd-by: Alyssa Ross <hi@alyssa.is> --- Changes since v4: - Only set -u because stdenv sets the reset. Update comment. Changes since v2: - Use UUIDs to name the rootfs and verity superblock. This will allow systemd-sysupdate to set the correct UUIDs on the rootfs and verity partitions, avoiding the need to use labels to find these partitions. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release.nix | 2 ++ release/update.nix | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/release.nix b/release.nix index a4fe66ee5925aeee3a1f5f1fac249c595cee0885..704abb39a3d01152eac3dfe313066834c3cd0a66 100644 --- a/release.nix +++ b/release.nix @@ -8,5 +8,7 @@ import lib/call-package.nix ({ callSpectrumPackage }: { checks = callSpectrumPackage release/checks {}; + updates = callSpectrumPackage release/update.nix {}; + combined = callSpectrumPackage release/combined/run-vm.nix {}; }) (_: {}) diff --git a/release/update.nix b/release/update.nix new file mode 100644 index 0000000000000000000000000000000000000000..18a91ac1eea56e9b2a941eb08244b3dee613b721 --- /dev/null +++ b/release/update.nix @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../lib/call-package.nix ( +{ callSpectrumPackage, config, runCommand, stdenv }: + +let + efi = import ../host/efi.nix {}; +in +runCommand "spectrum-update-directory" { + __structuredAttrs = true; + unsafeDiscardReferences = { out = true; }; + dontFixup = true; + env = { VERSION = config.version; }; +} '' + # stdenv sets -eo pipefail, but not -u + set -u + mkdir -- "$out" + cd -- "$out" + read -r roothash < ${efi.rootfs}/rootfs.verity.roothash + if ! [[ "$roothash" =~ ^[0-9a-f]{64}$ ]]; then + printf 'Internal error: bad root hash %q\n' "$roothash" + exit 1 + fi + cp -- ${efi} "Spectrum_$VERSION.efi" + cp -- ${efi.rootfs}/rootfs.verity.superblock "Spectrum_''${VERSION}_''${roothash:32:32}.verity" + cp -- ${efi.rootfs}/rootfs "Spectrum_''${VERSION}_''${roothash:0:32}.root" + sha256sum -b "Spectrum_$VERSION.efi" \ + "Spectrum_''${VERSION}_''${roothash:32:32}.verity" \ + "Spectrum_''${VERSION}_''${roothash:0:32}.root" > SHA256SUMS + '' +) (_: {}) -- 2.52.0
Include a new `spectrum-update` command to update the system. This tells the new sys.appvm-systemd-sysupdate VM to download the updates into a staging directory using systemd-sysupdate. The host then runs systemd-sysupdate to apply the updates itself. sys.appvm-systemd-sysupdate uses host-provided information to fetch the update. This allows editing files on the host to change the update URL and signing key. systemd-sysupdate requires /boot to be mounted so that systemd-sysupdate can update the unified kernel image. systemd-sysupdate also requires that /tmp is writable so that it can store temporary files, so put a tmpfs there. Furthermore, there needs to be a directory for storing downloaded updates. Create /home so that users can mount their persistent data there. The directory the VM downloads updates into is *not* reset (wiped) before or after the update. This allows the VM to know if the system is already up to date. Otherwise, it would redownload the entire multi-gigabyte update image. Updates are currently not compressed. This should be changed in the future, but it would add a small amount of additional complexity. In particular, the script generating the update directory would need to generate a SHA256SUMS containing the hash of both the compressed and uncompressed versions. More importantly, the VM must not be able to make the host use the compressed version. This would be a potential security risk because decompression happens before signature verification. GnuPG currently decompresses signatures, but in the future it will be replaced by Sequoia which does not. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Do not strip leading and trailing whitespace from update URLs. - Create a single script that does the work. Pass the paths to curl and systemd sysupdate to it as environment variables. Inline the awk script into it. - Rebase and fix merge conflict. Changes since v3: - Move builtins.path from lib/config.nix to host/rootfs/default.nix. - Change config options from "update-url" to "updateUrl" and "update-signing-key" to "updateSigningKey". Changes since v2: - Generate the transfer files in the guest, not the host. - Do not use an environment file. - Reject URLs that cannot work. - Escape sed metacharacters. - Escape backslashes for systemd-sysupdate. - Do not validate update URLs at build time, only at runtime. - Reject only update URLs that cannot possibly work. - Set the partition UUIDs according to systemd's recommendation. - Do not rely on finding partitions by label. - Strip leading and trailing whitespace from the update URL. - Rename the update command from `update` to `spectrum-update`. - Delete the list of steps. Replace it with comments in the script. The awk script in the VM rejects URLs that contain whitespace. This is because they can't work, and passing them to systemd-sysupdate would require figuring out how to escape them. Rejecting such bogus URLs is simpler than preventing them from being mangled. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 17 +++- host/rootfs/default.nix | 16 +++- host/rootfs/file-list.mk | 7 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++++ .../image/etc/vm-sysupdate.d/50-verity.transfer | 18 +++++ .../image/etc/vm-sysupdate.d/60-root.transfer | 18 +++++ .../image/etc/vm-sysupdate.d/70-kernel.transfer | 18 +++++ host/rootfs/image/usr/bin/spectrum-update | 92 ++++++++++++++++++++++ host/rootfs/os-release.in | 15 ++++ lib/config.default.nix | 2 + lib/fake-update-signing-key.gpg | 3 + vm/app/systemd-sysupdate/default.nix | 26 ++++++ vm/app/systemd-sysupdate/download-update | 68 ++++++++++++++++ 16 files changed, 355 insertions(+), 6 deletions(-) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index a6d9f23e9f5277b7c79a53105eb2dfe1bab1451e..74ff64019560aae6387df0e1b3409bc174251bdb 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -10,6 +10,7 @@ include file-list.mk ROOT_FS = build DIRS = \ + boot \ dev \ etc/s6-linux-init/env \ etc/s6-linux-init/run-image/configs \ @@ -33,13 +34,15 @@ DIRS = \ etc/s6-linux-init/run-image/vm/by-id \ etc/s6-linux-init/run-image/vm/by-name \ ext \ + home \ proc \ run \ - sys + sys \ + tmp FIFOS = etc/s6-linux-init/run-image/service/s6-svscan-log/fifo -BUILD_FILES = build/etc/s6-rc +BUILD_FILES = build/etc/s6-rc build/etc/os-release build/etc/update-url # This rule produces three files but Make only (portably) # supports one output per rule. Instead of resorting to temporary @@ -59,12 +62,22 @@ $(ROOT_FS_IMAGE): ../../scripts/make-erofs.sh $(PACKAGES_FILE) $(FILES) $(BUILD_ mkdir -p $(ROOT_FS) && \ { \ cat $(PACKAGES_FILE) ;\ + printf '%s\n%s\n' "$$UPDATE_SIGNING_KEY" /etc/systemd/import-pubring.gpg; \ for file in $(FILES) $(LINKS); do printf '%s\n%s\n' $$file "$${file#image/}"; done ;\ for file in $(BUILD_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ printf 'build/empty\n%s\n' $(DIRS) ;\ printf 'build/fifo\n%s\n' $(FIFOS) ;\ } | ../../scripts/make-erofs.sh $@ +build/etc/update-url: + mkdir -p build/etc + # might have metacharacters, so avoid interpolation + printf %s\\n "$${UPDATE_URL:?'update URL empty or missing'}" > build/etc/update-url + +build/etc/os-release: + mkdir -p build/etc + sed 's/@VERSION@/$(VERSION)/g' < os-release.in > build/etc/os-release + build/fifo: mkdir -p build mkfifo -m 0600 $@ diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 16a151971715f9a9d987dc92a1d06eb169de1144..8b62c78510fd4e41c2cd1e5075cc8fafc08fa415 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -13,6 +13,7 @@ pkgsStatic.callPackage ( , busybox, cloud-hypervisor, cryptsetup, dbus, execline, inkscape , iproute2, inotify-tools, jq, mdevd, s6, s6-linux-init, socat , util-linuxMinimal, virtiofsd, xorg, xdg-desktop-portal-spectrum-host +, btrfs-progs }: let @@ -33,8 +34,8 @@ let foot = pkgsGui.foot.override { allowPgo = false; }; packages = [ - cloud-hypervisor cryptsetup dbus execline inotify-tools iproute2 - jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools + btrfs-progs cloud-hypervisor cryptsetup dbus execline inotify-tools + iproute2 jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools util-linuxMinimal virtiofsd xdg-desktop-portal-spectrum-host (busybox.override { @@ -43,7 +44,7 @@ let }) # Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. - ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod systemd ]); + ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod ]); nixosAllHardware = nixos ({ modulesPath, ... }: { imports = [ (modulesPath + "/profiles/all-hardware.nix") ]; @@ -64,17 +65,19 @@ let # https://inbox.vuxu.org/musl/20251017-dlopen-use-rpath-of-caller-dso-v1-1-46c... usrPackages = [ appvm kernel.modules firmware netvm - ] ++ (with pkgsGui; [ dejavu_fonts kmod.lib mesa westonLite ]); + ] ++ (with pkgsGui; [ dejavu_fonts kmod.lib mesa westonLite systemd ]); appvms = { appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; appvm-foot = callSpectrumPackage ../../vm/app/foot.nix {}; appvm-gnome-text-editor = callSpectrumPackage ../../vm/app/gnome-text-editor.nix {}; + appvm-systemd-sysupdate = callSpectrumPackage ../../vm/app/systemd-sysupdate {}; }; packagesSysroot = runCommand "packages-sysroot" { depsBuildBuild = [ inkscape ]; nativeBuildInputs = [ xorg.lndir ]; + src = builtins.path { name = "os-release"; path = ./os-release.in; }; } '' mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps @@ -125,6 +128,11 @@ stdenvNoCC.mkDerivation { printf "%s\n/\n" ${packagesSysroot} >$out sed p ${writeClosure [ packagesSysroot] } >>$out ''; + UPDATE_SIGNING_KEY = builtins.path { + name = "signing-key"; + path = config.updateSigningKey; + }; + UPDATE_URL = config.updateUrl; VERSION = config.version; }; diff --git a/host/rootfs/file-list.mk b/host/rootfs/file-list.mk index 7625c54c0ae74ded2f3c9f4a860f21491f6e20a7..c08ecf0ab94a857fafc9ccdc9ea604885a57954f 100644 --- a/host/rootfs/file-list.mk +++ b/host/rootfs/file-list.mk @@ -37,13 +37,20 @@ FILES = \ image/etc/s6-linux-init/run-image/service/vmm/run \ image/etc/s6-linux-init/run-image/service/vmm/template/notification-fd \ image/etc/s6-linux-init/scripts/rc.init \ + image/etc/sysupdate.d/50-verity.transfer \ + image/etc/sysupdate.d/60-root.transfer \ + image/etc/sysupdate.d/70-kernel.transfer \ image/etc/udev/rules.d/99-spectrum.rules \ + image/etc/vm-sysupdate.d/50-verity.transfer \ + image/etc/vm-sysupdate.d/60-root.transfer \ + image/etc/vm-sysupdate.d/70-kernel.transfer \ image/etc/xdg/weston/autolaunch \ image/etc/xdg/weston/weston.ini \ image/usr/bin/assign-devices \ image/usr/bin/create-vm-dependencies \ image/usr/bin/run-appimage \ image/usr/bin/run-vmm \ + image/usr/bin/spectrum-update \ image/usr/bin/vm-console \ image/usr/bin/vm-import \ image/usr/bin/vm-start \ diff --git a/host/rootfs/image/etc/fstab b/host/rootfs/image/etc/fstab index 5dc9b2a3c4dff62ee49b2d827f53b45b7781a60f..6230d910a23339925fea0f2ffbc2baa5241ce3f2 100644 --- a/host/rootfs/image/etc/fstab +++ b/host/rootfs/image/etc/fstab @@ -4,3 +4,4 @@ proc /proc proc defaults 0 0 devpts /dev/pts devpts gid=5,mode=620 0 0 tmpfs /dev/shm tmpfs defaults 0 0 sysfs /sys sysfs defaults 0 0 +tmpfs /tmp tmpfs defaults 0 0 diff --git a/host/rootfs/image/etc/sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..9cd64b58ae55d55d378d99f5701f1ecef867e436 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v_@u.verity + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_@v.verity +MatchPartitionType=root-verity +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/60-root.transfer b/host/rootfs/image/etc/sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..cd12d2bd2b4ecd9bb5c7d26cc7c27a4bdb74cac8 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/60-root.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v_@u.root + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_@v +MatchPartitionType=root +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..e4190587a6bb127cb7315f38d59e48cf279318a4 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/EFI/Linux +PathRelativeTo=boot +MatchPattern=Spectrum_@v.efi +Mode=0644 +InstancesMax=2 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..ab4997c83605e3820a22b0b2178dcd76dfcf787e --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v_@u.verity + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v_@u.verity +Mode=0644 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer b/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..8a3175684f1697e0eca443eb0a1a97176e4f66d4 --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v_@u.root + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v_@u.root +Mode=0644 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..cb181239d71c5a6d0a5b3652d5534a23eda64183 --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v.efi +Mode=0644 diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update new file mode 100755 index 0000000000000000000000000000000000000000..613b43570d0538fce20296ccb1de2a6364e0df55 --- /dev/null +++ b/host/rootfs/image/usr/bin/spectrum-update @@ -0,0 +1,92 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { mkdir -p -m 0700 /run/updater } + +# Take a global lock to avoid races. +s6-setlock /run/update-lock + +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Delete any stale temporary files. Delete any existing signature + # files. If the VM is still running (it should not be), the VM might + # have write access to the directory. However, updates-dir-check is + # safe against that. + if { updates-dir-check cleanup shared } + + if { + foreground { + # TODO: suppress only "subvolume does not exist" errors. + redirfd -w 2 /dev/null + btrfs subvolume delete snapshot + } + rm -f snapshot + } + + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } + basename -- $id_path + } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. + + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + # + # In the future, this should use a bind mount instead of copying + # into a tmpfs. However, this would significantly complicate the + # cleanup code. Deleting fs/etc would require undoing the bind + # mounts instead of rm -rf. Once this code is in a separate mount + # namespace, the copies should be replaced by bind mounts. + if { + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } + umask 022 + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd + } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + # TODO: This is racy. If the update finishes before this code runs, + # the s6-svwait call will fail. + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} } + + # Remove the bind mount. + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Validate the update directory. Delete any stale temporary files. + # Check that a signature file was downloaded. + if { updates-dir-check check snapshot } + + unshare --mount + if { mount --bind -o ro -- snapshot /run/updater } + + /usr/lib/systemd/systemd-sysupdate update +} +importas -i sysupdate_exit_status ? +# Clean up. +foreground { btrfs subvolume delete -- snapshot } +exit $sysupdate_exit_status diff --git a/host/rootfs/os-release.in b/host/rootfs/os-release.in new file mode 100644 index 0000000000000000000000000000000000000000..d6e699e82f87dcb1c4656ac19d4e9986282f14a5 --- /dev/null +++ b/host/rootfs/os-release.in @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +NAME="Spectrum OS" +ID=spectrum +PRETTY_NAME="Spectrum @VERSION@" +VERSION=@VERSION@ +VERSION_ID=@VERSION@ +IMAGE_ID=spectrum-root +IMAGE_VERSION=@VERSION@ +RELEASE_TYPE=development +HOME_URL="https://spectrum-os.org" +BUG_REPORT_URL="mailto:discuss@spectrum-os.org" +ANSI_COLOR="1;34" +VENDOR_NAME=Spectrum +VENDOR_URL="https://spectrum-os.org" diff --git a/lib/config.default.nix b/lib/config.default.nix index 489c231490a8b66aa01f50053b25646060f7f963..f6b70fa5e8431bef79222c10c79e8015f7fe65be 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -5,4 +5,6 @@ pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; version = "0.0.0"; + updateUrl = "https://your-spectrum-os-update-server.invalid/download-directory"; + updateSigningKey = ./fake-update-signing-key.gpg; } diff --git a/lib/fake-update-signing-key.gpg b/lib/fake-update-signing-key.gpg new file mode 100644 index 0000000000000000000000000000000000000000..12e18f4c7c740e31692e1f1975282fa72ac1f2e3 --- /dev/null +++ b/lib/fake-update-signing-key.gpg @@ -0,0 +1,3 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +NOT A VALID KEY - UPDATES WILL NOT WORK diff --git a/vm/app/systemd-sysupdate/default.nix b/vm/app/systemd-sysupdate/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..69be0bab500ea2ea6cb3b6d71edbf1a3e7bddbba --- /dev/null +++ b/vm/app/systemd-sysupdate/default.nix @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../../lib/call-package.nix ( +{ callSpectrumPackage, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + downloadUpdate = builtins.path { + name = "download-update"; + path = ./download-update; + }; +in + +callSpectrumPackage ../../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = writeScript "run-script" '' +#!/usr/bin/env -S execlineb -WS0 +export CURL_PATH ${curl}/bin/curl +export SYSTEMD_SYSUPDATE_PATH ${systemd}/lib/systemd/systemd-sysupdate +${downloadUpdate} +''; +}) (_: {}) diff --git a/vm/app/systemd-sysupdate/download-update b/vm/app/systemd-sysupdate/download-update new file mode 100755 index 0000000000000000000000000000000000000000..eada41c6c8ad5edcedd9f4d76b76492e0b8be826 --- /dev/null +++ b/vm/app/systemd-sysupdate/download-update @@ -0,0 +1,68 @@ +#!/usr/bin/env -S execlineb -WS0 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +export LC_ALL C +export LANGUAGE C +if { mount -toverlay -olowerdir=/run/virtiofs/virtiofs0/etc:/etc -- overlay /etc } +backtick tmpdir { mktemp -d /tmp/sysupdate-XXXXXX } +# Not a useless use of cat: if there are NUL bytes in the URL +# busybox's awk might misbehave. +backtick update_url { cat /etc/update-url } +if { + backtick sed_rhs { + # Use awk to both validate the URL and to escape sed metacharacters. + # Reject URLs with control characters, query parameters, or fragments. + # They *cannot* work and so are rejected to produce better error messages. + # + # curl rejects control characters with "Malformed input to a URL function". + # Fragment specifiers ("#") and query parameters ("?") break concatenating + # /SHA256SUMS and /SHA256SUMS.sha256.asc onto the update URL. Also, it is + # simpler to reject update URLs that contain whitespace than to try to + # escape them. + # + # Backslash needs to be escaped once for systemd-sysupdate and again for sed. + # Ampersand needs to be escaped once for sed. + awk "BEGIN { + update_url = ENVIRON[\"update_url\"]; + if (update_url ~ /^[^\\001-\\040?#\\x7F]+$/) { + # Use & to avoid extra escaping (16 or 32 backslashes!) + # and a divergence between POSIX and GNU awk. + gsub(/\\\\/, \"&&&&\", update_url); + gsub(/&/, \"\\\\\\\\&\", update_url); + print update_url; + exit 0; + } else { + print ARGV[2] > \"/dev/stderr\"; + exit 100; + } + }" -- $3 + "Bad update URL from host: control characters, whitespace, query parameters, and fragment specifiers not allowed" + } + elglob -w -0 transfer_file_ /etc/vm-sysupdate.d/*.transfer + forx -E transfer_file { $transfer_file_ } + backtick target_basename { + basename -- $transfer_file + } + multisubstitute { + importas -iuS sed_rhs + importas -iuS target_basename + importas -iuS tmpdir + define sed_input $transfer_file + } + redirfd -w 1 ${tmpdir}/${target_basename} + sed -E -- "s#@UPDATE_URL@#${sed_rhs}#g" $sed_input +} +multisubstitute { + importas -iuS update_url + importas -iuS CURL_PATH + importas -iuS SYSTEMD_SYSUPDATE_PATH + importas -iuS tmpdir +} +if { $SYSTEMD_SYSUPDATE_PATH --definitions=${tmpdir} update } +# [ and ] are allowed in update URLs so that IPv6 addresses work, but +# they cause globbing in the curl command-line tool by default. Use --globoff +# to disable this feature. +if { $CURL_PATH -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS -- ${update_url}/SHA256SUMS } +$CURL_PATH -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS.sha256.asc -- ${update_url}/SHA256SUMS.sha256.asc -- 2.52.0
Demi Marie Obenour <demiobenour@gmail.com> writes:
diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index a6d9f23e9f5277b7c79a53105eb2dfe1bab1451e..74ff64019560aae6387df0e1b3409bc174251bdb 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -10,6 +10,7 @@ include file-list.mk ROOT_FS = build
DIRS = \ + boot \ dev \ etc/s6-linux-init/env \ etc/s6-linux-init/run-image/configs \ @@ -33,13 +34,15 @@ DIRS = \ etc/s6-linux-init/run-image/vm/by-id \ etc/s6-linux-init/run-image/vm/by-name \ ext \ + home \ proc \ run \ - sys + sys \ + tmp
FIFOS = etc/s6-linux-init/run-image/service/s6-svscan-log/fifo
-BUILD_FILES = build/etc/s6-rc +BUILD_FILES = build/etc/s6-rc build/etc/os-release build/etc/update-url
# This rule produces three files but Make only (portably) # supports one output per rule. Instead of resorting to temporary @@ -59,12 +62,22 @@ $(ROOT_FS_IMAGE): ../../scripts/make-erofs.sh $(PACKAGES_FILE) $(FILES) $(BUILD_ mkdir -p $(ROOT_FS) && \ { \ cat $(PACKAGES_FILE) ;\ + printf '%s\n%s\n' "$$UPDATE_SIGNING_KEY" /etc/systemd/import-pubring.gpg; \
Inconsistent use of shell variable instead of make macro.
for file in $(FILES) $(LINKS); do printf '%s\n%s\n' $$file "$${file#image/}"; done ;\ for file in $(BUILD_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ printf 'build/empty\n%s\n' $(DIRS) ;\ printf 'build/fifo\n%s\n' $(FIFOS) ;\ } | ../../scripts/make-erofs.sh $@
+build/etc/update-url: + mkdir -p build/etc + # might have metacharacters, so avoid interpolation + printf %s\\n "$${UPDATE_URL:?'update URL empty or missing'}" > build/etc/update-url
I'm learning so many shell parameter expansions I didn't know from you :)
diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update new file mode 100755 index 0000000000000000000000000000000000000000..613b43570d0538fce20296ccb1de2a6364e0df55 --- /dev/null +++ b/host/rootfs/image/usr/bin/spectrum-update @@ -0,0 +1,92 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { mkdir -p -m 0700 /run/updater } + +# Take a global lock to avoid races. +s6-setlock /run/update-lock + +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Delete any stale temporary files. Delete any existing signature + # files. If the VM is still running (it should not be), the VM might + # have write access to the directory. However, updates-dir-check is + # safe against that. + if { updates-dir-check cleanup shared } + + if { + foreground { + # TODO: suppress only "subvolume does not exist" errors. + redirfd -w 2 /dev/null + btrfs subvolume delete snapshot + } + rm -f snapshot + } + + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } + basename -- $id_path + } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. +
This still refers to a non-existent variable.
+ # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + # + # In the future, this should use a bind mount instead of copying + # into a tmpfs. However, this would significantly complicate the + # cleanup code. Deleting fs/etc would require undoing the bind + # mounts instead of rm -rf. Once this code is in a separate mount + # namespace, the copies should be replaced by bind mounts. + if { + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } + umask 022 + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd + } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + # TODO: This is racy. If the update finishes before this code runs, + # the s6-svwait call will fail. + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} } + + # Remove the bind mount. + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Validate the update directory. Delete any stale temporary files. + # Check that a signature file was downloaded. + if { updates-dir-check check snapshot } + + unshare --mount + if { mount --bind -o ro -- snapshot /run/updater } + + /usr/lib/systemd/systemd-sysupdate update
Why not just make a readonly snapshot? (btrfs subvolume snapshot -r)
diff --git a/vm/app/systemd-sysupdate/default.nix b/vm/app/systemd-sysupdate/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..69be0bab500ea2ea6cb3b6d71edbf1a3e7bddbba --- /dev/null +++ b/vm/app/systemd-sysupdate/default.nix @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../../lib/call-package.nix ( +{ callSpectrumPackage, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + downloadUpdate = builtins.path { + name = "download-update"; + path = ./download-update; + };
builtins.path is overkill here surely, as opposed to just writing ${./download-update} below?
+in + +callSpectrumPackage ../../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = writeScript "run-script" '' +#!/usr/bin/env -S execlineb -WS0
#!/bin/execlineb -WS0 would be fine — we know that'll exist in the VM.
diff --git a/vm/app/systemd-sysupdate/download-update b/vm/app/systemd-sysupdate/download-update new file mode 100755 index 0000000000000000000000000000000000000000..eada41c6c8ad5edcedd9f4d76b76492e0b8be826 --- /dev/null +++ b/vm/app/systemd-sysupdate/download-update @@ -0,0 +1,68 @@ +#!/usr/bin/env -S execlineb -WS0 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +export LC_ALL C +export LANGUAGE C +if { mount -toverlay -olowerdir=/run/virtiofs/virtiofs0/etc:/etc -- overlay /etc } +backtick tmpdir { mktemp -d /tmp/sysupdate-XXXXXX } +# Not a useless use of cat: if there are NUL bytes in the URL +# busybox's awk might misbehave. +backtick update_url { cat /etc/update-url } +if { + backtick sed_rhs { + # Use awk to both validate the URL and to escape sed metacharacters. + # Reject URLs with control characters, query parameters, or fragments. + # They *cannot* work and so are rejected to produce better error messages. + # + # curl rejects control characters with "Malformed input to a URL function". + # Fragment specifiers ("#") and query parameters ("?") break concatenating + # /SHA256SUMS and /SHA256SUMS.sha256.asc onto the update URL. Also, it is + # simpler to reject update URLs that contain whitespace than to try to + # escape them. + # + # Backslash needs to be escaped once for systemd-sysupdate and again for sed. + # Ampersand needs to be escaped once for sed. + awk "BEGIN { + update_url = ENVIRON[\"update_url\"]; + if (update_url ~ /^[^\\001-\\040?#\\x7F]+$/) { + # Use & to avoid extra escaping (16 or 32 backslashes!) + # and a divergence between POSIX and GNU awk. + gsub(/\\\\/, \"&&&&\", update_url); + gsub(/&/, \"\\\\\\\\&\", update_url); + print update_url; + exit 0; + } else { + print ARGV[2] > \"/dev/stderr\"; + exit 100; + } + }" -- $3 + "Bad update URL from host: control characters, whitespace, query parameters, and fragment specifiers not allowed" + } + elglob -w -0 transfer_file_ /etc/vm-sysupdate.d/*.transfer + forx -E transfer_file { $transfer_file_ } + backtick target_basename { + basename -- $transfer_file + } + multisubstitute { + importas -iuS sed_rhs + importas -iuS target_basename + importas -iuS tmpdir + define sed_input $transfer_file + }
You could avoid some serial substitution here if you wanted, by not passing -E to forx: forx transfer_file { $transfer_file_ } backtick target_basename { importas -iuS transfer_file basename -- $transfer_file } multisubstitute { … }
+ redirfd -w 1 ${tmpdir}/${target_basename} + sed -E -- "s#@UPDATE_URL@#${sed_rhs}#g" $sed_input
Using awk to escape stuff for sed seems a bit Rube Goldberg. Would it make more sense to just do the replacement in the awk program? Actually a lot of this might be nicer in awk than execline? Feel free to tell me to leave it this way for now, though.
+} +multisubstitute { + importas -iuS update_url + importas -iuS CURL_PATH + importas -iuS SYSTEMD_SYSUPDATE_PATH + importas -iuS tmpdir +} +if { $SYSTEMD_SYSUPDATE_PATH --definitions=${tmpdir} update } +# [ and ] are allowed in update URLs so that IPv6 addresses work, but +# they cause globbing in the curl command-line tool by default. Use --globoff +# to disable this feature. +if { $CURL_PATH -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS -- ${update_url}/SHA256SUMS } +$CURL_PATH -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS.sha256.asc -- ${update_url}/SHA256SUMS.sha256.asc
Much easier to understand now. Thanks!
On 11/28/25 08:47, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index a6d9f23e9f5277b7c79a53105eb2dfe1bab1451e..74ff64019560aae6387df0e1b3409bc174251bdb 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -10,6 +10,7 @@ include file-list.mk ROOT_FS = build
DIRS = \ + boot \ dev \ etc/s6-linux-init/env \ etc/s6-linux-init/run-image/configs \ @@ -33,13 +34,15 @@ DIRS = \ etc/s6-linux-init/run-image/vm/by-id \ etc/s6-linux-init/run-image/vm/by-name \ ext \ + home \ proc \ run \ - sys + sys \ + tmp
FIFOS = etc/s6-linux-init/run-image/service/s6-svscan-log/fifo
-BUILD_FILES = build/etc/s6-rc +BUILD_FILES = build/etc/s6-rc build/etc/os-release build/etc/update-url
# This rule produces three files but Make only (portably) # supports one output per rule. Instead of resorting to temporary @@ -59,12 +62,22 @@ $(ROOT_FS_IMAGE): ../../scripts/make-erofs.sh $(PACKAGES_FILE) $(FILES) $(BUILD_ mkdir -p $(ROOT_FS) && \ { \ cat $(PACKAGES_FILE) ;\ + printf '%s\n%s\n' "$$UPDATE_SIGNING_KEY" /etc/systemd/import-pubring.gpg; \
Inconsistent use of shell variable instead of make macro.
for file in $(FILES) $(LINKS); do printf '%s\n%s\n' $$file "$${file#image/}"; done ;\ for file in $(BUILD_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ printf 'build/empty\n%s\n' $(DIRS) ;\ printf 'build/fifo\n%s\n' $(FIFOS) ;\ } | ../../scripts/make-erofs.sh $@
+build/etc/update-url: + mkdir -p build/etc + # might have metacharacters, so avoid interpolation + printf %s\\n "$${UPDATE_URL:?'update URL empty or missing'}" > build/etc/update-url
I'm learning so many shell parameter expansions I didn't know from you :)
diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update new file mode 100755 index 0000000000000000000000000000000000000000..613b43570d0538fce20296ccb1de2a6364e0df55 --- /dev/null +++ b/host/rootfs/image/usr/bin/spectrum-update @@ -0,0 +1,92 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { mkdir -p -m 0700 /run/updater } + +# Take a global lock to avoid races. +s6-setlock /run/update-lock + +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Delete any stale temporary files. Delete any existing signature + # files. If the VM is still running (it should not be), the VM might + # have write access to the directory. However, updates-dir-check is + # safe against that. + if { updates-dir-check cleanup shared } + + if { + foreground { + # TODO: suppress only "subvolume does not exist" errors. + redirfd -w 2 /dev/null + btrfs subvolume delete snapshot + } + rm -f snapshot + } + + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } + basename -- $id_path + } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. +
This still refers to a non-existent variable.
+ # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + # + # In the future, this should use a bind mount instead of copying + # into a tmpfs. However, this would significantly complicate the + # cleanup code. Deleting fs/etc would require undoing the bind + # mounts instead of rm -rf. Once this code is in a separate mount + # namespace, the copies should be replaced by bind mounts. + if { + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } + umask 022 + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd + } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + # TODO: This is racy. If the update finishes before this code runs, + # the s6-svwait call will fail. + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} } + + # Remove the bind mount. + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Validate the update directory. Delete any stale temporary files. + # Check that a signature file was downloaded. + if { updates-dir-check check snapshot } + + unshare --mount + if { mount --bind -o ro -- snapshot /run/updater } + + /usr/lib/systemd/systemd-sysupdate update
Why not just make a readonly snapshot? (btrfs subvolume snapshot -r)
The checker will delete any temporary files it comes across, so it needs write access. A snapshot is much heavier than a bind mount and isn't automatically cleaned up.
diff --git a/vm/app/systemd-sysupdate/default.nix b/vm/app/systemd-sysupdate/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..69be0bab500ea2ea6cb3b6d71edbf1a3e7bddbba --- /dev/null +++ b/vm/app/systemd-sysupdate/default.nix @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../../lib/call-package.nix ( +{ callSpectrumPackage, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + downloadUpdate = builtins.path { + name = "download-update"; + path = ./download-update; + };
builtins.path is overkill here surely, as opposed to just writing ${./download-update} below?
${./download-update} includes the working directory in the Nix store hash, which means that renaming your source tree forces an unnecessary rebuild. builtins.path is the standard way to avoid this.
+in + +callSpectrumPackage ../../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = writeScript "run-script" '' +#!/usr/bin/env -S execlineb -WS0
#!/bin/execlineb -WS0 would be fine — we know that'll exist in the VM.
Will fix.
diff --git a/vm/app/systemd-sysupdate/download-update b/vm/app/systemd-sysupdate/download-update new file mode 100755 index 0000000000000000000000000000000000000000..eada41c6c8ad5edcedd9f4d76b76492e0b8be826 --- /dev/null +++ b/vm/app/systemd-sysupdate/download-update @@ -0,0 +1,68 @@ +#!/usr/bin/env -S execlineb -WS0 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +export LC_ALL C +export LANGUAGE C +if { mount -toverlay -olowerdir=/run/virtiofs/virtiofs0/etc:/etc -- overlay /etc } +backtick tmpdir { mktemp -d /tmp/sysupdate-XXXXXX } +# Not a useless use of cat: if there are NUL bytes in the URL +# busybox's awk might misbehave. +backtick update_url { cat /etc/update-url } +if { + backtick sed_rhs { + # Use awk to both validate the URL and to escape sed metacharacters. + # Reject URLs with control characters, query parameters, or fragments. + # They *cannot* work and so are rejected to produce better error messages. + # + # curl rejects control characters with "Malformed input to a URL function". + # Fragment specifiers ("#") and query parameters ("?") break concatenating + # /SHA256SUMS and /SHA256SUMS.sha256.asc onto the update URL. Also, it is + # simpler to reject update URLs that contain whitespace than to try to + # escape them. + # + # Backslash needs to be escaped once for systemd-sysupdate and again for sed. + # Ampersand needs to be escaped once for sed. + awk "BEGIN { + update_url = ENVIRON[\"update_url\"]; + if (update_url ~ /^[^\\001-\\040?#\\x7F]+$/) { + # Use & to avoid extra escaping (16 or 32 backslashes!) + # and a divergence between POSIX and GNU awk. + gsub(/\\\\/, \"&&&&\", update_url); + gsub(/&/, \"\\\\\\\\&\", update_url); + print update_url; + exit 0; + } else { + print ARGV[2] > \"/dev/stderr\"; + exit 100; + } + }" -- $3 + "Bad update URL from host: control characters, whitespace, query parameters, and fragment specifiers not allowed" + } + elglob -w -0 transfer_file_ /etc/vm-sysupdate.d/*.transfer + forx -E transfer_file { $transfer_file_ } + backtick target_basename { + basename -- $transfer_file + } + multisubstitute { + importas -iuS sed_rhs + importas -iuS target_basename + importas -iuS tmpdir + define sed_input $transfer_file + }
You could avoid some serial substitution here if you wanted, by not passing -E to forx:
forx transfer_file { $transfer_file_ } backtick target_basename { importas -iuS transfer_file basename -- $transfer_file } multisubstitute { … }
I considered it and decided that the extra define in the multisubstitute was cheaper than the extra importas process.
+ redirfd -w 1 ${tmpdir}/${target_basename} + sed -E -- "s#@UPDATE_URL@#${sed_rhs}#g" $sed_input
Using awk to escape stuff for sed seems a bit Rube Goldberg. Would it make more sense to just do the replacement in the awk program? Actually a lot of this might be nicer in awk than execline? Feel free to tell me to leave it this way for now, though.
I'd prefer to leave it this way for now. Maybe add a TODO to clean this up.
+} +multisubstitute { + importas -iuS update_url + importas -iuS CURL_PATH + importas -iuS SYSTEMD_SYSUPDATE_PATH + importas -iuS tmpdir +} +if { $SYSTEMD_SYSUPDATE_PATH --definitions=${tmpdir} update } +# [ and ] are allowed in update URLs so that IPv6 addresses work, but +# they cause globbing in the curl command-line tool by default. Use --globoff +# to disable this feature. +if { $CURL_PATH -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS -- ${update_url}/SHA256SUMS } +$CURL_PATH -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS.sha256.asc -- ${update_url}/SHA256SUMS.sha256.asc
Much easier to understand now. Thanks!
You're welcome! -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/28/25 08:47, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update new file mode 100755 index 0000000000000000000000000000000000000000..613b43570d0538fce20296ccb1de2a6364e0df55 --- /dev/null +++ b/host/rootfs/image/usr/bin/spectrum-update @@ -0,0 +1,92 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { mkdir -p -m 0700 /run/updater } + +# Take a global lock to avoid races. +s6-setlock /run/update-lock + +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Delete any stale temporary files. Delete any existing signature + # files. If the VM is still running (it should not be), the VM might + # have write access to the directory. However, updates-dir-check is + # safe against that. + if { updates-dir-check cleanup shared } + + if { + foreground { + # TODO: suppress only "subvolume does not exist" errors. + redirfd -w 2 /dev/null + btrfs subvolume delete snapshot + } + rm -f snapshot + } + + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } + basename -- $id_path + } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. +
This still refers to a non-existent variable.
+ # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + # + # In the future, this should use a bind mount instead of copying + # into a tmpfs. However, this would significantly complicate the + # cleanup code. Deleting fs/etc would require undoing the bind + # mounts instead of rm -rf. Once this code is in a separate mount + # namespace, the copies should be replaced by bind mounts. + if { + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } + umask 022 + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd + } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + # TODO: This is racy. If the update finishes before this code runs, + # the s6-svwait call will fail. + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} } + + # Remove the bind mount. + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Validate the update directory. Delete any stale temporary files. + # Check that a signature file was downloaded. + if { updates-dir-check check snapshot } + + unshare --mount + if { mount --bind -o ro -- snapshot /run/updater } + + /usr/lib/systemd/systemd-sysupdate update
Why not just make a readonly snapshot? (btrfs subvolume snapshot -r)
The checker will delete any temporary files it comes across, so it needs write access. A snapshot is much heavier than a bind mount and isn't automatically cleaned up.
Okay, but why do we need to block systemd-sysupdate from writing to this directory, but not to anywhere else on the system?
diff --git a/vm/app/systemd-sysupdate/default.nix b/vm/app/systemd-sysupdate/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..69be0bab500ea2ea6cb3b6d71edbf1a3e7bddbba --- /dev/null +++ b/vm/app/systemd-sysupdate/default.nix @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../../lib/call-package.nix ( +{ callSpectrumPackage, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + downloadUpdate = builtins.path { + name = "download-update"; + path = ./download-update; + };
builtins.path is overkill here surely, as opposed to just writing ${./download-update} below?
${./download-update} includes the working directory in the Nix store hash, which means that renaming your source tree forces an unnecessary rebuild. builtins.path is the standard way to avoid this.
Oh, I didn't realise it included the whole directory rather than just the file name. Very justified then.
On 11/28/25 15:41, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/28/25 08:47, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update new file mode 100755 index 0000000000000000000000000000000000000000..613b43570d0538fce20296ccb1de2a6364e0df55 --- /dev/null +++ b/host/rootfs/image/usr/bin/spectrum-update @@ -0,0 +1,92 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { mkdir -p -m 0700 /run/updater } + +# Take a global lock to avoid races. +s6-setlock /run/update-lock + +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Delete any stale temporary files. Delete any existing signature + # files. If the VM is still running (it should not be), the VM might + # have write access to the directory. However, updates-dir-check is + # safe against that. + if { updates-dir-check cleanup shared } + + if { + foreground { + # TODO: suppress only "subvolume does not exist" errors. + redirfd -w 2 /dev/null + btrfs subvolume delete snapshot + } + rm -f snapshot + } + + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } + basename -- $id_path + } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. +
This still refers to a non-existent variable.
Whoops!
+ # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + # + # In the future, this should use a bind mount instead of copying + # into a tmpfs. However, this would significantly complicate the + # cleanup code. Deleting fs/etc would require undoing the bind + # mounts instead of rm -rf. Once this code is in a separate mount + # namespace, the copies should be replaced by bind mounts. + if { + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } + umask 022 + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd + } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + # TODO: This is racy. If the update finishes before this code runs, + # the s6-svwait call will fail. + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} } + + # Remove the bind mount. + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Validate the update directory. Delete any stale temporary files. + # Check that a signature file was downloaded. + if { updates-dir-check check snapshot } + + unshare --mount + if { mount --bind -o ro -- snapshot /run/updater } + + /usr/lib/systemd/systemd-sysupdate update
Why not just make a readonly snapshot? (btrfs subvolume snapshot -r)
The checker will delete any temporary files it comes across, so it needs write access. A snapshot is much heavier than a bind mount and isn't automatically cleaned up.
Okay, but why do we need to block systemd-sysupdate from writing to this directory, but not to anywhere else on the system?
We don't. I'm fine with a writable mount. Either should work.
diff --git a/vm/app/systemd-sysupdate/default.nix b/vm/app/systemd-sysupdate/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..69be0bab500ea2ea6cb3b6d71edbf1a3e7bddbba --- /dev/null +++ b/vm/app/systemd-sysupdate/default.nix @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../../lib/call-package.nix ( +{ callSpectrumPackage, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + downloadUpdate = builtins.path { + name = "download-update"; + path = ./download-update; + };
builtins.path is overkill here surely, as opposed to just writing ${./download-update} below?
${./download-update} includes the working directory in the Nix store hash, which means that renaming your source tree forces an unnecessary rebuild. builtins.path is the standard way to avoid this.
Oh, I didn't realise it included the whole directory rather than just the file name. Very justified then.
Thanks! Also, changing the file name shouldn't cause a rebuild if the contents don't change. -- Sincerely, Demi Marie Obenour (she/her/hers)
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/28/25 15:41, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/28/25 08:47, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update new file mode 100755 index 0000000000000000000000000000000000000000..613b43570d0538fce20296ccb1de2a6364e0df55 --- /dev/null +++ b/host/rootfs/image/usr/bin/spectrum-update @@ -0,0 +1,92 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { mkdir -p -m 0700 /run/updater } + +# Take a global lock to avoid races. +s6-setlock /run/update-lock + +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Delete any stale temporary files. Delete any existing signature + # files. If the VM is still running (it should not be), the VM might + # have write access to the directory. However, updates-dir-check is + # safe against that. + if { updates-dir-check cleanup shared } + + if { + foreground { + # TODO: suppress only "subvolume does not exist" errors. + redirfd -w 2 /dev/null + btrfs subvolume delete snapshot + } + rm -f snapshot + } + + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } + basename -- $id_path + } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. +
This still refers to a non-existent variable.
Whoops!
+ # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + # + # In the future, this should use a bind mount instead of copying + # into a tmpfs. However, this would significantly complicate the + # cleanup code. Deleting fs/etc would require undoing the bind + # mounts instead of rm -rf. Once this code is in a separate mount + # namespace, the copies should be replaced by bind mounts. + if { + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } + umask 022 + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd + } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + # TODO: This is racy. If the update finishes before this code runs, + # the s6-svwait call will fail. + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} } + + # Remove the bind mount. + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Validate the update directory. Delete any stale temporary files. + # Check that a signature file was downloaded. + if { updates-dir-check check snapshot } + + unshare --mount + if { mount --bind -o ro -- snapshot /run/updater } + + /usr/lib/systemd/systemd-sysupdate update
Why not just make a readonly snapshot? (btrfs subvolume snapshot -r)
The checker will delete any temporary files it comes across, so it needs write access. A snapshot is much heavier than a bind mount and isn't automatically cleaned up.
Okay, but why do we need to block systemd-sysupdate from writing to this directory, but not to anywhere else on the system?
We don't. I'm fine with a writable mount. Either should work.
Then let's keep things simple and drop the unshare and mount. We can deprivilege it properly in the future.
On 11/28/25 16:08, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/28/25 15:41, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/28/25 08:47, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update new file mode 100755 index 0000000000000000000000000000000000000000..613b43570d0538fce20296ccb1de2a6364e0df55 --- /dev/null +++ b/host/rootfs/image/usr/bin/spectrum-update @@ -0,0 +1,92 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { mkdir -p -m 0700 /run/updater } + +# Take a global lock to avoid races. +s6-setlock /run/update-lock + +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Delete any stale temporary files. Delete any existing signature + # files. If the VM is still running (it should not be), the VM might + # have write access to the directory. However, updates-dir-check is + # safe against that. + if { updates-dir-check cleanup shared } + + if { + foreground { + # TODO: suppress only "subvolume does not exist" errors. + redirfd -w 2 /dev/null + btrfs subvolume delete snapshot + } + rm -f snapshot + } + + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } + basename -- $id_path + } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. +
This still refers to a non-existent variable.
Whoops!
+ # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + # + # In the future, this should use a bind mount instead of copying + # into a tmpfs. However, this would significantly complicate the + # cleanup code. Deleting fs/etc would require undoing the bind + # mounts instead of rm -rf. Once this code is in a separate mount + # namespace, the copies should be replaced by bind mounts. + if { + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } + umask 022 + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd + } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + # TODO: This is racy. If the update finishes before this code runs, + # the s6-svwait call will fail. + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} } + + # Remove the bind mount. + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Validate the update directory. Delete any stale temporary files. + # Check that a signature file was downloaded. + if { updates-dir-check check snapshot } + + unshare --mount + if { mount --bind -o ro -- snapshot /run/updater } + + /usr/lib/systemd/systemd-sysupdate update
Why not just make a readonly snapshot? (btrfs subvolume snapshot -r)
The checker will delete any temporary files it comes across, so it needs write access. A snapshot is much heavier than a bind mount and isn't automatically cleaned up.
Okay, but why do we need to block systemd-sysupdate from writing to this directory, but not to anywhere else on the system?
We don't. I'm fine with a writable mount. Either should work.
Then let's keep things simple and drop the unshare and mount. We can deprivilege it properly in the future.
The mount is absolutely necessary: it ensures that the update is available where the .transfer file says it will be. It just doesn't need to be read-only. The unshare is there to allow trivial cleanup by the kernel once systemd-sysupdate exits. -- Sincerely, Demi Marie Obenour (she/her/hers)
On Fri, Nov 28, 2025 at 04:28:20PM -0500, Demi Marie Obenour wrote:
On 11/28/25 16:08, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/28/25 15:41, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
On 11/28/25 08:47, Alyssa Ross wrote:
Demi Marie Obenour <demiobenour@gmail.com> writes:
> diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update > new file mode 100755 > index 0000000000000000000000000000000000000000..613b43570d0538fce20296ccb1de2a6364e0df55 > --- /dev/null > +++ b/host/rootfs/image/usr/bin/spectrum-update > @@ -0,0 +1,92 @@ > +#!/bin/execlineb -WS1 > +# SPDX-License-Identifier: EUPL-1.2+ > +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> > + > +if { mkdir -p -m 0700 /run/updater } > + > +# Take a global lock to avoid races. > +s6-setlock /run/update-lock > + > +foreground { redirfd -w 2 /dev/null rmdir -- $1 } > +if { umask 0077 mkdir -p -- $1 } > +cd $1 > +foreground { > + # If this exists already that is okay. > + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } > + > + # Delete any stale temporary files. Delete any existing signature > + # files. If the VM is still running (it should not be), the VM might > + # have write access to the directory. However, updates-dir-check is > + # safe against that. > + if { updates-dir-check cleanup shared } > + > + if { > + foreground { > + # TODO: suppress only "subvolume does not exist" errors. > + redirfd -w 2 /dev/null > + btrfs subvolume delete snapshot > + } > + rm -f snapshot > + } > + > + backtick -E update_vm_id { > + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } > + basename -- $id_path > + } > + > + # $fsdir is read-only to the guest, but read-write to the host. > + # Directories bind-mounted into it are read-write to the guest. > + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run > + # for details. > +
This still refers to a non-existent variable.
Whoops!
> + # Set up /etc with what the VM needs. The VM will overlay this > + # on its own /etc. > + # > + # In the future, this should use a bind mount instead of copying > + # into a tmpfs. However, this would significantly complicate the > + # cleanup code. Deleting fs/etc would require undoing the bind > + # mounts instead of rm -rf. Once this code is in a separate mount > + # namespace, the copies should be replaced by bind mounts. > + if { > + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } > + umask 022 > + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } > + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } > + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd > + } > + > + # If the directory is already mounted, unmount it. This prevents a > + # confusing error from mount. > + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates } > + > + # Share the update directory with the VM. > + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } > + > + # Start the update VM. > + if { vm-start $update_vm_id } > + > + # Wait for the VM to exit. > + # TODO: This is racy. If the update finishes before this code runs, > + # the s6-svwait call will fail. > + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} } > + > + # Remove the bind mount. > + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } > + > + # Ensure that the VM cannot change the directory > + # while systemd-sysupdate is using it. > + if { btrfs subvolume snapshot -- shared snapshot } > + > + # Validate the update directory. Delete any stale temporary files. > + # Check that a signature file was downloaded. > + if { updates-dir-check check snapshot } > + > + unshare --mount > + if { mount --bind -o ro -- snapshot /run/updater } > + > + /usr/lib/systemd/systemd-sysupdate update
Why not just make a readonly snapshot? (btrfs subvolume snapshot -r)
The checker will delete any temporary files it comes across, so it needs write access. A snapshot is much heavier than a bind mount and isn't automatically cleaned up.
Okay, but why do we need to block systemd-sysupdate from writing to this directory, but not to anywhere else on the system?
We don't. I'm fine with a writable mount. Either should work.
Then let's keep things simple and drop the unshare and mount. We can deprivilege it properly in the future.
The mount is absolutely necessary: it ensures that the update is available where the .transfer file says it will be. It just doesn't need to be read-only.
The unshare is there to allow trivial cleanup by the kernel once systemd-sysupdate exits.
Ah, then I was misreading. If the mount has to be there, it might as well be read-only. With the outdated comment removed: Reviewed-by: Alyssa Ross <hi@alyssa.is>
The documentation previously stated that updates were not possible without reinstalling. This is still the case by default, but it is possible for developers to enable updates for images they build. Update the documentaion to reflect this. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Move the documentation from the user section to the developer section. Changes since v2: - Move the documentation on how to enable updates to the part on build configuration. - Clarify what happens if an update is interrupted. - Move details to a technical note. - Link to systemd-sysupdate. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/development/build-configuration.adoc | 15 ++++++++ Documentation/development/index.adoc | 2 ++ Documentation/development/updates.adoc | 42 ++++++++++++++++++++++ Documentation/installation/index.adoc | 6 +++- 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/Documentation/development/build-configuration.adoc b/Documentation/development/build-configuration.adoc index 545aa8c05ac40a101b5ee280015cde7ec4f3a66f..49651d05890900b74cafb3d75945b3bcc5b86ce6 100644 --- a/Documentation/development/build-configuration.adoc +++ b/Documentation/development/build-configuration.adoc @@ -20,6 +20,21 @@ The configuration file should contain an attribute set. See https://spectrum-os.org/git/spectrum/tree/lib/config.default.nix[lib/config.default.nix] for supported configuration attributes and their default values. +To enable updates, you need to specify a version, an update URL, and an update signing key. +By default, the update URL is set to a .invalid domain and the update signing key is +an invalid key. Therefore, updates will not work. To enable updates, provide a valid key +and update server URL. + +Spectrum uses https://www.freedesktop.org/software/systemd/man/latest/systemd-sysupdate.html[systemd-sysupdate], +so see the https://www.freedesktop.org/software/systemd/man/latest/sysupdate.d.html[sysupdate.d] +documentation for what you need to put on your server. Building +https://spectrum-os.org/git/spectrum/tree/release/updates.nix[release/updates.nix] produces an +directory that is compatible with systemd-sysupdate, except that the signature +(`SHA256SUMS.sha256.asc`) is missing. + +Updates are signed, so the worst a compromised update +server can do is fill up your user data partition. + .config.nix to build Spectrum with a https://nixos.org/manual/nixpkgs/unstable/#sec-overlays-definition[Nixpkgs overlay] [example] [source,nix] diff --git a/Documentation/development/index.adoc b/Documentation/development/index.adoc index 6b48418ba218354ee0493cd82188c54141f63e9e..4e504253dc16286273e1af5cae9614789b2c4a12 100644 --- a/Documentation/development/index.adoc +++ b/Documentation/development/index.adoc @@ -18,6 +18,8 @@ Spectrum is free software, currently under active development. TIP: For information on writing guidelines, see xref:../contributing/writing_documentation.adoc[Documentation Style Guide]. +If you want to update Spectrum without reinstalling, see how to +xref:updates.adoc[Enable updates]. == Mailing Lists diff --git a/Documentation/development/updates.adoc b/Documentation/development/updates.adoc new file mode 100644 index 0000000000000000000000000000000000000000..8746f97e5d9b36d4960a64544af08f57ff89ce9a --- /dev/null +++ b/Documentation/development/updates.adoc @@ -0,0 +1,42 @@ += Updating the OS +:page-parent: Development + +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +// SPDX-License-Identifier: GFDL-1.3-no-invariants-or-later OR CC-BY-SA-4.0 + +Right now, there is no official update server or update signing key. +However, it is possible to run your own update server. See +xref:../development/build-configuration.adoc[build configuration] +for how to enable updates for your own Spectrum images. + +== Updating the system + +If you have built your image with updates enabled, you can update the +system using the `spectrum-update` command. This takes the path to a +staging directory as argument. This directory must be on a BTRFS +filesystem. It is strongly recommended to not use this directory +for any other purpose. However, it's safe to rename the directory +and use `spectrum-update` with the new path afterwards. + +If there is a problem with the update, it's safe to try again. +If that still doesn't work, you can delete the directory and +try again with an empty one. This will cause `spectrum-update` +to download the latest version even if it is already installed, but +is otherwise harmless. + +Updates are atomic and take effect after the system reboots. +If the system is rebooted, crashes, or loses power during an +update, the update will not take effect. It is safe to resume +an interrupted update. + +Since Spectrum's host has no network access, the VM that does the +updates (`sys.appvm-systemd-sysupdate`) is given a BTRFS subvolume to +write the updates into. It uses `systemd-sysupdate` to download the updates +into this directory. Once it exits, the host snapshots this directory and +checks it for malicious filenames or non-regular files. If the check +passes, this directory is used as the source for `systemd-sysupdate`, +which installs the updates to the OS volume and EFI system partition. + +See the documentation of +https://www.freedesktop.org/software/systemd/man/systemd-sysupdate.html[systemd-sysupdate]. +for some of the details. diff --git a/Documentation/installation/index.adoc b/Documentation/installation/index.adoc index d67c88dda062066c19c3b21e699f074cc18a6dbc..d1df2edc9b0ca902824ff729eec139270fb40777 100644 --- a/Documentation/installation/index.adoc +++ b/Documentation/installation/index.adoc @@ -18,6 +18,10 @@ development. == Uninstalling and Updating -Currently, there is no implementation for a software update. +Software updates are a work in progress and are not currently available. + +If you built Spectrum yourself, see +xref:../development/build-configuration.adoc[Build configuration] for how +to enable updates for it. You can replace Spectrum by installing another OS. -- 2.52.0
Wrong values for the version or update URL will cause very confusing build-time or runtime errors. Provide a better user experience by validating them up-front. The update URL validator is loose. It rejects only URLs that cannot possibly work: either appending /SHA256SUMS to them doesn't append to the path, or they will definitely be rejected by curl due to being malformed. The version validator is in lib/config.nix, as the version number is used in many places. It checks that the version only uses characters that are permitted by systemd's version number specification [1] and that will not break code that uses them in shell or sed commands. [1]: https://uapi-group.org/specifications/specs/version_format_specification Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Drop compression level. - Centralize validation. - Use camelCase for Nix identifiers. - Clean up formatting. Changes since v3: - Validate compression level. Changes since v2: - Use loose URL validation: allow anything that might work. - Only reject versions that violate the specification. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- lib/config.nix | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/config.nix b/lib/config.nix index e437cdbe9aa22dd0f9c8d7052ac331c8fccf6ce6..e641642de07c1549e69fc12e91c4e80e2f82d035 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -17,6 +17,31 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; + # Use builtins.fromJSON because it supports \uXXXX escapes. + # This is the same regex used by check-url.awk in the update VM. + # The update code is careful to escape any metacharacters, but some + # simply cannot be made to work. Concatenating the URL with /SHA256SUMS + # must append to the path portion of the URL, and the URL must be one + # that libcurl will accept. + urlRegex = builtins.fromJSON "\"^[^\\u0001- #?\\u007F]+$\""; in -default // callConfig config +# Version is used in many files, so validate it here. +# See https://uapi-group.org/specifications/specs/version_format_specification +# for allowed version strings. +if builtins.match "[[:alnum:]_.~^-]+" finalConfig.version == null then + builtins.abort '' + Version ${builtins.toJSON finalConfig.version} has forbidden characters. + Only ASCII alphanumerics, ".", "_", "~", "^", "+", and "-" are allowed. + See <https://uapi-group.org/specifications/specs/version_format_specification>. + '' +else +if builtins.match urlRegex finalConfig.updateUrl == null then + builtins.abort '' + Update URL ${builtins.toJSON finalConfig.updateUrl} has forbidden characters. + Query strings, and fragment specifiers are not supported. + ASCII control characters and whitespace must be %-encoded. + '' +else +finalConfig -- 2.52.0
This implements updates via systemd-sysupdate. See individual commit messages for details. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes in v6: - Remove build system changes that are not needed by the updater. - Rely on Alyssa's patches for partition size control. - Minor changes to individual patches. - Drop link to patchset that has already been applied. - Link to v5: https://spectrum-os.org/lists/archives/spectrum-devel/20251126-updates-v5-0-... Changes in v5: - Fix broken shell.nix files in intermediate patches. - See individual patches messages for more details. - Link to v4: https://spectrum-os.org/lists/archives/spectrum-devel/20251121-updates-v4-0-... Changes in v4: - Fix build errors in intermediate patches. - Apply suggestions from code review. - Link to v3: https://spectrum-os.org/lists/archives/spectrum-devel/20251119-updates-v3-0-... Changes in v3: - See individual commits for details. There are too many to mention here. - Link to v2: https://spectrum-os.org/lists/archives/spectrum-devel/20251112-updates-v2-0-... Changes in v2: - updates-dir-check: - Do not check that there is a SHA256SUMS or SHA256SUMS.gpg file in the update directory. systemd-sysupdate will fail if it cannot find a manifest or its signature. - Follow symlinks in opening the directory. The path is from a trusted source and will always point to a BTRFS snapshot, never a symlink. The only exception is the last component, which is still checked to not be a symlink. - VM: - Link SHA256SUMS.sha256.asc to SHA256SUMS.gpg. Recent systemd-sysupdate seems to use the former name. - Get update URL from host. - Use an execline script instead of a shell script. - Update script: - Unmount shared directory if already mounted. This avoids errors when mounting it again. - Delete old snapshot if present. - Provide the VM information with a different directory layout. - Do not bind-mount the information passed into the VM into the shared VM folder. Instead rely on this folder being read-only to the guest. This is enforced by a read-only bind mount in virtiofs's mount namespace. - Testing: - Lots of manual update testing. - Disable the test for the live image as it doesn't work anymore. - Nix: - Move validation to a separate low-priority patch. - Documentation: - Document that updating the system is now possible. - Installer: - Remove the "Try Spectrum" button. - Link to v1: https://spectrum-os.org/lists/archives/spectrum-devel/20251029-updates-v1-0-... --- Demi Marie Obenour (8): tools: Add directory checker for updates release: Compress installation images and remove live image Use OS version to set partition labels and UKI name Add B partitions to installation images release: Create directory with system update Support updates via systemd-sysupdate Documentation: Update support Validate configuration parameters Documentation/development/build-configuration.adoc | 15 +++ Documentation/development/index.adoc | 2 + Documentation/development/updates.adoc | 42 +++++++ Documentation/development/uuid-reference.adoc | 8 ++ Documentation/installation/getting-spectrum.adoc | 44 ++++--- Documentation/installation/index.adoc | 6 +- host/initramfs/Makefile | 12 +- host/initramfs/etc/probe | 20 --- host/initramfs/shell.nix | 2 + host/rootfs/Makefile | 21 +++- host/rootfs/default.nix | 21 +++- host/rootfs/file-list.mk | 7 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++ .../image/etc/vm-sysupdate.d/50-verity.transfer | 18 +++ .../image/etc/vm-sysupdate.d/60-root.transfer | 18 +++ .../image/etc/vm-sysupdate.d/70-kernel.transfer | 18 +++ host/rootfs/image/usr/bin/spectrum-update | 87 +++++++++++++ host/rootfs/os-release.in | 15 +++ host/rootfs/shell.nix | 2 + lib/config.default.nix | 3 + lib/config.nix | 27 ++++- lib/fake-update-signing-key.gpg | 3 + release.nix | 2 + release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ----- release/combined/eosimages.nix | 8 +- release/combined/grub.cfg.in | 5 - release/installer/run-vm.nix | 2 +- release/live/Makefile | 8 +- release/live/default.nix | 3 + release/live/shell.nix | 4 +- release/update.nix | 33 +++++ tools/default.nix | 1 + tools/meson.build | 4 + tools/updates-dir-check.c | 134 +++++++++++++++++++++ vm/app/systemd-sysupdate/default.nix | 26 ++++ vm/app/systemd-sysupdate/download-update | 68 +++++++++++ 40 files changed, 681 insertions(+), 100 deletions(-) --- base-commit: d76e9b29aea9f31238d07e21db50d3fe6a80da5a change-id: 20250928-updates-92e99849e231 prerequisite-patch-id: 930207e73dcd127b5288db63b7e6e1a9cded1d1a prerequisite-patch-id: 85ec04609f9a90ee9ad21d743655386fe39cfd5e -- Sincerely, Demi Marie Obenour (she/her/hers)
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either. Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. systemd-sysupdate can leave behind temporary files with names starting with '.', so delete them instead of failing. Linux can lose cache coherency if there is an I/O error, so call syncfs() on the directory before checking anything. For the same reason, fsync() the directory if any hidden files were deleted. The directory checker also serves another critical function: it checks if the VM actually downloaded anything. Otherwise, network problems could cause updates to silently do nothing. Specifically, it checks that the VM provided a file starting with the prefix "SHA256SUMS.". These will be the last ones the in-VM updater downloads. An additional mode is provided to clean out all such files. This will be used to ensure that before the in-VM updater runs, no such files are present. Hence, if the VM didn't actually download anything, the user will get a clear error instead of a false success message or a confusing error. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> Reviewed-by: Alyssa Ross <hi@alyssa.is> --- Changes since v2: - Purge leftover temporary files rather than returning an error. - Split into two modes: one that deletes signature files, and one that checks that at least one signature file exists. This allows checking that the VM actually sent something. --- tools/default.nix | 1 + tools/meson.build | 4 ++ tools/updates-dir-check.c | 134 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+) diff --git a/tools/default.nix b/tools/default.nix index 7cb7dc5b72b8394f5383c80ccf110fec55c44f21..da82f075fdba4655bd964ba35e819d669deff3f1 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -77,6 +77,7 @@ stdenv.mkDerivation (finalAttrs: { ./sd-notify-adapter.c ./start-vmm ./subprojects + ./updates-dir-check.c ] ++ lib.optionals driverSupport [ ./xdp-forwarder ])); diff --git a/tools/meson.build b/tools/meson.build index bfa290e891fafa2d03eabb221121b5df4d83fb29..666483b3304224fce9110a2788456955a2d71305 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -33,6 +33,10 @@ if get_option('host') install: true) subdir('start-vmm') + + executable('updates-dir-check', 'updates-dir-check.c', + c_args : '-D_GNU_SOURCE', + install: true) endif if get_option('build') diff --git a/tools/updates-dir-check.c b/tools/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..83af806bebf36754f8c794b04933bf6021338c38 --- /dev/null +++ b/tools/updates-dir-check.c @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> +#include <unistd.h> + +#include <err.h> + +[[noreturn]] static void bad_char(char c, char *msg_component) +{ + if (c >= 0x20 && c <= 0x7E) + errx(EXIT_FAILURE, "Forbidden %s character in filename: '%c'", + msg_component, c); + errx(EXIT_FAILURE, + "Forbidden %s character in filename: byte 0x%hhx", + msg_component, c); +} + +[[noreturn]] static void usage(void) +{ + errx(EXIT_FAILURE, "Usage: updates-dir-check [cleanup|check] DIRECTORIES..."); +} + +static void checkdir(int fd, bool check_sig) +{ + bool found_sig = false; + DIR *d = fdopendir(fd); + if (d == NULL) + err(EXIT_FAILURE, "fdopendir"); + // If there is an I/O error while there are dirty pages outstanding, + // the dirty pages are silently discarded. This means that the contents + // of the filesystem can change behind userspace's back. Flush all + // dirty pages in the filesystem with the directory to prevent this. + if (syncfs(fd) != 0) + err(EXIT_FAILURE, "syncfs"); + bool changed = false; + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(EXIT_FAILURE, "readdir"); + break; + } + const char *ptr = entry->d_name; + if (ptr[0] == '.') { + if (ptr[1] == '\0') + continue; + if (ptr[1] == '.' && ptr[2] == '\0') + continue; + // systemd-sysupdate uses these for temporary files. + // It normally cleans them up itself, but if there is an error + // it does not always clean them up. I'm not sure if it is + // guaranteed to clean up temporary files from a past run, so + // delete them instead of returning an error. + if (unlinkat(fd, ptr, 0)) + err(EXIT_FAILURE, "Failed to unlink temporary file"); + changed = true; + continue; + } + char c = ptr[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + bad_char(c, "initial"); + while ((c = *++ptr)) { + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) + bad_char(c, "subsequent"); + } + // Empty filenames are rejected as having a bad initial character, + // and POSIX forbids them from being returned anyway. Therefore, + // this cannot be out of bounds. + if (ptr[-1] == '.') + errx(EXIT_FAILURE, "Filename %s ends with a '.'", entry->d_name); + if (entry->d_type == DT_UNKNOWN) + errx(EXIT_FAILURE, "Filesystem didn't report type of file %s", entry->d_name); + if (entry->d_type != DT_REG) + errx(EXIT_FAILURE, "Entry contains non-regular file %s", entry->d_name); + if (strncmp(entry->d_name, "SHA256SUMS.", sizeof("SHA256SUMS.") - 1) == 0) { + // Found a signature file! + if (check_sig) + found_sig = true; + else { + if (unlinkat(fd, entry->d_name, 0)) + err(EXIT_FAILURE, "Unlinking old signature file"); + changed = true; + } + } + } + // If a change was made, enforcing cache coherency also requires + // another fsync() call. This is again because Linux can discard + // changes if there is an I/O error. + if (changed && fsync(fd)) + errx(EXIT_FAILURE, "fsync"); + if (check_sig && !found_sig) { + warnx("sys.appvm-systemd-sysupdate didn't send a signature file."); + warnx("There was probably a problem downloading the update."); + errx(EXIT_FAILURE, "Check its logs for more information."); + } + closedir(d); +} + +int main(int argc, char **argv) +{ + if (argc != 3) + usage(); + + bool check_sig; + if (strcmp(argv[1], "cleanup") == 0) + check_sig = false; + else if (strcmp(argv[1], "check") == 0) + check_sig = true; + else + usage(); + + for (int i = 2; i < argc; ++i) { + int fd = open(argv[i], O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + err(EXIT_FAILURE, "open(%s)", argv[i]); + checkdir(fd, check_sig); + } + return 0; +} -- 2.52.0
This patch has been committed as 7b3ef2a1a6cdab9e761ffd1ce8ec498687f47375, which can be viewed online at https://spectrum-os.org/git/spectrum/commit/?id=7b3ef2a1a6cdab9e761ffd1ce8ec.... This is an automated message. Send comments/questions/requests to: Alyssa Ross <hi@alyssa.is>
systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, the partitions need to be very large so that there is plenty of room for the OS to grow. Furthermore, systemd-sysupdate requires both A and B copies of both the root and verity partitions. mkfs.ext4 is not able to produce images with files large enough to hold both the primary and backup copy of the root partition [1]. Reducing the sizes of partitions to be little greater than the size of the root filesystem image does not help. The produced file is still too large. Therefore, compress the image, which causes it to be small enough that mkfs.ext4 can handle it. This breaks the option to use the installer as a live image. Therefore, remove it. This option will return once Spectrum switches to the GNOME OS installer [2]. However, it is still possible to build a live image that is separate from the installer. Document how to build and use it. GRUB2 does support compressed loopback images, but these presumably buffer the whole image in memory. Since the entire installer will be replaced, making it work is not considered worthwhile. [1]: https://github.com/tytso/e2fsprogs/issues/254 [2]: https://spectrum-os.org/lists/archives/spectrum-devel/87wm4dlkhz.fsf@alyssa.... Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> Reviewed-by: Alyssa Ross <hi@alyssa.is> --- Changes since v5; - Rebase. - Add Reviewed-by. - Switch from compression 9 to 1 as per off-list discussion. Changes since v4: - Replace a mention of "Try Spectrum" with a mention of the live image. - Combine instructions for building an installer and a live image. - Drop the config option for the compression level. - Drop unnecessary quoting changes. - Drop unnecessary 'set -euo pipefail'. Changes since v3: - Make the compression level configurable. The default is 1 so that development builds finish in a reasonable amount of time. Release builds should use compression level 9. Changes since v2: - Remove live image test instead of skipping it. - Document the change. - Document that there is still a live image available, though it is separate from the installer. - Document how to build the live image. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/installation/getting-spectrum.adoc | 44 ++++++++++++++++-------- host/initramfs/Makefile | 8 ----- host/initramfs/etc/probe | 20 ----------- release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ---------------- release/combined/eosimages.nix | 8 ++--- release/combined/grub.cfg.in | 5 --- 7 files changed, 35 insertions(+), 81 deletions(-) diff --git a/Documentation/installation/getting-spectrum.adoc b/Documentation/installation/getting-spectrum.adoc index 29803aa324b196119a03b22d7f1e2d7730e2c1eb..22c1fe310b56adecaac5ed7e87decbfd56881919 100644 --- a/Documentation/installation/getting-spectrum.adoc +++ b/Documentation/installation/getting-spectrum.adoc @@ -23,6 +23,17 @@ documentation for https://nix.dev/manual/nix/2.24/advanced-topics/cores-vs-jobs.html[tuning cores and jobs]. +To get Spectrum, run + +[source,shell] +---- +git clone https://spectrum-os.org/git/spectrum +---- + +Then follow one of the instructions below. If you haven't set up the +xref:binary-cache.adoc[binary cache], all of the following Nix commands +will take a very long time. + == Trying Spectrum If you want to try Spectrum out to get a feel for it, without @@ -31,27 +42,30 @@ applications. [source,shell] ---- -git clone https://spectrum-os.org/git/spectrum cd spectrum/host/rootfs nix-shell --run 'make run' ---- -This builds just enough of Spectrum to try it out in a VM, but it will -still take a very long time. +This builds just enough of Spectrum to try it out in a VM. == Installing Spectrum To install Spectrum on a computer, you can use a USB drive as a -bootable Spectrum installer device. When booting a system from the -installer device, you will be able to choose whether to try out -Spectrum without installing it on your system (as a live image), or to -install it to your computer's internal storage. +bootable Spectrum installer device. You will need to choose whether to +try out Spectrum without installing it on your system (as a live image), +or to install it to your computer's internal storage. -First, you need to build the Spectrum image: +To build a live image, run: + +[source,shell] +---- +nix-build spectrum/release/live +---- + +To build an installer, run: [source,shell] ---- -git clone https://spectrum-os.org/git/spectrum nix-build spectrum/release/combined ---- @@ -61,15 +75,17 @@ will take a very long time. When it's done, a symbolic link named installer image. Write that image to a USB drive, for example using `dd` (command line) or https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available -in Nixpkgs). Boot your system from the USB drive, and you should see -a menu allowing you to choose between "Try Spectrum" and "Install -Spectrum". +in Nixpkgs). Then boot your system from the USB drive + +If you built a live image, Spectrum should be ready for you to use. +If you built an installer, you should see a menu allowing you to +"Install Spectrum". NOTE: While it's possible to install Spectrum to your internal storage, at this point in Spectrum's development there is not much reason to, as OS updates are not yet implemented, and persistent -storage is not yet exposed to VMs. Using the "Try Spectrum" option to -boot Spectrum will let you try out everything in Spectrum, without +storage is not yet exposed to VMs. Using a live image to boot +Spectrum will let you try out everything in Spectrum, without having to go through the additional step of reinstalling Spectrum every time you want to use a newer version. diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 5e75b7c95a5309b7c980fc32f482be1240d8b8f4..8a93c8d27dd8c097888bc418900eedbe36cd65a3 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -41,14 +41,6 @@ build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scr $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ -build/loop.tar: build/live.img - $(TAR) -cf $@ build/live.img - -build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 - ../../scripts/make-gpt.sh $@.tmp \ - build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e - mv $@.tmp $@ - clean: rm -rf build .PHONY: clean diff --git a/host/initramfs/etc/probe b/host/initramfs/etc/probe index 4cbd00db52c1a7128b5c619a43d415675feaee0b..013092b6dcc5b82db7302c1ae7e6d8a4f5a0b802 100755 --- a/host/initramfs/etc/probe +++ b/host/initramfs/etc/probe @@ -2,26 +2,6 @@ # SPDX-License-Identifier: EUPL-1.2+ # SPDX-FileCopyrightText: 2021 Alyssa Ross <hi@alyssa.is> -if -n { - # If this is a Spectrum installer eosimages partition, we might be - # booting from the installer, and should loopback mount the images. - importas -i mdev MDEV - if { - backtick -E type { lsblk -lnpo PARTTYPE $mdev } - test $type = 56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e - } - if { - forx -pE module { ext4 loop } - modprobe $module - } - backtick -E uuid { lsblk -lnpo PARTUUID $mdev } - if { mkdir -p /mnt/${uuid} } - if { mount $mdev /mnt/${uuid} } - find /mnt/${uuid} -name *.img -exec - losetup -Pf {} - ; -} - # Check whether we now have all the partitions we need to boot. importas -i rootfs_uuid ROOTFS_UUID diff --git a/release/checks/integration/meson.build b/release/checks/integration/meson.build index 7214e47ba1ec23c247c8b76e5c8d94aff1ce1fd6..7bf8f51e4c762d2279ed6064ae1a87cb9b07494c 100644 --- a/release/checks/integration/meson.build +++ b/release/checks/integration/meson.build @@ -11,7 +11,7 @@ run_qemu = find_program('../../../scripts/run-qemu.sh') lib = static_library('spectrum-integration-test', 'lib.c') -foreach test : ['appimage', 'late-serial', 'networking', 'portal', 'try'] +foreach test : ['appimage', 'late-serial', 'networking', 'portal'] test(test, executable(test, test + '.c', link_with : lib), timeout : 400, args : [run_qemu]) diff --git a/release/checks/integration/try.c b/release/checks/integration/try.c deleted file mode 100644 index 4b874c0a7e9b48324497450fb5488e04576fd43b..0000000000000000000000000000000000000000 --- a/release/checks/integration/try.c +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is> - -#include "lib.h" - -#include <stdlib.h> -#include <string.h> - -void test(struct config c) -{ - struct vm *vm; - - c.drives.img = getenv_or_die("COMBINED_PATH"); - - vm = start_qemu(c); - - start_console_thread(vm, "GNU GRUB "); - wait_for_prompt(vm); - - start_console_thread(vm, "~ # "); - - // Assume that Try Spectrum is the first menu entry. - if (fputc('\n', vm_console_writer(vm)) == EOF) { - fputs("error writing to console\n", stderr); - exit(EXIT_FAILURE); - } - - wait_for_prompt(vm); -} diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..47d56a7fb4e9e1e9482d07bffc22e43527895b24 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -14,9 +14,9 @@ runCommand "eosimages.img" { } '' mkdir dir cd dir - ln -s $image $imageName - sha256sum $imageName > $imageName.sha256 - tar -chf $NIX_BUILD_TOP/eosimages.tar * - tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out + ln -s -- $image $imageName + gzip -1 < $image > $imageName.gz + sha256sum -- $imageName.gz > $imageName.gz.sha256 + tar -ch -- $imageName.gz $imageName.gz.sha256 | tar2ext4 -o $out e2label $out eosimages '') (_: {}) diff --git a/release/combined/grub.cfg.in b/release/combined/grub.cfg.in index a8e73a3b4dc0d643cf575e3cc545ec9ff72380cb..a22f5fc96ba6451d44c0f9768a15a1f48c5dce1c 100644 --- a/release/combined/grub.cfg.in +++ b/release/combined/grub.cfg.in @@ -15,11 +15,6 @@ set gfxpayload=keep terminal_output gfxterm terminal_output console -menuentry "Try Spectrum" { - loopback live (hd0,gpt3)/Spectrum-0.0-x86_64-generic.0.Live.img - chainloader (live,gpt1)/EFI/Linux/spectrum.efi -} - menuentry "Install Spectrum" { set root=(hd0,gpt2) linux @linux@ @kernelParams@ -- 2.52.0
This patch has been committed as 7a7c08084cb6a7199da1808afb58c7fec6b8c7f9, which can be viewed online at https://spectrum-os.org/git/spectrum/commit/?id=7a7c08084cb6a7199da1808afb58.... This is an automated message. Send comments/questions/requests to: Alyssa Ross <hi@alyssa.is>
systemd-sysupdate has strict requirements on the partition layout: - The label of the active partition must match the template in the .transfer file. For instance, the root filesystem of Spectrum 0.0.0 must be in a partition with label "Spectrum_0.0.0", and the verity partition must have the label "Spectrum_0.0.0.verity". - The label of the inactive partition must be that of the old version of Spectrum, or "_empty" for freshly installed systems. - The partition type UUID must conform to the Discoverable Partition Specification. Also, the UKI must have a name that includes the OS version. Otherwise, it will not be deleted during updates. Since the partition label includes the OS version, add an OS version number. Use 0.0.0 to indicate that Spectrum OS is still in very early development and should not be used. The version number can be overridden in the build configuration file. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> Reviewed-by: Alyssa Ross <hi@alyssa.is> --- Changes since v5: - Rebase and address merge conflicts. Changes since v4: - Rebase and address merge conflicts. - Add missing "VERSION = config.version" in Nix files. Changes since v2: - Split off into separate commit. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/initramfs/Makefile | 4 ++-- host/initramfs/shell.nix | 2 ++ host/rootfs/Makefile | 4 ++-- host/rootfs/default.nix | 5 +++-- host/rootfs/shell.nix | 2 ++ lib/config.default.nix | 1 + release/live/Makefile | 6 +++--- release/live/default.nix | 3 +++ release/live/shell.nix | 4 +++- 9 files changed, 21 insertions(+), 10 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 8a93c8d27dd8c097888bc418900eedbe36cd65a3..89f9a39b431022ec24476869e6ad10b11337fb1e 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -37,8 +37,8 @@ build/mountpoints: build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)"):Spectrum_'$(VERSION).verity' \ + $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))"):Spectrum_'$(VERSION)' mv $@.tmp $@ clean: diff --git a/host/initramfs/shell.nix b/host/initramfs/shell.nix index 8b47aa53bc19a818ebf563e281f22e82202a8ea5..44d4a985e969c1a57ad42d0666189c704aef9afd 100644 --- a/host/initramfs/shell.nix +++ b/host/initramfs/shell.nix @@ -4,6 +4,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, rootfs, pkgsStatic, stdenv , cryptsetup, jq, qemu_kvm, tar2ext4, util-linux +, config }: let @@ -18,5 +19,6 @@ initramfs.overrideAttrs ({ nativeBuildInputs ? [], env ? {}, ... }: { env = env // { KERNEL = "${rootfs.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; ROOT_FS = rootfs; + VERSION = config.version; }; })) (_: {}) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 5e902552ae22af3a353b8cbc7430329ef3794c79..065722d48951a17182ed94e168796700652db7b9 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -98,8 +98,8 @@ clean: build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/verity-timestamp $(ROOT_FS_IMAGES) ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)"):Spectrum_'$(VERSION).verity' \ + $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))"):Spectrum_'$(VERSION)' mv $@.tmp $@ debug: diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 4bbbe23ff14a2c3337ec96b36c74c3aa6569f1d1..4fe9058abdfaa1df7d63b84a629708d4d99388f4 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -3,8 +3,8 @@ # SPDX-FileCopyrightText: 2022 Unikie import ../../lib/call-package.nix ( -{ callSpectrumPackage, spectrum-build-tools, src -, pkgsMusl, inkscape, linux_latest, xorg +{ callSpectrumPackage, config, spectrum-build-tools +, src, pkgsMusl, inkscape, linux_latest, xorg }: pkgsMusl.callPackage ( @@ -118,6 +118,7 @@ stdenvNoCC.mkDerivation { printf "%s\n/\n" ${packagesSysroot} >$out sed p ${writeClosure [ packagesSysroot] } >>$out ''; + VERSION = config.version; }; # The Makefile uses $(ROOT_FS), not $(dest), so it can share code diff --git a/host/rootfs/shell.nix b/host/rootfs/shell.nix index 6df2f575fdfc7cdf8067ccfdb5fecaad9f6ea5e6..27f93e05fce036257d27cf9992fee8c925073f80 100644 --- a/host/rootfs/shell.nix +++ b/host/rootfs/shell.nix @@ -5,6 +5,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, rootfs, pkgsStatic, srcOnly, stdenv , btrfs-progs, cryptsetup, jq, netcat, qemu_kvm, reuse, util-linux +, config }: rootfs.overrideAttrs ( @@ -20,5 +21,6 @@ rootfs.overrideAttrs ( KERNEL = "${passthru.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; LINUX_SRC = srcOnly passthru.kernel.configfile; VMLINUX = "${passthru.kernel.dev}/vmlinux"; + VERSION = config.version; }; })) (_: {}) diff --git a/lib/config.default.nix b/lib/config.default.nix index a8422345cc00f9413bb19ec968fd89c82fed801b..489c231490a8b66aa01f50053b25646060f7f963 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -4,4 +4,5 @@ { pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; + version = "0.0.0"; } diff --git a/release/live/Makefile b/release/live/Makefile index 7ec1bfe36e930ed68b97fdf2dcf3caa7274b1317..e856f4ccb841fd003d419955d79669fac2593cd6 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -10,8 +10,8 @@ dest = build/live.img $(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)"):Spectrum_'$(VERSION).verity' \ + $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))"):Spectrum_'$(VERSION)' mv $@.tmp $@ build/boot.fat: $(SYSTEMD_BOOT_EFI) $(SPECTRUM_EFI) @@ -19,7 +19,7 @@ build/boot.fat: $(SYSTEMD_BOOT_EFI) $(SPECTRUM_EFI) $(TRUNCATE) -s 440401920 $@ $(MKFS_FAT) $@ $(MMD) -i $@ ::/EFI ::/EFI/BOOT ::/EFI/Linux - $(MCOPY) -i $@ $(SPECTRUM_EFI) ::/EFI/Linux/spectrum.efi + $(MCOPY) -i $@ $(SPECTRUM_EFI) ::/EFI/Linux/'Spectrum_$(VERSION).efi' $(MCOPY) -i $@ $(SYSTEMD_BOOT_EFI) ::/EFI/BOOT/$(EFINAME) clean: diff --git a/release/live/default.nix b/release/live/default.nix index 3b5fa061a5c3a745cc88c3811a1229622ab140ad..aa8dddd68497ced322961a6b551b73fe5ab4c475 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -1,11 +1,13 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2021-2023, 2025 Alyssa Ross <hi@alyssa.is> # SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src , lib, pkgsStatic, stdenvNoCC , cryptsetup, dosfstools, jq, mtools, util-linux +, config }: let @@ -43,6 +45,7 @@ stdenv.mkDerivation { SYSTEMD_BOOT_EFI = "${efi.systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; SPECTRUM_EFI = efi; EFINAME = "BOOT${toUpper efiArch}.EFI"; + VERSION = config.version; }; buildFlags = [ "dest=$(out)" ]; diff --git a/release/live/shell.nix b/release/live/shell.nix index 79cfe3a267236bd91a73b2c07ee6e274a662a123..e542793a66fb972cfde90f6be2204986442b7d4b 100644 --- a/release/live/shell.nix +++ b/release/live/shell.nix @@ -1,7 +1,8 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> -import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm }: +import ../../lib/call-package.nix ( +{ callSpectrumPackage, config, stdenv, qemu_kvm }: let efi = callSpectrumPackage ../../host/efi.nix {}; @@ -16,6 +17,7 @@ in OVMF_CODE = "${qemu_kvm}/share/qemu/edk2-${stdenv.hostPlatform.qemuArch}-code.fd"; ROOT_FS = efi.rootfs; EFI_IMAGE = efi; + VERSION = config.version; }; } )) (_: {}) -- 2.52.0
This patch has been committed as cd83e2fb84187c50a3ac9cc3b003822f5b2b4bc7, which can be viewed online at https://spectrum-os.org/git/spectrum/commit/?id=cd83e2fb84187c50a3ac9cc3b003.... This is an automated message. Send comments/questions/requests to: Alyssa Ross <hi@alyssa.is>
systemd-sysupdate never writes to the running OS partition. Instead, it requires a separate partition to write the update into. Create a separate partition for that purpose. systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, make the partitions very large so that there is plenty of room for the OS to grow. This requires rewriting the code that calculates the partition sizes. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> Reviewed-by: Alyssa Ross <hi@alyssa.is> --- Changes since v5: - Rebase with old patches missing. Changes since v4: - Leave B partitions empty. - Leave MiB unit implicit. - Use fixed GUIDs for blank partitions. Changes since v2: - Make into a standalone commit - Do not rely on separate script to generate the images. - Use a smaller size for the verity partition. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/development/uuid-reference.adoc | 8 ++++++++ release/installer/run-vm.nix | 2 +- release/live/Makefile | 6 ++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Documentation/development/uuid-reference.adoc b/Documentation/development/uuid-reference.adoc index 146615896104d5ab20c2e9353e5ed8f7a3dc54a6..16279c8a7e690bbaafdc3e0194f3130ba65c281c 100644 --- a/Documentation/development/uuid-reference.adoc +++ b/Documentation/development/uuid-reference.adoc @@ -59,6 +59,14 @@ Spectrum combined live system / installer image. The Spectrum installer system. +=== `18f2ccff-92f1-4bb1-a80e-24f76ecda90c` + +The not-yet-used B verity partition. + +=== `ec0c5ff3-f6b1-4adf-82b4-61336c4d135f` + +The not-yet-used B root filesystem partition. + ''' == Finding Undocumented UUIDs diff --git a/release/installer/run-vm.nix b/release/installer/run-vm.nix index 37b470cba890f65c3b45e743edcaaed7396e5d3b..b9907f7b6e9b502eaf4255304e3a8efb3e3632d7 100644 --- a/release/installer/run-vm.nix +++ b/release/installer/run-vm.nix @@ -25,7 +25,7 @@ in writeShellScript "run-spectrum-installer-vm.sh" '' export PATH=${makeBinPath [ coreutils qemu_kvm ]} img="$(mktemp spectrum-installer-target.XXXXXXXXXX.img)" - truncate -s 20G "$img" + truncate -s 40G "$img" exec 3<>"$img" rm -f "$img" exec ${../../scripts/run-qemu.sh} -cpu max -m 4G \ diff --git a/release/live/Makefile b/release/live/Makefile index e856f4ccb841fd003d419955d79669fac2593cd6..64d74aeadb8c26de73d892be3a1fb0f54a450d08 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -10,8 +10,10 @@ dest = build/live.img $(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)"):Spectrum_'$(VERSION).verity' \ - $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))"):Spectrum_'$(VERSION)' + $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)"):Spectrum_'$(VERSION).verity:162' \ + $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))"):Spectrum_'$(VERSION):20000' \ + /dev/null:verity:18f2ccff-92f1-4bb1-a80e-24f76ecda90c:_empty:162 \ + /dev/null:root:ec0c5ff3-f6b1-4adf-82b4-61336c4d135f:_empty:20000 mv $@.tmp $@ build/boot.fat: $(SYSTEMD_BOOT_EFI) $(SPECTRUM_EFI) -- 2.52.0
This patch has been committed as 316877d9b2e40cdccece14c1b09e97bd45bd158d, which can be viewed online at https://spectrum-os.org/git/spectrum/commit/?id=316877d9b2e40cdccece14c1b09e.... This is an automated message. Send comments/questions/requests to: Alyssa Ross <hi@alyssa.is>
Whenever a release is made, create a directory with the release files to be used for an update. After its SHA256SSUMS file is signed, the file is ready to be uploaded to a server for users to update from. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> Reviewed-by: Alyssa Ross <hi@alyssa.is> --- Changes since v4: - Only set -u because stdenv sets the reset. Update comment. Changes since v2: - Use UUIDs to name the rootfs and verity superblock. This will allow systemd-sysupdate to set the correct UUIDs on the rootfs and verity partitions, avoiding the need to use labels to find these partitions. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release.nix | 2 ++ release/update.nix | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/release.nix b/release.nix index a4fe66ee5925aeee3a1f5f1fac249c595cee0885..704abb39a3d01152eac3dfe313066834c3cd0a66 100644 --- a/release.nix +++ b/release.nix @@ -8,5 +8,7 @@ import lib/call-package.nix ({ callSpectrumPackage }: { checks = callSpectrumPackage release/checks {}; + updates = callSpectrumPackage release/update.nix {}; + combined = callSpectrumPackage release/combined/run-vm.nix {}; }) (_: {}) diff --git a/release/update.nix b/release/update.nix new file mode 100644 index 0000000000000000000000000000000000000000..18a91ac1eea56e9b2a941eb08244b3dee613b721 --- /dev/null +++ b/release/update.nix @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../lib/call-package.nix ( +{ callSpectrumPackage, config, runCommand, stdenv }: + +let + efi = import ../host/efi.nix {}; +in +runCommand "spectrum-update-directory" { + __structuredAttrs = true; + unsafeDiscardReferences = { out = true; }; + dontFixup = true; + env = { VERSION = config.version; }; +} '' + # stdenv sets -eo pipefail, but not -u + set -u + mkdir -- "$out" + cd -- "$out" + read -r roothash < ${efi.rootfs}/rootfs.verity.roothash + if ! [[ "$roothash" =~ ^[0-9a-f]{64}$ ]]; then + printf 'Internal error: bad root hash %q\n' "$roothash" + exit 1 + fi + cp -- ${efi} "Spectrum_$VERSION.efi" + cp -- ${efi.rootfs}/rootfs.verity.superblock "Spectrum_''${VERSION}_''${roothash:32:32}.verity" + cp -- ${efi.rootfs}/rootfs "Spectrum_''${VERSION}_''${roothash:0:32}.root" + sha256sum -b "Spectrum_$VERSION.efi" \ + "Spectrum_''${VERSION}_''${roothash:32:32}.verity" \ + "Spectrum_''${VERSION}_''${roothash:0:32}.root" > SHA256SUMS + '' +) (_: {}) -- 2.52.0
This patch has been committed as 49f33a139249b6270d3a9491fc0ba343c637ab42, which can be viewed online at https://spectrum-os.org/git/spectrum/commit/?id=49f33a139249b6270d3a9491fc0b.... This is an automated message. Send comments/questions/requests to: Alyssa Ross <hi@alyssa.is>
Include a new `spectrum-update` command to update the system. This tells the new sys.appvm-systemd-sysupdate VM to download the updates into a staging directory using systemd-sysupdate. The host then runs systemd-sysupdate to apply the updates itself. sys.appvm-systemd-sysupdate uses host-provided information to fetch the update. This allows editing files on the host to change the update URL and signing key. systemd-sysupdate requires /boot to be mounted so that systemd-sysupdate can update the unified kernel image. systemd-sysupdate also requires that /tmp is writable so that it can store temporary files, so put a tmpfs there. Furthermore, there needs to be a directory for storing downloaded updates. Create /home so that users can mount their persistent data there. The directory the VM downloads updates into is *not* reset (wiped) before or after the update. This allows the VM to know if the system is already up to date. Otherwise, it would redownload the entire multi-gigabyte update image. Updates are currently not compressed. This should be changed in the future, but it would add a small amount of additional complexity. In particular, the script generating the update directory would need to generate a SHA256SUMS containing the hash of both the compressed and uncompressed versions. More importantly, the VM must not be able to make the host use the compressed version. This would be a potential security risk because decompression happens before signature verification. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> Reviewed-by: Alyssa Ross <hi@alyssa.is> --- Changes since v5: - Remove statement about GnuPG that does not apply to the version in Nixpkgs. - Delete stale comment. - Rebase and fix merge conflict. Changes since v4: - Do not strip leading and trailing whitespace from update URLs. - Create a single script that does the work. Pass the paths to curl and systemd sysupdate to it as environment variables. Inline the awk script into it. - Rebase and fix merge conflict. Changes since v3: - Move builtins.path from lib/config.nix to host/rootfs/default.nix. - Change config options from "update-url" to "updateUrl" and "update-signing-key" to "updateSigningKey". Changes since v2: - Generate the transfer files in the guest, not the host. - Do not use an environment file. - Reject URLs that cannot work. - Escape sed metacharacters. - Escape backslashes for systemd-sysupdate. - Do not validate update URLs at build time, only at runtime. - Reject only update URLs that cannot possibly work. - Set the partition UUIDs according to systemd's recommendation. - Do not rely on finding partitions by label. - Strip leading and trailing whitespace from the update URL. - Rename the update command from `update` to `spectrum-update`. - Delete the list of steps. Replace it with comments in the script. The awk script in the VM rejects URLs that contain whitespace. This is because they can't work, and passing them to systemd-sysupdate would require figuring out how to escape them. Rejecting such bogus URLs is simpler than preventing them from being mangled. --- host/rootfs/Makefile | 17 ++++- host/rootfs/default.nix | 16 +++- host/rootfs/file-list.mk | 7 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++++ .../image/etc/vm-sysupdate.d/50-verity.transfer | 18 +++++ .../image/etc/vm-sysupdate.d/60-root.transfer | 18 +++++ .../image/etc/vm-sysupdate.d/70-kernel.transfer | 18 +++++ host/rootfs/image/usr/bin/spectrum-update | 87 ++++++++++++++++++++++ host/rootfs/os-release.in | 15 ++++ lib/config.default.nix | 2 + lib/fake-update-signing-key.gpg | 3 + vm/app/systemd-sysupdate/default.nix | 26 +++++++ vm/app/systemd-sysupdate/download-update | 68 +++++++++++++++++ 16 files changed, 350 insertions(+), 6 deletions(-) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 065722d48951a17182ed94e168796700652db7b9..c9c51a06962156d0fc1b2823bc3edb1678c70502 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -10,6 +10,7 @@ include file-list.mk ROOT_FS = build DIRS = \ + boot \ dev \ etc/s6-linux-init/env \ etc/s6-linux-init/run-image/configs \ @@ -33,15 +34,17 @@ DIRS = \ etc/s6-linux-init/run-image/vm/by-id \ etc/s6-linux-init/run-image/vm/by-name \ ext \ + home \ proc \ run \ - sys + sys \ + tmp FIFOS = \ etc/s6-linux-init/run-image/service/s6-svscan-log/fifo \ etc/s6-linux-init/run-image/service/s6-linux-init-shutdownd/fifo -BUILD_FILES = build/etc/s6-rc +BUILD_FILES = build/etc/s6-rc build/etc/os-release build/etc/update-url # This rule produces three files but Make only (portably) # supports one output per rule. Instead of resorting to temporary @@ -63,12 +66,22 @@ $(ROOT_FS_IMAGE): ../../scripts/make-erofs.sh $(PACKAGES_FILE) $(FILES) $(BUILD_ mkdir -p $(ROOT_FS) && \ { \ cat $(PACKAGES_FILE) ;\ + printf '%s\n%s\n' "$$UPDATE_SIGNING_KEY" /etc/systemd/import-pubring.gpg; \ for file in $(FILES) $(LINKS); do printf '%s\n%s\n' $$file "$${file#image/}"; done ;\ for file in $(BUILD_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ printf 'build/empty\n%s\n' $(DIRS) ;\ printf 'build/fifo\n%s\n' $(FIFOS) ;\ } | ../../scripts/make-erofs.sh $@ +build/etc/update-url: + mkdir -p build/etc + # might have metacharacters, so avoid interpolation + printf %s\\n "$${UPDATE_URL:?'update URL empty or missing'}" > build/etc/update-url + +build/etc/os-release: + mkdir -p build/etc + sed 's/@VERSION@/$(VERSION)/g' < os-release.in > build/etc/os-release + build/fifo: mkdir -p build mkfifo -m 0600 $@ diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 4fe9058abdfaa1df7d63b84a629708d4d99388f4..b441a517f3bbb78f84d8566ca6dfd9181d0302be 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -15,6 +15,7 @@ pkgsMusl.callPackage ( , jq, kmod, mdevd, mesa, s6, s6-linux-init, socat, systemd , util-linuxMinimal, virtiofsd, westonLite, xdg-desktop-portal , xdg-desktop-portal-gtk, xdg-desktop-portal-spectrum-host +, btrfs-progs }: let @@ -24,9 +25,9 @@ let trivial; packages = [ - cloud-hypervisor cosmic-files crosvm cryptsetup dbus execline - fuse3 inotify-tools iproute2 jq kmod mdevd s6 s6-linux-init s6-rc - socat spectrum-host-tools systemd util-linuxMinimal virtiofsd + btrfs-progs cloud-hypervisor cosmic-files crosvm cryptsetup dbus + execline fuse3 inotify-tools iproute2 jq kmod mdevd s6 s6-linux-init + s6-rc socat spectrum-host-tools util-linuxMinimal virtiofsd xdg-desktop-portal-spectrum-host (foot.override { allowPgo = false; }) @@ -56,18 +57,20 @@ let # https://inbox.vuxu.org/musl/20251017-dlopen-use-rpath-of-caller-dso-v1-1-46c... usrPackages = [ appvm dejavu_fonts kmod.lib mesa westonLite kernel.modules - firmware netvm + firmware netvm systemd ]; appvms = { appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; appvm-foot = callSpectrumPackage ../../vm/app/foot.nix {}; appvm-gnome-text-editor = callSpectrumPackage ../../vm/app/gnome-text-editor.nix {}; + appvm-systemd-sysupdate = callSpectrumPackage ../../vm/app/systemd-sysupdate {}; }; packagesSysroot = runCommand "packages-sysroot" { depsBuildBuild = [ inkscape ]; nativeBuildInputs = [ xorg.lndir ]; + src = builtins.path { name = "os-release"; path = ./os-release.in; }; } '' mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps @@ -118,6 +121,11 @@ stdenvNoCC.mkDerivation { printf "%s\n/\n" ${packagesSysroot} >$out sed p ${writeClosure [ packagesSysroot] } >>$out ''; + UPDATE_SIGNING_KEY = builtins.path { + name = "signing-key"; + path = config.updateSigningKey; + }; + UPDATE_URL = config.updateUrl; VERSION = config.version; }; diff --git a/host/rootfs/file-list.mk b/host/rootfs/file-list.mk index 613a9e7c692c8a3b1d556c3cd599ffaccf4372c4..56f693e53b918751bcc4b8614f027edde6312040 100644 --- a/host/rootfs/file-list.mk +++ b/host/rootfs/file-list.mk @@ -41,13 +41,20 @@ FILES = \ image/etc/s6-linux-init/scripts/rc.init \ image/etc/s6-linux-init/scripts/rc.shutdown \ image/etc/s6-linux-init/scripts/rc.shutdown.final \ + image/etc/sysupdate.d/50-verity.transfer \ + image/etc/sysupdate.d/60-root.transfer \ + image/etc/sysupdate.d/70-kernel.transfer \ image/etc/udev/rules.d/99-spectrum.rules \ + image/etc/vm-sysupdate.d/50-verity.transfer \ + image/etc/vm-sysupdate.d/60-root.transfer \ + image/etc/vm-sysupdate.d/70-kernel.transfer \ image/etc/xdg/weston/autolaunch \ image/etc/xdg/weston/weston.ini \ image/usr/bin/assign-devices \ image/usr/bin/create-vm-dependencies \ image/usr/bin/run-appimage \ image/usr/bin/run-vmm \ + image/usr/bin/spectrum-update \ image/usr/bin/vm-console \ image/usr/bin/vm-import \ image/usr/bin/vm-start \ diff --git a/host/rootfs/image/etc/fstab b/host/rootfs/image/etc/fstab index 5dc9b2a3c4dff62ee49b2d827f53b45b7781a60f..6230d910a23339925fea0f2ffbc2baa5241ce3f2 100644 --- a/host/rootfs/image/etc/fstab +++ b/host/rootfs/image/etc/fstab @@ -4,3 +4,4 @@ proc /proc proc defaults 0 0 devpts /dev/pts devpts gid=5,mode=620 0 0 tmpfs /dev/shm tmpfs defaults 0 0 sysfs /sys sysfs defaults 0 0 +tmpfs /tmp tmpfs defaults 0 0 diff --git a/host/rootfs/image/etc/sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..9cd64b58ae55d55d378d99f5701f1ecef867e436 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v_@u.verity + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_@v.verity +MatchPartitionType=root-verity +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/60-root.transfer b/host/rootfs/image/etc/sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..cd12d2bd2b4ecd9bb5c7d26cc7c27a4bdb74cac8 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/60-root.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v_@u.root + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_@v +MatchPartitionType=root +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..e4190587a6bb127cb7315f38d59e48cf279318a4 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/EFI/Linux +PathRelativeTo=boot +MatchPattern=Spectrum_@v.efi +Mode=0644 +InstancesMax=2 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..ab4997c83605e3820a22b0b2178dcd76dfcf787e --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v_@u.verity + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v_@u.verity +Mode=0644 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer b/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..8a3175684f1697e0eca443eb0a1a97176e4f66d4 --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v_@u.root + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v_@u.root +Mode=0644 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..cb181239d71c5a6d0a5b3652d5534a23eda64183 --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v.efi +Mode=0644 diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update new file mode 100755 index 0000000000000000000000000000000000000000..bf5b0b0c50cb5381af177c6df6a05d215f775489 --- /dev/null +++ b/host/rootfs/image/usr/bin/spectrum-update @@ -0,0 +1,87 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { mkdir -p -m 0700 /run/updater } + +# Take a global lock to avoid races. +s6-setlock /run/update-lock + +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Delete any stale temporary files. Delete any existing signature + # files. If the VM is still running (it should not be), the VM might + # have write access to the directory. However, updates-dir-check is + # safe against that. + if { updates-dir-check cleanup shared } + + if { + foreground { + # TODO: suppress only "subvolume does not exist" errors. + redirfd -w 2 /dev/null + btrfs subvolume delete snapshot + } + rm -f snapshot + } + + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } + basename -- $id_path + } + + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + # + # In the future, this should use a bind mount instead of copying + # into a tmpfs. However, this would significantly complicate the + # cleanup code. Deleting fs/etc would require undoing the bind + # mounts instead of rm -rf. Once this code is in a separate mount + # namespace, the copies should be replaced by bind mounts. + if { + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } + umask 022 + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd + } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + # TODO: This is racy. If the update finishes before this code runs, + # the s6-svwait call will fail. + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} } + + # Remove the bind mount. + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Validate the update directory. Delete any stale temporary files. + # Check that a signature file was downloaded. + if { updates-dir-check check snapshot } + + unshare --mount + if { mount --bind -o ro -- snapshot /run/updater } + + /usr/lib/systemd/systemd-sysupdate update +} +importas -i sysupdate_exit_status ? +# Clean up. +foreground { btrfs subvolume delete -- snapshot } +exit $sysupdate_exit_status diff --git a/host/rootfs/os-release.in b/host/rootfs/os-release.in new file mode 100644 index 0000000000000000000000000000000000000000..d6e699e82f87dcb1c4656ac19d4e9986282f14a5 --- /dev/null +++ b/host/rootfs/os-release.in @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +NAME="Spectrum OS" +ID=spectrum +PRETTY_NAME="Spectrum @VERSION@" +VERSION=@VERSION@ +VERSION_ID=@VERSION@ +IMAGE_ID=spectrum-root +IMAGE_VERSION=@VERSION@ +RELEASE_TYPE=development +HOME_URL="https://spectrum-os.org" +BUG_REPORT_URL="mailto:discuss@spectrum-os.org" +ANSI_COLOR="1;34" +VENDOR_NAME=Spectrum +VENDOR_URL="https://spectrum-os.org" diff --git a/lib/config.default.nix b/lib/config.default.nix index 489c231490a8b66aa01f50053b25646060f7f963..f6b70fa5e8431bef79222c10c79e8015f7fe65be 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -5,4 +5,6 @@ pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; version = "0.0.0"; + updateUrl = "https://your-spectrum-os-update-server.invalid/download-directory"; + updateSigningKey = ./fake-update-signing-key.gpg; } diff --git a/lib/fake-update-signing-key.gpg b/lib/fake-update-signing-key.gpg new file mode 100644 index 0000000000000000000000000000000000000000..12e18f4c7c740e31692e1f1975282fa72ac1f2e3 --- /dev/null +++ b/lib/fake-update-signing-key.gpg @@ -0,0 +1,3 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +NOT A VALID KEY - UPDATES WILL NOT WORK diff --git a/vm/app/systemd-sysupdate/default.nix b/vm/app/systemd-sysupdate/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..db64f0105da6e1f9951d13f67a2ca8527ae6f73d --- /dev/null +++ b/vm/app/systemd-sysupdate/default.nix @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../../lib/call-package.nix ( +{ callSpectrumPackage, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + downloadUpdate = builtins.path { + name = "download-update"; + path = ./download-update; + }; +in + +callSpectrumPackage ../../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = writeScript "run-script" '' +#!/usr/bin/execlineb -WS0 +export CURL_PATH ${curl}/bin/curl +export SYSTEMD_SYSUPDATE_PATH ${systemd}/lib/systemd/systemd-sysupdate +${downloadUpdate} +''; +}) (_: {}) diff --git a/vm/app/systemd-sysupdate/download-update b/vm/app/systemd-sysupdate/download-update new file mode 100755 index 0000000000000000000000000000000000000000..eada41c6c8ad5edcedd9f4d76b76492e0b8be826 --- /dev/null +++ b/vm/app/systemd-sysupdate/download-update @@ -0,0 +1,68 @@ +#!/usr/bin/env -S execlineb -WS0 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +export LC_ALL C +export LANGUAGE C +if { mount -toverlay -olowerdir=/run/virtiofs/virtiofs0/etc:/etc -- overlay /etc } +backtick tmpdir { mktemp -d /tmp/sysupdate-XXXXXX } +# Not a useless use of cat: if there are NUL bytes in the URL +# busybox's awk might misbehave. +backtick update_url { cat /etc/update-url } +if { + backtick sed_rhs { + # Use awk to both validate the URL and to escape sed metacharacters. + # Reject URLs with control characters, query parameters, or fragments. + # They *cannot* work and so are rejected to produce better error messages. + # + # curl rejects control characters with "Malformed input to a URL function". + # Fragment specifiers ("#") and query parameters ("?") break concatenating + # /SHA256SUMS and /SHA256SUMS.sha256.asc onto the update URL. Also, it is + # simpler to reject update URLs that contain whitespace than to try to + # escape them. + # + # Backslash needs to be escaped once for systemd-sysupdate and again for sed. + # Ampersand needs to be escaped once for sed. + awk "BEGIN { + update_url = ENVIRON[\"update_url\"]; + if (update_url ~ /^[^\\001-\\040?#\\x7F]+$/) { + # Use & to avoid extra escaping (16 or 32 backslashes!) + # and a divergence between POSIX and GNU awk. + gsub(/\\\\/, \"&&&&\", update_url); + gsub(/&/, \"\\\\\\\\&\", update_url); + print update_url; + exit 0; + } else { + print ARGV[2] > \"/dev/stderr\"; + exit 100; + } + }" -- $3 + "Bad update URL from host: control characters, whitespace, query parameters, and fragment specifiers not allowed" + } + elglob -w -0 transfer_file_ /etc/vm-sysupdate.d/*.transfer + forx -E transfer_file { $transfer_file_ } + backtick target_basename { + basename -- $transfer_file + } + multisubstitute { + importas -iuS sed_rhs + importas -iuS target_basename + importas -iuS tmpdir + define sed_input $transfer_file + } + redirfd -w 1 ${tmpdir}/${target_basename} + sed -E -- "s#@UPDATE_URL@#${sed_rhs}#g" $sed_input +} +multisubstitute { + importas -iuS update_url + importas -iuS CURL_PATH + importas -iuS SYSTEMD_SYSUPDATE_PATH + importas -iuS tmpdir +} +if { $SYSTEMD_SYSUPDATE_PATH --definitions=${tmpdir} update } +# [ and ] are allowed in update URLs so that IPv6 addresses work, but +# they cause globbing in the curl command-line tool by default. Use --globoff +# to disable this feature. +if { $CURL_PATH -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS -- ${update_url}/SHA256SUMS } +$CURL_PATH -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS.sha256.asc -- ${update_url}/SHA256SUMS.sha256.asc -- 2.52.0
This patch has been committed as 067c6a5d50971242f9cb8ac0ac76e20d88a9b5c1, which can be viewed online at https://spectrum-os.org/git/spectrum/commit/?id=067c6a5d50971242f9cb8ac0ac76.... This is an automated message. Send comments/questions/requests to: Alyssa Ross <hi@alyssa.is>
The documentation previously stated that updates were not possible without reinstalling. This is still the case by default, but it is possible for developers to enable updates for images they build. Update the documentaion to reflect this. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Move the documentation from the user section to the developer section. Changes since v2: - Move the documentation on how to enable updates to the part on build configuration. - Clarify what happens if an update is interrupted. - Move details to a technical note. - Link to systemd-sysupdate. --- Documentation/development/build-configuration.adoc | 15 ++++++++ Documentation/development/index.adoc | 2 ++ Documentation/development/updates.adoc | 42 ++++++++++++++++++++++ Documentation/installation/index.adoc | 6 +++- 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/Documentation/development/build-configuration.adoc b/Documentation/development/build-configuration.adoc index 545aa8c05ac40a101b5ee280015cde7ec4f3a66f..49651d05890900b74cafb3d75945b3bcc5b86ce6 100644 --- a/Documentation/development/build-configuration.adoc +++ b/Documentation/development/build-configuration.adoc @@ -20,6 +20,21 @@ The configuration file should contain an attribute set. See https://spectrum-os.org/git/spectrum/tree/lib/config.default.nix[lib/config.default.nix] for supported configuration attributes and their default values. +To enable updates, you need to specify a version, an update URL, and an update signing key. +By default, the update URL is set to a .invalid domain and the update signing key is +an invalid key. Therefore, updates will not work. To enable updates, provide a valid key +and update server URL. + +Spectrum uses https://www.freedesktop.org/software/systemd/man/latest/systemd-sysupdate.html[systemd-sysupdate], +so see the https://www.freedesktop.org/software/systemd/man/latest/sysupdate.d.html[sysupdate.d] +documentation for what you need to put on your server. Building +https://spectrum-os.org/git/spectrum/tree/release/updates.nix[release/updates.nix] produces an +directory that is compatible with systemd-sysupdate, except that the signature +(`SHA256SUMS.sha256.asc`) is missing. + +Updates are signed, so the worst a compromised update +server can do is fill up your user data partition. + .config.nix to build Spectrum with a https://nixos.org/manual/nixpkgs/unstable/#sec-overlays-definition[Nixpkgs overlay] [example] [source,nix] diff --git a/Documentation/development/index.adoc b/Documentation/development/index.adoc index 6b48418ba218354ee0493cd82188c54141f63e9e..4e504253dc16286273e1af5cae9614789b2c4a12 100644 --- a/Documentation/development/index.adoc +++ b/Documentation/development/index.adoc @@ -18,6 +18,8 @@ Spectrum is free software, currently under active development. TIP: For information on writing guidelines, see xref:../contributing/writing_documentation.adoc[Documentation Style Guide]. +If you want to update Spectrum without reinstalling, see how to +xref:updates.adoc[Enable updates]. == Mailing Lists diff --git a/Documentation/development/updates.adoc b/Documentation/development/updates.adoc new file mode 100644 index 0000000000000000000000000000000000000000..8746f97e5d9b36d4960a64544af08f57ff89ce9a --- /dev/null +++ b/Documentation/development/updates.adoc @@ -0,0 +1,42 @@ += Updating the OS +:page-parent: Development + +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +// SPDX-License-Identifier: GFDL-1.3-no-invariants-or-later OR CC-BY-SA-4.0 + +Right now, there is no official update server or update signing key. +However, it is possible to run your own update server. See +xref:../development/build-configuration.adoc[build configuration] +for how to enable updates for your own Spectrum images. + +== Updating the system + +If you have built your image with updates enabled, you can update the +system using the `spectrum-update` command. This takes the path to a +staging directory as argument. This directory must be on a BTRFS +filesystem. It is strongly recommended to not use this directory +for any other purpose. However, it's safe to rename the directory +and use `spectrum-update` with the new path afterwards. + +If there is a problem with the update, it's safe to try again. +If that still doesn't work, you can delete the directory and +try again with an empty one. This will cause `spectrum-update` +to download the latest version even if it is already installed, but +is otherwise harmless. + +Updates are atomic and take effect after the system reboots. +If the system is rebooted, crashes, or loses power during an +update, the update will not take effect. It is safe to resume +an interrupted update. + +Since Spectrum's host has no network access, the VM that does the +updates (`sys.appvm-systemd-sysupdate`) is given a BTRFS subvolume to +write the updates into. It uses `systemd-sysupdate` to download the updates +into this directory. Once it exits, the host snapshots this directory and +checks it for malicious filenames or non-regular files. If the check +passes, this directory is used as the source for `systemd-sysupdate`, +which installs the updates to the OS volume and EFI system partition. + +See the documentation of +https://www.freedesktop.org/software/systemd/man/systemd-sysupdate.html[systemd-sysupdate]. +for some of the details. diff --git a/Documentation/installation/index.adoc b/Documentation/installation/index.adoc index d67c88dda062066c19c3b21e699f074cc18a6dbc..d1df2edc9b0ca902824ff729eec139270fb40777 100644 --- a/Documentation/installation/index.adoc +++ b/Documentation/installation/index.adoc @@ -18,6 +18,10 @@ development. == Uninstalling and Updating -Currently, there is no implementation for a software update. +Software updates are a work in progress and are not currently available. + +If you built Spectrum yourself, see +xref:../development/build-configuration.adoc[Build configuration] for how +to enable updates for it. You can replace Spectrum by installing another OS. -- 2.52.0
This patch has been committed as cf131bd816a34095af34345f0e5f148c47268a04, which can be viewed online at https://spectrum-os.org/git/spectrum/commit/?id=cf131bd816a34095af34345f0e5f.... This is an automated message. Send comments/questions/requests to: Alyssa Ross <hi@alyssa.is>
Wrong values for the version or update URL will cause very confusing build-time or runtime errors. Provide a better user experience by validating them up-front. The update URL validator is loose. It rejects only URLs that cannot possibly work: either appending /SHA256SUMS to them doesn't append to the path, or they will definitely be rejected by curl due to being malformed. The version validator is in lib/config.nix, as the version number is used in many places. It checks that the version only uses characters that are permitted by systemd's version number specification [1] and that will not break code that uses them in shell or sed commands. [1]: https://uapi-group.org/specifications/specs/version_format_specification Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Drop compression level. - Centralize validation. - Use camelCase for Nix identifiers. - Clean up formatting. Changes since v3: - Validate compression level. Changes since v2: - Use loose URL validation: allow anything that might work. - Only reject versions that violate the specification. --- lib/config.nix | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/config.nix b/lib/config.nix index e437cdbe9aa22dd0f9c8d7052ac331c8fccf6ce6..e641642de07c1549e69fc12e91c4e80e2f82d035 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -17,6 +17,31 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; + # Use builtins.fromJSON because it supports \uXXXX escapes. + # This is the same regex used by check-url.awk in the update VM. + # The update code is careful to escape any metacharacters, but some + # simply cannot be made to work. Concatenating the URL with /SHA256SUMS + # must append to the path portion of the URL, and the URL must be one + # that libcurl will accept. + urlRegex = builtins.fromJSON "\"^[^\\u0001- #?\\u007F]+$\""; in -default // callConfig config +# Version is used in many files, so validate it here. +# See https://uapi-group.org/specifications/specs/version_format_specification +# for allowed version strings. +if builtins.match "[[:alnum:]_.~^-]+" finalConfig.version == null then + builtins.abort '' + Version ${builtins.toJSON finalConfig.version} has forbidden characters. + Only ASCII alphanumerics, ".", "_", "~", "^", "+", and "-" are allowed. + See <https://uapi-group.org/specifications/specs/version_format_specification>. + '' +else +if builtins.match urlRegex finalConfig.updateUrl == null then + builtins.abort '' + Update URL ${builtins.toJSON finalConfig.updateUrl} has forbidden characters. + Query strings, and fragment specifiers are not supported. + ASCII control characters and whitespace must be %-encoded. + '' +else +finalConfig -- 2.52.0
This implements updates via systemd-sysupdate. See individual commit messages for details. This depends on "Move verity and EFI creation to separate Nix derivations", at <https://spectrum-os.org/lists/archives/spectrum-devel/20251126-refactor-verity-v6-0-f09555546a85@gmail.com>. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes in v5: - Fix broken shell.nix files in intermediate patches. - See individual patches messages for more details. - Link to v4: https://spectrum-os.org/lists/archives/spectrum-devel/20251121-updates-v4-0-... Changes in v4: - Fix build errors in intermediate patches. - Apply suggestions from code review. - Link to v3: https://spectrum-os.org/lists/archives/spectrum-devel/20251119-updates-v3-0-... Changes in v3: - See individual commits for details. There are too many to mention here. - Link to v2: https://spectrum-os.org/lists/archives/spectrum-devel/20251112-updates-v2-0-... Changes in v2: - updates-dir-check: - Do not check that there is a SHA256SUMS or SHA256SUMS.gpg file in the update directory. systemd-sysupdate will fail if it cannot find a manifest or its signature. - Follow symlinks in opening the directory. The path is from a trusted source and will always point to a BTRFS snapshot, never a symlink. The only exception is the last component, which is still checked to not be a symlink. - VM: - Link SHA256SUMS.sha256.asc to SHA256SUMS.gpg. Recent systemd-sysupdate seems to use the former name. - Get update URL from host. - Use an execline script instead of a shell script. - Update script: - Unmount shared directory if already mounted. This avoids errors when mounting it again. - Delete old snapshot if present. - Provide the VM information with a different directory layout. - Do not bind-mount the information passed into the VM into the shared VM folder. Instead rely on this folder being read-only to the guest. This is enforced by a read-only bind mount in virtiofs's mount namespace. - Testing: - Lots of manual update testing. - Disable the test for the live image as it doesn't work anymore. - Nix: - Move validation to a separate low-priority patch. - Documentation: - Document that updating the system is now possible. - Installer: - Remove the "Try Spectrum" button. - Link to v1: https://spectrum-os.org/lists/archives/spectrum-devel/20251029-updates-v1-0-... --- Demi Marie Obenour (13): tools: Add directory checker for updates scripts: port make-gpt.sh to bash scripts/make-gpt.sh: Allow specifying partition size Port scripts/format-uuid.sh to awk Use set and a command substitution to set UUID variables scripts: Use shell expansion to get partition path release: Compress installation images and remove live image Use OS version to set partition labels and UKI name Add B partitions to installation images release: Create directory with system update Support updates via systemd-sysupdate Documentation: Update support Validate configuration parameters Documentation/development/build-configuration.adoc | 15 +++ Documentation/development/index.adoc | 2 + Documentation/development/updates.adoc | 42 +++++++ Documentation/development/uuid-reference.adoc | 8 ++ Documentation/installation/getting-spectrum.adoc | 44 ++++--- Documentation/installation/index.adoc | 6 +- host/initramfs/Makefile | 18 +-- host/initramfs/etc/probe | 20 --- host/initramfs/shell.nix | 2 + host/rootfs/Makefile | 27 ++++- host/rootfs/default.nix | 21 +++- host/rootfs/file-list.mk | 7 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++ .../image/etc/vm-sysupdate.d/50-verity.transfer | 18 +++ .../image/etc/vm-sysupdate.d/60-root.transfer | 18 +++ .../image/etc/vm-sysupdate.d/70-kernel.transfer | 18 +++ host/rootfs/image/usr/bin/spectrum-update | 92 ++++++++++++++ host/rootfs/os-release.in | 15 +++ host/rootfs/shell.nix | 2 + img/app/Makefile | 2 +- lib/config.default.nix | 3 + lib/config.nix | 27 ++++- lib/fake-update-signing-key.gpg | 3 + release.nix | 2 + release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ----- release/combined/eosimages.nix | 8 +- release/combined/grub.cfg.in | 5 - release/live/Makefile | 17 ++- release/live/default.nix | 5 +- release/live/shell.nix | 3 +- release/update.nix | 33 +++++ scripts/format-uuid.awk | 19 +++ scripts/format-uuid.sh | 19 --- scripts/make-gpt.sh | 30 ++--- tools/default.nix | 1 + tools/meson.build | 4 + tools/updates-dir-check.c | 134 +++++++++++++++++++++ vm/app/systemd-sysupdate/default.nix | 26 ++++ vm/app/systemd-sysupdate/download-update | 68 +++++++++++ vm/sys/net/Makefile | 2 +- 44 files changed, 733 insertions(+), 145 deletions(-) --- base-commit: 64131d7c2c0e5af7ee3a8ee45f3003ba7b71a771 change-id: 20250928-updates-92e99849e231 prerequisite-patch-id: b4c17d0046f0e413bc57eaf795fcf65825839480 prerequisite-patch-id: 08a5517294d2bc746bc555820ed44cf3d2cfe8d0 -- Sincerely, Demi Marie Obenour (she/her/hers)
Spectrum OS's host has no network access. Updates must be downloaded by VMs. The downloads are placed into a bind-mounted directory. The VM can write whatever it wants into that directory. This includes symlinks that subsequent code might open, which would create a path traversal vulnerability. It also includes paths with names containing containing terminal escape sequences, newlines, or other nastiness. Furthermore, the directory should not have any subdirectories either. Add a simple C program that checks for such ugliness and indicates (via its exit code) if the VM misbehaved. systemd-sysupdate can leave behind temporary files with names starting with '.', so delete them instead of failing. Linux can lose cache coherency if there is an I/O error, so call syncfs() on the directory before checking anything. For the same reason, fsync() the directory if any hidden files were deleted. The directory checker also serves another critical function: it checks if the VM actually downloaded anything. Otherwise, network problems could cause updates to silently do nothing. Specifically, it checks that the VM provided a file starting with the prefix "SHA256SUMS.". These will be the last ones the in-VM updater downloads. An additional mode is provided to clean out all such files. This will be used to ensure that before the in-VM updater runs, no such files are present. Hence, if the VM didn't actually download anything, the user will get a clear error instead of a false success message or a confusing error. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> Reviewed-by: Alyssa Ross <hi@alyssa.is> --- Changes since v2: - Purge leftover temporary files rather than returning an error. - Split into two modes: one that deletes signature files, and one that checks that at least one signature file exists. This allows checking that the VM actually sent something. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- tools/default.nix | 1 + tools/meson.build | 4 ++ tools/updates-dir-check.c | 134 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+) diff --git a/tools/default.nix b/tools/default.nix index 7cb7dc5b72b8394f5383c80ccf110fec55c44f21..da82f075fdba4655bd964ba35e819d669deff3f1 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -77,6 +77,7 @@ stdenv.mkDerivation (finalAttrs: { ./sd-notify-adapter.c ./start-vmm ./subprojects + ./updates-dir-check.c ] ++ lib.optionals driverSupport [ ./xdp-forwarder ])); diff --git a/tools/meson.build b/tools/meson.build index bfa290e891fafa2d03eabb221121b5df4d83fb29..666483b3304224fce9110a2788456955a2d71305 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -33,6 +33,10 @@ if get_option('host') install: true) subdir('start-vmm') + + executable('updates-dir-check', 'updates-dir-check.c', + c_args : '-D_GNU_SOURCE', + install: true) endif if get_option('build') diff --git a/tools/updates-dir-check.c b/tools/updates-dir-check.c new file mode 100644 index 0000000000000000000000000000000000000000..83af806bebf36754f8c794b04933bf6021338c38 --- /dev/null +++ b/tools/updates-dir-check.c @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +#include <assert.h> +#include <errno.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> +#include <unistd.h> + +#include <err.h> + +[[noreturn]] static void bad_char(char c, char *msg_component) +{ + if (c >= 0x20 && c <= 0x7E) + errx(EXIT_FAILURE, "Forbidden %s character in filename: '%c'", + msg_component, c); + errx(EXIT_FAILURE, + "Forbidden %s character in filename: byte 0x%hhx", + msg_component, c); +} + +[[noreturn]] static void usage(void) +{ + errx(EXIT_FAILURE, "Usage: updates-dir-check [cleanup|check] DIRECTORIES..."); +} + +static void checkdir(int fd, bool check_sig) +{ + bool found_sig = false; + DIR *d = fdopendir(fd); + if (d == NULL) + err(EXIT_FAILURE, "fdopendir"); + // If there is an I/O error while there are dirty pages outstanding, + // the dirty pages are silently discarded. This means that the contents + // of the filesystem can change behind userspace's back. Flush all + // dirty pages in the filesystem with the directory to prevent this. + if (syncfs(fd) != 0) + err(EXIT_FAILURE, "syncfs"); + bool changed = false; + for (;;) { + errno = 0; + struct dirent *entry = readdir(d); + if (entry == NULL) { + if (errno) + err(EXIT_FAILURE, "readdir"); + break; + } + const char *ptr = entry->d_name; + if (ptr[0] == '.') { + if (ptr[1] == '\0') + continue; + if (ptr[1] == '.' && ptr[2] == '\0') + continue; + // systemd-sysupdate uses these for temporary files. + // It normally cleans them up itself, but if there is an error + // it does not always clean them up. I'm not sure if it is + // guaranteed to clean up temporary files from a past run, so + // delete them instead of returning an error. + if (unlinkat(fd, ptr, 0)) + err(EXIT_FAILURE, "Failed to unlink temporary file"); + changed = true; + continue; + } + char c = ptr[0]; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'))) + bad_char(c, "initial"); + while ((c = *++ptr)) { + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-') || + (c == '.'))) + bad_char(c, "subsequent"); + } + // Empty filenames are rejected as having a bad initial character, + // and POSIX forbids them from being returned anyway. Therefore, + // this cannot be out of bounds. + if (ptr[-1] == '.') + errx(EXIT_FAILURE, "Filename %s ends with a '.'", entry->d_name); + if (entry->d_type == DT_UNKNOWN) + errx(EXIT_FAILURE, "Filesystem didn't report type of file %s", entry->d_name); + if (entry->d_type != DT_REG) + errx(EXIT_FAILURE, "Entry contains non-regular file %s", entry->d_name); + if (strncmp(entry->d_name, "SHA256SUMS.", sizeof("SHA256SUMS.") - 1) == 0) { + // Found a signature file! + if (check_sig) + found_sig = true; + else { + if (unlinkat(fd, entry->d_name, 0)) + err(EXIT_FAILURE, "Unlinking old signature file"); + changed = true; + } + } + } + // If a change was made, enforcing cache coherency also requires + // another fsync() call. This is again because Linux can discard + // changes if there is an I/O error. + if (changed && fsync(fd)) + errx(EXIT_FAILURE, "fsync"); + if (check_sig && !found_sig) { + warnx("sys.appvm-systemd-sysupdate didn't send a signature file."); + warnx("There was probably a problem downloading the update."); + errx(EXIT_FAILURE, "Check its logs for more information."); + } + closedir(d); +} + +int main(int argc, char **argv) +{ + if (argc != 3) + usage(); + + bool check_sig; + if (strcmp(argv[1], "cleanup") == 0) + check_sig = false; + else if (strcmp(argv[1], "check") == 0) + check_sig = true; + else + usage(); + + for (int i = 2; i < argc; ++i) { + int fd = open(argv[i], O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + err(EXIT_FAILURE, "open(%s)", argv[i]); + checkdir(fd, check_sig); + } + return 0; +} -- 2.52.0
Future changes will be using bash-specific features. Use bash-specific code for this. Also add some error checks. No other functional change intended. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Improve commit message. Changes since v2: - Do not use wrapper script. - Make script non-executable. - Invoke script as 'bash FILE' to work around /usr/bin/env not working during a Nix build. --- host/initramfs/Makefile | 4 ++-- host/rootfs/Makefile | 2 +- img/app/Makefile | 2 +- release/live/Makefile | 2 +- scripts/make-gpt.sh | 20 ++++++++++---------- vm/sys/net/Makefile | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index f27f5e07c4707914962197b4fea8f385729370aa..2304b0885a152d8a659dddcc58f948d096034e2d 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -36,7 +36,7 @@ build/mountpoints: find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';' build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ @@ -45,7 +45,7 @@ build/loop.tar: build/live.img $(TAR) -cf $@ build/live.img build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e mv $@.tmp $@ diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index 4cb048ac08b3265b0435d6ff6fc612a58c169ce9..f45758041f2f682618cb0f9e54e5a74f0b49874e 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -93,7 +93,7 @@ clean: .PHONY: clean build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/verity-timestamp $(ROOT_FS_IMAGES) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ diff --git a/img/app/Makefile b/img/app/Makefile index 48eba871339d314479f730101246ace3fa39e2db..f16c2df1b90bbec6750f2980da4dbaf49c9cb0ea 100644 --- a/img/app/Makefile +++ b/img/app/Makefile @@ -26,7 +26,7 @@ $(imgdir)/appvm/vmlinux: $(KERNEL) $(imgdir)/appvm/blk/root.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:5460386f-2203-4911-8694-91400125c604:root mv $@.tmp $@ diff --git a/release/live/Makefile b/release/live/Makefile index b37ccce42feb3ac7e8ce4faf96a67902b55be808..c712db3727b7008105388a278552fd3d81eb3b4c 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -10,7 +10,7 @@ DTBS ?= build/empty dest = build/live.img $(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) - ../../scripts/make-gpt.sh $@.tmp \ + bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh old mode 100755 new mode 100644 index 96f0d2c8494c093558c0e32e7e920b569bb078ef..0ae3d0005e5ae7d6214270fedb2fafb0a7064cf5 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -1,11 +1,11 @@ -#!/bin/sh -eu -# +#!/usr/bin/env -S bash -- # SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> # SPDX-FileCopyrightText: 2022 Unikie # SPDX-License-Identifier: EUPL-1.2+ # -# usage: make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... +# usage: bash make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]... +set -xeuo pipefail ONE_MiB=1048576 # Prints the number of 1MiB blocks required to store the file named @@ -40,16 +40,15 @@ scriptsDir="$(dirname "$0")" out="$1" shift -nl=' -' table="label: gpt" # Keep 1MiB free at the start, and 1MiB free at the end. gptBytes=$((ONE_MiB * 2)) for partition; do - sizeMiB="$(sizeMiB "$(partitionPath "$partition")")" - table="$table${nl}size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" - gptBytes="$((gptBytes + sizeMiB * ONE_MiB))" + partitionPath=$(partitionPath "$partition") + sizeMiB=$(sizeMiB "$partitionPath") + table+=$'\n'"size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" + gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) done rm -f "$out" @@ -60,6 +59,7 @@ EOF n=0 for partition; do - fillPartition "$out" "$n" "$(partitionPath "$partition")" - n="$((n + 1))" + partitionPath=$(partitionPath "$partition") + fillPartition "$out" "$n" "$partitionPath" + n=$((n + 1)) done diff --git a/vm/sys/net/Makefile b/vm/sys/net/Makefile index d71c2325eff3bae921f33c61f799846d35e401c2..e403d697d90c0021a7a8d1dbc1553cfbda74a117 100644 --- a/vm/sys/net/Makefile +++ b/vm/sys/net/Makefile @@ -25,7 +25,7 @@ $(vmdir)/netvm/vmlinux: $(KERNEL) $(vmdir)/netvm/blk/root.img: ../../../scripts/make-gpt.sh ../../../scripts/sfdisk-field.awk build/rootfs.erofs mkdir -p $$(dirname $@) - ../../../scripts/make-gpt.sh $@.tmp \ + bash ../../../scripts/make-gpt.sh $@.tmp \ build/rootfs.erofs:root:ea21da27-0391-48da-9235-9d2ab2ca7844:root mv $@.tmp $@ -- 2.52.0
systemd-sysupdate requires that partitions are large enough to hold the newly downloaded images. This requires that they be large enough to have room to grow. Allow specifying the partition size manually, overriding the default (the size of the file that will be copied into the partition). Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Remove the MiB suffix. Changes since v2: - Split into separate commit. --- scripts/make-gpt.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh index 0ae3d0005e5ae7d6214270fedb2fafb0a7064cf5..c3f16e6c029d1d27d3da9e05e50945a56bfad9f8 100644 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -45,8 +45,13 @@ table="label: gpt" # Keep 1MiB free at the start, and 1MiB free at the end. gptBytes=$((ONE_MiB * 2)) for partition; do - partitionPath=$(partitionPath "$partition") - sizeMiB=$(sizeMiB "$partitionPath") + if [[ "$partition" =~ :([1-9][0-9]*)$ ]]; then + sizeMiB=${BASH_REMATCH[1]} + partition=${partition%:*} + else + partitionPath=$(partitionPath "$partition") + sizeMiB=$(sizeMiB "$partitionPath") + fi table+=$'\n'"size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) done -- 2.52.0
This makes it significantly easier to extend. Future changes will require it to be able to output multiple UUIDs at once. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Convert script from sh to awk before making any functional changes. --- host/initramfs/Makefile | 6 +++--- host/rootfs/Makefile | 6 +++--- release/live/Makefile | 6 +++--- release/live/default.nix | 2 +- scripts/format-uuid.awk | 9 +++++++++ scripts/format-uuid.sh | 19 ------------------- 6 files changed, 19 insertions(+), 29 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 2304b0885a152d8a659dddcc58f948d096034e2d..e04e2ff471750410926f14099cee9786d582de86 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -35,10 +35,10 @@ build/mountpoints: cd build/mountpoints && mkdir -p $(MOUNTPOINTS) find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';' -build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) +build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$(awk -f ../../scripts/format-uuid.awk "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ + $(ROOT_FS_IMAGE):root:$$(awk -f ../../scripts/format-uuid.awk "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ build/loop.tar: build/live.img diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index f45758041f2f682618cb0f9e54e5a74f0b49874e..b2c0c6176fe0de4a99d1a3737d50054b532af598 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -92,10 +92,10 @@ clean: rm -rf build .PHONY: clean -build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/verity-timestamp $(ROOT_FS_IMAGES) +build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/verity-timestamp $(ROOT_FS_IMAGES) bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$(awk -f ../../scripts/format-uuid.awk "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ + $(ROOT_FS_IMAGE):root:$$(awk -f ../../scripts/format-uuid.awk "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ debug: diff --git a/release/live/Makefile b/release/live/Makefile index c712db3727b7008105388a278552fd3d81eb3b4c..48df3ef4ad3faab4e0ad09380bd70dbdc980109f 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -9,11 +9,11 @@ DTBS ?= build/empty dest = build/live.img -$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) +$(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$(awk -f ../../scripts/format-uuid.awk "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ + $(ROOT_FS_IMAGE):root:$$(awk -f ../../scripts/format-uuid.awk "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") mv $@.tmp $@ build/empty: diff --git a/release/live/default.nix b/release/live/default.nix index ba9bb17e697a6ecfe81e52a4ffbc375ef443b6f3..d1e2422e9f1ba666af7ad7a5cce1c80a242d0777 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -29,7 +29,7 @@ stdenv.mkDerivation { fileset = lib.fileset.intersection src (lib.fileset.unions [ ./. ../../lib/common.mk - ../../scripts/format-uuid.sh + ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk ]); diff --git a/scripts/format-uuid.awk b/scripts/format-uuid.awk new file mode 100644 index 0000000000000000000000000000000000000000..17831221bbef2d2d038f4822b22f88939eab7437 --- /dev/null +++ b/scripts/format-uuid.awk @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +BEGIN { + print (substr(ARGV[1], 1, 8) "-" \ + substr(ARGV[1], 9, 4) "-" \ + substr(ARGV[1], 13, 4) "-" \ + substr(ARGV[1], 17, 4) "-" \ + substr(ARGV[1], 21, 12)); +} diff --git a/scripts/format-uuid.sh b/scripts/format-uuid.sh deleted file mode 100755 index 3b38278aef640b2cd540d6606b05dd62018e48a6..0000000000000000000000000000000000000000 --- a/scripts/format-uuid.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -eu -# -# SPDX-FileCopyrightText: 2021-2022 Alyssa Ross <hi@alyssa.is> -# SPDX-FileCopyrightText: 2022 Unikie -# SPDX-License-Identifier: EUPL-1.2+ - -substr() { - str=$1 - beg=$2 - end=$3 - echo "$str" | cut -c "$beg-$end" -} - -u1=$(substr "$1" 1 8) -u2=$(substr "$1" 9 12) -u3=$(substr "$1" 13 16) -u4=$(substr "$1" 17 20) -u5=$(substr "$1" 21 32) -printf "%s\n" "$u1-$u2-$u3-$u4-$u5" -- 2.52.0
No functional change intended. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/initramfs/Makefile | 6 ++++-- host/rootfs/Makefile | 6 ++++-- release/live/Makefile | 6 ++++-- scripts/format-uuid.awk | 20 +++++++++++++++----- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index e04e2ff471750410926f14099cee9786d582de86..392dcfc8af3d6924fae717025124f228a2362b94 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -36,9 +36,11 @@ build/mountpoints: find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';' build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(awk -f ../../scripts/format-uuid.awk "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(awk -f ../../scripts/format-uuid.awk "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$2 \ + $(ROOT_FS_IMAGE):root:$$1 mv $@.tmp $@ build/loop.tar: build/live.img diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index b2c0c6176fe0de4a99d1a3737d50054b532af598..ab24263c6f327e47cd1d012ca8d729b0ea5eb8f3 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -93,9 +93,11 @@ clean: .PHONY: clean build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/verity-timestamp $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$(awk -f ../../scripts/format-uuid.awk "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(awk -f ../../scripts/format-uuid.awk "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$2 \ + $(ROOT_FS_IMAGE):root:$$1 mv $@.tmp $@ debug: diff --git a/release/live/Makefile b/release/live/Makefile index 48df3ef4ad3faab4e0ad09380bd70dbdc980109f..5ab93451de109949af0e7ed7f70bf6827fefbf69 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -10,10 +10,12 @@ DTBS ?= build/empty dest = build/live.img $(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) + uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ + set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$(awk -f ../../scripts/format-uuid.awk "$$(dd if=$(ROOT_FS_VERITY_ROOTHASH) bs=32 skip=1 count=1 status=none)") \ - $(ROOT_FS_IMAGE):root:$$(awk -f ../../scripts/format-uuid.awk "$$(head -c 32 $(ROOT_FS_VERITY_ROOTHASH))") + $(ROOT_FS_VERITY):verity:$$2 \ + $(ROOT_FS_IMAGE):root:$$1 mv $@.tmp $@ build/empty: diff --git a/scripts/format-uuid.awk b/scripts/format-uuid.awk index 17831221bbef2d2d038f4822b22f88939eab7437..d4c1a75d97ed86e17a118d8b2d3252cd78c77286 100644 --- a/scripts/format-uuid.awk +++ b/scripts/format-uuid.awk @@ -1,9 +1,19 @@ # SPDX-License-Identifier: EUPL-1.2+ # SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +function format_uuid(arg) { + print (substr(arg, 1, 8) "-" \ + substr(arg, 9, 4) "-" \ + substr(arg, 13, 4) "-" \ + substr(arg, 17, 4) "-" \ + substr(arg, 21, 12)); +} + BEGIN { - print (substr(ARGV[1], 1, 8) "-" \ - substr(ARGV[1], 9, 4) "-" \ - substr(ARGV[1], 13, 4) "-" \ - substr(ARGV[1], 17, 4) "-" \ - substr(ARGV[1], 21, 12)); + FS = ""; + if (getline != 1) { + print "Empty input file" > "/dev/stderr"; + exit 1; + } + format_uuid(substr($0, 1, 32)); + format_uuid(substr($0, 33, 32)); } -- 2.52.0
Avoids a pointless call to awk. No functional change intended. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v2: - Move into separate commit. --- scripts/make-gpt.sh | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh index c3f16e6c029d1d27d3da9e05e50945a56bfad9f8..7baa3cd42ad8b0b664f56c68c1f254f3c2965623 100644 --- a/scripts/make-gpt.sh +++ b/scripts/make-gpt.sh @@ -28,13 +28,6 @@ fillPartition() { lseek -S 1 "$start" cat "$3" 1<>"$1" } -# Prints the partition path from a PATH:PARTTYPE[:PARTUUID[:PARTLABEL]] string. -partitionPath() { - awk -F: '{print $1}' <<EOF -$1 -EOF -} - scriptsDir="$(dirname "$0")" out="$1" @@ -49,8 +42,7 @@ for partition; do sizeMiB=${BASH_REMATCH[1]} partition=${partition%:*} else - partitionPath=$(partitionPath "$partition") - sizeMiB=$(sizeMiB "$partitionPath") + sizeMiB=$(sizeMiB "${partition%%:*}") fi table+=$'\n'"size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")" gptBytes=$((gptBytes + sizeMiB * ONE_MiB)) @@ -64,7 +56,6 @@ EOF n=0 for partition; do - partitionPath=$(partitionPath "$partition") - fillPartition "$out" "$n" "$partitionPath" + fillPartition "$out" "$n" "${partition%%:*}" n=$((n + 1)) done -- 2.52.0
systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, the partitions need to be very large so that there is plenty of room for the OS to grow. Furthermore, systemd-sysupdate requires both A and B copies of both the root and verity partitions. mkfs.ext4 is not able to produce images with files large enough to hold both the primary and backup copy of the root partition [1]. Reducing the sizes of partitions to be little greater than the size of the root filesystem image does not help. The produced file is still too large. Therefore, compress the image, which causes it to be small enough that mkfs.ext4 can handle it. This breaks the option to use the installer as a live image. Therefore, remove it. This option will return once Spectrum switches to the GNOME OS installer [2]. However, it is still possible to build a live image that is separate from the installer. Document how to build and use it. GRUB2 does support compressed loopback images, but these presumably buffer the whole image in memory. Since the entire installer will be replaced, making it work is not considered worthwhile. [1]: https://github.com/tytso/e2fsprogs/issues/254 [2]: https://spectrum-os.org/lists/archives/spectrum-devel/87wm4dlkhz.fsf@alyssa.... Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Replace a mention of "Try Spectrum" with a mention of the live image. - Combine instructions for building an installer and a live image. - Drop the config option for the compression level. - Drop unnecessary quoting changes. - Drop unnecessary 'set -euo pipefail'. Changes since v3: - Make the compression level configurable. The default is 1 so that development builds finish in a reasonable amount of time. Release builds should use compression level 9. Changes since v2: - Remove live image test instead of skipping it. - Document the change. - Document that there is still a live image available, though it is separate from the installer. - Document how to build the live image. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/installation/getting-spectrum.adoc | 44 ++++++++++++++++-------- host/initramfs/Makefile | 8 ----- host/initramfs/etc/probe | 20 ----------- release/checks/integration/meson.build | 2 +- release/checks/integration/try.c | 29 ---------------- release/combined/eosimages.nix | 8 ++--- release/combined/grub.cfg.in | 5 --- 7 files changed, 35 insertions(+), 81 deletions(-) diff --git a/Documentation/installation/getting-spectrum.adoc b/Documentation/installation/getting-spectrum.adoc index 29803aa324b196119a03b22d7f1e2d7730e2c1eb..22c1fe310b56adecaac5ed7e87decbfd56881919 100644 --- a/Documentation/installation/getting-spectrum.adoc +++ b/Documentation/installation/getting-spectrum.adoc @@ -23,6 +23,17 @@ documentation for https://nix.dev/manual/nix/2.24/advanced-topics/cores-vs-jobs.html[tuning cores and jobs]. +To get Spectrum, run + +[source,shell] +---- +git clone https://spectrum-os.org/git/spectrum +---- + +Then follow one of the instructions below. If you haven't set up the +xref:binary-cache.adoc[binary cache], all of the following Nix commands +will take a very long time. + == Trying Spectrum If you want to try Spectrum out to get a feel for it, without @@ -31,27 +42,30 @@ applications. [source,shell] ---- -git clone https://spectrum-os.org/git/spectrum cd spectrum/host/rootfs nix-shell --run 'make run' ---- -This builds just enough of Spectrum to try it out in a VM, but it will -still take a very long time. +This builds just enough of Spectrum to try it out in a VM. == Installing Spectrum To install Spectrum on a computer, you can use a USB drive as a -bootable Spectrum installer device. When booting a system from the -installer device, you will be able to choose whether to try out -Spectrum without installing it on your system (as a live image), or to -install it to your computer's internal storage. +bootable Spectrum installer device. You will need to choose whether to +try out Spectrum without installing it on your system (as a live image), +or to install it to your computer's internal storage. -First, you need to build the Spectrum image: +To build a live image, run: + +[source,shell] +---- +nix-build spectrum/release/live +---- + +To build an installer, run: [source,shell] ---- -git clone https://spectrum-os.org/git/spectrum nix-build spectrum/release/combined ---- @@ -61,15 +75,17 @@ will take a very long time. When it's done, a symbolic link named installer image. Write that image to a USB drive, for example using `dd` (command line) or https://gitlab.com/bztsrc/usbimager[`usbimager`] (graphical, available -in Nixpkgs). Boot your system from the USB drive, and you should see -a menu allowing you to choose between "Try Spectrum" and "Install -Spectrum". +in Nixpkgs). Then boot your system from the USB drive + +If you built a live image, Spectrum should be ready for you to use. +If you built an installer, you should see a menu allowing you to +"Install Spectrum". NOTE: While it's possible to install Spectrum to your internal storage, at this point in Spectrum's development there is not much reason to, as OS updates are not yet implemented, and persistent -storage is not yet exposed to VMs. Using the "Try Spectrum" option to -boot Spectrum will let you try out everything in Spectrum, without +storage is not yet exposed to VMs. Using a live image to boot +Spectrum will let you try out everything in Spectrum, without having to go through the additional step of reinstalling Spectrum every time you want to use a newer version. diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index 392dcfc8af3d6924fae717025124f228a2362b94..c3d600ad5a55d81b8ca9c7a3e182ef5f4fd90f4b 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -43,14 +43,6 @@ build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../sc $(ROOT_FS_IMAGE):root:$$1 mv $@.tmp $@ -build/loop.tar: build/live.img - $(TAR) -cf $@ build/live.img - -build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4 - bash ../../scripts/make-gpt.sh $@.tmp \ - build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e - mv $@.tmp $@ - clean: rm -rf build .PHONY: clean diff --git a/host/initramfs/etc/probe b/host/initramfs/etc/probe index 4cbd00db52c1a7128b5c619a43d415675feaee0b..013092b6dcc5b82db7302c1ae7e6d8a4f5a0b802 100755 --- a/host/initramfs/etc/probe +++ b/host/initramfs/etc/probe @@ -2,26 +2,6 @@ # SPDX-License-Identifier: EUPL-1.2+ # SPDX-FileCopyrightText: 2021 Alyssa Ross <hi@alyssa.is> -if -n { - # If this is a Spectrum installer eosimages partition, we might be - # booting from the installer, and should loopback mount the images. - importas -i mdev MDEV - if { - backtick -E type { lsblk -lnpo PARTTYPE $mdev } - test $type = 56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e - } - if { - forx -pE module { ext4 loop } - modprobe $module - } - backtick -E uuid { lsblk -lnpo PARTUUID $mdev } - if { mkdir -p /mnt/${uuid} } - if { mount $mdev /mnt/${uuid} } - find /mnt/${uuid} -name *.img -exec - losetup -Pf {} - ; -} - # Check whether we now have all the partitions we need to boot. importas -i rootfs_uuid ROOTFS_UUID diff --git a/release/checks/integration/meson.build b/release/checks/integration/meson.build index 7214e47ba1ec23c247c8b76e5c8d94aff1ce1fd6..7bf8f51e4c762d2279ed6064ae1a87cb9b07494c 100644 --- a/release/checks/integration/meson.build +++ b/release/checks/integration/meson.build @@ -11,7 +11,7 @@ run_qemu = find_program('../../../scripts/run-qemu.sh') lib = static_library('spectrum-integration-test', 'lib.c') -foreach test : ['appimage', 'late-serial', 'networking', 'portal', 'try'] +foreach test : ['appimage', 'late-serial', 'networking', 'portal'] test(test, executable(test, test + '.c', link_with : lib), timeout : 400, args : [run_qemu]) diff --git a/release/checks/integration/try.c b/release/checks/integration/try.c deleted file mode 100644 index 4b874c0a7e9b48324497450fb5488e04576fd43b..0000000000000000000000000000000000000000 --- a/release/checks/integration/try.c +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is> - -#include "lib.h" - -#include <stdlib.h> -#include <string.h> - -void test(struct config c) -{ - struct vm *vm; - - c.drives.img = getenv_or_die("COMBINED_PATH"); - - vm = start_qemu(c); - - start_console_thread(vm, "GNU GRUB "); - wait_for_prompt(vm); - - start_console_thread(vm, "~ # "); - - // Assume that Try Spectrum is the first menu entry. - if (fputc('\n', vm_console_writer(vm)) == EOF) { - fputs("error writing to console\n", stderr); - exit(EXIT_FAILURE); - } - - wait_for_prompt(vm); -} diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..38210922ec165a36c33d99d08f04beabdc01ed53 100644 --- a/release/combined/eosimages.nix +++ b/release/combined/eosimages.nix @@ -14,9 +14,9 @@ runCommand "eosimages.img" { } '' mkdir dir cd dir - ln -s $image $imageName - sha256sum $imageName > $imageName.sha256 - tar -chf $NIX_BUILD_TOP/eosimages.tar * - tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out + ln -s -- $image $imageName + gzip -9 < $image > $imageName.gz + sha256sum -- $imageName.gz > $imageName.gz.sha256 + tar -ch -- $imageName.gz $imageName.gz.sha256 | tar2ext4 -o $out e2label $out eosimages '') (_: {}) diff --git a/release/combined/grub.cfg.in b/release/combined/grub.cfg.in index a8e73a3b4dc0d643cf575e3cc545ec9ff72380cb..a22f5fc96ba6451d44c0f9768a15a1f48c5dce1c 100644 --- a/release/combined/grub.cfg.in +++ b/release/combined/grub.cfg.in @@ -15,11 +15,6 @@ set gfxpayload=keep terminal_output gfxterm terminal_output console -menuentry "Try Spectrum" { - loopback live (hd0,gpt3)/Spectrum-0.0-x86_64-generic.0.Live.img - chainloader (live,gpt1)/EFI/Linux/spectrum.efi -} - menuentry "Install Spectrum" { set root=(hd0,gpt2) linux @linux@ @kernelParams@ -- 2.52.0
systemd-sysupdate has strict requirements on the partition layout: - The label of the active partition must match the template in the .transfer file. For instance, the root filesystem of Spectrum 0.0.0 must be in a partition with label "Spectrum_0.0.0", and the verity partition must have the label "Spectrum_0.0.0.verity". - The label of the inactive partition must be that of the old version of Spectrum, or "_empty" for freshly installed systems. - The partition type UUID must conform to the Discoverable Partition Specification. Also, the UKI must have a name that includes the OS version. Otherwise, it will not be deleted during updates. Since the partition label includes the OS version, add an OS version number. Use 0.0.0 to indicate that Spectrum OS is still in very early development and should not be used. The version number can be overridden in the build configuration file. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> Reviewed-by: Alyssa Ross <hi@alyssa.is> --- Changes since v4: - Rebase and address merge conflicts. - Add missing "VERSION = config.version" in Nix files. Changes since v2: - Split off into separate commit. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/initramfs/Makefile | 4 ++-- host/initramfs/shell.nix | 2 ++ host/rootfs/Makefile | 4 ++-- host/rootfs/default.nix | 5 +++-- host/rootfs/shell.nix | 2 ++ lib/config.default.nix | 1 + release/live/Makefile | 8 ++++---- release/live/default.nix | 3 +++ release/live/shell.nix | 3 ++- 9 files changed, 21 insertions(+), 11 deletions(-) diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile index c3d600ad5a55d81b8ca9c7a3e182ef5f4fd90f4b..a7f7bb22255b2cc3f845da7e85cadd7aab1efdb9 100644 --- a/host/initramfs/Makefile +++ b/host/initramfs/Makefile @@ -39,8 +39,8 @@ build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../sc uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$2 \ - $(ROOT_FS_IMAGE):root:$$1 + $(ROOT_FS_VERITY):verity:$$2:Spectrum_'$(VERSION).verity' \ + $(ROOT_FS_IMAGE):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ clean: diff --git a/host/initramfs/shell.nix b/host/initramfs/shell.nix index 8b47aa53bc19a818ebf563e281f22e82202a8ea5..44d4a985e969c1a57ad42d0666189c704aef9afd 100644 --- a/host/initramfs/shell.nix +++ b/host/initramfs/shell.nix @@ -4,6 +4,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, rootfs, pkgsStatic, stdenv , cryptsetup, jq, qemu_kvm, tar2ext4, util-linux +, config }: let @@ -18,5 +19,6 @@ initramfs.overrideAttrs ({ nativeBuildInputs ? [], env ? {}, ... }: { env = env // { KERNEL = "${rootfs.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; ROOT_FS = rootfs; + VERSION = config.version; }; })) (_: {}) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index ab24263c6f327e47cd1d012ca8d729b0ea5eb8f3..a6d9f23e9f5277b7c79a53105eb2dfe1bab1451e 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -96,8 +96,8 @@ build/live.img: ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../sc uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ - $(ROOT_FS_VERITY):verity:$$2 \ - $(ROOT_FS_IMAGE):root:$$1 + $(ROOT_FS_VERITY):verity:$$2:Spectrum_'$(VERSION).verity' \ + $(ROOT_FS_IMAGE):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ debug: diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 941c04e619baa7652d1812f4eb50445c607d5884..16a151971715f9a9d987dc92a1d06eb169de1144 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -3,8 +3,8 @@ # SPDX-FileCopyrightText: 2022 Unikie import ../../lib/call-package.nix ( -{ callSpectrumPackage, spectrum-build-tools, src -, pkgsMusl, pkgsStatic, linux_latest +{ callSpectrumPackage, config, spectrum-build-tools +, src, pkgsMusl, pkgsStatic, linux_latest }: pkgsStatic.callPackage ( @@ -125,6 +125,7 @@ stdenvNoCC.mkDerivation { printf "%s\n/\n" ${packagesSysroot} >$out sed p ${writeClosure [ packagesSysroot] } >>$out ''; + VERSION = config.version; }; # The Makefile uses $(ROOT_FS), not $(dest), so it can share code diff --git a/host/rootfs/shell.nix b/host/rootfs/shell.nix index 6df2f575fdfc7cdf8067ccfdb5fecaad9f6ea5e6..27f93e05fce036257d27cf9992fee8c925073f80 100644 --- a/host/rootfs/shell.nix +++ b/host/rootfs/shell.nix @@ -5,6 +5,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, rootfs, pkgsStatic, srcOnly, stdenv , btrfs-progs, cryptsetup, jq, netcat, qemu_kvm, reuse, util-linux +, config }: rootfs.overrideAttrs ( @@ -20,5 +21,6 @@ rootfs.overrideAttrs ( KERNEL = "${passthru.kernel}/${stdenv.hostPlatform.linux-kernel.target}"; LINUX_SRC = srcOnly passthru.kernel.configfile; VMLINUX = "${passthru.kernel.dev}/vmlinux"; + VERSION = config.version; }; })) (_: {}) diff --git a/lib/config.default.nix b/lib/config.default.nix index a8422345cc00f9413bb19ec968fd89c82fed801b..489c231490a8b66aa01f50053b25646060f7f963 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -4,4 +4,5 @@ { pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; + version = "0.0.0"; } diff --git a/release/live/Makefile b/release/live/Makefile index 5ab93451de109949af0e7ed7f70bf6827fefbf69..46628bdaa5b4a02aca3dd15be4477c3b2c194993 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -14,8 +14,8 @@ $(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/s set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$2 \ - $(ROOT_FS_IMAGE):root:$$1 + $(ROOT_FS_VERITY):verity:$$2:Spectrum_'$(VERSION).verity' \ + $(ROOT_FS_IMAGE):root:$$1:Spectrum_'$(VERSION)' mv $@.tmp $@ build/empty: @@ -27,8 +27,8 @@ build/boot.fat: $(SYSTEMD_BOOT_EFI) $(EFI_IMAGE) build/empty $(MMD) -i $@ ::/EFI ::/EFI/BOOT ::/EFI/Linux # This symlink is necessary. Copying $(EFI_IMAGE) directly # results in an unbootable image. TODO: figure out why. - ln -s $(EFI_IMAGE) build/spectrum.efi - $(MCOPY) -i $@ build/spectrum.efi ::/EFI/Linux + ln -s $(EFI_IMAGE) 'build/Spectrum_$(VERSION).efi' + $(MCOPY) -i $@ 'build/Spectrum_$(VERSION).efi' ::/EFI/Linux $(MCOPY) -i $@ $(SYSTEMD_BOOT_EFI) ::/EFI/BOOT/$(EFINAME) clean: diff --git a/release/live/default.nix b/release/live/default.nix index d1e2422e9f1ba666af7ad7a5cce1c80a242d0777..aa5c5869b9c82ce3722fc39029f6aabd7d8c874d 100644 --- a/release/live/default.nix +++ b/release/live/default.nix @@ -1,11 +1,13 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2021-2023, 2025 Alyssa Ross <hi@alyssa.is> # SPDX-FileCopyrightText: 2022 Unikie +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> import ../../lib/call-package.nix ( { callSpectrumPackage, spectrum-build-tools, src , lib, pkgsStatic, stdenvNoCC , cryptsetup, dosfstools, jq, mtools, util-linux +, config }: let @@ -46,6 +48,7 @@ stdenv.mkDerivation { SYSTEMD_BOOT_EFI = "${systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; EFI_IMAGE = efi; EFINAME = "BOOT${toUpper efiArch}.EFI"; + VERSION = config.version; }; buildFlags = [ "dest=$(out)" ]; diff --git a/release/live/shell.nix b/release/live/shell.nix index b0bf957c085d1581a24d8916925611da0a60ec8b..e542793a66fb972cfde90f6be2204986442b7d4b 100644 --- a/release/live/shell.nix +++ b/release/live/shell.nix @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> import ../../lib/call-package.nix ( -{ callSpectrumPackage, stdenv, qemu_kvm }: +{ callSpectrumPackage, config, stdenv, qemu_kvm }: let efi = callSpectrumPackage ../../host/efi.nix {}; @@ -17,6 +17,7 @@ in OVMF_CODE = "${qemu_kvm}/share/qemu/edk2-${stdenv.hostPlatform.qemuArch}-code.fd"; ROOT_FS = efi.rootfs; EFI_IMAGE = efi; + VERSION = config.version; }; } )) (_: {}) -- 2.52.0
systemd-sysupdate never writes to the running OS partition. Instead, it requires a separate partition to write the update into. Create a separate partition for that purpose. systemd-sysupdate will fail if the OS image does not fit in the partitions that the installer created. Therefor, make the partitions very large so that there is plenty of room for the OS to grow. This requires rewriting the code that calculates the partition sizes. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Leave B partitions empty. - Leave MiB unit implicit. - Use fixed GUIDs for blank partitions. Changes since v2: - Make into a standalone commit - Do not rely on separate script to generate the images. - Use a smaller size for the verity partition. --- Documentation/development/uuid-reference.adoc | 8 ++++++++ release/live/Makefile | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Documentation/development/uuid-reference.adoc b/Documentation/development/uuid-reference.adoc index 146615896104d5ab20c2e9353e5ed8f7a3dc54a6..16279c8a7e690bbaafdc3e0194f3130ba65c281c 100644 --- a/Documentation/development/uuid-reference.adoc +++ b/Documentation/development/uuid-reference.adoc @@ -59,6 +59,14 @@ Spectrum combined live system / installer image. The Spectrum installer system. +=== `18f2ccff-92f1-4bb1-a80e-24f76ecda90c` + +The not-yet-used B verity partition. + +=== `ec0c5ff3-f6b1-4adf-82b4-61336c4d135f` + +The not-yet-used B root filesystem partition. + ''' == Finding Undocumented UUIDs diff --git a/release/live/Makefile b/release/live/Makefile index 46628bdaa5b4a02aca3dd15be4477c3b2c194993..12b13d6e730c494086d1e7f763495ffbd4bd4b88 100644 --- a/release/live/Makefile +++ b/release/live/Makefile @@ -10,12 +10,15 @@ DTBS ?= build/empty dest = build/live.img $(dest): ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_IMAGES) +# 162MiB was calculated by running `veritysetup format` on 20GiB from /dev/urandom uuids=$$(awk -f ../../scripts/format-uuid.awk < $(ROOT_FS_VERITY_ROOTHASH)) && \ set -u -- $$uuids && \ bash ../../scripts/make-gpt.sh $@.tmp \ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \ - $(ROOT_FS_VERITY):verity:$$2:Spectrum_'$(VERSION).verity' \ - $(ROOT_FS_IMAGE):root:$$1:Spectrum_'$(VERSION)' + $(ROOT_FS_VERITY):verity:$$2:Spectrum_'$(VERSION).verity:162' \ + $(ROOT_FS_IMAGE):root:$$1:Spectrum_'$(VERSION):20000' \ + /dev/null:verity:18f2ccff-92f1-4bb1-a80e-24f76ecda90c:_empty:162 \ + /dev/null:root:ec0c5ff3-f6b1-4adf-82b4-61336c4d135f:_empty:20000 mv $@.tmp $@ build/empty: -- 2.52.0
Whenever a release is made, create a directory with the release files to be used for an update. After its SHA256SSUMS file is signed, the file is ready to be uploaded to a server for users to update from. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> Reviewd-by: Alyssa Ross <hi@alyssa.is> --- Changes since v4: - Only set -u because stdenv sets the reset. Update comment. Changes since v2: - Use UUIDs to name the rootfs and verity superblock. This will allow systemd-sysupdate to set the correct UUIDs on the rootfs and verity partitions, avoiding the need to use labels to find these partitions. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- release.nix | 2 ++ release/update.nix | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/release.nix b/release.nix index a4fe66ee5925aeee3a1f5f1fac249c595cee0885..704abb39a3d01152eac3dfe313066834c3cd0a66 100644 --- a/release.nix +++ b/release.nix @@ -8,5 +8,7 @@ import lib/call-package.nix ({ callSpectrumPackage }: { checks = callSpectrumPackage release/checks {}; + updates = callSpectrumPackage release/update.nix {}; + combined = callSpectrumPackage release/combined/run-vm.nix {}; }) (_: {}) diff --git a/release/update.nix b/release/update.nix new file mode 100644 index 0000000000000000000000000000000000000000..18a91ac1eea56e9b2a941eb08244b3dee613b721 --- /dev/null +++ b/release/update.nix @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../lib/call-package.nix ( +{ callSpectrumPackage, config, runCommand, stdenv }: + +let + efi = import ../host/efi.nix {}; +in +runCommand "spectrum-update-directory" { + __structuredAttrs = true; + unsafeDiscardReferences = { out = true; }; + dontFixup = true; + env = { VERSION = config.version; }; +} '' + # stdenv sets -eo pipefail, but not -u + set -u + mkdir -- "$out" + cd -- "$out" + read -r roothash < ${efi.rootfs}/rootfs.verity.roothash + if ! [[ "$roothash" =~ ^[0-9a-f]{64}$ ]]; then + printf 'Internal error: bad root hash %q\n' "$roothash" + exit 1 + fi + cp -- ${efi} "Spectrum_$VERSION.efi" + cp -- ${efi.rootfs}/rootfs.verity.superblock "Spectrum_''${VERSION}_''${roothash:32:32}.verity" + cp -- ${efi.rootfs}/rootfs "Spectrum_''${VERSION}_''${roothash:0:32}.root" + sha256sum -b "Spectrum_$VERSION.efi" \ + "Spectrum_''${VERSION}_''${roothash:32:32}.verity" \ + "Spectrum_''${VERSION}_''${roothash:0:32}.root" > SHA256SUMS + '' +) (_: {}) -- 2.52.0
Include a new `spectrum-update` command to update the system. This tells the new sys.appvm-systemd-sysupdate VM to download the updates into a staging directory using systemd-sysupdate. The host then runs systemd-sysupdate to apply the updates itself. sys.appvm-systemd-sysupdate uses host-provided information to fetch the update. This allows editing files on the host to change the update URL and signing key. systemd-sysupdate requires /boot to be mounted so that systemd-sysupdate can update the unified kernel image. systemd-sysupdate also requires that /tmp is writable so that it can store temporary files, so put a tmpfs there. Furthermore, there needs to be a directory for storing downloaded updates. Create /home so that users can mount their persistent data there. The directory the VM downloads updates into is *not* reset (wiped) before or after the update. This allows the VM to know if the system is already up to date. Otherwise, it would redownload the entire multi-gigabyte update image. Updates are currently not compressed. This should be changed in the future, but it would add a small amount of additional complexity. In particular, the script generating the update directory would need to generate a SHA256SUMS containing the hash of both the compressed and uncompressed versions. More importantly, the VM must not be able to make the host use the compressed version. This would be a potential security risk because decompression happens before signature verification. GnuPG currently decompresses signatures, but in the future it will be replaced by Sequoia which does not. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Do not strip leading and trailing whitespace from update URLs. - Create a single script that does the work. Pass the paths to curl and systemd sysupdate to it as environment variables. Inline the awk script into it. - Rebase and fix merge conflict. Changes since v3: - Move builtins.path from lib/config.nix to host/rootfs/default.nix. - Change config options from "update-url" to "updateUrl" and "update-signing-key" to "updateSigningKey". Changes since v2: - Generate the transfer files in the guest, not the host. - Do not use an environment file. - Reject URLs that cannot work. - Escape sed metacharacters. - Escape backslashes for systemd-sysupdate. - Do not validate update URLs at build time, only at runtime. - Reject only update URLs that cannot possibly work. - Set the partition UUIDs according to systemd's recommendation. - Do not rely on finding partitions by label. - Strip leading and trailing whitespace from the update URL. - Rename the update command from `update` to `spectrum-update`. - Delete the list of steps. Replace it with comments in the script. The awk script in the VM rejects URLs that contain whitespace. This is because they can't work, and passing them to systemd-sysupdate would require figuring out how to escape them. Rejecting such bogus URLs is simpler than preventing them from being mangled. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- host/rootfs/Makefile | 17 +++- host/rootfs/default.nix | 16 +++- host/rootfs/file-list.mk | 7 ++ host/rootfs/image/etc/fstab | 1 + .../image/etc/sysupdate.d/50-verity.transfer | 20 +++++ host/rootfs/image/etc/sysupdate.d/60-root.transfer | 20 +++++ .../image/etc/sysupdate.d/70-kernel.transfer | 20 +++++ .../image/etc/vm-sysupdate.d/50-verity.transfer | 18 +++++ .../image/etc/vm-sysupdate.d/60-root.transfer | 18 +++++ .../image/etc/vm-sysupdate.d/70-kernel.transfer | 18 +++++ host/rootfs/image/usr/bin/spectrum-update | 92 ++++++++++++++++++++++ host/rootfs/os-release.in | 15 ++++ lib/config.default.nix | 2 + lib/fake-update-signing-key.gpg | 3 + vm/app/systemd-sysupdate/default.nix | 26 ++++++ vm/app/systemd-sysupdate/download-update | 68 ++++++++++++++++ 16 files changed, 355 insertions(+), 6 deletions(-) diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index a6d9f23e9f5277b7c79a53105eb2dfe1bab1451e..74ff64019560aae6387df0e1b3409bc174251bdb 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -10,6 +10,7 @@ include file-list.mk ROOT_FS = build DIRS = \ + boot \ dev \ etc/s6-linux-init/env \ etc/s6-linux-init/run-image/configs \ @@ -33,13 +34,15 @@ DIRS = \ etc/s6-linux-init/run-image/vm/by-id \ etc/s6-linux-init/run-image/vm/by-name \ ext \ + home \ proc \ run \ - sys + sys \ + tmp FIFOS = etc/s6-linux-init/run-image/service/s6-svscan-log/fifo -BUILD_FILES = build/etc/s6-rc +BUILD_FILES = build/etc/s6-rc build/etc/os-release build/etc/update-url # This rule produces three files but Make only (portably) # supports one output per rule. Instead of resorting to temporary @@ -59,12 +62,22 @@ $(ROOT_FS_IMAGE): ../../scripts/make-erofs.sh $(PACKAGES_FILE) $(FILES) $(BUILD_ mkdir -p $(ROOT_FS) && \ { \ cat $(PACKAGES_FILE) ;\ + printf '%s\n%s\n' "$$UPDATE_SIGNING_KEY" /etc/systemd/import-pubring.gpg; \ for file in $(FILES) $(LINKS); do printf '%s\n%s\n' $$file "$${file#image/}"; done ;\ for file in $(BUILD_FILES); do printf '%s\n%s\n' $$file $${file#build/}; done ;\ printf 'build/empty\n%s\n' $(DIRS) ;\ printf 'build/fifo\n%s\n' $(FIFOS) ;\ } | ../../scripts/make-erofs.sh $@ +build/etc/update-url: + mkdir -p build/etc + # might have metacharacters, so avoid interpolation + printf %s\\n "$${UPDATE_URL:?'update URL empty or missing'}" > build/etc/update-url + +build/etc/os-release: + mkdir -p build/etc + sed 's/@VERSION@/$(VERSION)/g' < os-release.in > build/etc/os-release + build/fifo: mkdir -p build mkfifo -m 0600 $@ diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 16a151971715f9a9d987dc92a1d06eb169de1144..8b62c78510fd4e41c2cd1e5075cc8fafc08fa415 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -13,6 +13,7 @@ pkgsStatic.callPackage ( , busybox, cloud-hypervisor, cryptsetup, dbus, execline, inkscape , iproute2, inotify-tools, jq, mdevd, s6, s6-linux-init, socat , util-linuxMinimal, virtiofsd, xorg, xdg-desktop-portal-spectrum-host +, btrfs-progs }: let @@ -33,8 +34,8 @@ let foot = pkgsGui.foot.override { allowPgo = false; }; packages = [ - cloud-hypervisor cryptsetup dbus execline inotify-tools iproute2 - jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools + btrfs-progs cloud-hypervisor cryptsetup dbus execline inotify-tools + iproute2 jq mdevd s6 s6-linux-init s6-rc socat spectrum-host-tools util-linuxMinimal virtiofsd xdg-desktop-portal-spectrum-host (busybox.override { @@ -43,7 +44,7 @@ let }) # Take kmod from pkgsGui since we use pkgsGui.kmod.lib below anyway. - ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod systemd ]); + ] ++ (with pkgsGui; [ cosmic-files crosvm foot fuse3 kmod ]); nixosAllHardware = nixos ({ modulesPath, ... }: { imports = [ (modulesPath + "/profiles/all-hardware.nix") ]; @@ -64,17 +65,19 @@ let # https://inbox.vuxu.org/musl/20251017-dlopen-use-rpath-of-caller-dso-v1-1-46c... usrPackages = [ appvm kernel.modules firmware netvm - ] ++ (with pkgsGui; [ dejavu_fonts kmod.lib mesa westonLite ]); + ] ++ (with pkgsGui; [ dejavu_fonts kmod.lib mesa westonLite systemd ]); appvms = { appvm-firefox = callSpectrumPackage ../../vm/app/firefox.nix {}; appvm-foot = callSpectrumPackage ../../vm/app/foot.nix {}; appvm-gnome-text-editor = callSpectrumPackage ../../vm/app/gnome-text-editor.nix {}; + appvm-systemd-sysupdate = callSpectrumPackage ../../vm/app/systemd-sysupdate {}; }; packagesSysroot = runCommand "packages-sysroot" { depsBuildBuild = [ inkscape ]; nativeBuildInputs = [ xorg.lndir ]; + src = builtins.path { name = "os-release"; path = ./os-release.in; }; } '' mkdir -p $out/usr/bin $out/usr/share/dbus-1/services \ $out/usr/share/icons/hicolor/20x20/apps @@ -125,6 +128,11 @@ stdenvNoCC.mkDerivation { printf "%s\n/\n" ${packagesSysroot} >$out sed p ${writeClosure [ packagesSysroot] } >>$out ''; + UPDATE_SIGNING_KEY = builtins.path { + name = "signing-key"; + path = config.updateSigningKey; + }; + UPDATE_URL = config.updateUrl; VERSION = config.version; }; diff --git a/host/rootfs/file-list.mk b/host/rootfs/file-list.mk index 7625c54c0ae74ded2f3c9f4a860f21491f6e20a7..c08ecf0ab94a857fafc9ccdc9ea604885a57954f 100644 --- a/host/rootfs/file-list.mk +++ b/host/rootfs/file-list.mk @@ -37,13 +37,20 @@ FILES = \ image/etc/s6-linux-init/run-image/service/vmm/run \ image/etc/s6-linux-init/run-image/service/vmm/template/notification-fd \ image/etc/s6-linux-init/scripts/rc.init \ + image/etc/sysupdate.d/50-verity.transfer \ + image/etc/sysupdate.d/60-root.transfer \ + image/etc/sysupdate.d/70-kernel.transfer \ image/etc/udev/rules.d/99-spectrum.rules \ + image/etc/vm-sysupdate.d/50-verity.transfer \ + image/etc/vm-sysupdate.d/60-root.transfer \ + image/etc/vm-sysupdate.d/70-kernel.transfer \ image/etc/xdg/weston/autolaunch \ image/etc/xdg/weston/weston.ini \ image/usr/bin/assign-devices \ image/usr/bin/create-vm-dependencies \ image/usr/bin/run-appimage \ image/usr/bin/run-vmm \ + image/usr/bin/spectrum-update \ image/usr/bin/vm-console \ image/usr/bin/vm-import \ image/usr/bin/vm-start \ diff --git a/host/rootfs/image/etc/fstab b/host/rootfs/image/etc/fstab index 5dc9b2a3c4dff62ee49b2d827f53b45b7781a60f..6230d910a23339925fea0f2ffbc2baa5241ce3f2 100644 --- a/host/rootfs/image/etc/fstab +++ b/host/rootfs/image/etc/fstab @@ -4,3 +4,4 @@ proc /proc proc defaults 0 0 devpts /dev/pts devpts gid=5,mode=620 0 0 tmpfs /dev/shm tmpfs defaults 0 0 sysfs /sys sysfs defaults 0 0 +tmpfs /tmp tmpfs defaults 0 0 diff --git a/host/rootfs/image/etc/sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..9cd64b58ae55d55d378d99f5701f1ecef867e436 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/50-verity.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v_@u.verity + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_@v.verity +MatchPartitionType=root-verity +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/60-root.transfer b/host/rootfs/image/etc/sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..cd12d2bd2b4ecd9bb5c7d26cc7c27a4bdb74cac8 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/60-root.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v_@u.root + +[Target] +Type=partition +Path=auto +MatchPattern=Spectrum_@v +MatchPartitionType=root +PartitionFlags=0 +ReadOnly=1 diff --git a/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..e4190587a6bb127cb7315f38d59e48cf279318a4 --- /dev/null +++ b/host/rootfs/image/etc/sysupdate.d/70-kernel.transfer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=file:///run/updater +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/EFI/Linux +PathRelativeTo=boot +MatchPattern=Spectrum_@v.efi +Mode=0644 +InstancesMax=2 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer b/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer new file mode 100644 index 0000000000000000000000000000000000000000..ab4997c83605e3820a22b0b2178dcd76dfcf787e --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/50-verity.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v_@u.verity + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v_@u.verity +Mode=0644 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer b/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer new file mode 100644 index 0000000000000000000000000000000000000000..8a3175684f1697e0eca443eb0a1a97176e4f66d4 --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/60-root.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v_@u.root + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v_@u.root +Mode=0644 diff --git a/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer b/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer new file mode 100644 index 0000000000000000000000000000000000000000..cb181239d71c5a6d0a5b3652d5534a23eda64183 --- /dev/null +++ b/host/rootfs/image/etc/vm-sysupdate.d/70-kernel.transfer @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +# Uses example code from systemd man pages which is under MIT-0 +# (no attribution required). +[Transfer] +Verify=yes + +[Source] +Type=url-file +Path=@UPDATE_URL@ +MatchPattern=Spectrum_@v.efi + +[Target] +Type=regular-file +Path=/run/virtiofs/virtiofs0/updates +MatchPattern=Spectrum_@v.efi +Mode=0644 diff --git a/host/rootfs/image/usr/bin/spectrum-update b/host/rootfs/image/usr/bin/spectrum-update new file mode 100755 index 0000000000000000000000000000000000000000..613b43570d0538fce20296ccb1de2a6364e0df55 --- /dev/null +++ b/host/rootfs/image/usr/bin/spectrum-update @@ -0,0 +1,92 @@ +#!/bin/execlineb -WS1 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +if { mkdir -p -m 0700 /run/updater } + +# Take a global lock to avoid races. +s6-setlock /run/update-lock + +foreground { redirfd -w 2 /dev/null rmdir -- $1 } +if { umask 0077 mkdir -p -- $1 } +cd $1 +foreground { + # If this exists already that is okay. + foreground { redirfd -w 2 /dev/null btrfs subvolume create -- shared } + + # Delete any stale temporary files. Delete any existing signature + # files. If the VM is still running (it should not be), the VM might + # have write access to the directory. However, updates-dir-check is + # safe against that. + if { updates-dir-check cleanup shared } + + if { + foreground { + # TODO: suppress only "subvolume does not exist" errors. + redirfd -w 2 /dev/null + btrfs subvolume delete snapshot + } + rm -f snapshot + } + + backtick -E update_vm_id { + backtick -E id_path { readlink /run/vm/by-name/sys.appvm-systemd-sysupdate } + basename -- $id_path + } + + # $fsdir is read-only to the guest, but read-write to the host. + # Directories bind-mounted into it are read-write to the guest. + # See etc/s6-linux-init/run-image/service/vhost-user-fs/template/run + # for details. + + # Set up /etc with what the VM needs. The VM will overlay this + # on its own /etc. + # + # In the future, this should use a bind mount instead of copying + # into a tmpfs. However, this would significantly complicate the + # cleanup code. Deleting fs/etc would require undoing the bind + # mounts instead of rm -rf. Once this code is in a separate mount + # namespace, the copies should be replaced by bind mounts. + if { + if { rm -rf -- /run/vm/by-id/${update_vm_id}/fs/etc } + umask 022 + if { mkdir -p -- /run/vm/by-id/${update_vm_id}/fs/updates /run/vm/by-id/${update_vm_id}/fs/etc/systemd } + if { cp -R -- /etc/vm-sysupdate.d /etc/update-url /run/vm/by-id/${update_vm_id}/fs/etc } + cp -- /etc/systemd/import-pubring.gpg /run/vm/by-id/${update_vm_id}/fs/etc/systemd + } + + # If the directory is already mounted, unmount it. This prevents a + # confusing error from mount. + foreground { redirfd -w 2 /dev/null umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Share the update directory with the VM. + if { mount --bind -- shared /run/vm/by-id/${update_vm_id}/fs/updates } + + # Start the update VM. + if { vm-start $update_vm_id } + + # Wait for the VM to exit. + # TODO: This is racy. If the update finishes before this code runs, + # the s6-svwait call will fail. + if { s6-svwait -D /run/service/vmm/instance/${update_vm_id} } + + # Remove the bind mount. + if { umount -- /run/vm/by-id/${update_vm_id}/fs/updates } + + # Ensure that the VM cannot change the directory + # while systemd-sysupdate is using it. + if { btrfs subvolume snapshot -- shared snapshot } + + # Validate the update directory. Delete any stale temporary files. + # Check that a signature file was downloaded. + if { updates-dir-check check snapshot } + + unshare --mount + if { mount --bind -o ro -- snapshot /run/updater } + + /usr/lib/systemd/systemd-sysupdate update +} +importas -i sysupdate_exit_status ? +# Clean up. +foreground { btrfs subvolume delete -- snapshot } +exit $sysupdate_exit_status diff --git a/host/rootfs/os-release.in b/host/rootfs/os-release.in new file mode 100644 index 0000000000000000000000000000000000000000..d6e699e82f87dcb1c4656ac19d4e9986282f14a5 --- /dev/null +++ b/host/rootfs/os-release.in @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +NAME="Spectrum OS" +ID=spectrum +PRETTY_NAME="Spectrum @VERSION@" +VERSION=@VERSION@ +VERSION_ID=@VERSION@ +IMAGE_ID=spectrum-root +IMAGE_VERSION=@VERSION@ +RELEASE_TYPE=development +HOME_URL="https://spectrum-os.org" +BUG_REPORT_URL="mailto:discuss@spectrum-os.org" +ANSI_COLOR="1;34" +VENDOR_NAME=Spectrum +VENDOR_URL="https://spectrum-os.org" diff --git a/lib/config.default.nix b/lib/config.default.nix index 489c231490a8b66aa01f50053b25646060f7f963..f6b70fa5e8431bef79222c10c79e8015f7fe65be 100644 --- a/lib/config.default.nix +++ b/lib/config.default.nix @@ -5,4 +5,6 @@ pkgsFun = import ./nixpkgs.default.nix; pkgsArgs = {}; version = "0.0.0"; + updateUrl = "https://your-spectrum-os-update-server.invalid/download-directory"; + updateSigningKey = ./fake-update-signing-key.gpg; } diff --git a/lib/fake-update-signing-key.gpg b/lib/fake-update-signing-key.gpg new file mode 100644 index 0000000000000000000000000000000000000000..12e18f4c7c740e31692e1f1975282fa72ac1f2e3 --- /dev/null +++ b/lib/fake-update-signing-key.gpg @@ -0,0 +1,3 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +NOT A VALID KEY - UPDATES WILL NOT WORK diff --git a/vm/app/systemd-sysupdate/default.nix b/vm/app/systemd-sysupdate/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..69be0bab500ea2ea6cb3b6d71edbf1a3e7bddbba --- /dev/null +++ b/vm/app/systemd-sysupdate/default.nix @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> + +import ../../../lib/call-package.nix ( +{ callSpectrumPackage, curl, lib, src +, runCommand, systemd, writeScript +}: + +let + downloadUpdate = builtins.path { + name = "download-update"; + path = ./download-update; + }; +in + +callSpectrumPackage ../../make-vm.nix {} { + providers.net = [ "sys.netvm" ]; + type = "nix"; + run = writeScript "run-script" '' +#!/usr/bin/env -S execlineb -WS0 +export CURL_PATH ${curl}/bin/curl +export SYSTEMD_SYSUPDATE_PATH ${systemd}/lib/systemd/systemd-sysupdate +${downloadUpdate} +''; +}) (_: {}) diff --git a/vm/app/systemd-sysupdate/download-update b/vm/app/systemd-sysupdate/download-update new file mode 100755 index 0000000000000000000000000000000000000000..eada41c6c8ad5edcedd9f4d76b76492e0b8be826 --- /dev/null +++ b/vm/app/systemd-sysupdate/download-update @@ -0,0 +1,68 @@ +#!/usr/bin/env -S execlineb -WS0 +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +export LC_ALL C +export LANGUAGE C +if { mount -toverlay -olowerdir=/run/virtiofs/virtiofs0/etc:/etc -- overlay /etc } +backtick tmpdir { mktemp -d /tmp/sysupdate-XXXXXX } +# Not a useless use of cat: if there are NUL bytes in the URL +# busybox's awk might misbehave. +backtick update_url { cat /etc/update-url } +if { + backtick sed_rhs { + # Use awk to both validate the URL and to escape sed metacharacters. + # Reject URLs with control characters, query parameters, or fragments. + # They *cannot* work and so are rejected to produce better error messages. + # + # curl rejects control characters with "Malformed input to a URL function". + # Fragment specifiers ("#") and query parameters ("?") break concatenating + # /SHA256SUMS and /SHA256SUMS.sha256.asc onto the update URL. Also, it is + # simpler to reject update URLs that contain whitespace than to try to + # escape them. + # + # Backslash needs to be escaped once for systemd-sysupdate and again for sed. + # Ampersand needs to be escaped once for sed. + awk "BEGIN { + update_url = ENVIRON[\"update_url\"]; + if (update_url ~ /^[^\\001-\\040?#\\x7F]+$/) { + # Use & to avoid extra escaping (16 or 32 backslashes!) + # and a divergence between POSIX and GNU awk. + gsub(/\\\\/, \"&&&&\", update_url); + gsub(/&/, \"\\\\\\\\&\", update_url); + print update_url; + exit 0; + } else { + print ARGV[2] > \"/dev/stderr\"; + exit 100; + } + }" -- $3 + "Bad update URL from host: control characters, whitespace, query parameters, and fragment specifiers not allowed" + } + elglob -w -0 transfer_file_ /etc/vm-sysupdate.d/*.transfer + forx -E transfer_file { $transfer_file_ } + backtick target_basename { + basename -- $transfer_file + } + multisubstitute { + importas -iuS sed_rhs + importas -iuS target_basename + importas -iuS tmpdir + define sed_input $transfer_file + } + redirfd -w 1 ${tmpdir}/${target_basename} + sed -E -- "s#@UPDATE_URL@#${sed_rhs}#g" $sed_input +} +multisubstitute { + importas -iuS update_url + importas -iuS CURL_PATH + importas -iuS SYSTEMD_SYSUPDATE_PATH + importas -iuS tmpdir +} +if { $SYSTEMD_SYSUPDATE_PATH --definitions=${tmpdir} update } +# [ and ] are allowed in update URLs so that IPv6 addresses work, but +# they cause globbing in the curl command-line tool by default. Use --globoff +# to disable this feature. +if { $CURL_PATH -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS -- ${update_url}/SHA256SUMS } +$CURL_PATH -L --proto-redir =http,https --globoff + -o /run/virtiofs/virtiofs0/updates/SHA256SUMS.sha256.asc -- ${update_url}/SHA256SUMS.sha256.asc -- 2.52.0
The documentation previously stated that updates were not possible without reinstalling. This is still the case by default, but it is possible for developers to enable updates for images they build. Update the documentaion to reflect this. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Move the documentation from the user section to the developer section. Changes since v2: - Move the documentation on how to enable updates to the part on build configuration. - Clarify what happens if an update is interrupted. - Move details to a technical note. - Link to systemd-sysupdate. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Documentation/development/build-configuration.adoc | 15 ++++++++ Documentation/development/index.adoc | 2 ++ Documentation/development/updates.adoc | 42 ++++++++++++++++++++++ Documentation/installation/index.adoc | 6 +++- 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/Documentation/development/build-configuration.adoc b/Documentation/development/build-configuration.adoc index 545aa8c05ac40a101b5ee280015cde7ec4f3a66f..49651d05890900b74cafb3d75945b3bcc5b86ce6 100644 --- a/Documentation/development/build-configuration.adoc +++ b/Documentation/development/build-configuration.adoc @@ -20,6 +20,21 @@ The configuration file should contain an attribute set. See https://spectrum-os.org/git/spectrum/tree/lib/config.default.nix[lib/config.default.nix] for supported configuration attributes and their default values. +To enable updates, you need to specify a version, an update URL, and an update signing key. +By default, the update URL is set to a .invalid domain and the update signing key is +an invalid key. Therefore, updates will not work. To enable updates, provide a valid key +and update server URL. + +Spectrum uses https://www.freedesktop.org/software/systemd/man/latest/systemd-sysupdate.html[systemd-sysupdate], +so see the https://www.freedesktop.org/software/systemd/man/latest/sysupdate.d.html[sysupdate.d] +documentation for what you need to put on your server. Building +https://spectrum-os.org/git/spectrum/tree/release/updates.nix[release/updates.nix] produces an +directory that is compatible with systemd-sysupdate, except that the signature +(`SHA256SUMS.sha256.asc`) is missing. + +Updates are signed, so the worst a compromised update +server can do is fill up your user data partition. + .config.nix to build Spectrum with a https://nixos.org/manual/nixpkgs/unstable/#sec-overlays-definition[Nixpkgs overlay] [example] [source,nix] diff --git a/Documentation/development/index.adoc b/Documentation/development/index.adoc index 6b48418ba218354ee0493cd82188c54141f63e9e..4e504253dc16286273e1af5cae9614789b2c4a12 100644 --- a/Documentation/development/index.adoc +++ b/Documentation/development/index.adoc @@ -18,6 +18,8 @@ Spectrum is free software, currently under active development. TIP: For information on writing guidelines, see xref:../contributing/writing_documentation.adoc[Documentation Style Guide]. +If you want to update Spectrum without reinstalling, see how to +xref:updates.adoc[Enable updates]. == Mailing Lists diff --git a/Documentation/development/updates.adoc b/Documentation/development/updates.adoc new file mode 100644 index 0000000000000000000000000000000000000000..8746f97e5d9b36d4960a64544af08f57ff89ce9a --- /dev/null +++ b/Documentation/development/updates.adoc @@ -0,0 +1,42 @@ += Updating the OS +:page-parent: Development + +// SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com> +// SPDX-License-Identifier: GFDL-1.3-no-invariants-or-later OR CC-BY-SA-4.0 + +Right now, there is no official update server or update signing key. +However, it is possible to run your own update server. See +xref:../development/build-configuration.adoc[build configuration] +for how to enable updates for your own Spectrum images. + +== Updating the system + +If you have built your image with updates enabled, you can update the +system using the `spectrum-update` command. This takes the path to a +staging directory as argument. This directory must be on a BTRFS +filesystem. It is strongly recommended to not use this directory +for any other purpose. However, it's safe to rename the directory +and use `spectrum-update` with the new path afterwards. + +If there is a problem with the update, it's safe to try again. +If that still doesn't work, you can delete the directory and +try again with an empty one. This will cause `spectrum-update` +to download the latest version even if it is already installed, but +is otherwise harmless. + +Updates are atomic and take effect after the system reboots. +If the system is rebooted, crashes, or loses power during an +update, the update will not take effect. It is safe to resume +an interrupted update. + +Since Spectrum's host has no network access, the VM that does the +updates (`sys.appvm-systemd-sysupdate`) is given a BTRFS subvolume to +write the updates into. It uses `systemd-sysupdate` to download the updates +into this directory. Once it exits, the host snapshots this directory and +checks it for malicious filenames or non-regular files. If the check +passes, this directory is used as the source for `systemd-sysupdate`, +which installs the updates to the OS volume and EFI system partition. + +See the documentation of +https://www.freedesktop.org/software/systemd/man/systemd-sysupdate.html[systemd-sysupdate]. +for some of the details. diff --git a/Documentation/installation/index.adoc b/Documentation/installation/index.adoc index d67c88dda062066c19c3b21e699f074cc18a6dbc..d1df2edc9b0ca902824ff729eec139270fb40777 100644 --- a/Documentation/installation/index.adoc +++ b/Documentation/installation/index.adoc @@ -18,6 +18,10 @@ development. == Uninstalling and Updating -Currently, there is no implementation for a software update. +Software updates are a work in progress and are not currently available. + +If you built Spectrum yourself, see +xref:../development/build-configuration.adoc[Build configuration] for how +to enable updates for it. You can replace Spectrum by installing another OS. -- 2.52.0
Wrong values for the version or update URL will cause very confusing build-time or runtime errors. Provide a better user experience by validating them up-front. The update URL validator is loose. It rejects only URLs that cannot possibly work: either appending /SHA256SUMS to them doesn't append to the path, or they will definitely be rejected by curl due to being malformed. The version validator is in lib/config.nix, as the version number is used in many places. It checks that the version only uses characters that are permitted by systemd's version number specification [1] and that will not break code that uses them in shell or sed commands. [1]: https://uapi-group.org/specifications/specs/version_format_specification Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- Changes since v4: - Drop compression level. - Centralize validation. - Use camelCase for Nix identifiers. - Clean up formatting. Changes since v3: - Validate compression level. Changes since v2: - Use loose URL validation: allow anything that might work. - Only reject versions that violate the specification. Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com> --- lib/config.nix | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/config.nix b/lib/config.nix index e437cdbe9aa22dd0f9c8d7052ac331c8fccf6ce6..e641642de07c1549e69fc12e91c4e80e2f82d035 100644 --- a/lib/config.nix +++ b/lib/config.nix @@ -17,6 +17,31 @@ let callConfig = config: if builtins.typeOf config == "lambda" then config { inherit default; } else config; + finalConfig = default // callConfig config; + # Use builtins.fromJSON because it supports \uXXXX escapes. + # This is the same regex used by check-url.awk in the update VM. + # The update code is careful to escape any metacharacters, but some + # simply cannot be made to work. Concatenating the URL with /SHA256SUMS + # must append to the path portion of the URL, and the URL must be one + # that libcurl will accept. + urlRegex = builtins.fromJSON "\"^[^\\u0001- #?\\u007F]+$\""; in -default // callConfig config +# Version is used in many files, so validate it here. +# See https://uapi-group.org/specifications/specs/version_format_specification +# for allowed version strings. +if builtins.match "[[:alnum:]_.~^-]+" finalConfig.version == null then + builtins.abort '' + Version ${builtins.toJSON finalConfig.version} has forbidden characters. + Only ASCII alphanumerics, ".", "_", "~", "^", "+", and "-" are allowed. + See <https://uapi-group.org/specifications/specs/version_format_specification>. + '' +else +if builtins.match urlRegex finalConfig.updateUrl == null then + builtins.abort '' + Update URL ${builtins.toJSON finalConfig.updateUrl} has forbidden characters. + Query strings, and fragment specifiers are not supported. + ASCII control characters and whitespace must be %-encoded. + '' +else +finalConfig -- 2.52.0
participants (3)
-
Alyssa Ross -
Alyssa Ross -
Demi Marie Obenour