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 84D38C8CD; Sun, 14 Dec 2025 01:48:10 +0000 (UTC) Received: by atuin.qyliss.net (Postfix, from userid 993) id 5781EC8B1; Sun, 14 Dec 2025 01:48:04 +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.8 required=3.0 tests=DKIM_SIGNED,DKIM_VALID, DKIM_VALID_AU,DMARC_MISSING,RCVD_IN_DNSWL_LOW,SPF_HELO_PASS autolearn=unavailable autolearn_force=no version=4.0.1 Received: from fout-a2-smtp.messagingengine.com (fout-a2-smtp.messagingengine.com [103.168.172.145]) by atuin.qyliss.net (Postfix) with ESMTPS id 2E746C94E for ; Sun, 14 Dec 2025 01:48:03 +0000 (UTC) Received: from phl-compute-04.internal (phl-compute-04.internal [10.202.2.44]) by mailfout.phl.internal (Postfix) with ESMTP id 08FDEEC0677; Sat, 13 Dec 2025 20:48:01 -0500 (EST) Received: from phl-mailfrontend-02 ([10.202.2.163]) by phl-compute-04.internal (MEProxy); Sat, 13 Dec 2025 20:48:01 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=alyssa.is; h=cc :cc:content-transfer-encoding:content-type:date:date:from:from :in-reply-to:in-reply-to:message-id:mime-version:references :reply-to:subject:subject:to:to; s=fm3; t=1765676881; x= 1765763281; bh=5+mMoZbM/KLMWyFiWPZQ2wNIfWGBfzjdMUkgyt06Tzc=; b=N E2Z4HvBL9dPnNcuCy2yMol7i+jJ7j+ftQc6iiuz7kXe32BKrn0dB/dDMYFoCEku4 PW/6BwFhKHYdZw6yjLkvwm9GcEvQOawngEf4iEnGzeMgNsyAJRPBLh51Ytvx/UZK scILYmXbfoH708/8JUzf4s/Zp5ksIVToVRtsKhDM024MbnEuGt7+1JH1A6qyZY2m N5j0JpEVh2gbHxpoJJoUSRqcSuSnKCq7fb6SuUAuYsbNLJzG5bw+hgga4WXLP37C 6WcAXhGUfxToSHeXy72HVMBgDwoOjizWpplXPFylokQW4FoRuSuA4k5tj5kwPZqa e3kubW5tEp4Um/uQ3Mllw== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-transfer-encoding :content-type:date:date:feedback-id:feedback-id:from:from :in-reply-to:in-reply-to:message-id:mime-version:references :reply-to:subject:subject:to:to:x-me-proxy:x-me-sender :x-me-sender:x-sasl-enc; s=fm1; t=1765676881; x=1765763281; bh=5 +mMoZbM/KLMWyFiWPZQ2wNIfWGBfzjdMUkgyt06Tzc=; b=IhoPydXxfD9EH7v0t udTOvbApRr3Sk2aqHTYNov23BEaAT+5xmd8MCWfSGOedQHAK/Gml3rkgLU7NKm0J MbZsnD6VD45FK0Uuj/RhgRxG2gcryeioYUhnD96OLVxQTp5Md1/SXH3s7EpDXlbt d2bmbfTOC25lFO6qCyxn3ueUOXuqrhNMKW3E8L2dzF/py4haSoRbiNQTOoX79bvN /nB8P/YZf2OXEb2bHG908f93FHK2cBt1YyXwaI7kQySjV2UD9Vg1pGXpfrDM7jT2 L2u4ozusnV5ZHCTL/Z+RY5WBKx6ApLWRi2nVmEIR3qvrklHDsYNH/xxmfjGJ1Bq/ wk5+w== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeefgedrtddtgdefvdeiiecutefuodetggdotefrod ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpuffrtefokffrpgfnqfghnecuuegr ihhlohhuthemuceftddtnecusecvtfgvtghiphhivghnthhsucdlqddutddtmdenucfjug hrpefhvfevufffkffojghfggfgsedtkeertdertddtnecuhfhrohhmpeetlhihshhsrgcu tfhoshhsuceohhhisegrlhihshhsrgdrihhsqeenucggtffrrghtthgvrhhnpeegueeiud eukeekiedvteffgeekudfhudevvdelvdekhfefieetkeduudejffefgeenucevlhhushht vghrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpehhihesrghlhihsshgrrd hishdpnhgspghrtghpthhtohepvddpmhhouggvpehsmhhtphhouhhtpdhrtghpthhtohep uggvmhhiohgsvghnohhurhesghhmrghilhdrtghomhdprhgtphhtthhopeguvghvvghlse hsphgvtghtrhhumhdqohhsrdhorhhg X-ME-Proxy: Feedback-ID: i12284293:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA; Sat, 13 Dec 2025 20:48:00 -0500 (EST) Received: by fw12.qyliss.net (Postfix, from userid 1000) id DCC637BF721D; Sun, 14 Dec 2025 02:47:58 +0100 (CET) From: Alyssa Ross To: devel@spectrum-os.org Subject: [PATCH v2 5/7] tools/vm-set-persist.c: init Date: Sun, 14 Dec 2025 02:42:32 +0100 Message-ID: <20251214014229.775825-10-hi@alyssa.is> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20251214014229.775825-2-hi@alyssa.is> References: <20251214014229.775825-2-hi@alyssa.is> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Message-ID-Hash: SRMCIPHGNTKJHQFBTAB33O3Y5AEBEPBC X-Message-ID-Hash: SRMCIPHGNTKJHQFBTAB33O3Y5AEBEPBC X-MailFrom: hi@alyssa.is 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 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: This allows the disk-backed directory of a running VM to be made persistent, with a user-provided name. This is done by renaming the directory to have a "persist." prefix rather than the "tmp." one the cleaner will look for. Since the VM's virtiofsd accesses the directory via a bind mount, this rename will be unnoticeable to the guest. musl has quite a bit of catching up to do with the APIs used here, which requires the use of a lot of raw syscalls. This even applies to some syscalls musl has wrappers for, like mkdirat(2), because musl's mkdirat() comes from , which defines a struct statx that's missing the stx_mnt_id member we need. There doesn't even seem to be a SYS_statmount, so we use __NR_ constants throughout for consistency. Signed-off-by: Alyssa Ross --- v2: new this round tools/default.nix | 1 + tools/meson.build | 4 + tools/vm-set-persist.c | 179 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 tools/vm-set-persist.c diff --git a/tools/default.nix b/tools/default.nix index 56f41cd9..f094594f 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -78,6 +78,7 @@ stdenv.mkDerivation (finalAttrs: { ./start-vmm ./subprojects ./updates-dir-check.c + ./vm-set-persist.c ] ++ lib.optionals driverSupport [ ./xdp-forwarder ])); diff --git a/tools/meson.build b/tools/meson.build index 666483b3..06aa24d7 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -37,6 +37,10 @@ if get_option('host') executable('updates-dir-check', 'updates-dir-check.c', c_args : '-D_GNU_SOURCE', install: true) + + executable('vm-set-persist', 'vm-set-persist.c', + c_args : '-D_GNU_SOURCE', + install: true) endif if get_option('build') diff --git a/tools/vm-set-persist.c b/tools/vm-set-persist.c new file mode 100644 index 00000000..ac759504 --- /dev/null +++ b/tools/vm-set-persist.c @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: 2025 Alyssa Ross +// SPDX-License-Identifier: EUPL-1.2+ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// No until musl declares stx_mnt_id. +#include + +#include +#include +#include +#include +#include + +// Including trailing NUL bytes. +static const int MNT_ROOT_MAX_LEN = 43; +static const int SOURCE_MAX_LEN = 28; + +static void set_mount_namespace(const char vm_id[static 1]) +{ + char ns_path[28]; + int r = snprintf(ns_path, sizeof ns_path, + "/run/vm/by-id/%s/ns/mnt", vm_id); + + if (r == -1) + err(EXIT_FAILURE, "snprintf"); + if ((size_t)r >= sizeof ns_path) + errx(EXIT_FAILURE, "VM ID unexpectedly long"); + + if ((r = open(ns_path, O_RDONLY | O_CLOEXEC)) == -1) + err(EXIT_FAILURE, "open"); + if (setns(r, CLONE_NEWNS) == -1) + err(EXIT_FAILURE, "setns"); + close(r); +} + +static void do_statx(const char path[static 1], + mode_t mode[static 1], uint64_t mnt_id[static 1]) +{ + struct statx stx; + + if (syscall(__NR_statx, AT_FDCWD, path, AT_SYMLINK_NOFOLLOW, + STATX_MODE | STATX_MNT_ID_UNIQUE, &stx) == -1) + err(EXIT_FAILURE, "statx"); + + if (!(stx.stx_attributes & STATX_ATTR_MOUNT_ROOT)) { + if (stx.stx_attributes_mask & STATX_ATTR_MOUNT_ROOT) + errx(EXIT_FAILURE, + "VM disk-backed directory not mounted"); + + errx(EXIT_FAILURE, "statx didn't return STATX_ATTR_MOUNT_ROOT"); + } + + if (!(stx.stx_mask & STATX_MNT_ID_UNIQUE)) + errx(EXIT_FAILURE, "statx didn't return STATX_MNT_ID_UNIQUE"); + if (!(stx.stx_mask & STATX_MODE)) + errx(EXIT_FAILURE, "statx didn't return STATX_MODE"); + + *mode = stx.stx_mode; + *mnt_id = stx.stx_mnt_id; +} + +static int do_mount(const char source[static 1]) +{ + int mnt, fs = syscall(__NR_fsopen, "btrfs", FSOPEN_CLOEXEC); + if (fs == -1) + err(EXIT_FAILURE, "fsopen"); + if (syscall(__NR_fsconfig, fs, FSCONFIG_SET_STRING, + "source", source, 0) == -1) + err(EXIT_FAILURE, "FSCONFIG_SET_STRING source"); + if (syscall(__NR_fsconfig, fs, FSCONFIG_SET_FLAG, + "rw", nullptr, 0) == -1) + err(EXIT_FAILURE, "FSCONFIG_SET_FLAG rw"); + if (syscall(__NR_fsconfig, fs, FSCONFIG_CMD_CREATE, + nullptr, nullptr, 0) == -1) + err(EXIT_FAILURE, "FSCONFIG_CMD_CREATE"); + if ((mnt = syscall(__NR_fsmount, fs, FSMOUNT_CLOEXEC, + MOUNT_ATTR_NOSUID | MOUNT_ATTR_NOSYMFOLLOW | + MOUNT_ATTR_NOEXEC | MOUNT_ATTR_NODEV)) == -1) + err(EXIT_FAILURE, "fsmount"); + close(fs); + return mnt; +} + +static void do_statmount(uint64_t mnt_id, + char mnt_root[static MNT_ROOT_MAX_LEN], + char source[static SOURCE_MAX_LEN]) +{ + int r; + char sm_buf[sizeof(struct statmount) + + MNT_ROOT_MAX_LEN + SOURCE_MAX_LEN]; + struct statmount *sm = (struct statmount *)sm_buf; + struct mnt_id_req req = { + .size = sizeof req, + .mnt_id = mnt_id, + .param = STATMOUNT_MNT_ROOT | STATMOUNT_SB_SOURCE, + }; + + if (syscall(__NR_statmount, &req, sm, sizeof sm_buf, 0) == -1) + err(EXIT_FAILURE, "statmount"); + + r = snprintf(mnt_root, MNT_ROOT_MAX_LEN, "%s", sm->str + sm->mnt_root); + if (r == -1) + err(EXIT_FAILURE, "snprintf"); + if (r >= MNT_ROOT_MAX_LEN) + errx(EXIT_FAILURE, "unexpectedly long mnt_root"); + + r = snprintf(source, SOURCE_MAX_LEN, "%s", sm->str + sm->sb_source); + if (r == -1) + err(EXIT_FAILURE, "snprintf"); + if (r >= SOURCE_MAX_LEN) + errx(EXIT_FAILURE, "unexpectedly long sb_source"); +} + +static void do_rename(int mnt, const char dir_name[static 1], + const char old_name[static 1], + const char new_name[static 1], mode_t mode) +{ + struct open_how how = { + .flags = O_PATH | O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW, + .resolve = RESOLVE_NO_MAGICLINKS | RESOLVE_IN_ROOT | + RESOLVE_NO_SYMLINKS | RESOLVE_NO_XDEV, + }; + int dir = syscall(__NR_openat2, mnt, dir_name, &how, sizeof how); + if (dir == -1) + err(EXIT_FAILURE, "openat2"); + + if (syscall(__NR_mkdirat, dir, new_name, mode) == -1) + err(EXIT_FAILURE, "mkdirat"); + if (syscall(__NR_renameat2, dir, old_name, dir, new_name, + RENAME_EXCHANGE) == -1) + err(EXIT_FAILURE, "renameat2"); +} + +int main(int argc, char *argv[]) +{ + int mnt; + mode_t mode; + uint64_t mnt_id; + char *disk_path, *dir_name, *old_name, *new_name, + mnt_root[MNT_ROOT_MAX_LEN], source[SOURCE_MAX_LEN]; + + if (argc != 3) { + fprintf(stderr, "Usage: vm-set-persist ID INSTANCE\n"); + exit(EXIT_FAILURE); + } + + if (strchr(argv[1], '/')) + errx(EXIT_FAILURE, "invalid VM ID"); + if (strchr(argv[2], '/')) + errx(EXIT_FAILURE, "invalid persistent directory name"); + + if (asprintf(&disk_path, "/run/fs/%s/disk", argv[1]) == -1) + err(EXIT_FAILURE, "asprintf"); + if (asprintf(&new_name, "persist.%s", argv[2]) == -1) + err(EXIT_FAILURE, "asprintf"); + + set_mount_namespace(argv[1]); + + do_statx(disk_path, &mode, &mnt_id); + do_statmount(mnt_id, mnt_root, source); + + if (!(dir_name = strdup(mnt_root))) + err(EXIT_FAILURE, "strdup"); + dir_name = dirname(dir_name); + old_name = basename(mnt_root); + + mnt = do_mount(source); + + do_rename(mnt, dir_name, old_name, new_name, mode); +} -- 2.51.0