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 DBFE51CAA2; Sat, 29 Nov 2025 09:51:30 +0000 (UTC) Received: by atuin.qyliss.net (Postfix, from userid 993) id 14C561CA3F; Sat, 29 Nov 2025 09:51:27 +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-yx1-xb136.google.com (mail-yx1-xb136.google.com [IPv6:2607:f8b0:4864:20::b136]) by atuin.qyliss.net (Postfix) with ESMTPS id C47681C9CD for ; Sat, 29 Nov 2025 09:51:18 +0000 (UTC) Received: by mail-yx1-xb136.google.com with SMTP id 956f58d0204a3-641e9422473so2153758d50.2 for ; Sat, 29 Nov 2025 01:51:17 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1764409877; x=1765014677; 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=QUixPJm9BjWIxWR4g2NyWz63DuiU6bnBzK+YGdtmK8E=; b=a+aAW++LPUfLYx3QGIu84zE3Q+juXFPoO49Tmv+q5SJIqTBbBKGB8PPsYj9XY1ZWd5 YVOkJB8Y88WvcKCPakt93xXHdXpQoeu9toSvKhnoJtVRj7d6fnQ4zz1C3T/fYJ7uH0Mb e2nLjZv0QSBMiRXIEgLtr4iNEdcj5p63NhgwR6nKFzCWvxNxmz6D0NCyrU6ZC7RCx2Sc XcWsdfLJV2pKfEZ0ZVSniIfcDnb4QMUbhBmiYABli9AOWe/oNMr6rWCpF05WLbJunvcP 4N7ZMVDVclWXl5XUDT0Kupi6ez4ZmjtCnpEa5+JPp6i0vB7wXwcoHoSnmP0b9CHFNYAT hVlw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1764409877; x=1765014677; 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=QUixPJm9BjWIxWR4g2NyWz63DuiU6bnBzK+YGdtmK8E=; b=xCFi9d4vgw4eWSxCpETOMnsU+i9+IMuB8pPHJJvV4ASL4TEnj9SI2rQCd4zkJ6i7g6 zXhT0agM53H0xbKUEQuloZgVKhJx8fkcV8cDV7fFFUoffKmCtpzNEpu/+9zl8BpVjyho V9eXM2fx9xzet+qYFSP6sBUtI19QzLbIYMMkVKBCT6pIiOkltjECm5RmQhCTR1UdJ8Eb LYoafTXFMffoYVTGWAKWjvl1lzf0ydtarvcgzUW4Vj76njiVt47fibs8YJdJLoL/K6LD 9UC+wOaMpeqF2RFM4Gq9CFfwYBNkoqOWHsJDXDVK/1iu1i7lSpqH3TOYHXKxWeg3S9HC YjVw== X-Gm-Message-State: AOJu0Yw+p4nluFZ/uOi0DUIkbUpQ9Rz0wmmQJW0phCemZjoeReAdpDUx 8lJg2+R6cds2q8vYeyQwTM2uHAL1Uxwl3h30fJ+mxFtLHoBtsuP+eBQw4nxQHw== X-Gm-Gg: ASbGncuhAVnV1vbxqNmc2Vffdh1k5nhH+8du2Bf27PMo9zywJr6MGPiXqRY26IEIanq /yEmgiwRAfjlwFARNsYeNwxyQ0LjEuhE+kFHPfEAR5t1l6U8A/9TfR6kcnzwqYmFpyP1fSw7cnt oUkgTnEiq+H7pfbk6Rk2d3pVhv53ad8kJDJDblgN4n76BuA6IyNZS/dyeEcqSb6QNQlC8OEiF5R 16zGU/XV3gLoUAUZg6SJEzthqQynvzPJVSi4FOKD9+ThgW7iDXMIJKfyB8hOicHj9ewnowUosLC BviN9rkF9wwYBDNtn2+nIPxk0ZrnBeoqVKXt9dRdxGo6899G7WAxwrIxFaKnZxEgSqLNWka2xQA iB31NSY2ubOgbNv7+bOIOhhiEo3iKCmpScnu0edS/Y4NvdhH6osCMXKtLhOI9H10IkeKJAWKres BnZ0CD1rAalhSJ3OULiGd3vJn5DoapwEYHf4fTCSz4pucYnARYxwP4fOCkd5F9i3yY+7MKp0Yx7 ++b0j6FjtuSsHYA1s9VECx062kJDIa4qYY= X-Google-Smtp-Source: AGHT+IFFMpaI/8K8gwfMxjKCPmBNN0B6Mb3k/vYVX77nTmxHCMatq43QTI+2ZYvdCGDT3itIlmKHWA== X-Received: by 2002:a05:690c:2e0c:b0:786:5f03:2b33 with SMTP id 00721157ae682-78ab6d6cef2mr210498207b3.1.1764409876166; Sat, 29 Nov 2025 01:51:16 -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-78ad0d41113sm24218397b3.4.2025.11.29.01.51.13 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sat, 29 Nov 2025 01:51:14 -0800 (PST) From: Demi Marie Obenour Date: Sat, 29 Nov 2025 04:50:03 -0500 Subject: [PATCH v6 6/8] Support updates via systemd-sysupdate MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 7bit Message-Id: <20251129-updates-v6-6-9edb87a2e509@gmail.com> References: <20251129-updates-v6-0-9edb87a2e509@gmail.com> In-Reply-To: <20251129-updates-v6-0-9edb87a2e509@gmail.com> To: Spectrum OS Development X-Mailer: b4 0.14.3 X-Developer-Signature: v=1; a=ed25519-sha256; t=1764409797; l=24155; i=demiobenour@gmail.com; s=20250729; h=from:subject:message-id; bh=F7wWDoEOeTKSY/5Ibn2fKwB/XOL3TBVwQacf8y9MaoY=; b=SFXxrTQS4zArLsc0sWSEYcvA7f/25bDPc47MPtjtVOKztJ2YSrZ82brFPZCCNFYHVNslUbi0H HcYgI7c9GlSBOlQkq1W+DEIKMGygK+x+J/1lQqQ4C8hGGlEjHR4Wku1 X-Developer-Key: i=demiobenour@gmail.com; a=ed25519; pk=X57Q4/YQDj9t4SBeKaDwvXYKB6quZJVx/DE2Ly2out0= Message-ID-Hash: CA6YVUZVFV7NRAM34PIKISDVIDIGWN42 X-Message-ID-Hash: CA6YVUZVFV7NRAM34PIKISDVIDIGWN42 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. Signed-off-by: Demi Marie Obenour Reviewed-by: Alyssa Ross --- 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 + +# 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..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 + +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 +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..db64f0105da6e1f9951d13f67a2ca8527ae6f73d --- /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/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