[Devel] [PATCH VZ10 8/8] selftests/ve: regression test for CLONE_NEWVE owner correctness

Pavel Tikhomirov ptikhomirov at virtuozzo.com
Fri May 15 14:53:00 MSK 2026



On 5/14/26 17:35, Konstantin Khorenko wrote:
>   1. Double-close of file descriptors: In both tests the parent closes two pipe ends manually, then calls sync_pipes_close() which closes all four. This results in a double-close on two fds:
> 
>    tools/testing/selftests/ve/ve_ns_owner_test.c lines 325-326
> 
>       close(ctx.sync.child_to_parent[1]);
>       close(ctx.sync.parent_to_child[0]);
> 
>   And then at the end:
> 
>    tools/testing/selftests/ve/ve_ns_owner_test.c lines 341-341
> 
>       sync_pipes_close(&ctx.sync);
> 
>   Harmless in a single-threaded test, but technically a bug. Either drop sync_pipes_close() at the end and close the remaining two fds explicitly, or set closed fds to -1 and check in sync_pipes_close:
> 
>   static void sync_pipe_close_fd(int *fd)
>   {
>       if (*fd >= 0) {
>           close(*fd);
>           *fd = -1;
>       }
>   }
> 
>   The same issue exists in the unshare test at lines 400-401 / 413.

will do

> 
> ======================
> 2. clone3 child theoretical fallthrough
> 
> Here is the current clone_child_func:
> 
>    tools/testing/selftests/ve/ve_ns_owner_test.c lines 168-182
> 
>   static int clone_child_func(void *arg)
>   {
>       struct clone_args_ctx *ctx = arg;
>       char ack;
>       close(ctx->sync.child_to_parent[0]);
>       close(ctx->sync.parent_to_child[1]);
>       if (write(ctx->sync.child_to_parent[1], "R", 1) != 1)
>           _exit(11);
>       if (read(ctx->sync.parent_to_child[0], &ack, 1) != 1)
>           _exit(12);
>       _exit(0);
>   }
> 
>   The function always terminates via _exit() - three different paths, all three call _exit. Formally it is impossible to fall through. But the call site looks like this:
> 
>    tools/testing/selftests/ve/ve_ns_owner_test.c lines 316-317
> 
>       if (pid == 0)
>           clone_child_func(&ctx);
> 
>   There is no _exit or return after clone_child_func(&ctx). If someone later refactors clone_child_func and replaces _exit(0) with return 0 (by analogy with unshare_child_func, which does exactly that), the child will fall through into the parent's test code - two processes will simultaneously work with the pipes, call waitpid, etc.
> 
>   For comparison, unshare_child_func is written differently:
> 
>    tools/testing/selftests/ve/ve_ns_owner_test.c lines 353-369
> 
>   static int unshare_child_func(struct clone_args_ctx *ctx)
>   {
>       char ack;
>       close(ctx->sync.child_to_parent[0]);
>       close(ctx->sync.parent_to_child[1]);
>       if (unshare(CLONE_NEWVE | CLONE_NEWNET | CLONE_NEWNS) < 0)
>           return 21;
>       if (write(ctx->sync.child_to_parent[1], "R", 1) != 1)
>           return 22;
>       if (read(ctx->sync.parent_to_child[0], &ack, 1) != 1)
>           return 23;
>       return 0;
>   }
> 
>   It uses return, and _exit is at the call site:
> 
>    tools/testing/selftests/ve/ve_ns_owner_test.c lines 395-396
> 
>       if (pid == 0)
>           _exit(unshare_child_func(&ctx));
> 
>   This is safer: _exit is guaranteed at the call site regardless of what the function does internally. If clone_child_func is brought to the same style (replace _exit(N) with return N and call it as
>   _exit(clone_child_func(&ctx))), both paths become uniform and protected against fallthrough.

will do

