From: David Matlack <dmatlack@google.com>
To: kexec@lists.infradead.org, linux-doc@vger.kernel.org,
linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org,
linux-mm@kvack.org
Cc: Alexander Graf <graf@amazon.com>,
Andrew Morton <akpm@linux-foundation.org>,
Baolin Wang <baolin.wang@linux.alibaba.com>,
Christian Brauner <brauner@kernel.org>,
David Hildenbrand <david@kernel.org>,
Hugh Dickins <hughd@google.com>, Jonathan Corbet <corbet@lwn.net>,
"Liam R. Howlett" <liam@infradead.org>,
Lorenzo Stoakes <ljs@kernel.org>, Michal Hocko <mhocko@suse.com>,
Mike Rapoport <rppt@kernel.org>,
Pasha Tatashin <pasha.tatashin@soleen.com>,
Pratyush Yadav <pratyush@kernel.org>,
Randy Dunlap <rdunlap@infradead.org>,
Samiullah Khawaja <skhawaja@google.com>,
Shuah Khan <skhan@linuxfoundation.org>,
Suren Baghdasaryan <surenb@google.com>,
Vlastimil Babka <vbabka@kernel.org>,
David Matlack <dmatlack@google.com>
Subject: [PATCH v4 14/19] selftests/liveupdate: Test which tmpfs files can be preserved
Date: Wed, 11 Nov 2026 00:23:52 +0000 [thread overview]
Message-ID: <20261111002358.1842967-15-dmatlack@google.com> (raw)
In-Reply-To: <20261111002358.1842967-1-dmatlack@google.com>
Add tests to liveupdate.c for the checks done when preserving a tmpfs
file: A file can be preserved through a read-only fd, but not a
write-only fd. Preserving it requires permission to write to it, except
for the owner of an immutable file or of a file on a read-only
superblock. Append-only files can't be preserved, and files can be
preserved through a read-only mount. Also check that the size of a
preserved file is frozen until the file is unpreserved.
These checks don't need a kexec, so they run as part of the regular
liveupdate selftest.
Enable CONFIG_TMPFS_XATTR for FS_IOC_SETFLAGS. The test cases that use
it are skipped without it.
Signed-off-by: David Matlack <dmatlack@google.com>
---
tools/testing/selftests/liveupdate/config | 1 +
.../testing/selftests/liveupdate/liveupdate.c | 303 ++++++++++++++++++
2 files changed, 304 insertions(+)
diff --git a/tools/testing/selftests/liveupdate/config b/tools/testing/selftests/liveupdate/config
index 016d009dba13..37230f4c488f 100644
--- a/tools/testing/selftests/liveupdate/config
+++ b/tools/testing/selftests/liveupdate/config
@@ -9,4 +9,5 @@ CONFIG_LIVEUPDATE=y
CONFIG_LIVEUPDATE_TEST=y
CONFIG_MEMFD_CREATE=y
CONFIG_TMPFS=y
+CONFIG_TMPFS_XATTR=y
CONFIG_SHMEM=y
diff --git a/tools/testing/selftests/liveupdate/liveupdate.c b/tools/testing/selftests/liveupdate/liveupdate.c
index 4c4272167635..3d943b5672e6 100644
--- a/tools/testing/selftests/liveupdate/liveupdate.c
+++ b/tools/testing/selftests/liveupdate/liveupdate.c
@@ -16,15 +16,24 @@
* - Resource preservation: successfully preserving individual and multiple memfds,
* verifying contents remain accessible.
* - Complex multi-session scenarios involving mixed empty and populated files.
+ * - Which tmpfs files can be preserved, and the frozen size of preserved files.
*/
#include <errno.h>
#include <fcntl.h>
+#include <grp.h>
#include <stddef.h>
+#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
#include <unistd.h>
+#include <linux/falloc.h>
+#include <linux/fs.h>
+
#include <libliveupdate.h>
#include <linux/liveupdate.h>
@@ -574,4 +583,298 @@ TEST_F(liveupdate_device, preserve_memfd_with_arg)
ASSERT_EQ(close(mem_fd2), 0);
}
+#define NOBODY 65534
+
+FIXTURE(preserve_tmpfs) {
+ char dir[32];
+ int luo_fd;
+ int session_fd;
+ bool mounted;
+};
+
+FIXTURE_SETUP(preserve_tmpfs)
+{
+ self->luo_fd = -1;
+ self->session_fd = -1;
+ self->mounted = false;
+
+ self->luo_fd = open(LIVEUPDATE_DEV, O_RDWR);
+ if (self->luo_fd < 0 && errno == ENOENT)
+ SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
+ ASSERT_GE(self->luo_fd, 0);
+
+ self->session_fd = luo_create_session(self->luo_fd, "preserve-tmpfs");
+ ASSERT_GE(self->session_fd, 0);
+
+ strcpy(self->dir, "/tmp/luo-tmpfs-XXXXXX");
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ if (mount("tmpfs", self->dir, "tmpfs", 0, "size=1M"))
+ SKIP(return, "Failed to mount tmpfs: %s", strerror(errno));
+ self->mounted = true;
+ ASSERT_EQ(chdir(self->dir), 0);
+}
+
+FIXTURE_TEARDOWN(preserve_tmpfs)
+{
+ if (self->session_fd >= 0)
+ close(self->session_fd);
+ if (self->luo_fd >= 0)
+ close(self->luo_fd);
+ if (chdir("/"))
+ return;
+ if (self->mounted)
+ umount2(self->dir, MNT_DETACH);
+ rmdir(self->dir);
+}
+
+/* Create the file @path with some contents and the mode @mode. */
+static int create_file(const char *path, mode_t mode)
+{
+ int fd;
+
+ fd = open(path, O_CREAT | O_EXCL | O_RDWR, mode);
+ if (fd < 0)
+ return -1;
+
+ if (write(fd, "data", 4) != 4 || fchmod(fd, mode)) {
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+/* Open @path with @flags and preserve it. Return 0 or -errno. */
+static int preserve_path(int session_fd, const char *path, int flags,
+ __u64 token)
+{
+ int fd, ret;
+
+ fd = open(path, flags);
+ if (fd < 0)
+ return -errno;
+
+ ret = luo_session_preserve_fd(session_fd, fd, token);
+ close(fd);
+ return ret;
+}
+
+/* Like preserve_path(), but as the user nobody. */
+static int preserve_as_nobody(int session_fd, const char *path, __u64 token)
+{
+ int status;
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0)
+ return -errno;
+
+ if (!pid) {
+ if (setgroups(0, NULL) || setresgid(NOBODY, NOBODY, NOBODY) ||
+ setresuid(NOBODY, NOBODY, NOBODY))
+ _exit(255);
+ _exit(-preserve_path(session_fd, path, O_RDONLY, token));
+ }
+
+ if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -ECHILD;
+
+ return -WEXITSTATUS(status);
+}
+
+/* Add the inode flag @flag, e.g. FS_IMMUTABLE_FL, to the file @fd. */
+static int add_inode_flag(int fd, int flag)
+{
+ int flags;
+
+ if (ioctl(fd, FS_IOC_GETFLAGS, &flags))
+ return -errno;
+
+ flags |= flag;
+ if (ioctl(fd, FS_IOC_SETFLAGS, &flags))
+ return -errno;
+
+ return 0;
+}
+
+/*
+ * Test Case: Preserve Through Read-Only and Write-Only Fds
+ *
+ * A file can be preserved through a read-only fd, but not through a
+ * write-only fd, since the retrieved file can be read.
+ */
+TEST_F(preserve_tmpfs, access_mode)
+{
+ int fd;
+
+ fd = create_file("file", 0644);
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ EXPECT_EQ(preserve_path(self->session_fd, "file", O_WRONLY, 1), -EBADF);
+ EXPECT_EQ(preserve_path(self->session_fd, "file", O_RDONLY, 2), 0);
+}
+
+/*
+ * Test Case: Preserve Without Write Permission
+ *
+ * Preserving a file freezes its size for all of its users, so it requires
+ * permission to write to the file.
+ */
+TEST_F(preserve_tmpfs, unwritable)
+{
+ int fd;
+
+ fd = create_file("file", 0644);
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "file", 1), -EACCES);
+ EXPECT_EQ(preserve_path(self->session_fd, "file", O_RDONLY, 2), 0);
+}
+
+/*
+ * Test Case: Preserve an Append-Only File
+ *
+ * Freezing the size of an append-only file would stop all writes to it, so
+ * append-only files can't be preserved.
+ */
+TEST_F(preserve_tmpfs, append_only)
+{
+ int fd, ret;
+
+ fd = create_file("file", 0644);
+ ASSERT_GE(fd, 0);
+ ret = add_inode_flag(fd, FS_APPEND_FL);
+ close(fd);
+ if (ret == -ENOTTY || ret == -EOPNOTSUPP)
+ SKIP(return, "FS_IOC_SETFLAGS is not supported");
+ ASSERT_EQ(ret, 0);
+
+ EXPECT_EQ(preserve_path(self->session_fd, "file", O_RDONLY, 1), -EPERM);
+}
+
+/*
+ * Test Case: Preserve an Immutable File
+ *
+ * The size of an immutable file can't change anyway, so its owner may preserve
+ * it. Others still need permission to write to it.
+ */
+TEST_F(preserve_tmpfs, immutable)
+{
+ int fd, ret;
+
+ fd = create_file("own", 0644);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fchown(fd, NOBODY, NOBODY), 0);
+ ret = add_inode_flag(fd, FS_IMMUTABLE_FL);
+ close(fd);
+ if (ret == -ENOTTY || ret == -EOPNOTSUPP)
+ SKIP(return, "FS_IOC_SETFLAGS is not supported");
+ ASSERT_EQ(ret, 0);
+
+ fd = create_file("other", 0666);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(add_inode_flag(fd, FS_IMMUTABLE_FL), 0);
+ close(fd);
+
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "own", 1), 0);
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "other", 2), -EPERM);
+}
+
+/*
+ * Test Case: Preserve Through a Read-Only Mount
+ *
+ * Preserving a file only needs permission to write to the file, not a writable
+ * mount, so it works through a read-only mount.
+ */
+TEST_F(preserve_tmpfs, rdonly_mount)
+{
+ int fd;
+
+ ASSERT_EQ(mkdir("src", 0755), 0);
+ fd = create_file("src/file", 0644);
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ ASSERT_EQ(mkdir("ro", 0755), 0);
+ ASSERT_EQ(mount("src", "ro", NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, "ro", NULL, MS_REMOUNT | MS_BIND | MS_RDONLY,
+ NULL), 0);
+
+ EXPECT_EQ(preserve_path(self->session_fd, "ro/file", O_RDONLY, 1), 0);
+ umount2("ro", MNT_DETACH);
+}
+
+/*
+ * Test Case: Preserve on a Read-Only Superblock
+ *
+ * The size of a file on a read-only superblock can't change anyway, so its
+ * owner may preserve it. Others still need permission to write to it.
+ */
+TEST_F(preserve_tmpfs, rdonly_sb)
+{
+ int fd;
+
+ fd = create_file("own", 0644);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fchown(fd, NOBODY, NOBODY), 0);
+ close(fd);
+
+ fd = create_file("other", 0666);
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ ASSERT_EQ(mount(NULL, self->dir, NULL, MS_REMOUNT | MS_RDONLY, NULL),
+ 0);
+
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "own", 1), 0);
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "other", 2), -EROFS);
+}
+
+/*
+ * Test Case: Frozen Size of a Preserved File
+ *
+ * While a file is preserved, its size can't change and no holes can be punched
+ * into it. Writes within the file still work. Closing the session unpreserves
+ * the file, after which its size can change again.
+ */
+TEST_F(preserve_tmpfs, frozen_size)
+{
+ long page_size = getpagesize();
+ struct stat st;
+ char c;
+ int fd;
+
+ fd = create_file("file", 0644);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(ftruncate(fd, 2 * page_size), 0);
+ ASSERT_EQ(luo_session_preserve_fd(self->session_fd, fd, 1), 0);
+
+ EXPECT_EQ(ftruncate(fd, page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+ EXPECT_EQ(ftruncate(fd, 4 * page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+ EXPECT_EQ(fallocate(fd, 0, 0, 4 * page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+ EXPECT_EQ(fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0,
+ page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+ EXPECT_EQ(pwrite(fd, "x", 1, 2 * page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+
+ /* The size didn't change, but writes within the file work. */
+ ASSERT_EQ(fstat(fd, &st), 0);
+ EXPECT_EQ(st.st_size, 2 * page_size);
+ EXPECT_EQ(pwrite(fd, "x", 1, page_size), 1);
+ EXPECT_EQ(pread(fd, &c, 1, page_size), 1);
+ EXPECT_EQ(c, 'x');
+
+ ASSERT_EQ(close(self->session_fd), 0);
+ self->session_fd = -1;
+ EXPECT_EQ(ftruncate(fd, page_size), 0);
+
+ close(fd);
+}
+
TEST_HARNESS_MAIN
--
2.56.0.542.g3ef89b7020-goog
next prev parent reply other threads:[~2026-11-11 0:24 UTC|newest]
Thread overview: 20+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
2026-11-11 0:23 ` [PATCH v4 01/19] liveupdate: Remove the list of steps from luo_preserve_file()'s comment David Matlack
2026-11-11 0:23 ` [PATCH v4 02/19] liveupdate: Pass user argument to file preserve and retrieve David Matlack
2026-11-11 0:23 ` [PATCH v4 03/19] selftests/liveupdate: Add tests for preserve and retrieve user argument David Matlack
2026-11-11 0:23 ` [PATCH v4 04/19] liveupdate: Allow file handlers to mark retrieve failures as retryable David Matlack
2026-11-11 0:23 ` [PATCH v4 05/19] mm: Rename memfd_luo to shmem_luo David Matlack
2026-11-11 0:23 ` [PATCH v4 06/19] liveupdate: Don't WARN if tracking a retrieved file fails David Matlack
2026-11-11 0:23 ` [PATCH v4 07/19] liveupdate: Add helpers to track the files that LUO manages David Matlack
2026-11-11 0:23 ` [PATCH v4 08/19] liveupdate: Let file handlers retrieve into an existing file David Matlack
2026-11-11 0:23 ` [PATCH v4 09/19] mm/shmem_luo: Leave freeing the preserved state to finish() David Matlack
2026-11-11 0:23 ` [PATCH v4 10/19] mm/shmem_luo: Don't make early retrieve failures permanent David Matlack
2026-11-11 0:23 ` [PATCH v4 11/19] mm/shmem_luo: Require permission to preserve a file David Matlack
2026-11-11 0:23 ` [PATCH v4 12/19] mm/shmem_luo: Retrieve tmpfs files into a file created by userspace David Matlack
2026-11-11 0:23 ` [PATCH v4 13/19] mm/shmem_luo: Allow preserving tmpfs files David Matlack
2026-11-11 0:23 ` David Matlack [this message]
2026-11-11 0:23 ` [PATCH v4 15/19] selftests/liveupdate: Add a test for retrieving " David Matlack
2026-11-11 0:23 ` [PATCH v4 16/19] selftests/liveupdate: Test memfds and LUO interactions in luo_tmpfs David Matlack
2026-11-11 0:23 ` [PATCH v4 17/19] selftests/liveupdate: Test the targets of retrieved tmpfs files David Matlack
2026-11-11 0:23 ` [PATCH v4 18/19] selftests/liveupdate: Test retrieving tmpfs files preserved read-only David Matlack
2026-11-11 0:23 ` [PATCH v4 19/19] selftests/liveupdate: Test block accounting of retrieved tmpfs files David Matlack
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261111002358.1842967-15-dmatlack@google.com \
--to=dmatlack@google.com \
--cc=akpm@linux-foundation.org \
--cc=baolin.wang@linux.alibaba.com \
--cc=brauner@kernel.org \
--cc=corbet@lwn.net \
--cc=david@kernel.org \
--cc=graf@amazon.com \
--cc=hughd@google.com \
--cc=kexec@lists.infradead.org \
--cc=liam@infradead.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=ljs@kernel.org \
--cc=mhocko@suse.com \
--cc=pasha.tatashin@soleen.com \
--cc=pratyush@kernel.org \
--cc=rdunlap@infradead.org \
--cc=rppt@kernel.org \
--cc=skhan@linuxfoundation.org \
--cc=skhawaja@google.com \
--cc=surenb@google.com \
--cc=vbabka@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®