From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from atuin.qyliss.net (localhost [IPv6:::1]) by atuin.qyliss.net (Postfix) with ESMTP id 76275BA16; Wed, 26 Nov 2025 19:42:25 +0000 (UTC) Received: by atuin.qyliss.net (Postfix, from userid 993) id AAC50B8EA; Wed, 26 Nov 2025 19:42:15 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-26) on atuin.qyliss.net X-Spam-Level: X-Spam-Status: No, score=-0.1 required=3.0 tests=DKIM_SIGNED,DKIM_VALID, DKIM_VALID_AU,DMARC_PASS,FREEMAIL_FROM,RCVD_IN_DNSWL_NONE, SPF_HELO_NONE autolearn=unavailable autolearn_force=no version=4.0.1 Received: from mail-yw1-x112e.google.com (mail-yw1-x112e.google.com [IPv6:2607:f8b0:4864:20::112e]) by atuin.qyliss.net (Postfix) with ESMTPS id 96329B7C6 for ; Wed, 26 Nov 2025 19:41:55 +0000 (UTC) Received: by mail-yw1-x112e.google.com with SMTP id 00721157ae682-787eb2d8663so2981277b3.0 for ; Wed, 26 Nov 2025 11:41:54 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1764186113; x=1764790913; darn=spectrum-os.org; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :mime-version:subject:date:from:from:to:cc:subject:date:message-id :reply-to; bh=hg1GFDiz7DgnQQ8QIGrCQV4pHOdTz8trNPbZ8VWexP4=; b=hDihEZrKckIVdzHlMOcb9vUduPzIXV3wYcQnA//WoiphxVY3ZuFnNyX2fT3i80+hkW vg0C8hn1yosL43bH8tdHkND20EeZD86AcC+r8C/3zoMg2NV1vwx3nvYzou4kw+nLD84H fCNXB226KgXOoapVo/zitVvEKI3eSWxFUU0gb38sXEj1Q5Ls1SjtNeeBZNbeVwBn0V1T 3i4g1MMKZR4qyaNqI71bijwpMsogHSRkuBbF4XALxmaL+5ngDKvFVSg+WgaWLK0Y1Fmz 9I9emzD/bT5dqXrqcv9bEeOlKBmdd7LTw69vCG+L9V4byXiUhtzcAJCBs9ELuE7Mti64 WJLw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1764186113; x=1764790913; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :mime-version:subject:date:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to; bh=hg1GFDiz7DgnQQ8QIGrCQV4pHOdTz8trNPbZ8VWexP4=; b=beNr3g0vgGP8u2ccDK5GEO+egl3Fch6fQIiGlh19gercm82J4NKaTALZJIv1yF41wp V7UX8WlzO1uaZZHvZ+IeCbp5BmdLIQqw+vJrBQ2Oa2eS0Yj8SONYFwzFMNFMEYfERv6U 7Smg2MtNdvflLwgZZ1RtQvMK461TcAoRlC8/L9ygDgu8oU5/zqvNA4BQbuj1Z6YmG8KZ xDt2yNThlRdbLhJJHhuI/EERi0vjBWyzAC+hCj5ynDEDvmeua8+hIJjLksY4rswWAUof E7LCly0OlRxA3Qy3VuUGPQVIQT3Xtz+NVuOv4NS9EqrRggCdoDkEs+tGmLCsbi34cDR/ B5qQ== X-Gm-Message-State: AOJu0YzgR+Gdu6OCJzmnM4DqbiKbQlnwLegz0ypwEguRNrdqNN7KGmbh u/xz0//Dr9i/vqM48zJGuFe7T8yygwRXPsGeMbbv4wW/SdTsv/SNaJucGg2kEg== X-Gm-Gg: ASbGncuokxjCbjqnPJLCFpYKrfRdt1fdI2183i1MDvBurSTPLjusPpk+Mulhp/uc0xa Wmp0cuZmHd7oSirxGc9qGR0Nk0gPc/2ctaWH8YD8tu7lKwZsnE0j+GtYqWnAUAoJIBVc89wIXsc z7hK6mkc+CZmn5oT/V67P8yoz6l4vJdWk5ntPraQNN3+vfniLZR4JODUuFl5jrjRsSyIMavI/Gy aUnI68OHmExUGToIq/BNo8Q6dpFCsNTfPtEOTC828A1dToltG5j05vAtIvOXt8siJNFTbVsNFB3 NunAbhGtEmLkMK3F19RDlmzyntoNbFfoMwRh9xK3CqTvi/MKOQuBSd0pzcO0ZlR/XCpvjv/FzbV ua6fM5t05nOVtf5miHYKMlAQPa4M6XXt7fY7o2NwfGrX7n7std+vtkmI0zam4NnLVuWfKaWd+4s 3byE1ZVC1HF0Kb1xuhnSC+mRs7XpFpwCBw/6lXGCFdwyg85E/5IMgeLiQsPTpnxEU0bXoabHVbR fV3wOTz/M3oCwGI6bv4ZToR5TJaCTHRgv8= X-Google-Smtp-Source: AGHT+IHNEhmQDd/bXCw0iEWNoHz77N6eBlVCs1+VOMlZvoEgEfU9wrlUHvwVCEvHDE0r7su/7qjX3g== X-Received: by 2002:a05:690c:660b:b0:78a:859c:63b1 with SMTP id 00721157ae682-78a859c6465mr184122947b3.10.1764186113152; Wed, 26 Nov 2025 11:41:53 -0800 (PST) Received: from localhost.localdomain (h96-60-249-169.cncrtn.broadband.dynamic.tds.net. [96.60.249.169]) by smtp.gmail.com with UTF8SMTPSA id 00721157ae682-78a7987f206sm70300757b3.6.2025.11.26.11.41.52 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 26 Nov 2025 11:41:52 -0800 (PST) From: Demi Marie Obenour Date: Wed, 26 Nov 2025 14:40:51 -0500 Subject: [PATCH v5 11/13] Support updates via systemd-sysupdate MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 7bit Message-Id: <20251126-updates-v5-11-fd746748febd@gmail.com> References: <20251126-updates-v5-0-fd746748febd@gmail.com> In-Reply-To: <20251126-updates-v5-0-fd746748febd@gmail.com> To: Spectrum OS Development X-Mailer: b4 0.14.3 X-Developer-Signature: v=1; a=ed25519-sha256; t=1764186041; l=24677; i=demiobenour@gmail.com; s=20250729; h=from:subject:message-id; bh=E5X5ebbrF1EWmdDUoPuWVi4Jxn6FnpD/orHySHSEx/g=; b=UbTVhDZcUQUYGPF/ztulAvzzG3uDYAE8RUqXnFRCMcgEMBI/1t2NqleClr1DLujnrMbLxCsjo Ns67gNnBB52BOygdcp6IIuem22Q418fPBAyy48CSL2e8TloyUbeo25N X-Developer-Key: i=demiobenour@gmail.com; a=ed25519; pk=X57Q4/YQDj9t4SBeKaDwvXYKB6quZJVx/DE2Ly2out0= Message-ID-Hash: GPHNI6AOLMJG7PR5FT2WMAR27PMVPORW X-Message-ID-Hash: GPHNI6AOLMJG7PR5FT2WMAR27PMVPORW X-MailFrom: demiobenour@gmail.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; header-match-devel.spectrum-os.org-0; header-match-devel.spectrum-os.org-1; header-match-devel.spectrum-os.org-2; header-match-devel.spectrum-os.org-3; header-match-devel.spectrum-os.org-4; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Demi Marie Obenour , Alyssa Ross X-Mailman-Version: 3.3.9 Precedence: list List-Id: Patches and low-level development discussion Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- 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 --- 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 + +# 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 + +# 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 + +# 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 + +# 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 + +# 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 + +# 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 + +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 +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 +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 +# SPDX-FileCopyrightText: 2025 Demi Marie Obenour + +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 +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