[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