[LTP] [PATCH v2] syscalls/cachestat05: Add test for permission checks

Xuewen Wang wangxuewen_ky@163.com
Fri Nov 6 04:29:29 CET 2026


From: Xuewen Wang <wangxuewen@kylinos.cn>

Verify that cachestat() only reveals page cache statistics for files the
calling process has write access to, owns, or has open for writing, as
required by the permission check added in 5f537664e705 ("cachestat: fix
page cache statistics permission checking") to prevent page cache
residency information leaks.

A child process opens the test files as write accessor, owner, writer
and reader, drops privileges and calls cachestat(), which either
succeeds or fails with EPERM. The write-accessor descriptor is opened
on a read-only file before the privileges are dropped and the owned
file is read-only for its owner, so that each of the three permission
conditions is tested in isolation. Kernels without the fix fail the
read-only case and the test prints the missing fix hint.

Tested on 6.6.160 (with the fix backported) and 7.3.0-rc5, x86_64:
4/4 TPASS.

Signed-off-by: Xuewen Wang <wangxuewen@kylinos.cn>
---
Changes in v2 (addressing the AI review):

- Use :manpage:`cachestat(2)` for the syscall references in the doc
  comment
- Remove the kernel version check and tc->needs_fix: kernels without
  the fix fail the read-only case and the .tags linux-git hint points
  to the fix commit
- Open the test file before dropping privileges and use the read-only
  file with O_RDWR for the write-accessor case, so that the
  FMODE_WRITE condition is not confounded with the file write
  permission
- Create the owned file with mode 0400 so that the ownership condition
  is not confounded with the owner write permission
- Reword the summary to cover all three permission conditions: files
  the caller has write access to, owns, or has open for writing

 runtest/syscalls                              |   1 +
 .../kernel/syscalls/cachestat/.gitignore      |   1 +
 .../kernel/syscalls/cachestat/cachestat05.c   | 133 ++++++++++++++++++
 3 files changed, 135 insertions(+)
 create mode 100644 testcases/kernel/syscalls/cachestat/cachestat05.c

diff --git a/runtest/syscalls b/runtest/syscalls
index ff3de1b15..3bd2c98d8 100644
--- a/runtest/syscalls
+++ b/runtest/syscalls
@@ -66,6 +66,7 @@ cachestat01 cachestat01
 cachestat02 cachestat02
 cachestat03 cachestat03
 cachestat04 cachestat04
+cachestat05 cachestat05
 
 chdir01 chdir01
 chdir02 chdir02
diff --git a/testcases/kernel/syscalls/cachestat/.gitignore b/testcases/kernel/syscalls/cachestat/.gitignore
index a3611a533..76e0d90a8 100644
--- a/testcases/kernel/syscalls/cachestat/.gitignore
+++ b/testcases/kernel/syscalls/cachestat/.gitignore
@@ -2,3 +2,4 @@ cachestat01
 cachestat02
 cachestat03
 cachestat04
+/cachestat05
diff --git a/testcases/kernel/syscalls/cachestat/cachestat05.c b/testcases/kernel/syscalls/cachestat/cachestat05.c
new file mode 100644
index 000000000..c3b5ffcc0
--- /dev/null
+++ b/testcases/kernel/syscalls/cachestat/cachestat05.c
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 Xuewen Wang <wangxuewen@kylinos.cn>
+ */
+
+/*\
+ * Verify that :manpage:`cachestat(2)` only reveals page cache statistics for
+ * files the calling process has write access to, owns, or has open for
+ * writing.
+ *
+ * This is a test for the permission check implemented in
+ * 5f537664e705 ("cachestat: fix page cache statistics permission checking")
+ *
+ * Before the fix, :manpage:`cachestat(2)` could be used to query page cache
+ * statistics of arbitrary readable files, which leaks information about the
+ * file access patterns of other users. Since v6.14 the call fails with EPERM
+ * unless one of these applies:
+ *
+ * - the file descriptor was open for writing
+ * - the caller owns the file
+ * - the caller has write permission to the file
+ *
+ * The descriptor for the write-access case is opened while the child is
+ * still root, so that it refers to a file the unprivileged caller could
+ * not open for writing.
+ *
+ * [Algorithm]
+ *
+ * - fork a child which opens a test file with the open flags of the test
+ *   case, then drops all privileges to an unassigned uid and gid
+ * - the child calls :manpage:`cachestat(2)` on the file descriptor
+ * - the call either succeeds, or fails with EPERM if the descriptor is not
+ *   open for writing and the child neither owns the file nor has write
+ *   permission to it
+ */
+
+#include <stdlib.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include "tst_test.h"
+#include "tst_uid.h"
+#include "cachestat.h"
+
+#define FILE_RDWR   "cachestat05_rw"
+#define FILE_OWNED  "cachestat05_owned"
+#define FILE_RDONLY "cachestat05_ro"
+
+static uid_t uid;
+static gid_t gid;
+static struct cachestat *cs;
+static struct cachestat_range *cs_range;
+
+static struct tcase {
+	const char *file;
+	int open_flags;
+	int exp_errno;
+	const char *desc;
+} tcases[] = {
+	{FILE_RDONLY, O_RDWR, 0, "descriptor open for writing"},
+	{FILE_OWNED, O_RDONLY, 0, "file owned by the caller"},
+	{FILE_RDWR, O_RDONLY, 0, "file writable by the caller"},
+	{FILE_RDONLY, O_RDONLY, EPERM, "file read-only for the caller"},
+};
+
+static void verify_cachestat(unsigned int n)
+{
+	struct tcase *tc = &tcases[n];
+	int fd;
+
+	if (SAFE_FORK())
+		return;
+
+	fd = SAFE_OPEN(tc->file, tc->open_flags);
+
+	SAFE_SETGROUPS(0, NULL);
+	SAFE_SETGID(gid);
+	SAFE_SETUID(uid);
+
+	if (tc->exp_errno) {
+		TST_EXP_FAIL(cachestat(fd, cs_range, cs, 0), tc->exp_errno,
+			     "%s", tc->desc);
+	} else {
+		TST_EXP_PASS(cachestat(fd, cs_range, cs, 0), "%s", tc->desc);
+		print_cachestat(cs);
+	}
+
+	SAFE_CLOSE(fd);
+}
+
+static void setup(void)
+{
+	int fd;
+
+	uid = tst_get_free_uid(0);
+	gid = tst_get_free_gid(0);
+
+	umask(0);
+
+	/* len == 0 queries the whole file */
+	cs_range->off = 0;
+	cs_range->len = 0;
+
+	if (cachestat(-1, cs_range, cs, 0) != -1)
+		tst_brk(TBROK, "cachestat(-1) unexpectedly succeeded");
+
+	fd = SAFE_OPEN(FILE_RDWR, O_CREAT | O_RDWR, 0666);
+	SAFE_CLOSE(fd);
+
+	fd = SAFE_OPEN(FILE_OWNED, O_CREAT | O_RDWR, 0400);
+	SAFE_CLOSE(fd);
+	SAFE_CHOWN(FILE_OWNED, uid, -1);
+
+	fd = SAFE_OPEN(FILE_RDONLY, O_CREAT | O_RDONLY, 0444);
+	SAFE_CLOSE(fd);
+}
+
+static struct tst_test test = {
+	.setup = setup,
+	.test = verify_cachestat,
+	.tcnt = ARRAY_SIZE(tcases),
+	.needs_root = 1,
+	.forks_child = 1,
+	.needs_tmpdir = 1,
+	.bufs = (struct tst_buffers []) {
+		{&cs, .size = sizeof(struct cachestat)},
+		{&cs_range, .size = sizeof(struct cachestat_range)},
+		{}
+	},
+	.tags = (const struct tst_tag[]) {
+		{"linux-git", "5f537664e705"},
+		{}
+	},
+};
-- 
2.25.1



More information about the ltp mailing list