[Devel] [PATCH vz10 v3 9/9] selftests/ve: add ve_perms test for proc and sysfs
Mirian Shilakadze
mirian.shilakadze at virtuozzo.com
Fri Jul 17 14:51:36 MSK 2026
On 7/17/26 2:34 PM, Vasileios Almpanis wrote:
>
> On 7/17/26 1:25 AM, Mirian Shilakadze wrote:
>> Add a test for the per-VE permission allowlist. It runs against both
>> interfaces through a fixture variant, ve.proc_permissions and
>> ve.sysfs_permissions, which share the leaf logic in fs/ve_perms.c.
>> Each variant names a few host-only files and a host-only directory,
>> all registered nodes outside a container's default view. Reading the
>> list back is a seq file, so the test adds a read_file_at helper to the
>> shared ve_selftest.h.
>>
>> From the host it grants read on a file, widens to rwx and removes it
>> checking the list each time, confirms an entry granted to one VE is
>> not seen by another, and confirms an unknown path is rejected. It
>> covers the write parser in one test: comment lines skipped, several
>> entries in one write, a later grant replacing an earlier one, a
>> duplicate op keeping the last, remove of an absent entry, and a
>> missing mask rejected, and checks each entry renders at its true
>> depth with a directory getting a trailing slash.
>>
>> Enforcement is checked end to end from inside a VE, split into an
>> allowed and a denied test. A task enters the VE cgroup, unshares
>> CLONE_NEWVE and opens the entry on the inherited host /proc or /sys.
>> Granting r makes each file readable and granting x on the directory
>> lets it be traversed, while an ungranted file stays hidden, a file
>> granted only w cannot be read, and a directory granted only r cannot
>> be traversed.
>>
>> https://virtuozzo.atlassian.net/browse/VSTOR-135286
>> Signed-off-by: Mirian Shilakadze <mirian.shilakadze at virtuozzo.com>
>> ---
>> tools/testing/selftests/ve/.gitignore | 1 +
>> tools/testing/selftests/ve/Makefile | 1 +
>> tools/testing/selftests/ve/ve_perms_test.c | 360 +++++++++++++++++++++
>> tools/testing/selftests/ve/ve_selftest.h | 26 ++
>> 4 files changed, 388 insertions(+)
>> create mode 100644 tools/testing/selftests/ve/ve_perms_test.c
>>
>> diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/
>> selftests/ve/.gitignore
>> index 7bfff054f9e8..afa4c568c2c9 100644
>> --- a/tools/testing/selftests/ve/.gitignore
>> +++ b/tools/testing/selftests/ve/.gitignore
>> @@ -1 +1,2 @@
>> ve_ns_owner_test
>> +ve_perms_test
>> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/
>> selftests/ve/Makefile
>> index aa03ab02dda9..ec40cbc7b3a1 100644
>> --- a/tools/testing/selftests/ve/Makefile
>> +++ b/tools/testing/selftests/ve/Makefile
>> @@ -3,5 +3,6 @@
>> CFLAGS += -g -Wall -O2
>> TEST_GEN_PROGS += ve_ns_owner_test
>> +TEST_GEN_PROGS += ve_perms_test
>> include ../lib.mk
>> diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/
>> testing/selftests/ve/ve_perms_test.c
>> new file mode 100644
>> index 000000000000..64bf8ab6fcc6
>> --- /dev/null
>> +++ b/tools/testing/selftests/ve/ve_perms_test.c
>> @@ -0,0 +1,360 @@
>> +// SPDX-License-Identifier: GPL-2.0
>> +/*
>> + * ve_perms selftest
>> + *
>> + * Drives the per-VE permission allowlist from the host and checks
>> the per-VE
>> + * bookkeeping through cgroupfs. The same tests run against both
>> interfaces via
>> + * a fixture variant: ve.proc_permissions (procfs) and
>> ve.sysfs_permissions
>> + * (sysfs), which share the leaf logic in fs/ve_perms.c. Each variant
>> names a
>> + * few host-only files and a host-only directory, all registered
>> nodes outside
>> + * a container's default view. The tests:
>> + *
>> + * - grant read on a file, widen to rwx, then remove it (list each
>> time)
>> + * - confirm an entry granted to one VE is not seen by another
>> + * - confirm a path that is not a registered entry is rejected
>> + * - cover the write parser (masks, comments, several entries,
>> replace,
>> + * duplicate op, remove of an absent entry, missing mask) and
>> check each
>> + * entry renders at its true depth, a directory with a trailing
>> slash
>> + * - enforce the r and x mask bits end to end from inside a VE
>> + */
>> +#define _GNU_SOURCE
>> +#include <sched.h>
>> +#include <stdio.h>
>> +#include <string.h>
>> +#include <unistd.h>
>> +#include <fcntl.h>
>> +#include <limits.h>
>> +#include <sys/wait.h>
>> +#include <errno.h>
>> +
>> +#include "../kselftest_harness.h"
>> +#include "ve_selftest.h"
>> +
>> +#define NR_FILES 2
>> +
>> +FIXTURE(ve_perms)
>> +{
>> + int cgv2_fd;
>> + int ctid_a;
>> + int ctid_b;
>> +};
>> +
>> +FIXTURE_VARIANT(ve_perms)
>> +{
>> + const char *fs; /* "proc"/"sysfs": names the cgroup
>> file */
>> + const char *dir_prefix; /* host mount point: "/proc" or "/
>> sys" */
>> + const char *files[NR_FILES]; /* host-only readable leaves */
>> + const char *dir; /* host-only directory */
>> +};
>> +
>> +FIXTURE_VARIANT_ADD(ve_perms, proc)
>> +{
>> + .fs = "proc",
>> + .dir_prefix = "/proc",
>> + .files = { "interrupts", "slabinfo" },
>> + .dir = "bus",
>> +};
>> +
>> +FIXTURE_VARIANT_ADD(ve_perms, sysfs)
>> +{
>> + .fs = "sysfs",
>> + .dir_prefix = "/sys",
>> + .files = { "kernel/vmcoreinfo", "kernel/notes" },
>> + .dir = "power",
>> +};
>> +
>> +/* The per-VE cgroup file for this ctid and filesystem. */
>> +static void ve_file(char *buf, size_t n, int ctid, const char *fs)
>> +{
>> + snprintf(buf, n, "%d/ve.%s_permissions", ctid, fs);
>> +}
>> +
>> +/*
>> + * Fork a child that joins VE cgroup @ctid, unshares into a fresh VE,
>> and opens
>> + * @path with @oflag. Return 0 if the open succeeded, else the errno
>> it failed
>> + * with, or -1 on a harness error. unshare needs a single threaded
>> caller,
>> + * which the freshly forked child is.
>> + */
>> +static int ve_open(int cgv2_fd, int ctid, const char *path, int oflag)
>> +{
>> + int status;
>> + pid_t pid;
>> +
>> + pid = fork();
>> + if (pid < 0)
>> + return -1;
>> + if (pid == 0) {
>> + int fd;
>> +
>> + if (enter_cgroup(cgv2_fd, ctid) < 0)
>> + _exit(255);
>> + if (unshare(CLONE_NEWVE) < 0)
>> + _exit(255);
>> + fd = open(path, oflag);
>> + _exit(fd >= 0 ? 0 : errno);
>> + }
>> + if (waitpid(pid, &status, 0) < 0 || !WIFEXITED(status))
>> + return -1;
>> + return WEXITSTATUS(status);
>> +}
>> +
>> +/* As ve_open(), for an entry named relative to the filesystem mount
>> point. */
>> +static int ve_open_rel(int cgv2_fd, int ctid, const char *prefix,
>> + const char *entry, int oflag)
>> +{
>> + char abs[PATH_MAX];
>> +
>> + if (snprintf(abs, sizeof(abs), "%s/%s", prefix, entry) >=
>> (int)sizeof(abs))
>> + return -1;
>> + return ve_open(cgv2_fd, ctid, abs, oflag);
>> +}
>> +
>> +FIXTURE_SETUP(ve_perms)
>> +{
>> + self->cgv2_fd = mount_cg2_fd();
>> + ASSERT_GE(self->cgv2_fd, 0);
>> +
>> + ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
>> + VE_CONTROLLERS), 0);
>> +
>> + self->ctid_a = make_ve(self->cgv2_fd, CTID_MIN);
>> + ASSERT_GE(self->ctid_a, 0);
>> + self->ctid_b = make_ve(self->cgv2_fd, self->ctid_a + 1);
>> + ASSERT_GE(self->ctid_b, 0);
>> +}
>> +
>> +FIXTURE_TEARDOWN(ve_perms)
>> +{
>> + destroy_ve(self->cgv2_fd, self->ctid_b);
>> + destroy_ve(self->cgv2_fd, self->ctid_a);
>> + close(self->cgv2_fd);
>> +}
>> +
>> +/* Grant read, widen to rwx, then remove, checking the list each
>> time. */
>> +TEST_F(ve_perms, grant_widen_revoke)
>> +{
>> + const char *file = variant->files[0];
>> + char path[PATH_MAX], line[128], buf[8192];
>> +
>> + ve_file(path, sizeof(path), self->ctid_a, variant->fs);
>> +
>> + ASSERT_GE(read_file_at(self->cgv2_fd, path, buf, sizeof(buf)), 0);
>> + EXPECT_EQ(strstr(buf, file), NULL);
>> +
>> + snprintf(line, sizeof(line), "%s r", file);
>> + ASSERT_EQ(write_file_at(self->cgv2_fd, path, line), 0);
>> + ASSERT_GT(read_file_at(self->cgv2_fd, path, buf, sizeof(buf)), 0);
>> + snprintf(line, sizeof(line), "%s r\n", file);
>> + EXPECT_NE(strstr(buf, line), NULL);
>> +
>> + snprintf(line, sizeof(line), "%s rwx", file);
>> + ASSERT_EQ(write_file_at(self->cgv2_fd, path, line), 0);
>> + ASSERT_GT(read_file_at(self->cgv2_fd, path, buf, sizeof(buf)), 0);
>> + snprintf(line, sizeof(line), "%s rwx\n", file);
>> + EXPECT_NE(strstr(buf, line), NULL);
>> +
>> + snprintf(line, sizeof(line), "%s -", file);
>> + ASSERT_EQ(write_file_at(self->cgv2_fd, path, line), 0);
>> + ASSERT_GE(read_file_at(self->cgv2_fd, path, buf, sizeof(buf)), 0);
>> + EXPECT_EQ(strstr(buf, file), NULL);
>> +}
>> +
>> +/* An entry granted to VE A must not appear for VE B. */
>> +TEST_F(ve_perms, per_ve_isolation)
>> +{
>> + const char *file = variant->files[0];
>> + char pa[PATH_MAX], pb[PATH_MAX], line[128], buf[8192];
>> +
>> + ve_file(pa, sizeof(pa), self->ctid_a, variant->fs);
>> + ve_file(pb, sizeof(pb), self->ctid_b, variant->fs);
>> +
>> + snprintf(line, sizeof(line), "%s r", file);
>> + ASSERT_EQ(write_file_at(self->cgv2_fd, pa, line), 0);
>> +
>> + ASSERT_GT(read_file_at(self->cgv2_fd, pa, buf, sizeof(buf)), 0);
>> + EXPECT_NE(strstr(buf, file), NULL);
>> +
>> + ASSERT_GE(read_file_at(self->cgv2_fd, pb, buf, sizeof(buf)), 0);
>> + EXPECT_EQ(strstr(buf, file), NULL);
>> +}
>> +
>> +/* A path that is not a registered entry is rejected. */
>> +TEST_F(ve_perms, reject_unknown_path)
>> +{
>> + char path[PATH_MAX];
>> +
>> + ve_file(path, sizeof(path), self->ctid_a, variant->fs);
>> + EXPECT_LT(write_file_at(self->cgv2_fd, path, "no_such_entry_xyzzy
>> r"), 0);
>> +}
>> +
>> +/* Masks round-trip, are emitted in r,w,x order, and bad chars are
>> rejected. */
>> +TEST_F(ve_perms, mask_parsing)
>> +{
>> + const char *file = variant->files[0];
>> + char path[PATH_MAX], line[128], buf[8192];
>> +
>> + ve_file(path, sizeof(path), self->ctid_a, variant->fs);
>> +
>> + /* A single bit. */
>> + snprintf(line, sizeof(line), "%s w", file);
>> + ASSERT_EQ(write_file_at(self->cgv2_fd, path, line), 0);
>> + ASSERT_GT(read_file_at(self->cgv2_fd, path, buf, sizeof(buf)), 0);
>> + snprintf(line, sizeof(line), "%s w\n", file);
>> + EXPECT_NE(strstr(buf, line), NULL);
>> +
>> + /* Bits canonicalise to r, w, x order regardless of input order. */
>> + snprintf(line, sizeof(line), "%s xwr", file);
>> + ASSERT_EQ(write_file_at(self->cgv2_fd, path, line), 0);
>> + ASSERT_GT(read_file_at(self->cgv2_fd, path, buf, sizeof(buf)), 0);
>> + snprintf(line, sizeof(line), "%s rwx\n", file);
>> + EXPECT_NE(strstr(buf, line), NULL);
>> +
>> + /* An unknown mask character is rejected. */
>> + snprintf(line, sizeof(line), "%s z", file);
>> + EXPECT_LT(write_file_at(self->cgv2_fd, path, line), 0);
>> +}
>> +
>> +/*
>> + * The write parser: comment lines are skipped, several entries take
>> effect in
>> + * one write, a later grant replaces an earlier one, two ops on one
>> entry keep
>> + * the last, removing an absent entry is a no-op, and a missing mask
>> is rejected.
>> + */
>> +TEST_F(ve_perms, write_parser)
>> +{
>> + const char *file = variant->files[0];
>> + char path[PATH_MAX], line[256], buf[8192];
>> + size_t off;
>> + int i;
>> +
>> + ve_file(path, sizeof(path), self->ctid_a, variant->fs);
>> +
>> + /* Comment skipped, every file and the dir granted in one write. */
>> + off = snprintf(line, sizeof(line), "# a comment\n");
>> + for (i = 0; i < NR_FILES; i++)
>> + off += snprintf(line + off, sizeof(line) - off, "%s r\n",
>> + variant->files[i]);
>> + snprintf(line + off, sizeof(line) - off, "%s r", variant->dir);
>> + ASSERT_EQ(write_file_at(self->cgv2_fd, path, line), 0);
>> +
>> + /* Each file renders at its true depth, the dir with a trailing
>> slash. */
>> + ASSERT_GT(read_file_at(self->cgv2_fd, path, buf, sizeof(buf)), 0);
>> + for (i = 0; i < NR_FILES; i++) {
>> + snprintf(line, sizeof(line), "%s r\n", variant->files[i]);
>> + EXPECT_NE(strstr(buf, line), NULL);
>> + }
>> + snprintf(line, sizeof(line), "%s/ r\n", variant->dir);
>> + EXPECT_NE(strstr(buf, line), NULL);
>> +
>> + /* A later grant replaces the mask, and a duplicate keeps the
>> last op. */
>> + snprintf(line, sizeof(line), "%s r\n%s w", file, file);
>> + ASSERT_EQ(write_file_at(self->cgv2_fd, path, line), 0);
>> + ASSERT_GT(read_file_at(self->cgv2_fd, path, buf, sizeof(buf)), 0);
>> + snprintf(line, sizeof(line), "%s w\n", file);
>> + EXPECT_NE(strstr(buf, line), NULL);
>> + snprintf(line, sizeof(line), "%s r\n", file);
>> + EXPECT_EQ(strstr(buf, line), NULL);
>> +
>> + /* Remove succeeds, and removing again (now absent) is still a
>> no-op. */
>> + snprintf(line, sizeof(line), "%s -", file);
>> + EXPECT_EQ(write_file_at(self->cgv2_fd, path, line), 0);
>> + EXPECT_EQ(write_file_at(self->cgv2_fd, path, line), 0);
>> +
>> + /* A path with no mask is rejected. */
>> + EXPECT_LT(write_file_at(self->cgv2_fd, path, file), 0);
>> +}
>> +
>> +/* Grant @mask on every file in one write to @path. */
>> +static void grant_files(int cgv2_fd, const char *path,
>> + const char *const files[], const char *mask)
> I would suggest to change the return type to int here and ASSERT_EQ in
> callers
> that files were really granted before the rest of the test proceeds.
Agreed, will be done in v4.
>> +{
>> + char line[256];
>> + size_t off = 0;
>> + int i;
>> +
>> + for (i = 0; i < NR_FILES; i++)
>> + off += snprintf(line + off, sizeof(line) - off, "%s %s\n",
>> + files[i], mask);
>> + write_file_at(cgv2_fd, path, line);
>> +}
>> +
>> +/* Nonzero iff every file and the directory exist under @prefix on
>> this host. */
>> +static int enf_nodes_present(const char *prefix, const char *const
>> files[],
>> + const char *dir)
>> +{
>> + char probe[PATH_MAX];
>> + int i;
>> +
>> + for (i = 0; i < NR_FILES; i++) {
>> + snprintf(probe, sizeof(probe), "%s/%s", prefix, files[i]);
>> + if (access(probe, F_OK) != 0)
>> + return 0;
>> + }
>> + snprintf(probe, sizeof(probe), "%s/%s", prefix, dir);
>> + return access(probe, F_OK) == 0;
>> +}
>> +
>> +/*
>> + * Enforcement, allowed side: from inside a VE, granting r lets each
>> file be
>> + * read, and granting x on the directory lets it be traversed (the
>> lookup then
>> + * fails ENOENT on the missing child, not EACCES).
>> + */
>> +TEST_F(ve_perms, enforce_allows)
>> +{
>> + char path[PATH_MAX], line[128], absent[128];
>> + int i;
>> +
>> + if (!enf_nodes_present(variant->dir_prefix, variant->files,
>> variant->dir))
>> + SKIP(return, "enforcement nodes absent under %s", variant-
>> >dir_prefix);
>> +
>> + ve_file(path, sizeof(path), self->ctid_a, variant->fs);
>> +
>> + grant_files(self->cgv2_fd, path, variant->files, "r");
>> + for (i = 0; i < NR_FILES; i++)
>> + EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
>> + variant->dir_prefix, variant->files[i],
>> + O_RDONLY), 0);
>> +
>> + snprintf(line, sizeof(line), "%s rx", variant->dir);
>> + ASSERT_EQ(write_file_at(self->cgv2_fd, path, line), 0);
>> + snprintf(absent, sizeof(absent), "%s/ve_perms_absent", variant-
>> >dir);
>> + EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a, variant-
>> >dir_prefix,
>> + absent, O_RDONLY), ENOENT);
>> +}
>> +
>> +/*
>> + * Enforcement, denied side: an ungranted file is hidden, a file
>> granted only w
>> + * cannot be read (the r bit is required and distinct), and a
>> directory granted
>> + * only r cannot be traversed (the x bit is required).
>> + */
>> +TEST_F(ve_perms, enforce_denies)
>> +{
>> + char path[PATH_MAX], line[128], absent[128];
>> + int i;
>> +
>> + if (!enf_nodes_present(variant->dir_prefix, variant->files,
>> variant->dir))
>> + SKIP(return, "enforcement nodes absent under %s", variant-
>> >dir_prefix);
>> +
>> + ve_file(path, sizeof(path), self->ctid_a, variant->fs);
>> +
>> + /* Ungranted: every file is hidden (the open fails). */
>> + for (i = 0; i < NR_FILES; i++)
>> + EXPECT_NE(ve_open_rel(self->cgv2_fd, self->ctid_a,
>> + variant->dir_prefix, variant->files[i],
>> + O_RDONLY), 0);
>> +
>> + /* Granted w only: visible but not readable. */
>> + grant_files(self->cgv2_fd, path, variant->files, "w");
>> + for (i = 0; i < NR_FILES; i++)
>> + EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
>> + variant->dir_prefix, variant->files[i],
>> + O_RDONLY), EACCES);
>> +
>> + /* Directory granted r only: cannot be traversed. */
>> + snprintf(line, sizeof(line), "%s r", variant->dir);
>> + ASSERT_EQ(write_file_at(self->cgv2_fd, path, line), 0);
>> + snprintf(absent, sizeof(absent), "%s/ve_perms_absent", variant-
>> >dir);
>> + EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a, variant-
>> >dir_prefix,
>> + absent, O_RDONLY), EACCES);
>> +}
>> +
>> +TEST_HARNESS_MAIN
>> diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/
>> selftests/ve/ve_selftest.h
>> index dddb09e948f9..0c6e54f0241c 100644
>> --- a/tools/testing/selftests/ve/ve_selftest.h
>> +++ b/tools/testing/selftests/ve/ve_selftest.h
>> @@ -45,6 +45,32 @@ static inline int write_file_at(int dirfd, const
>> char *path, const char *val)
>> return (ret == (int)len) ? 0 : -1;
>> }
>> +static inline int read_file_at(int dirfd, const char *path, char *buf,
>> + size_t buflen)
>> +{
>> + size_t off = 0;
>> + int fd, ret;
>> +
>> + fd = openat(dirfd, path, O_RDONLY);
>> + if (fd < 0)
>> + return -1;
>> +
>> + /* A seq file may hand back its contents over several reads. */
>> + while (off < buflen - 1) {
>> + ret = read(fd, buf + off, buflen - 1 - off);
>> + if (ret < 0) {
>> + close(fd);
>> + return -1;
>> + }
>> + if (ret == 0)
>> + break;
>> + off += ret;
>> + }
>> + close(fd);
>> + buf[off] = '\0';
>> + return off;
>> +}
>> +
>> static inline int read_u64_at(int dirfd, const char *path,
>> unsigned long long *out)
>> {
>
--
Best regards, Mirian Shilakadze
Linux C Virtualization Developer, Virtuozzo.
More information about the Devel
mailing list