* [PATCH 0/7] System updates based on systemd-sysupdate
@ 2025-10-29 10:12 Demi Marie Obenour
2025-10-29 10:12 ` [PATCH 1/7] host/rootfs: Use full util-linux and systemd Demi Marie Obenour
` (7 more replies)
0 siblings, 8 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-10-29 10:12 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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)
^ permalink raw reply [flat|nested] 177+ messages in thread
* [PATCH 1/7] host/rootfs: Use full util-linux and systemd
2025-10-29 10:12 [PATCH 0/7] System updates based on systemd-sysupdate Demi Marie Obenour
@ 2025-10-29 10:12 ` Demi Marie Obenour
2025-10-29 11:36 ` Alyssa Ross
2025-10-29 10:12 ` [PATCH 2/7] release/combined: Compress installation image Demi Marie Obenour
` (6 subsequent siblings)
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-10-29 10:12 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH 2/7] release/combined: Compress installation image
2025-10-29 10:12 [PATCH 0/7] System updates based on systemd-sysupdate Demi Marie Obenour
2025-10-29 10:12 ` [PATCH 1/7] host/rootfs: Use full util-linux and systemd Demi Marie Obenour
@ 2025-10-29 10:12 ` Demi Marie Obenour
2025-10-29 11:50 ` Alyssa Ross
2025-10-29 10:12 ` [PATCH 3/7] tools: Add directory checker for updates Demi Marie Obenour
` (5 subsequent siblings)
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-10-29 10:12 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH 3/7] tools: Add directory checker for updates
2025-10-29 10:12 [PATCH 0/7] System updates based on systemd-sysupdate Demi Marie Obenour
2025-10-29 10:12 ` [PATCH 1/7] host/rootfs: Use full util-linux and systemd Demi Marie Obenour
2025-10-29 10:12 ` [PATCH 2/7] release/combined: Compress installation image Demi Marie Obenour
@ 2025-10-29 10:12 ` Demi Marie Obenour
2025-10-29 12:01 ` Alyssa Ross
2025-10-29 10:12 ` [PATCH 4/7] Adjust partition layout to support updates Demi Marie Obenour
` (4 subsequent siblings)
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-10-29 10:12 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH 4/7] Adjust partition layout to support updates
2025-10-29 10:12 [PATCH 0/7] System updates based on systemd-sysupdate Demi Marie Obenour
` (2 preceding siblings ...)
2025-10-29 10:12 ` [PATCH 3/7] tools: Add directory checker for updates Demi Marie Obenour
@ 2025-10-29 10:12 ` Demi Marie Obenour
2025-10-29 15:49 ` Alyssa Ross
2025-10-29 10:12 ` [PATCH 5/7] release: add install step Demi Marie Obenour
` (3 subsequent siblings)
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-10-29 10:12 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH 5/7] release: add install step
2025-10-29 10:12 [PATCH 0/7] System updates based on systemd-sysupdate Demi Marie Obenour
` (3 preceding siblings ...)
2025-10-29 10:12 ` [PATCH 4/7] Adjust partition layout to support updates Demi Marie Obenour
@ 2025-10-29 10:12 ` Demi Marie Obenour
2025-10-29 12:20 ` Alyssa Ross
2025-10-29 10:12 ` [PATCH 6/7] Factor out dm-verity build rules Demi Marie Obenour
` (2 subsequent siblings)
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-10-29 10:12 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH 6/7] Factor out dm-verity build rules
2025-10-29 10:12 [PATCH 0/7] System updates based on systemd-sysupdate Demi Marie Obenour
` (4 preceding siblings ...)
2025-10-29 10:12 ` [PATCH 5/7] release: add install step Demi Marie Obenour
@ 2025-10-29 10:12 ` Demi Marie Obenour
2025-10-29 12:22 ` Alyssa Ross
2025-10-29 10:12 ` [PATCH 7/7] Support updates via systemd-sysupdate Demi Marie Obenour
2025-11-12 22:14 ` [PATCH v2 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-10-29 10:12 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH 7/7] Support updates via systemd-sysupdate
2025-10-29 10:12 [PATCH 0/7] System updates based on systemd-sysupdate Demi Marie Obenour
` (5 preceding siblings ...)
2025-10-29 10:12 ` [PATCH 6/7] Factor out dm-verity build rules Demi Marie Obenour
@ 2025-10-29 10:12 ` Demi Marie Obenour
2025-10-29 15:48 ` Alyssa Ross
2025-11-12 22:14 ` [PATCH v2 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-10-29 10:12 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* Re: [PATCH 1/7] host/rootfs: Use full util-linux and systemd
2025-10-29 10:12 ` [PATCH 1/7] host/rootfs: Use full util-linux and systemd Demi Marie Obenour
@ 2025-10-29 11:36 ` Alyssa Ross
2025-11-01 3:25 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-10-29 11:36 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 4356 bytes --]
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 2/7] release/combined: Compress installation image
2025-10-29 10:12 ` [PATCH 2/7] release/combined: Compress installation image Demi Marie Obenour
@ 2025-10-29 11:50 ` Alyssa Ross
2025-10-29 16:51 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-10-29 11:50 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 3216 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-10-29 10:12 ` [PATCH 3/7] tools: Add directory checker for updates Demi Marie Obenour
@ 2025-10-29 12:01 ` Alyssa Ross
2025-10-31 20:31 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-10-29 12:01 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 8303 bytes --]
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;
> +}
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 5/7] release: add install step
2025-10-29 10:12 ` [PATCH 5/7] release: add install step Demi Marie Obenour
@ 2025-10-29 12:20 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-10-29 12:20 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 6826 bytes --]
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 6/7] Factor out dm-verity build rules
2025-10-29 10:12 ` [PATCH 6/7] Factor out dm-verity build rules Demi Marie Obenour
@ 2025-10-29 12:22 ` Alyssa Ross
2025-10-31 6:39 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-10-29 12:22 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 570 bytes --]
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?
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 7/7] Support updates via systemd-sysupdate
2025-10-29 10:12 ` [PATCH 7/7] Support updates via systemd-sysupdate Demi Marie Obenour
@ 2025-10-29 15:48 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-10-29 15:48 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 18245 bytes --]
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 4/7] Adjust partition layout to support updates
2025-10-29 10:12 ` [PATCH 4/7] Adjust partition layout to support updates Demi Marie Obenour
@ 2025-10-29 15:49 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-10-29 15:49 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 2845 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 2/7] release/combined: Compress installation image
2025-10-29 11:50 ` Alyssa Ross
@ 2025-10-29 16:51 ` Alyssa Ross
2025-11-01 22:15 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-10-29 16:51 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 1975 bytes --]
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".
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 6/7] Factor out dm-verity build rules
2025-10-29 12:22 ` Alyssa Ross
@ 2025-10-31 6:39 ` Demi Marie Obenour
0 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-10-31 6:39 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 742 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-10-29 12:01 ` Alyssa Ross
@ 2025-10-31 20:31 ` Demi Marie Obenour
2025-11-01 12:17 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-10-31 20:31 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 9213 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 1/7] host/rootfs: Use full util-linux and systemd
2025-10-29 11:36 ` Alyssa Ross
@ 2025-11-01 3:25 ` Demi Marie Obenour
2025-11-01 12:13 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-01 3:25 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 5413 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 1/7] host/rootfs: Use full util-linux and systemd
2025-11-01 3:25 ` Demi Marie Obenour
@ 2025-11-01 12:13 ` Alyssa Ross
2025-11-06 9:15 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-01 12:13 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 3207 bytes --]
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?
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-10-31 20:31 ` Demi Marie Obenour
@ 2025-11-01 12:17 ` Alyssa Ross
2025-11-01 14:09 ` Alyssa Ross
2025-11-01 18:36 ` Demi Marie Obenour
0 siblings, 2 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-01 12:17 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 3659 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-11-01 12:17 ` Alyssa Ross
@ 2025-11-01 14:09 ` Alyssa Ross
2025-11-01 18:36 ` Demi Marie Obenour
1 sibling, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-01 14:09 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 1959 bytes --]
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?)
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-11-01 12:17 ` Alyssa Ross
2025-11-01 14:09 ` Alyssa Ross
@ 2025-11-01 18:36 ` Demi Marie Obenour
2025-11-02 12:18 ` Alyssa Ross
1 sibling, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-01 18:36 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 4165 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 2/7] release/combined: Compress installation image
2025-10-29 16:51 ` Alyssa Ross
@ 2025-11-01 22:15 ` Demi Marie Obenour
2025-11-02 0:18 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-01 22:15 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 2518 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 2/7] release/combined: Compress installation image
2025-11-01 22:15 ` Demi Marie Obenour
@ 2025-11-02 0:18 ` Demi Marie Obenour
2025-11-02 12:05 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-02 0:18 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 3224 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 2/7] release/combined: Compress installation image
2025-11-02 0:18 ` Demi Marie Obenour
@ 2025-11-02 12:05 ` Alyssa Ross
2025-11-02 14:42 ` Alyssa Ross
2025-11-02 19:38 ` Demi Marie Obenour
0 siblings, 2 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-02 12:05 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 3369 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-11-01 18:36 ` Demi Marie Obenour
@ 2025-11-02 12:18 ` Alyssa Ross
2025-11-02 12:43 ` Alyssa Ross
2025-11-02 19:21 ` Demi Marie Obenour
0 siblings, 2 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-02 12:18 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 4795 bytes --]
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/d00f840c573103c2d51aed2b169386f7acfe7026/mesonbuild/compilers/compilers.py#L255-L264
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?
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-11-02 12:18 ` Alyssa Ross
@ 2025-11-02 12:43 ` Alyssa Ross
2025-11-02 19:34 ` Demi Marie Obenour
2025-11-02 19:21 ` Demi Marie Obenour
1 sibling, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-02 12:43 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 2955 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 2/7] release/combined: Compress installation image
2025-11-02 12:05 ` Alyssa Ross
@ 2025-11-02 14:42 ` Alyssa Ross
2025-11-02 19:38 ` Demi Marie Obenour
1 sibling, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-02 14:42 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 3857 bytes --]
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.)
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-11-02 12:18 ` Alyssa Ross
2025-11-02 12:43 ` Alyssa Ross
@ 2025-11-02 19:21 ` Demi Marie Obenour
2025-11-04 15:27 ` Alyssa Ross
1 sibling, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-02 19:21 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 5202 bytes --]
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/d00f840c573103c2d51aed2b169386f7acfe7026/mesonbuild/compilers/compilers.py#L255-L264
>
> 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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-11-02 12:43 ` Alyssa Ross
@ 2025-11-02 19:34 ` Demi Marie Obenour
2025-11-04 15:26 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-02 19:34 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 3643 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 2/7] release/combined: Compress installation image
2025-11-02 12:05 ` Alyssa Ross
2025-11-02 14:42 ` Alyssa Ross
@ 2025-11-02 19:38 ` Demi Marie Obenour
1 sibling, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-02 19:38 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 3363 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-11-02 19:34 ` Demi Marie Obenour
@ 2025-11-04 15:26 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-04 15:26 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 3756 bytes --]
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. :)
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-11-02 19:21 ` Demi Marie Obenour
@ 2025-11-04 15:27 ` Alyssa Ross
2025-11-04 22:56 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-04 15:27 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 5416 bytes --]
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/d00f840c573103c2d51aed2b169386f7acfe7026/mesonbuild/compilers/compilers.py#L255-L264
>>
>> 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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-11-04 15:27 ` Alyssa Ross
@ 2025-11-04 22:56 ` Demi Marie Obenour
2025-11-06 10:15 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-04 22:56 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 5716 bytes --]
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/d00f840c573103c2d51aed2b169386f7acfe7026/mesonbuild/compilers/compilers.py#L255-L264
>>>
>>> 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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 1/7] host/rootfs: Use full util-linux and systemd
2025-11-01 12:13 ` Alyssa Ross
@ 2025-11-06 9:15 ` Demi Marie Obenour
0 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-06 9:15 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 3798 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH 3/7] tools: Add directory checker for updates
2025-11-04 22:56 ` Demi Marie Obenour
@ 2025-11-06 10:15 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-06 10:15 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 3187 bytes --]
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* [PATCH v2 0/8] System updates based on systemd-sysupdate
2025-10-29 10:12 [PATCH 0/7] System updates based on systemd-sysupdate Demi Marie Obenour
` (6 preceding siblings ...)
2025-10-29 10:12 ` [PATCH 7/7] Support updates via systemd-sysupdate Demi Marie Obenour
@ 2025-11-12 22:14 ` Demi Marie Obenour
2025-11-12 22:14 ` [PATCH v2 1/8] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
` (8 more replies)
7 siblings, 9 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-12 22:14 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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-401c1be2a11b@gmail.com
---
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)
^ permalink raw reply [flat|nested] 177+ messages in thread
* [PATCH v2 1/8] host/rootfs: Install all programs from util-linuxMinimal
2025-11-12 22:14 ` [PATCH v2 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
@ 2025-11-12 22:14 ` Demi Marie Obenour
2025-11-13 12:35 ` Alyssa Ross
2025-11-12 22:14 ` [PATCH v2 2/8] host/rootfs: Install systemd-pull Demi Marie Obenour
` (7 subsequent siblings)
8 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-12 22:14 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v2 2/8] host/rootfs: Install systemd-pull
2025-11-12 22:14 ` [PATCH v2 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-12 22:14 ` [PATCH v2 1/8] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
@ 2025-11-12 22:14 ` Demi Marie Obenour
2025-11-13 15:22 ` Alyssa Ross
2025-11-12 22:14 ` [PATCH v2 3/8] tools: Add directory checker for updates Demi Marie Obenour
` (6 subsequent siblings)
8 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-12 22:14 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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-46c69eda1473@iscas.ac.cn/
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v2 3/8] tools: Add directory checker for updates
2025-11-12 22:14 ` [PATCH v2 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-12 22:14 ` [PATCH v2 1/8] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
2025-11-12 22:14 ` [PATCH v2 2/8] host/rootfs: Install systemd-pull Demi Marie Obenour
@ 2025-11-12 22:14 ` Demi Marie Obenour
2025-11-13 13:21 ` Alyssa Ross
2025-11-12 22:14 ` [PATCH v2 4/8] Adjust partition layout to support updates Demi Marie Obenour
` (5 subsequent siblings)
8 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-12 22:14 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v2 4/8] Adjust partition layout to support updates
2025-11-12 22:14 ` [PATCH v2 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
` (2 preceding siblings ...)
2025-11-12 22:14 ` [PATCH v2 3/8] tools: Add directory checker for updates Demi Marie Obenour
@ 2025-11-12 22:14 ` Demi Marie Obenour
2025-11-13 16:00 ` Alyssa Ross
2025-11-12 22:14 ` [PATCH v2 5/8] release: Create directory with system update Demi Marie Obenour
` (4 subsequent siblings)
8 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-12 22:14 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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.is/
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v2 5/8] release: Create directory with system update
2025-11-12 22:14 ` [PATCH v2 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
` (3 preceding siblings ...)
2025-11-12 22:14 ` [PATCH v2 4/8] Adjust partition layout to support updates Demi Marie Obenour
@ 2025-11-12 22:14 ` Demi Marie Obenour
2025-11-13 16:04 ` Alyssa Ross
2025-11-12 22:15 ` [PATCH v2 6/8] Support updates via systemd-sysupdate Demi Marie Obenour
` (3 subsequent siblings)
8 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-12 22:14 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v2 6/8] Support updates via systemd-sysupdate
2025-11-12 22:14 ` [PATCH v2 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
` (4 preceding siblings ...)
2025-11-12 22:14 ` [PATCH v2 5/8] release: Create directory with system update Demi Marie Obenour
@ 2025-11-12 22:15 ` Demi Marie Obenour
2025-11-13 16:44 ` Alyssa Ross
2025-11-12 22:15 ` [PATCH v2 7/8] Documentation: Update support Demi Marie Obenour
` (2 subsequent siblings)
8 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-12 22:15 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v2 7/8] Documentation: Update support
2025-11-12 22:14 ` [PATCH v2 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
` (5 preceding siblings ...)
2025-11-12 22:15 ` [PATCH v2 6/8] Support updates via systemd-sysupdate Demi Marie Obenour
@ 2025-11-12 22:15 ` Demi Marie Obenour
2025-11-13 16:49 ` Alyssa Ross
2025-11-12 22:15 ` [PATCH v2 8/8] lib/config.nix: Validate configuration parameters Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
8 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-12 22:15 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v2 8/8] lib/config.nix: Validate configuration parameters
2025-11-12 22:14 ` [PATCH v2 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
` (6 preceding siblings ...)
2025-11-12 22:15 ` [PATCH v2 7/8] Documentation: Update support Demi Marie Obenour
@ 2025-11-12 22:15 ` Demi Marie Obenour
2025-11-13 17:16 ` Alyssa Ross
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
8 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-12 22:15 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* Re: [PATCH v2 1/8] host/rootfs: Install all programs from util-linuxMinimal
2025-11-12 22:14 ` [PATCH v2 1/8] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
@ 2025-11-13 12:35 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-13 12:35 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 3617 bytes --]
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 3/8] tools: Add directory checker for updates
2025-11-12 22:14 ` [PATCH v2 3/8] tools: Add directory checker for updates Demi Marie Obenour
@ 2025-11-13 13:21 ` Alyssa Ross
2025-11-13 17:53 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-13 13:21 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 5517 bytes --]
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 2/8] host/rootfs: Install systemd-pull
2025-11-12 22:14 ` [PATCH v2 2/8] host/rootfs: Install systemd-pull Demi Marie Obenour
@ 2025-11-13 15:22 ` Alyssa Ross
2025-11-13 23:46 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-13 15:22 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 3130 bytes --]
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-46c69eda1473@iscas.ac.cn/
> 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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 4/8] Adjust partition layout to support updates
2025-11-12 22:14 ` [PATCH v2 4/8] Adjust partition layout to support updates Demi Marie Obenour
@ 2025-11-13 16:00 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-13 16:00 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 18628 bytes --]
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.is/
>
> 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.is
> 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?
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 5/8] release: Create directory with system update
2025-11-12 22:14 ` [PATCH v2 5/8] release: Create directory with system update Demi Marie Obenour
@ 2025-11-13 16:04 ` Alyssa Ross
2025-11-13 18:23 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-13 16:04 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 2434 bytes --]
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 6/8] Support updates via systemd-sysupdate
2025-11-12 22:15 ` [PATCH v2 6/8] Support updates via systemd-sysupdate Demi Marie Obenour
@ 2025-11-13 16:44 ` Alyssa Ross
2025-11-13 20:25 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-13 16:44 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 15638 bytes --]
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 7/8] Documentation: Update support
2025-11-12 22:15 ` [PATCH v2 7/8] Documentation: Update support Demi Marie Obenour
@ 2025-11-13 16:49 ` Alyssa Ross
2025-11-13 22:24 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-13 16:49 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 4257 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 8/8] lib/config.nix: Validate configuration parameters
2025-11-12 22:15 ` [PATCH v2 8/8] lib/config.nix: Validate configuration parameters Demi Marie Obenour
@ 2025-11-13 17:16 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-13 17:16 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 3677 bytes --]
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 3/8] tools: Add directory checker for updates
2025-11-13 13:21 ` Alyssa Ross
@ 2025-11-13 17:53 ` Demi Marie Obenour
2025-11-13 18:01 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-13 17:53 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 6321 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 3/8] tools: Add directory checker for updates
2025-11-13 17:53 ` Demi Marie Obenour
@ 2025-11-13 18:01 ` Alyssa Ross
2025-11-13 18:03 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-13 18:01 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 2961 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 3/8] tools: Add directory checker for updates
2025-11-13 18:01 ` Alyssa Ross
@ 2025-11-13 18:03 ` Demi Marie Obenour
2025-11-14 13:08 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-13 18:03 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 3239 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 5/8] release: Create directory with system update
2025-11-13 16:04 ` Alyssa Ross
@ 2025-11-13 18:23 ` Demi Marie Obenour
2025-11-13 19:09 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-13 18:23 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 2977 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 5/8] release: Create directory with system update
2025-11-13 18:23 ` Demi Marie Obenour
@ 2025-11-13 19:09 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-13 19:09 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 1824 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 6/8] Support updates via systemd-sysupdate
2025-11-13 16:44 ` Alyssa Ross
@ 2025-11-13 20:25 ` Demi Marie Obenour
2025-11-14 12:14 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-13 20:25 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 17265 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 7/8] Documentation: Update support
2025-11-13 16:49 ` Alyssa Ross
@ 2025-11-13 22:24 ` Demi Marie Obenour
2025-11-14 12:16 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-13 22:24 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 5142 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 2/8] host/rootfs: Install systemd-pull
2025-11-13 15:22 ` Alyssa Ross
@ 2025-11-13 23:46 ` Demi Marie Obenour
2025-11-14 11:59 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-13 23:46 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 3611 bytes --]
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-46c69eda1473@iscas.ac.cn/
>> 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
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 2/8] host/rootfs: Install systemd-pull
2025-11-13 23:46 ` Demi Marie Obenour
@ 2025-11-14 11:59 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-14 11:59 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 2747 bytes --]
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-46c69eda1473@iscas.ac.cn/
>>> 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
>
> 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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 6/8] Support updates via systemd-sysupdate
2025-11-13 20:25 ` Demi Marie Obenour
@ 2025-11-14 12:14 ` Alyssa Ross
2025-11-14 23:16 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-14 12:14 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 7938 bytes --]
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.
>
> https://github.com/systemd/systemd/issues/39273
"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. :)
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 7/8] Documentation: Update support
2025-11-13 22:24 ` Demi Marie Obenour
@ 2025-11-14 12:16 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-14 12:16 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 2382 bytes --]
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?
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 3/8] tools: Add directory checker for updates
2025-11-13 18:03 ` Demi Marie Obenour
@ 2025-11-14 13:08 ` Alyssa Ross
2025-11-14 18:37 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-14 13:08 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 1241 bytes --]
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?
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 3/8] tools: Add directory checker for updates
2025-11-14 13:08 ` Alyssa Ross
@ 2025-11-14 18:37 ` Demi Marie Obenour
2025-11-15 15:20 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-14 18:37 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 1446 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 6/8] Support updates via systemd-sysupdate
2025-11-14 12:14 ` Alyssa Ross
@ 2025-11-14 23:16 ` Demi Marie Obenour
2025-11-20 14:56 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-14 23:16 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 9725 bytes --]
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.
>>
>> https://github.com/systemd/systemd/issues/39273
>
> "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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 3/8] tools: Add directory checker for updates
2025-11-14 18:37 ` Demi Marie Obenour
@ 2025-11-15 15:20 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-15 15:20 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 1501 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* [PATCH v3 00/14] System updates based on systemd-sysupdate
2025-11-12 22:14 ` [PATCH v2 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
` (7 preceding siblings ...)
2025-11-12 22:15 ` [PATCH v2 8/8] lib/config.nix: Validate configuration parameters Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 01/14] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
` (15 more replies)
8 siblings, 16 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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-88d96bf81b79@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-401c1be2a11b@gmail.com
---
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)
^ permalink raw reply [flat|nested] 177+ messages in thread
* [PATCH v3 01/14] host/rootfs: Install all programs from util-linuxMinimal
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-19 14:14 ` Alyssa Ross
2025-11-19 8:18 ` [PATCH v3 02/14] host/rootfs: Install systemd-pull Demi Marie Obenour
` (14 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 02/14] host/rootfs: Install systemd-pull
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 01/14] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 03/14] tools: Add directory checker for updates Demi Marie Obenour
` (13 subsequent siblings)
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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-46c69eda1473@iscas.ac.cn/
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 03/14] tools: Add directory checker for updates
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 01/14] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 02/14] host/rootfs: Install systemd-pull Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-19 14:45 ` Alyssa Ross
2025-11-19 8:18 ` [PATCH v3 04/14] scripts: port make-gpt.sh to bash Demi Marie Obenour
` (12 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 04/14] scripts: port make-gpt.sh to bash
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (2 preceding siblings ...)
2025-11-19 8:18 ` [PATCH v3 03/14] tools: Add directory checker for updates Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-20 10:28 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 05/14] scripts/make-gpt.sh: Allow specifying partition size Demi Marie Obenour
` (11 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 05/14] scripts/make-gpt.sh: Allow specifying partition size
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (3 preceding siblings ...)
2025-11-19 8:18 ` [PATCH v3 04/14] scripts: port make-gpt.sh to bash Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 06/14] Support generating multiple partition UUIDs Demi Marie Obenour
` (10 subsequent siblings)
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 06/14] Support generating multiple partition UUIDs
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (4 preceding siblings ...)
2025-11-19 8:18 ` [PATCH v3 05/14] scripts/make-gpt.sh: Allow specifying partition size Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 07/14] scripts: Use shell expansion to get partition path Demi Marie Obenour
` (9 subsequent siblings)
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 07/14] scripts: Use shell expansion to get partition path
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (5 preceding siblings ...)
2025-11-19 8:18 ` [PATCH v3 06/14] Support generating multiple partition UUIDs Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 08/14] Use OS version to set partition labels and UKI name Demi Marie Obenour
` (8 subsequent siblings)
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 08/14] Use OS version to set partition labels and UKI name
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (6 preceding siblings ...)
2025-11-19 8:18 ` [PATCH v3 07/14] scripts: Use shell expansion to get partition path Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-20 12:11 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 09/14] release: Compress installation images and remove live image Demi Marie Obenour
` (7 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 09/14] release: Compress installation images and remove live image
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (7 preceding siblings ...)
2025-11-19 8:18 ` [PATCH v3 08/14] Use OS version to set partition labels and UKI name Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-20 12:14 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 10/14] Add B partitions to installation images Demi Marie Obenour
` (6 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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.is/
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 10/14] Add B partitions to installation images
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (8 preceding siblings ...)
2025-11-19 8:18 ` [PATCH v3 09/14] release: Compress installation images and remove live image Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 11/14] release: Create directory with system update Demi Marie Obenour
` (5 subsequent siblings)
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 11/14] release: Create directory with system update
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (9 preceding siblings ...)
2025-11-19 8:18 ` [PATCH v3 10/14] Add B partitions to installation images Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 12/14] Support updates via systemd-sysupdate Demi Marie Obenour
` (4 subsequent siblings)
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 12/14] Support updates via systemd-sysupdate
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (10 preceding siblings ...)
2025-11-19 8:18 ` [PATCH v3 11/14] release: Create directory with system update Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 13/14] Documentation: Update support Demi Marie Obenour
` (3 subsequent siblings)
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 13/14] Documentation: Update support
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (11 preceding siblings ...)
2025-11-19 8:18 ` [PATCH v3 12/14] Support updates via systemd-sysupdate Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 14/14] Validate configuration parameters Demi Marie Obenour
` (2 subsequent siblings)
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v3 14/14] Validate configuration parameters
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (12 preceding siblings ...)
2025-11-19 8:18 ` [PATCH v3 13/14] Documentation: Update support Demi Marie Obenour
@ 2025-11-19 8:18 ` Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 8:18 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* Re: [PATCH v3 01/14] host/rootfs: Install all programs from util-linuxMinimal
2025-11-19 8:18 ` [PATCH v3 01/14] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
@ 2025-11-19 14:14 ` Alyssa Ross
2025-11-20 0:12 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-19 14:14 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 6893 bytes --]
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.is
> ${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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v3 03/14] tools: Add directory checker for updates
2025-11-19 8:18 ` [PATCH v3 03/14] tools: Add directory checker for updates Demi Marie Obenour
@ 2025-11-19 14:45 ` Alyssa Ross
2025-11-19 23:58 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-19 14:45 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 7415 bytes --]
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v3 03/14] tools: Add directory checker for updates
2025-11-19 14:45 ` Alyssa Ross
@ 2025-11-19 23:58 ` Demi Marie Obenour
0 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-19 23:58 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 8424 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v3 01/14] host/rootfs: Install all programs from util-linuxMinimal
2025-11-19 14:14 ` Alyssa Ross
@ 2025-11-20 0:12 ` Demi Marie Obenour
0 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-20 0:12 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 7301 bytes --]
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.is
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v3 04/14] scripts: port make-gpt.sh to bash
2025-11-19 8:18 ` [PATCH v3 04/14] scripts: port make-gpt.sh to bash Demi Marie Obenour
@ 2025-11-20 10:28 ` Demi Marie Obenour
0 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-20 10:28 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Alyssa Ross
[-- Attachment #1.1.1: Type: text/plain, Size: 4721 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v3 08/14] Use OS version to set partition labels and UKI name
2025-11-19 8:18 ` [PATCH v3 08/14] Use OS version to set partition labels and UKI name Demi Marie Obenour
@ 2025-11-20 12:11 ` Demi Marie Obenour
0 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-20 12:11 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Alyssa Ross
[-- Attachment #1.1.1: Type: text/plain, Size: 687 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v3 09/14] release: Compress installation images and remove live image
2025-11-19 8:18 ` [PATCH v3 09/14] release: Compress installation images and remove live image Demi Marie Obenour
@ 2025-11-20 12:14 ` Demi Marie Obenour
0 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-20 12:14 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Alyssa Ross
[-- Attachment #1.1.1: Type: text/plain, Size: 1303 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 6/8] Support updates via systemd-sysupdate
2025-11-14 23:16 ` Demi Marie Obenour
@ 2025-11-20 14:56 ` Alyssa Ross
2025-11-20 19:42 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-20 14:56 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 6814 bytes --]
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.
>>>
>>> https://github.com/systemd/systemd/issues/39273
>>
>> "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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v2 6/8] Support updates via systemd-sysupdate
2025-11-20 14:56 ` Alyssa Ross
@ 2025-11-20 19:42 ` Demi Marie Obenour
0 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-20 19:42 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 7465 bytes --]
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.
>>>>
>>>> https://github.com/systemd/systemd/issues/39273
>>>
>>> "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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* [PATCH v4 00/14] System updates based on systemd-sysupdate
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (13 preceding siblings ...)
2025-11-19 8:18 ` [PATCH v3 14/14] Validate configuration parameters Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 01/14] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
` (15 more replies)
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
15 siblings, 16 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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-b88a99915509@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-88d96bf81b79@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-401c1be2a11b@gmail.com
---
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)
^ permalink raw reply [flat|nested] 177+ messages in thread
* [PATCH v4 01/14] host/rootfs: Install all programs from util-linuxMinimal
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-25 11:56 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 02/14] host/rootfs: Install systemd-pull Demi Marie Obenour
` (14 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 02/14] host/rootfs: Install systemd-pull
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 01/14] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-25 7:36 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 03/14] tools: Add directory checker for updates Demi Marie Obenour
` (13 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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-46c69eda1473@iscas.ac.cn/
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 03/14] tools: Add directory checker for updates
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 01/14] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 02/14] host/rootfs: Install systemd-pull Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 04/14] scripts: port make-gpt.sh to bash Demi Marie Obenour
` (12 subsequent siblings)
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 04/14] scripts: port make-gpt.sh to bash
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (2 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 03/14] tools: Add directory checker for updates Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 05/14] scripts/make-gpt.sh: Allow specifying partition size Demi Marie Obenour
` (11 subsequent siblings)
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 05/14] scripts/make-gpt.sh: Allow specifying partition size
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (3 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 04/14] scripts: port make-gpt.sh to bash Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 06/14] Support generating multiple partition UUIDs Demi Marie Obenour
` (10 subsequent siblings)
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 06/14] Support generating multiple partition UUIDs
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (4 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 05/14] scripts/make-gpt.sh: Allow specifying partition size Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-25 13:02 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 07/14] scripts: Use shell expansion to get partition path Demi Marie Obenour
` (9 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 07/14] scripts: Use shell expansion to get partition path
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (5 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 06/14] Support generating multiple partition UUIDs Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 08/14] release: Compress installation images and remove live image Demi Marie Obenour
` (8 subsequent siblings)
15 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 08/14] release: Compress installation images and remove live image
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (6 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 07/14] scripts: Use shell expansion to get partition path Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-25 13:19 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 09/14] Use OS version to set partition labels and UKI name Demi Marie Obenour
` (7 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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.is/
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 09/14] Use OS version to set partition labels and UKI name
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (7 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 08/14] release: Compress installation images and remove live image Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-25 14:11 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 10/14] Add B partitions to installation images Demi Marie Obenour
` (6 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 10/14] Add B partitions to installation images
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (8 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 09/14] Use OS version to set partition labels and UKI name Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-25 16:31 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 11/14] release: Create directory with system update Demi Marie Obenour
` (5 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 11/14] release: Create directory with system update
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (9 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 10/14] Add B partitions to installation images Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-25 16:50 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 12/14] Support updates via systemd-sysupdate Demi Marie Obenour
` (4 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 12/14] Support updates via systemd-sysupdate
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (10 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 11/14] release: Create directory with system update Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-25 17:54 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 13/14] Documentation: Update support Demi Marie Obenour
` (3 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 13/14] Documentation: Update support
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (11 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 12/14] Support updates via systemd-sysupdate Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-25 18:00 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 14/14] Validate configuration parameters Demi Marie Obenour
` (2 subsequent siblings)
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 14/14] Validate configuration parameters
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (12 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 13/14] Documentation: Update support Demi Marie Obenour
@ 2025-11-22 1:23 ` Demi Marie Obenour
2025-11-25 18:06 ` Alyssa Ross
2025-11-25 12:22 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Alyssa Ross
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
15 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-22 1:23 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* Re: [PATCH v4 02/14] host/rootfs: Install systemd-pull
2025-11-22 1:23 ` [PATCH v4 02/14] host/rootfs: Install systemd-pull Demi Marie Obenour
@ 2025-11-25 7:36 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-25 7:36 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 1466 bytes --]
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.)
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 01/14] host/rootfs: Install all programs from util-linuxMinimal
2025-11-22 1:23 ` [PATCH v4 01/14] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
@ 2025-11-25 11:56 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-25 11:56 UTC (permalink / raw)
To: Demi Marie Obenour, Spectrum OS Development
Cc: Demi Marie Obenour, Alyssa Ross
This patch has been committed as f63e2598ec681dedf3b3b29c8f603a0bf2ac1512,
which can be viewed online at
https://spectrum-os.org/git/spectrum/commit/?id=f63e2598ec681dedf3b3b29c8f603a0bf2ac1512.
This is an automated message. Send comments/questions/requests to:
Alyssa Ross <hi@alyssa.is>
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 00/14] System updates based on systemd-sysupdate
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (13 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 14/14] Validate configuration parameters Demi Marie Obenour
@ 2025-11-25 12:22 ` Alyssa Ross
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
15 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-25 12:22 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 8959 bytes --]
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-b88a99915509@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-88d96bf81b79@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-401c1be2a11b@gmail.com
>
> ---
> 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.is
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 06/14] Support generating multiple partition UUIDs
2025-11-22 1:23 ` [PATCH v4 06/14] Support generating multiple partition UUIDs Demi Marie Obenour
@ 2025-11-25 13:02 ` Alyssa Ross
2025-11-26 18:26 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-25 13:02 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 7544 bytes --]
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));
> +}
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 08/14] release: Compress installation images and remove live image
2025-11-22 1:23 ` [PATCH v4 08/14] release: Compress installation images and remove live image Demi Marie Obenour
@ 2025-11-25 13:19 ` Alyssa Ross
2025-11-25 22:38 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-25 13:19 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 8841 bytes --]
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.is/
>
> 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.is
https://spectrum-os.org/lists/archives/spectrum-devel/87ikfdapmx.fsf@alyssa.is
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 09/14] Use OS version to set partition labels and UKI name
2025-11-22 1:23 ` [PATCH v4 09/14] Use OS version to set partition labels and UKI name Demi Marie Obenour
@ 2025-11-25 14:11 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-25 14:11 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 1508 bytes --]
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>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 10/14] Add B partitions to installation images
2025-11-22 1:23 ` [PATCH v4 10/14] Add B partitions to installation images Demi Marie Obenour
@ 2025-11-25 16:31 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-25 16:31 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 2100 bytes --]
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 11/14] release: Create directory with system update
2025-11-22 1:23 ` [PATCH v4 11/14] release: Create directory with system update Demi Marie Obenour
@ 2025-11-25 16:50 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-25 16:50 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 2343 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 12/14] Support updates via systemd-sysupdate
2025-11-22 1:23 ` [PATCH v4 12/14] Support updates via systemd-sysupdate Demi Marie Obenour
@ 2025-11-25 17:54 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-25 17:54 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 22865 bytes --]
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?
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 13/14] Documentation: Update support
2025-11-22 1:23 ` [PATCH v4 13/14] Documentation: Update support Demi Marie Obenour
@ 2025-11-25 18:00 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-25 18:00 UTC (permalink / raw)
To: Demi Marie Obenour, Spectrum OS Development; +Cc: Demi Marie Obenour
[-- Attachment #1: Type: text/plain, Size: 8609 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 14/14] Validate configuration parameters
2025-11-22 1:23 ` [PATCH v4 14/14] Validate configuration parameters Demi Marie Obenour
@ 2025-11-25 18:06 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-25 18:06 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 6893 bytes --]
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 08/14] release: Compress installation images and remove live image
2025-11-25 13:19 ` Alyssa Ross
@ 2025-11-25 22:38 ` Demi Marie Obenour
2025-11-28 11:09 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-25 22:38 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 9504 bytes --]
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.is/
>>
>> 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.is
> https://spectrum-os.org/lists/archives/spectrum-devel/87ikfdapmx.fsf@alyssa.is
I'll try to do better in the future.
--
Sincerely,
Demi Marie Obenour (she/her/hers)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 06/14] Support generating multiple partition UUIDs
2025-11-25 13:02 ` Alyssa Ross
@ 2025-11-26 18:26 ` Demi Marie Obenour
0 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 18:26 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 8274 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* [PATCH v4 00/13] System updates based on systemd-sysupdate
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (14 preceding siblings ...)
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
@ 2025-11-26 19:33 ` Demi Marie Obenour
2025-11-26 19:33 ` [PATCH v4 01/13] tools: Add directory checker for updates Demi Marie Obenour
` (12 more replies)
15 siblings, 13 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:33 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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-d4561c42776e@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-b88a99915509@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-88d96bf81b79@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-401c1be2a11b@gmail.com
---
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)
^ permalink raw reply [flat|nested] 177+ messages in thread
* [PATCH v4 01/13] tools: Add directory checker for updates
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
@ 2025-11-26 19:33 ` Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 02/13] scripts: port make-gpt.sh to bash Demi Marie Obenour
` (11 subsequent siblings)
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:33 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 02/13] scripts: port make-gpt.sh to bash
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-26 19:33 ` [PATCH v4 01/13] tools: Add directory checker for updates Demi Marie Obenour
@ 2025-11-26 19:34 ` Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 03/13] scripts/make-gpt.sh: Allow specifying partition size Demi Marie Obenour
` (10 subsequent siblings)
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:34 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 03/13] scripts/make-gpt.sh: Allow specifying partition size
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-26 19:33 ` [PATCH v4 01/13] tools: Add directory checker for updates Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 02/13] scripts: port make-gpt.sh to bash Demi Marie Obenour
@ 2025-11-26 19:34 ` Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 04/13] Port scripts/format-uuid.sh to awk Demi Marie Obenour
` (9 subsequent siblings)
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:34 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 04/13] Port scripts/format-uuid.sh to awk
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
` (2 preceding siblings ...)
2025-11-26 19:34 ` [PATCH v4 03/13] scripts/make-gpt.sh: Allow specifying partition size Demi Marie Obenour
@ 2025-11-26 19:34 ` Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 05/13] Use set and a command substitution to set UUID variables Demi Marie Obenour
` (8 subsequent siblings)
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:34 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 05/13] Use set and a command substitution to set UUID variables
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
` (3 preceding siblings ...)
2025-11-26 19:34 ` [PATCH v4 04/13] Port scripts/format-uuid.sh to awk Demi Marie Obenour
@ 2025-11-26 19:34 ` Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 06/13] scripts: Use shell expansion to get partition path Demi Marie Obenour
` (7 subsequent siblings)
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:34 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 06/13] scripts: Use shell expansion to get partition path
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
` (4 preceding siblings ...)
2025-11-26 19:34 ` [PATCH v4 05/13] Use set and a command substitution to set UUID variables Demi Marie Obenour
@ 2025-11-26 19:34 ` Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 07/13] release: Compress installation images and remove live image Demi Marie Obenour
` (6 subsequent siblings)
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:34 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 07/13] release: Compress installation images and remove live image
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
` (5 preceding siblings ...)
2025-11-26 19:34 ` [PATCH v4 06/13] scripts: Use shell expansion to get partition path Demi Marie Obenour
@ 2025-11-26 19:34 ` Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 08/13] Use OS version to set partition labels and UKI name Demi Marie Obenour
` (5 subsequent siblings)
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:34 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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.is/
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 08/13] Use OS version to set partition labels and UKI name
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
` (6 preceding siblings ...)
2025-11-26 19:34 ` [PATCH v4 07/13] release: Compress installation images and remove live image Demi Marie Obenour
@ 2025-11-26 19:34 ` Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 09/13] Add B partitions to installation images Demi Marie Obenour
` (4 subsequent siblings)
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:34 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 09/13] Add B partitions to installation images
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
` (7 preceding siblings ...)
2025-11-26 19:34 ` [PATCH v4 08/13] Use OS version to set partition labels and UKI name Demi Marie Obenour
@ 2025-11-26 19:34 ` Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 10/13] release: Create directory with system update Demi Marie Obenour
` (3 subsequent siblings)
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:34 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 10/13] release: Create directory with system update
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
` (8 preceding siblings ...)
2025-11-26 19:34 ` [PATCH v4 09/13] Add B partitions to installation images Demi Marie Obenour
@ 2025-11-26 19:34 ` Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 11/13] Support updates via systemd-sysupdate Demi Marie Obenour
` (2 subsequent siblings)
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:34 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 11/13] Support updates via systemd-sysupdate
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
` (9 preceding siblings ...)
2025-11-26 19:34 ` [PATCH v4 10/13] release: Create directory with system update Demi Marie Obenour
@ 2025-11-26 19:34 ` Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 12/13] Documentation: Update support Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 13/13] Validate configuration parameters Demi Marie Obenour
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:34 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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-46c69eda1473@iscas.ac.cn/
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 12/13] Documentation: Update support
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
` (10 preceding siblings ...)
2025-11-26 19:34 ` [PATCH v4 11/13] Support updates via systemd-sysupdate Demi Marie Obenour
@ 2025-11-26 19:34 ` Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 13/13] Validate configuration parameters Demi Marie Obenour
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:34 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v4 13/13] Validate configuration parameters
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
` (11 preceding siblings ...)
2025-11-26 19:34 ` [PATCH v4 12/13] Documentation: Update support Demi Marie Obenour
@ 2025-11-26 19:34 ` Demi Marie Obenour
12 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:34 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 00/13] System updates based on systemd-sysupdate
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
` (14 preceding siblings ...)
2025-11-25 12:22 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Alyssa Ross
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 01/13] tools: Add directory checker for updates Demi Marie Obenour
` (13 more replies)
15 siblings, 14 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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-d4561c42776e@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-b88a99915509@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-88d96bf81b79@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-401c1be2a11b@gmail.com
---
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)
^ permalink raw reply [flat|nested] 177+ messages in thread
* [PATCH v5 01/13] tools: Add directory checker for updates
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 02/13] scripts: port make-gpt.sh to bash Demi Marie Obenour
` (12 subsequent siblings)
13 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 02/13] scripts: port make-gpt.sh to bash
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 01/13] tools: Add directory checker for updates Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 03/13] scripts/make-gpt.sh: Allow specifying partition size Demi Marie Obenour
` (11 subsequent siblings)
13 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 03/13] scripts/make-gpt.sh: Allow specifying partition size
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 01/13] tools: Add directory checker for updates Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 02/13] scripts: port make-gpt.sh to bash Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 04/13] Port scripts/format-uuid.sh to awk Demi Marie Obenour
` (10 subsequent siblings)
13 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 04/13] Port scripts/format-uuid.sh to awk
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
` (2 preceding siblings ...)
2025-11-26 19:40 ` [PATCH v5 03/13] scripts/make-gpt.sh: Allow specifying partition size Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 05/13] Use set and a command substitution to set UUID variables Demi Marie Obenour
` (9 subsequent siblings)
13 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 05/13] Use set and a command substitution to set UUID variables
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
` (3 preceding siblings ...)
2025-11-26 19:40 ` [PATCH v5 04/13] Port scripts/format-uuid.sh to awk Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 06/13] scripts: Use shell expansion to get partition path Demi Marie Obenour
` (8 subsequent siblings)
13 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 06/13] scripts: Use shell expansion to get partition path
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
` (4 preceding siblings ...)
2025-11-26 19:40 ` [PATCH v5 05/13] Use set and a command substitution to set UUID variables Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-28 11:20 ` Alyssa Ross
2025-11-26 19:40 ` [PATCH v5 07/13] release: Compress installation images and remove live image Demi Marie Obenour
` (7 subsequent siblings)
13 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 07/13] release: Compress installation images and remove live image
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
` (5 preceding siblings ...)
2025-11-26 19:40 ` [PATCH v5 06/13] scripts: Use shell expansion to get partition path Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-28 11:21 ` Alyssa Ross
2025-11-26 19:40 ` [PATCH v5 08/13] Use OS version to set partition labels and UKI name Demi Marie Obenour
` (6 subsequent siblings)
13 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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.is/
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 08/13] Use OS version to set partition labels and UKI name
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
` (6 preceding siblings ...)
2025-11-26 19:40 ` [PATCH v5 07/13] release: Compress installation images and remove live image Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 09/13] Add B partitions to installation images Demi Marie Obenour
` (5 subsequent siblings)
13 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 09/13] Add B partitions to installation images
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
` (7 preceding siblings ...)
2025-11-26 19:40 ` [PATCH v5 08/13] Use OS version to set partition labels and UKI name Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-28 11:23 ` Alyssa Ross
2025-11-26 19:40 ` [PATCH v5 10/13] release: Create directory with system update Demi Marie Obenour
` (4 subsequent siblings)
13 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 10/13] release: Create directory with system update
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
` (8 preceding siblings ...)
2025-11-26 19:40 ` [PATCH v5 09/13] Add B partitions to installation images Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 11/13] Support updates via systemd-sysupdate Demi Marie Obenour
` (3 subsequent siblings)
13 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 11/13] Support updates via systemd-sysupdate
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
` (9 preceding siblings ...)
2025-11-26 19:40 ` [PATCH v5 10/13] release: Create directory with system update Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-28 13:47 ` Alyssa Ross
2025-11-26 19:40 ` [PATCH v5 12/13] Documentation: Update support Demi Marie Obenour
` (2 subsequent siblings)
13 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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-46c69eda1473@iscas.ac.cn/
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 12/13] Documentation: Update support
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
` (10 preceding siblings ...)
2025-11-26 19:40 ` [PATCH v5 11/13] Support updates via systemd-sysupdate Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 13/13] Validate configuration parameters Demi Marie Obenour
2025-11-29 9:49 ` [PATCH v6 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
13 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v5 13/13] Validate configuration parameters
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
` (11 preceding siblings ...)
2025-11-26 19:40 ` [PATCH v5 12/13] Documentation: Update support Demi Marie Obenour
@ 2025-11-26 19:40 ` Demi Marie Obenour
2025-11-29 9:49 ` [PATCH v6 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
13 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-26 19:40 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* Re: [PATCH v4 08/14] release: Compress installation images and remove live image
2025-11-25 22:38 ` Demi Marie Obenour
@ 2025-11-28 11:09 ` Alyssa Ross
2025-11-28 19:45 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-28 11:09 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 1020 bytes --]
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…
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v5 06/13] scripts: Use shell expansion to get partition path
2025-11-26 19:40 ` [PATCH v5 06/13] scripts: Use shell expansion to get partition path Demi Marie Obenour
@ 2025-11-28 11:20 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-28 11:20 UTC (permalink / raw)
To: Demi Marie Obenour, Spectrum OS Development
Cc: Demi Marie Obenour, Alyssa Ross
This patch has been committed as 1970f432152730ec19bf97a8db4613a591ee00be,
which can be viewed online at
https://spectrum-os.org/git/spectrum/commit/?id=1970f432152730ec19bf97a8db4613a591ee00be.
This is an automated message. Send comments/questions/requests to:
Alyssa Ross <hi@alyssa.is>
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v5 07/13] release: Compress installation images and remove live image
2025-11-26 19:40 ` [PATCH v5 07/13] release: Compress installation images and remove live image Demi Marie Obenour
@ 2025-11-28 11:21 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-28 11:21 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 2927 bytes --]
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.is/
>
> 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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v5 09/13] Add B partitions to installation images
2025-11-26 19:40 ` [PATCH v5 09/13] Add B partitions to installation images Demi Marie Obenour
@ 2025-11-28 11:23 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-28 11:23 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 1088 bytes --]
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>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v5 11/13] Support updates via systemd-sysupdate
2025-11-26 19:40 ` [PATCH v5 11/13] Support updates via systemd-sysupdate Demi Marie Obenour
@ 2025-11-28 13:47 ` Alyssa Ross
2025-11-28 20:27 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-28 13:47 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 10903 bytes --]
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!
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v4 08/14] release: Compress installation images and remove live image
2025-11-28 11:09 ` Alyssa Ross
@ 2025-11-28 19:45 ` Demi Marie Obenour
0 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-28 19:45 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 1180 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v5 11/13] Support updates via systemd-sysupdate
2025-11-28 13:47 ` Alyssa Ross
@ 2025-11-28 20:27 ` Demi Marie Obenour
2025-11-28 20:41 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-28 20:27 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 11920 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v5 11/13] Support updates via systemd-sysupdate
2025-11-28 20:27 ` Demi Marie Obenour
@ 2025-11-28 20:41 ` Alyssa Ross
2025-11-28 20:44 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-28 20:41 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 5801 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v5 11/13] Support updates via systemd-sysupdate
2025-11-28 20:41 ` Alyssa Ross
@ 2025-11-28 20:44 ` Demi Marie Obenour
2025-11-28 21:08 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-28 20:44 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 6207 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v5 11/13] Support updates via systemd-sysupdate
2025-11-28 20:44 ` Demi Marie Obenour
@ 2025-11-28 21:08 ` Alyssa Ross
2025-11-28 21:28 ` Demi Marie Obenour
0 siblings, 1 reply; 177+ messages in thread
From: Alyssa Ross @ 2025-11-28 21:08 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 5111 bytes --]
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.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v5 11/13] Support updates via systemd-sysupdate
2025-11-28 21:08 ` Alyssa Ross
@ 2025-11-28 21:28 ` Demi Marie Obenour
2025-11-28 21:30 ` Alyssa Ross
0 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-28 21:28 UTC (permalink / raw)
To: Alyssa Ross; +Cc: Spectrum OS Development
[-- Attachment #1.1.1: Type: text/plain, Size: 5579 bytes --]
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)
[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v5 11/13] Support updates via systemd-sysupdate
2025-11-28 21:28 ` Demi Marie Obenour
@ 2025-11-28 21:30 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-28 21:30 UTC (permalink / raw)
To: Demi Marie Obenour; +Cc: Spectrum OS Development
[-- Attachment #1: Type: text/plain, Size: 5889 bytes --]
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>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply [flat|nested] 177+ messages in thread
* [PATCH v6 0/8] System updates based on systemd-sysupdate
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
` (12 preceding siblings ...)
2025-11-26 19:40 ` [PATCH v5 13/13] Validate configuration parameters Demi Marie Obenour
@ 2025-11-29 9:49 ` Demi Marie Obenour
2025-11-29 9:49 ` [PATCH v6 1/8] tools: Add directory checker for updates Demi Marie Obenour
` (7 more replies)
13 siblings, 8 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-29 9:49 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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-fd746748febd@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-d4561c42776e@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-b88a99915509@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-88d96bf81b79@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-401c1be2a11b@gmail.com
---
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)
^ permalink raw reply [flat|nested] 177+ messages in thread
* [PATCH v6 1/8] tools: Add directory checker for updates
2025-11-29 9:49 ` [PATCH v6 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
@ 2025-11-29 9:49 ` Demi Marie Obenour
2025-11-29 11:16 ` Alyssa Ross
2025-11-29 9:49 ` [PATCH v6 2/8] release: Compress installation images and remove live image Demi Marie Obenour
` (6 subsequent siblings)
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-29 9:49 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v6 2/8] release: Compress installation images and remove live image
2025-11-29 9:49 ` [PATCH v6 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-29 9:49 ` [PATCH v6 1/8] tools: Add directory checker for updates Demi Marie Obenour
@ 2025-11-29 9:49 ` Demi Marie Obenour
2025-11-29 11:16 ` Alyssa Ross
2025-11-29 9:50 ` [PATCH v6 3/8] Use OS version to set partition labels and UKI name Demi Marie Obenour
` (5 subsequent siblings)
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-29 9:49 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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.is/
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v6 3/8] Use OS version to set partition labels and UKI name
2025-11-29 9:49 ` [PATCH v6 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-29 9:49 ` [PATCH v6 1/8] tools: Add directory checker for updates Demi Marie Obenour
2025-11-29 9:49 ` [PATCH v6 2/8] release: Compress installation images and remove live image Demi Marie Obenour
@ 2025-11-29 9:50 ` Demi Marie Obenour
2025-11-29 11:16 ` Alyssa Ross
2025-11-29 9:50 ` [PATCH v6 4/8] Add B partitions to installation images Demi Marie Obenour
` (4 subsequent siblings)
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-29 9:50 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v6 4/8] Add B partitions to installation images
2025-11-29 9:49 ` [PATCH v6 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
` (2 preceding siblings ...)
2025-11-29 9:50 ` [PATCH v6 3/8] Use OS version to set partition labels and UKI name Demi Marie Obenour
@ 2025-11-29 9:50 ` Demi Marie Obenour
2025-11-29 11:16 ` Alyssa Ross
2025-11-29 9:50 ` [PATCH v6 5/8] release: Create directory with system update Demi Marie Obenour
` (3 subsequent siblings)
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-29 9:50 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v6 5/8] release: Create directory with system update
2025-11-29 9:49 ` [PATCH v6 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
` (3 preceding siblings ...)
2025-11-29 9:50 ` [PATCH v6 4/8] Add B partitions to installation images Demi Marie Obenour
@ 2025-11-29 9:50 ` Demi Marie Obenour
2025-11-29 11:16 ` Alyssa Ross
2025-11-29 9:50 ` [PATCH v6 6/8] Support updates via systemd-sysupdate Demi Marie Obenour
` (2 subsequent siblings)
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-29 9:50 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v6 6/8] Support updates via systemd-sysupdate
2025-11-29 9:49 ` [PATCH v6 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
` (4 preceding siblings ...)
2025-11-29 9:50 ` [PATCH v6 5/8] release: Create directory with system update Demi Marie Obenour
@ 2025-11-29 9:50 ` Demi Marie Obenour
2025-11-29 11:16 ` Alyssa Ross
2025-11-29 9:50 ` [PATCH v6 7/8] Documentation: Update support Demi Marie Obenour
2025-11-29 9:50 ` [PATCH v6 8/8] Validate configuration parameters Demi Marie Obenour
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-29 9:50 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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-46c69eda1473@iscas.ac.cn/
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v6 7/8] Documentation: Update support
2025-11-29 9:49 ` [PATCH v6 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
` (5 preceding siblings ...)
2025-11-29 9:50 ` [PATCH v6 6/8] Support updates via systemd-sysupdate Demi Marie Obenour
@ 2025-11-29 9:50 ` Demi Marie Obenour
2025-11-30 21:46 ` Alyssa Ross
2025-11-29 9:50 ` [PATCH v6 8/8] Validate configuration parameters Demi Marie Obenour
7 siblings, 1 reply; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-29 9:50 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* [PATCH v6 8/8] Validate configuration parameters
2025-11-29 9:49 ` [PATCH v6 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
` (6 preceding siblings ...)
2025-11-29 9:50 ` [PATCH v6 7/8] Documentation: Update support Demi Marie Obenour
@ 2025-11-29 9:50 ` Demi Marie Obenour
7 siblings, 0 replies; 177+ messages in thread
From: Demi Marie Obenour @ 2025-11-29 9:50 UTC (permalink / raw)
To: Spectrum OS Development; +Cc: Demi Marie Obenour, Alyssa Ross
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
^ permalink raw reply related [flat|nested] 177+ messages in thread
* Re: [PATCH v6 1/8] tools: Add directory checker for updates
2025-11-29 9:49 ` [PATCH v6 1/8] tools: Add directory checker for updates Demi Marie Obenour
@ 2025-11-29 11:16 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-29 11:16 UTC (permalink / raw)
To: Demi Marie Obenour, Spectrum OS Development
Cc: Demi Marie Obenour, Alyssa Ross
This patch has been committed as 7b3ef2a1a6cdab9e761ffd1ce8ec498687f47375,
which can be viewed online at
https://spectrum-os.org/git/spectrum/commit/?id=7b3ef2a1a6cdab9e761ffd1ce8ec498687f47375.
This is an automated message. Send comments/questions/requests to:
Alyssa Ross <hi@alyssa.is>
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v6 2/8] release: Compress installation images and remove live image
2025-11-29 9:49 ` [PATCH v6 2/8] release: Compress installation images and remove live image Demi Marie Obenour
@ 2025-11-29 11:16 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-29 11:16 UTC (permalink / raw)
To: Demi Marie Obenour, Spectrum OS Development
Cc: Demi Marie Obenour, Alyssa Ross
This patch has been committed as 7a7c08084cb6a7199da1808afb58c7fec6b8c7f9,
which can be viewed online at
https://spectrum-os.org/git/spectrum/commit/?id=7a7c08084cb6a7199da1808afb58c7fec6b8c7f9.
This is an automated message. Send comments/questions/requests to:
Alyssa Ross <hi@alyssa.is>
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v6 3/8] Use OS version to set partition labels and UKI name
2025-11-29 9:50 ` [PATCH v6 3/8] Use OS version to set partition labels and UKI name Demi Marie Obenour
@ 2025-11-29 11:16 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-29 11:16 UTC (permalink / raw)
To: Demi Marie Obenour, Spectrum OS Development
Cc: Demi Marie Obenour, Alyssa Ross
This patch has been committed as cd83e2fb84187c50a3ac9cc3b003822f5b2b4bc7,
which can be viewed online at
https://spectrum-os.org/git/spectrum/commit/?id=cd83e2fb84187c50a3ac9cc3b003822f5b2b4bc7.
This is an automated message. Send comments/questions/requests to:
Alyssa Ross <hi@alyssa.is>
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v6 4/8] Add B partitions to installation images
2025-11-29 9:50 ` [PATCH v6 4/8] Add B partitions to installation images Demi Marie Obenour
@ 2025-11-29 11:16 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-29 11:16 UTC (permalink / raw)
To: Demi Marie Obenour, Spectrum OS Development
Cc: Demi Marie Obenour, Alyssa Ross
This patch has been committed as 316877d9b2e40cdccece14c1b09e97bd45bd158d,
which can be viewed online at
https://spectrum-os.org/git/spectrum/commit/?id=316877d9b2e40cdccece14c1b09e97bd45bd158d.
This is an automated message. Send comments/questions/requests to:
Alyssa Ross <hi@alyssa.is>
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v6 5/8] release: Create directory with system update
2025-11-29 9:50 ` [PATCH v6 5/8] release: Create directory with system update Demi Marie Obenour
@ 2025-11-29 11:16 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-29 11:16 UTC (permalink / raw)
To: Demi Marie Obenour, Spectrum OS Development
Cc: Demi Marie Obenour, Alyssa Ross
This patch has been committed as 49f33a139249b6270d3a9491fc0ba343c637ab42,
which can be viewed online at
https://spectrum-os.org/git/spectrum/commit/?id=49f33a139249b6270d3a9491fc0ba343c637ab42.
This is an automated message. Send comments/questions/requests to:
Alyssa Ross <hi@alyssa.is>
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v6 6/8] Support updates via systemd-sysupdate
2025-11-29 9:50 ` [PATCH v6 6/8] Support updates via systemd-sysupdate Demi Marie Obenour
@ 2025-11-29 11:16 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-29 11:16 UTC (permalink / raw)
To: Demi Marie Obenour, Spectrum OS Development
Cc: Demi Marie Obenour, Alyssa Ross
This patch has been committed as 067c6a5d50971242f9cb8ac0ac76e20d88a9b5c1,
which can be viewed online at
https://spectrum-os.org/git/spectrum/commit/?id=067c6a5d50971242f9cb8ac0ac76e20d88a9b5c1.
This is an automated message. Send comments/questions/requests to:
Alyssa Ross <hi@alyssa.is>
^ permalink raw reply [flat|nested] 177+ messages in thread
* Re: [PATCH v6 7/8] Documentation: Update support
2025-11-29 9:50 ` [PATCH v6 7/8] Documentation: Update support Demi Marie Obenour
@ 2025-11-30 21:46 ` Alyssa Ross
0 siblings, 0 replies; 177+ messages in thread
From: Alyssa Ross @ 2025-11-30 21:46 UTC (permalink / raw)
To: Demi Marie Obenour, Spectrum OS Development
Cc: Demi Marie Obenour, Alyssa Ross
This patch has been committed as cf131bd816a34095af34345f0e5f148c47268a04,
which can be viewed online at
https://spectrum-os.org/git/spectrum/commit/?id=cf131bd816a34095af34345f0e5f148c47268a04.
This is an automated message. Send comments/questions/requests to:
Alyssa Ross <hi@alyssa.is>
^ permalink raw reply [flat|nested] 177+ messages in thread
end of thread, other threads:[~2025-11-30 21:46 UTC | newest]
Thread overview: 177+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-10-29 10:12 [PATCH 0/7] System updates based on systemd-sysupdate Demi Marie Obenour
2025-10-29 10:12 ` [PATCH 1/7] host/rootfs: Use full util-linux and systemd Demi Marie Obenour
2025-10-29 11:36 ` Alyssa Ross
2025-11-01 3:25 ` Demi Marie Obenour
2025-11-01 12:13 ` Alyssa Ross
2025-11-06 9:15 ` Demi Marie Obenour
2025-10-29 10:12 ` [PATCH 2/7] release/combined: Compress installation image Demi Marie Obenour
2025-10-29 11:50 ` Alyssa Ross
2025-10-29 16:51 ` Alyssa Ross
2025-11-01 22:15 ` Demi Marie Obenour
2025-11-02 0:18 ` Demi Marie Obenour
2025-11-02 12:05 ` Alyssa Ross
2025-11-02 14:42 ` Alyssa Ross
2025-11-02 19:38 ` Demi Marie Obenour
2025-10-29 10:12 ` [PATCH 3/7] tools: Add directory checker for updates Demi Marie Obenour
2025-10-29 12:01 ` Alyssa Ross
2025-10-31 20:31 ` Demi Marie Obenour
2025-11-01 12:17 ` Alyssa Ross
2025-11-01 14:09 ` Alyssa Ross
2025-11-01 18:36 ` Demi Marie Obenour
2025-11-02 12:18 ` Alyssa Ross
2025-11-02 12:43 ` Alyssa Ross
2025-11-02 19:34 ` Demi Marie Obenour
2025-11-04 15:26 ` Alyssa Ross
2025-11-02 19:21 ` Demi Marie Obenour
2025-11-04 15:27 ` Alyssa Ross
2025-11-04 22:56 ` Demi Marie Obenour
2025-11-06 10:15 ` Alyssa Ross
2025-10-29 10:12 ` [PATCH 4/7] Adjust partition layout to support updates Demi Marie Obenour
2025-10-29 15:49 ` Alyssa Ross
2025-10-29 10:12 ` [PATCH 5/7] release: add install step Demi Marie Obenour
2025-10-29 12:20 ` Alyssa Ross
2025-10-29 10:12 ` [PATCH 6/7] Factor out dm-verity build rules Demi Marie Obenour
2025-10-29 12:22 ` Alyssa Ross
2025-10-31 6:39 ` Demi Marie Obenour
2025-10-29 10:12 ` [PATCH 7/7] Support updates via systemd-sysupdate Demi Marie Obenour
2025-10-29 15:48 ` Alyssa Ross
2025-11-12 22:14 ` [PATCH v2 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-12 22:14 ` [PATCH v2 1/8] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
2025-11-13 12:35 ` Alyssa Ross
2025-11-12 22:14 ` [PATCH v2 2/8] host/rootfs: Install systemd-pull Demi Marie Obenour
2025-11-13 15:22 ` Alyssa Ross
2025-11-13 23:46 ` Demi Marie Obenour
2025-11-14 11:59 ` Alyssa Ross
2025-11-12 22:14 ` [PATCH v2 3/8] tools: Add directory checker for updates Demi Marie Obenour
2025-11-13 13:21 ` Alyssa Ross
2025-11-13 17:53 ` Demi Marie Obenour
2025-11-13 18:01 ` Alyssa Ross
2025-11-13 18:03 ` Demi Marie Obenour
2025-11-14 13:08 ` Alyssa Ross
2025-11-14 18:37 ` Demi Marie Obenour
2025-11-15 15:20 ` Alyssa Ross
2025-11-12 22:14 ` [PATCH v2 4/8] Adjust partition layout to support updates Demi Marie Obenour
2025-11-13 16:00 ` Alyssa Ross
2025-11-12 22:14 ` [PATCH v2 5/8] release: Create directory with system update Demi Marie Obenour
2025-11-13 16:04 ` Alyssa Ross
2025-11-13 18:23 ` Demi Marie Obenour
2025-11-13 19:09 ` Alyssa Ross
2025-11-12 22:15 ` [PATCH v2 6/8] Support updates via systemd-sysupdate Demi Marie Obenour
2025-11-13 16:44 ` Alyssa Ross
2025-11-13 20:25 ` Demi Marie Obenour
2025-11-14 12:14 ` Alyssa Ross
2025-11-14 23:16 ` Demi Marie Obenour
2025-11-20 14:56 ` Alyssa Ross
2025-11-20 19:42 ` Demi Marie Obenour
2025-11-12 22:15 ` [PATCH v2 7/8] Documentation: Update support Demi Marie Obenour
2025-11-13 16:49 ` Alyssa Ross
2025-11-13 22:24 ` Demi Marie Obenour
2025-11-14 12:16 ` Alyssa Ross
2025-11-12 22:15 ` [PATCH v2 8/8] lib/config.nix: Validate configuration parameters Demi Marie Obenour
2025-11-13 17:16 ` Alyssa Ross
2025-11-19 8:18 ` [PATCH v3 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 01/14] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
2025-11-19 14:14 ` Alyssa Ross
2025-11-20 0:12 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 02/14] host/rootfs: Install systemd-pull Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 03/14] tools: Add directory checker for updates Demi Marie Obenour
2025-11-19 14:45 ` Alyssa Ross
2025-11-19 23:58 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 04/14] scripts: port make-gpt.sh to bash Demi Marie Obenour
2025-11-20 10:28 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 05/14] scripts/make-gpt.sh: Allow specifying partition size Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 06/14] Support generating multiple partition UUIDs Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 07/14] scripts: Use shell expansion to get partition path Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 08/14] Use OS version to set partition labels and UKI name Demi Marie Obenour
2025-11-20 12:11 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 09/14] release: Compress installation images and remove live image Demi Marie Obenour
2025-11-20 12:14 ` Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 10/14] Add B partitions to installation images Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 11/14] release: Create directory with system update Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 12/14] Support updates via systemd-sysupdate Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 13/14] Documentation: Update support Demi Marie Obenour
2025-11-19 8:18 ` [PATCH v3 14/14] Validate configuration parameters Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 01/14] host/rootfs: Install all programs from util-linuxMinimal Demi Marie Obenour
2025-11-25 11:56 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 02/14] host/rootfs: Install systemd-pull Demi Marie Obenour
2025-11-25 7:36 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 03/14] tools: Add directory checker for updates Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 04/14] scripts: port make-gpt.sh to bash Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 05/14] scripts/make-gpt.sh: Allow specifying partition size Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 06/14] Support generating multiple partition UUIDs Demi Marie Obenour
2025-11-25 13:02 ` Alyssa Ross
2025-11-26 18:26 ` Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 07/14] scripts: Use shell expansion to get partition path Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 08/14] release: Compress installation images and remove live image Demi Marie Obenour
2025-11-25 13:19 ` Alyssa Ross
2025-11-25 22:38 ` Demi Marie Obenour
2025-11-28 11:09 ` Alyssa Ross
2025-11-28 19:45 ` Demi Marie Obenour
2025-11-22 1:23 ` [PATCH v4 09/14] Use OS version to set partition labels and UKI name Demi Marie Obenour
2025-11-25 14:11 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 10/14] Add B partitions to installation images Demi Marie Obenour
2025-11-25 16:31 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 11/14] release: Create directory with system update Demi Marie Obenour
2025-11-25 16:50 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 12/14] Support updates via systemd-sysupdate Demi Marie Obenour
2025-11-25 17:54 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 13/14] Documentation: Update support Demi Marie Obenour
2025-11-25 18:00 ` Alyssa Ross
2025-11-22 1:23 ` [PATCH v4 14/14] Validate configuration parameters Demi Marie Obenour
2025-11-25 18:06 ` Alyssa Ross
2025-11-25 12:22 ` [PATCH v4 00/14] System updates based on systemd-sysupdate Alyssa Ross
2025-11-26 19:40 ` [PATCH v5 00/13] " Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 01/13] tools: Add directory checker for updates Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 02/13] scripts: port make-gpt.sh to bash Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 03/13] scripts/make-gpt.sh: Allow specifying partition size Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 04/13] Port scripts/format-uuid.sh to awk Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 05/13] Use set and a command substitution to set UUID variables Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 06/13] scripts: Use shell expansion to get partition path Demi Marie Obenour
2025-11-28 11:20 ` Alyssa Ross
2025-11-26 19:40 ` [PATCH v5 07/13] release: Compress installation images and remove live image Demi Marie Obenour
2025-11-28 11:21 ` Alyssa Ross
2025-11-26 19:40 ` [PATCH v5 08/13] Use OS version to set partition labels and UKI name Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 09/13] Add B partitions to installation images Demi Marie Obenour
2025-11-28 11:23 ` Alyssa Ross
2025-11-26 19:40 ` [PATCH v5 10/13] release: Create directory with system update Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 11/13] Support updates via systemd-sysupdate Demi Marie Obenour
2025-11-28 13:47 ` Alyssa Ross
2025-11-28 20:27 ` Demi Marie Obenour
2025-11-28 20:41 ` Alyssa Ross
2025-11-28 20:44 ` Demi Marie Obenour
2025-11-28 21:08 ` Alyssa Ross
2025-11-28 21:28 ` Demi Marie Obenour
2025-11-28 21:30 ` Alyssa Ross
2025-11-26 19:40 ` [PATCH v5 12/13] Documentation: Update support Demi Marie Obenour
2025-11-26 19:40 ` [PATCH v5 13/13] Validate configuration parameters Demi Marie Obenour
2025-11-29 9:49 ` [PATCH v6 0/8] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-29 9:49 ` [PATCH v6 1/8] tools: Add directory checker for updates Demi Marie Obenour
2025-11-29 11:16 ` Alyssa Ross
2025-11-29 9:49 ` [PATCH v6 2/8] release: Compress installation images and remove live image Demi Marie Obenour
2025-11-29 11:16 ` Alyssa Ross
2025-11-29 9:50 ` [PATCH v6 3/8] Use OS version to set partition labels and UKI name Demi Marie Obenour
2025-11-29 11:16 ` Alyssa Ross
2025-11-29 9:50 ` [PATCH v6 4/8] Add B partitions to installation images Demi Marie Obenour
2025-11-29 11:16 ` Alyssa Ross
2025-11-29 9:50 ` [PATCH v6 5/8] release: Create directory with system update Demi Marie Obenour
2025-11-29 11:16 ` Alyssa Ross
2025-11-29 9:50 ` [PATCH v6 6/8] Support updates via systemd-sysupdate Demi Marie Obenour
2025-11-29 11:16 ` Alyssa Ross
2025-11-29 9:50 ` [PATCH v6 7/8] Documentation: Update support Demi Marie Obenour
2025-11-30 21:46 ` Alyssa Ross
2025-11-29 9:50 ` [PATCH v6 8/8] Validate configuration parameters Demi Marie Obenour
2025-11-26 19:33 ` [PATCH v4 00/13] System updates based on systemd-sysupdate Demi Marie Obenour
2025-11-26 19:33 ` [PATCH v4 01/13] tools: Add directory checker for updates Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 02/13] scripts: port make-gpt.sh to bash Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 03/13] scripts/make-gpt.sh: Allow specifying partition size Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 04/13] Port scripts/format-uuid.sh to awk Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 05/13] Use set and a command substitution to set UUID variables Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 06/13] scripts: Use shell expansion to get partition path Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 07/13] release: Compress installation images and remove live image Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 08/13] Use OS version to set partition labels and UKI name Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 09/13] Add B partitions to installation images Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 10/13] release: Create directory with system update Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 11/13] Support updates via systemd-sysupdate Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 12/13] Documentation: Update support Demi Marie Obenour
2025-11-26 19:34 ` [PATCH v4 13/13] Validate configuration parameters Demi Marie Obenour
Code repositories for project(s) associated with this public inbox
https://spectrum-os.org/git/crosvm
https://spectrum-os.org/git/doc
https://spectrum-os.org/git/mktuntap
https://spectrum-os.org/git/nixpkgs
https://spectrum-os.org/git/spectrum
https://spectrum-os.org/git/ucspi-vsock
https://spectrum-os.org/git/www
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).