> 
> ==========================
>   3. CTID collision: The free-CTID scan over CTID_MIN..CTID_MAX works but the range is narrow (92 values). If other tests left stale cgroups behind, the scan can exhaust the range and fail. Not critical for a selftest, but a mkdtemp-like approach (e.g. random or PID-based suffix) would be more robust.
> 
> 

We have same code in other tests (this is basically a copy-paste in this series), hopefully it's not that important now. Will need to fix later if amount of ids would not be enough...

> 
> On 4/29/26 15:41, Pavel Tikhomirov wrote:
>> Add a small kselftest that exercises the case fixed by the preceding
>> patches: combining CLONE_NEWVE with CLONE_NEWNET and CLONE_NEWNS in
>> a single clone3() or unshare(), and verifying that the resulting net
>> and mount namespaces are owned by the *new* ve, not the parent ve.
>>
>> Two test cases share the same shape:
>>
>>    clone_newve_newnet_newns
>>      Do clone3(CLONE_NEWVE | CLONE_NEWNET | CLONE_NEWNS) in a fresh
>>      ve cgroup. Once the child signals readiness via a sync pipe,
>>      the parent inspects the new ve's counters.
>>
>>    unshare_newve_newnet_newns
>>      A fork()ed child does unshare(CLONE_NEWVE | CLONE_NEWNET |
>>      CLONE_NEWNS) in a fresh ve cgroup. Once the child signals readiness
>>      via a sync pipe, the parent inspects the new ve's counters.
>>
>> Both tests call a single check_new_ve_owner() helper that asserts:
>>
>>    ve.netns_avail_nr == VE_NETNS_MAX - 1
>>      FIXTURE_SETUP caps the new ve's ve.netns_max_nr to a small
>>      value (3) so the single netns charged to it is unambiguously
>>      detectable. Pre-fix this counter would have stayed at the cap
>>      because copy_net_ns() charged the parent ve via get_exec_env().
>>
>>    ve.mnt_nr > 0
>>      copy_mnt_ns() / copy_tree() populates the new mntns by cloning
>>      the parent's mounts; each clone is charged to the new ve via
>>      ve_mount_nr_inc(). FIXTURE_SETUP additionally asserts that
>>      ve.mnt_nr starts at 0 on the freshly created ve cgroup, so the
>>      post-clone '> 0' assertion has well-defined meaning. Pre-fix
>>      the counter would have stayed at 0 (mounts charged to parent).
>>
>> Note: The mount-side check relies on the ve.mnt_nr cgroup file added by
>> the preceding patch.
>>
>> Wire the new directory into tools/testing/selftests/Makefile.
>>
>> https://virtuozzo.atlassian.net/browse/VSTOR-129744
>> Signed-off-by: Pavel Tikhomirov <ptikhomirov at virtuozzo.com>
>> Feature: ve: ve generic structures
>> ---
>>   tools/testing/selftests/Makefile              |   1 +
>>   tools/testing/selftests/ve/.gitignore         |   1 +
>>   tools/testing/selftests/ve/Makefile           |   7 +
>>   tools/testing/selftests/ve/ve_ns_owner_test.c | 425 ++++++++++++++++++
>>   4 files changed, 434 insertions(+)
>>   create mode 100644 tools/testing/selftests/ve/.gitignore
>>   create mode 100644 tools/testing/selftests/ve/Makefile
>>   create mode 100644 tools/testing/selftests/ve/ve_ns_owner_test.c
>>
>> diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
>> index b30e45957202..700ee6bd916f 100644
>> --- a/tools/testing/selftests/Makefile
>> +++ b/tools/testing/selftests/Makefile
>> @@ -113,6 +113,7 @@ TARGETS += tty
>>   TARGETS += uevent
>>   TARGETS += user_events
>>   TARGETS += vDSO
>> +TARGETS += ve
>>   TARGETS += ve_printk
>>   TARGETS += mm
>>   TARGETS += x86
>> diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
>> new file mode 100644
>> index 000000000000..7bfff054f9e8
>> --- /dev/null
>> +++ b/tools/testing/selftests/ve/.gitignore
>> @@ -0,0 +1 @@
>> +ve_ns_owner_test
>> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
>> new file mode 100644
>> index 000000000000..aa03ab02dda9
>> --- /dev/null
>> +++ b/tools/testing/selftests/ve/Makefile
>> @@ -0,0 +1,7 @@
>> +# SPDX-License-Identifier: GPL-2.0
>> +# Makefile for ve selftests.
>> +CFLAGS += -g -Wall -O2
>> +
>> +TEST_GEN_PROGS += ve_ns_owner_test
>> +
>> +include ../lib.mk
>> diff --git a/tools/testing/selftests/ve/ve_ns_owner_test.c b/tools/testing/selftests/ve/ve_ns_owner_test.c
>> new file mode 100644
>> index 000000000000..1f82955eb4c3
>> --- /dev/null
>> +++ b/tools/testing/selftests/ve/ve_ns_owner_test.c
>> @@ -0,0 +1,425 @@
>> +// SPDX-License-Identifier: GPL-2.0
>> +/*
>> + * ve_ns_owner selftests
>> + *
>> + * Regression tests for the case where CLONE_NEWVE is combined with
>> + * CLONE_NEWNET and CLONE_NEWNS in a single clone3() or unshare()
>> + * syscall.
>> + *
>> + * Historically copy_net_ns() and copy_mnt_ns() resolved the owning ve
>> + * via get_exec_env() at the time of the call, which since we've
>> + * switched from cgroup based to namespace based get_exec_env() pointed
>> + * at the parent's ve when copy_ve_ns() had just installed a new ve on
>> + * the child (clone path) or before unshare_ve_namespace()'s result was
>> + * committed (unshare path). That left the freshly created network and
>> + * mount namespaces wired to the wrong ve.
>> + *
>> + * Both tests follow the same shape: a child blocks inside a fresh ve
>> + * with a new netns and mntns, the parent reads the new ve's counters
>> + * via cgroupfs and asserts they reflect the just-created namespaces:
>> + *   - ve.netns_avail_nr drops by exactly one (the new netns);
>> + *   - ve.mnt_nr is strictly greater than zero (mounts copied into the
>> + *     new mntns are accounted to the new ve).
>> + *
>> + * We never assert against the parent ve's counters: those are shared
>> + * with everything else on the host (systemd, container managers, ...)
>> + * and observing them is racy.
>> + */
>> +#define _GNU_SOURCE
>> +#include <linux/sched.h>
>> +#include <linux/mount.h>
>> +#include <sched.h>
>> +#include <sys/wait.h>
>> +#include <sys/syscall.h>
>> +#include <unistd.h>
>> +#include <asm/unistd.h>
>> +#include <stdio.h>
>> +#include <stdlib.h>
>> +#include <fcntl.h>
>> +#include <string.h>
>> +#include <sys/stat.h>
>> +#include <sys/mount.h>
>> +#include <linux/limits.h>
>> +#include <errno.h>
>> +
>> +#include "../kselftest_harness.h"
>> +
>> +#define CTID_MIN        108
>> +#define CTID_MAX        200
>> +
>> +#ifndef CLONE_NEWVE
>> +#define CLONE_NEWVE        0x00000040
>> +#endif
>> +
>> +/*
>> + * Make ve.netns_avail_nr movements easy to detect: a small cap means
>> + * any spurious accounting against the parent ve would overflow it.
>> + */
>> +#define VE_NETNS_MAX        3
>> +
>> +static int write_file_at(int dirfd, const char *path, const char *val)
>> +{
>> +    int fd, ret;
>> +    size_t len = strlen(val);
>> +
>> +    fd = openat(dirfd, path, O_WRONLY);
>> +    if (fd < 0)
>> +        return -1;
>> +
>> +    ret = write(fd, val, len);
>> +    close(fd);
>> +    return (ret == (int)len) ? 0 : -1;
>> +}
>> +
>> +static int read_u64_at(int dirfd, const char *path, unsigned long long *out)
>> +{
>> +    char buf[32] = {0};
>> +    int fd, ret;
>> +
>> +    fd = openat(dirfd, path, O_RDONLY);
>> +    if (fd < 0)
>> +        return -1;
>> +
>> +    ret = read(fd, buf, sizeof(buf) - 1);
>> +    close(fd);
>> +    if (ret <= 0)
>> +        return -1;
>> +
>> +    *out = strtoull(buf, NULL, 10);
>> +    return 0;
>> +}
>> +
>> +static int mount_cg2_fd(void)
>> +{
>> +    int fs_fd, mnt_fd;
>> +
>> +    fs_fd = syscall(__NR_fsopen, "cgroup2", 0);
>> +    if (fs_fd < 0)
>> +        return -1;
>> +
>> +    if (syscall(__NR_fsconfig, fs_fd, FSCONFIG_CMD_CREATE,
>> +            NULL, NULL, 0) < 0) {
>> +        close(fs_fd);
>> +        return -1;
>> +    }
>> +
>> +    mnt_fd = syscall(__NR_fsmount, fs_fd, 0, 0);
>> +    close(fs_fd);
>> +    return mnt_fd;
>> +}
>> +
>> +static int enter_cgroup(int cgv2_fd, int ctid)
>> +{
>> +    char cg_path[64];
>> +    char pid_str[64];
>> +    int fd;
>> +    int ret;
>> +
>> +    if (ctid)
>> +        snprintf(cg_path, sizeof(cg_path), "%d/cgroup.procs", ctid);
>> +    else
>> +        snprintf(cg_path, sizeof(cg_path), "cgroup.procs");
>> +    fd = openat(cgv2_fd, cg_path, O_WRONLY);
>> +    if (fd < 0)
>> +        return -1;
>> +
>> +    snprintf(pid_str, sizeof(pid_str), "%d", getpid());
>> +    ret = write(fd, pid_str, strlen(pid_str));
>> +    if (ret < 0 || ret != (int)strlen(pid_str))
>> +        ret = -1;
>> +
>> +    close(fd);
>> +    return ret;
>> +}
>> +
>> +/*
>> + * Synchronisation across the clone() boundary: child does its setup,
>> + * tells parent it is ready, then blocks until parent acknowledges.
>> + */
>> +struct sync_pipes {
>> +    int child_to_parent[2];
>> +    int parent_to_child[2];
>> +};
>> +
>> +static int sync_pipes_init(struct sync_pipes *s)
>> +{
>> +    if (pipe(s->child_to_parent) < 0)
>> +        return -1;
>> +    if (pipe(s->parent_to_child) < 0) {
>> +        close(s->child_to_parent[0]);
>> +        close(s->child_to_parent[1]);
>> +        return -1;
>> +    }
>> +    return 0;
>> +}
>> +
>> +static void sync_pipes_close(struct sync_pipes *s)
>> +{
>> +    close(s->child_to_parent[0]);
>> +    close(s->child_to_parent[1]);
>> +    close(s->parent_to_child[0]);
>> +    close(s->parent_to_child[1]);
>> +}
>> +
>> +/*
>> + * Per-test context shared between parent and child.
>> + */
>> +struct clone_args_ctx {
>> +    int cgv2_fd;
>> +    int ctid;
>> +    struct sync_pipes sync;
>> +};
>> +
>> +/*
>> + * Child of the clone3 test. The interesting work (creating the new
>> + * ve / netns / mntns and accounting them to the new ve) was done by
>> + * the kernel during clone3 itself, so the child only needs to keep
>> + * those namespaces alive while the parent inspects ve.* counters.
>> + */
>> +static int clone_child_func(void *arg)
>> +{
>> +    struct clone_args_ctx *ctx = arg;
>> +    char ack;
>> +
>> +    close(ctx->sync.child_to_parent[0]);
>> +    close(ctx->sync.parent_to_child[1]);
>> +
>> +    if (write(ctx->sync.child_to_parent[1], "R", 1) != 1)
>> +        _exit(11);
>> +    if (read(ctx->sync.parent_to_child[0], &ack, 1) != 1)
>> +        _exit(12);
>> +
>> +    _exit(0);
>> +}
>> +
>> +/*
>> + * Before fix:
>> + *   - clone path: ve.netns_avail_nr stays at VE_NETNS_MAX and
>> + *     ve.mnt_nr stays at 0 because copy_net_ns()/copy_mnt_ns()
>> + *     charged the parent ve via get_exec_env().
>> + *   - unshare path: the syscall itself returned -EINVAL, so this
>> + *     check was unreachable.
>> + *
>> + * After fix: the new ve is charged for the netns and for every mount
>> + * copy_tree() puts into the new mntns.
>> + */
>> +static void check_new_ve_owner(struct __test_metadata *_metadata,
>> +                   int cgv2_fd, int ctid)
>> +{
>> +    unsigned long long avail, mnt;
>> +    char path[64];
>> +
>> +    snprintf(path, sizeof(path), "%d/ve.netns_avail_nr", ctid);
>> +    ASSERT_EQ(read_u64_at(cgv2_fd, path, &avail), 0);
>> +    EXPECT_EQ(avail, VE_NETNS_MAX - 1);
>> +
>> +    snprintf(path, sizeof(path), "%d/ve.mnt_nr", ctid);
>> +    ASSERT_EQ(read_u64_at(cgv2_fd, path, &mnt), 0);
>> +    EXPECT_GT(mnt, 0);
>> +}
>> +
>> +FIXTURE(ve_ns_owner)
>> +{
>> +    int cgv2_fd;
>> +    int ctid;
>> +};
>> +
>> +FIXTURE_SETUP(ve_ns_owner)
>> +{
>> +    unsigned long long initial_mnt_nr;
>> +    char ctid_str[16];
>> +    char val[16];
>> +    char path[64];
>> +
>> +    self->cgv2_fd = mount_cg2_fd();
>> +    ASSERT_GE(self->cgv2_fd, 0);
>> +
>> +    ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
>> +          "+cpuset +cpu +cpuacct +io +memory +hugetlb +pids +rdma +misc +ve"), 0);
>> +
>> +    ASSERT_EQ(write_file_at(self->cgv2_fd,
>> +          "ve.default_sysfs_permissions", "/ rx"), 0);
>> +    ASSERT_EQ(write_file_at(self->cgv2_fd,
>> +          "ve.default_sysfs_permissions", "fs rx"), 0);
>> +    ASSERT_EQ(write_file_at(self->cgv2_fd,
>> +          "ve.default_sysfs_permissions", "fs/cgroup rw"), 0);
>> +
>> +    self->ctid = CTID_MIN;
>> +    while (self->ctid < CTID_MAX) {
>> +        snprintf(ctid_str, sizeof(ctid_str), "%d", self->ctid);
>> +        if (faccessat(self->cgv2_fd, ctid_str, F_OK, 0) != 0 &&
>> +            errno == ENOENT)
>> +            break;
>> +        self->ctid++;
>> +    }
>> +    ASSERT_LT(self->ctid, CTID_MAX);
>> +
>> +    ASSERT_EQ(mkdirat(self->cgv2_fd, ctid_str, 0755), 0);
>> +
>> +    snprintf(path, sizeof(path), "%d/cgroup.controllers_hidden", self->ctid);
>> +    ASSERT_EQ(write_file_at(self->cgv2_fd, path, "-ve"), 0);
>> +
>> +    /*
>> +     * ve.veid and ve.features are deliberately not configured: the
>> +     * tests do not call ve.state=START, so a real veid identity and
>> +     * feature mask are not needed. The owner_ve accounting we are
>> +     * checking happens during clone3()/unshare() regardless.
>> +     *
>> +     * Cap the new ve's netns count so we can detect a single new
>> +     * netns being accounted to it. We only assert against the new
>> +     * ve's counter; the parent ve's counter is shared with the rest
>> +     * of the host (systemd, container managers, ...) and is racy.
>> +     */
>> +    snprintf(path, sizeof(path), "%d/ve.netns_max_nr", self->ctid);
>> +    snprintf(val, sizeof(val), "%d", VE_NETNS_MAX);
>> +    ASSERT_EQ(write_file_at(self->cgv2_fd, path, val), 0);
>> +
>> +    /*
>> +     * The new ve cgroup has not been entered by anything yet, so its
>> +     * mnt_nr counter must start at 0. Each test below verifies that
>> +     * the clone/unshare populates the new mntns under this ve, i.e.
>> +     * mnt_nr rises strictly above zero.
>> +     */
>> +    snprintf(path, sizeof(path), "%d/ve.mnt_nr", self->ctid);
>> +    ASSERT_EQ(read_u64_at(self->cgv2_fd, path, &initial_mnt_nr), 0);
>> +    ASSERT_EQ(initial_mnt_nr, 0);
>> +};
>> +
>> +FIXTURE_TEARDOWN(ve_ns_owner)
>> +{
>> +    char path[64];
>> +
>> +    enter_cgroup(self->cgv2_fd, 0);
>> +    snprintf(path, sizeof(path), "%d", self->ctid);
>> +    unlinkat(self->cgv2_fd, path, AT_REMOVEDIR);
>> +    close(self->cgv2_fd);
>> +}
>> +
>> +/*
>> + * clone3(CLONE_NEWVE | CLONE_NEWNET | CLONE_NEWNS): the new netns and
>> + * mntns must be accounted to the *new* ve, not to the parent ve.
>> + *
>> + * Before the fix, copy_net_ns()/copy_mnt_ns() looked up the owning ve
>> + * via get_exec_env(), which inside copy_namespaces() still resolved to
>> + * the parent ve - copy_ve_ns() had stored the new ve on the child but
>> + * the running task was still the parent. After the fix copy_namespaces()
>> + * forwards tsk->task_ve to both helpers and the right ve is charged.
>> + */
>> +TEST_F(ve_ns_owner, clone_newve_newnet_newns)
>> +{
>> +    struct clone_args_ctx ctx = {
>> +        .cgv2_fd = self->cgv2_fd,
>> +        .ctid = self->ctid,
>> +    };
>> +    struct clone_args cargs = {
>> +        .flags = CLONE_NEWVE | CLONE_NEWNET | CLONE_NEWNS,
>> +        .exit_signal = SIGCHLD,
>> +    };
>> +    char ready;
>> +    int status;
>> +    pid_t pid;
>> +
>> +    ASSERT_EQ(sync_pipes_init(&ctx.sync), 0);
>> +    ASSERT_GE(enter_cgroup(self->cgv2_fd, self->ctid), 0);
>> +
>> +    pid = syscall(__NR_clone3, &cargs, sizeof(cargs));
>> +    ASSERT_GE(pid, 0);
>> +    if (pid == 0)
>> +        clone_child_func(&ctx);
>> +
>> +    close(ctx.sync.child_to_parent[1]);
>> +    close(ctx.sync.parent_to_child[0]);
>> +
>> +    ASSERT_GE(enter_cgroup(self->cgv2_fd, 0), 0);
>> +
>> +    /* Wait for the child to be settled before measuring. */
>> +    ASSERT_EQ(read(ctx.sync.child_to_parent[0], &ready, 1), 1);
>> +    ASSERT_EQ(ready, 'R');
>> +
>> +    check_new_ve_owner(_metadata, self->cgv2_fd, self->ctid);
>> +
>> +    /* Release the child. */
>> +    ASSERT_EQ(write(ctx.sync.parent_to_child[1], "G", 1), 1);
>> +    ASSERT_GE(waitpid(pid, &status, 0), 0);
>> +    ASSERT_TRUE(WIFEXITED(status));
>> +    ASSERT_EQ(WEXITSTATUS(status), 0);
>> +
>> +    sync_pipes_close(&ctx.sync);
>> +}
>> +
>> +/*
>> + * unshare(CLONE_NEWVE | CLONE_NEWNET | CLONE_NEWNS) used to be
>> + * rejected with -EINVAL by check_unshare_flags() because of the same
>> + * "wrong owner_ve" problem. After the fix the syscall succeeds and
>> + * the resulting net/mnt namespaces are owned by the new ve.
>> + *
>> + * The accounting check is identical to the clone3 case and uses the
>> + * same check_new_ve_owner() helper.
>> + */
>> +static int unshare_child_func(struct clone_args_ctx *ctx)
>> +{
>> +    char ack;
>> +
>> +    close(ctx->sync.child_to_parent[0]);
>> +    close(ctx->sync.parent_to_child[1]);
>> +
>> +    if (unshare(CLONE_NEWVE | CLONE_NEWNET | CLONE_NEWNS) < 0)
>> +        return 21;
>> +
>> +    if (write(ctx->sync.child_to_parent[1], "R", 1) != 1)
>> +        return 22;
>> +    if (read(ctx->sync.parent_to_child[0], &ack, 1) != 1)
>> +        return 23;
>> +
>> +    return 0;
>> +}
>> +
>> +TEST_F(ve_ns_owner, unshare_newve_newnet_newns)
>> +{
>> +    struct clone_args_ctx ctx = {
>> +        .cgv2_fd = self->cgv2_fd,
>> +        .ctid = self->ctid,
>> +    };
>> +    char ready;
>> +    int status;
>> +    pid_t pid;
>> +
>> +    ASSERT_EQ(sync_pipes_init(&ctx.sync), 0);
>> +    ASSERT_GE(enter_cgroup(self->cgv2_fd, self->ctid), 0);
>> +
>> +    /*
>> +     * Use a plain fork: the unshare(CLONE_NEWVE) is performed by
>> +     * the child itself, since unshare requires single-threaded
>> +     * context and we don't want to permanently move the test
>> +     * process into a new ve.
>> +     */
>> +    pid = fork();
>> +    ASSERT_GE(pid, 0);
>> +    if (pid == 0)
>> +        _exit(unshare_child_func(&ctx));
>> +
>> +    close(ctx.sync.child_to_parent[1]);
>> +    close(ctx.sync.parent_to_child[0]);
>> +
>> +    ASSERT_GE(enter_cgroup(self->cgv2_fd, 0), 0);
>> +
>> +    /*
>> +     * If the child failed before writing 'R' the pipe read returns
>> +     * 0 (EOF). Reap the child first so the assertion message
>> +     * carries the meaningful exit code rather than a generic
>> +     * "expected 1, got 0".
>> +     */
>> +    ASSERT_EQ(read(ctx.sync.child_to_parent[0], &ready, 1), 1);
>> +    ASSERT_EQ(ready, 'R');
>> +
>> +    check_new_ve_owner(_metadata, self->cgv2_fd, self->ctid);
>> +
>> +    ASSERT_EQ(write(ctx.sync.parent_to_child[1], "G", 1), 1);
>> +    ASSERT_GE(waitpid(pid, &status, 0), 0);
>> +    ASSERT_TRUE(WIFEXITED(status));
>> +    ASSERT_EQ(WEXITSTATUS(status), 0);
>> +
>> +    sync_pipes_close(&ctx.sync);
>> +}
>> +
>> +TEST_HARNESS_MAIN
> 

-- 
Best regards, Pavel Tikhomirov
Senior Software Developer, Virtuozzo.



More information about the Devel mailing list