]> git.proxmox.com Git - mirror_lxcfs.git/commitdiff
Fix checking of parent directories
authorSerge Hallyn <serge.hallyn@ubuntu.com>
Fri, 13 Nov 2015 23:07:36 +0000 (17:07 -0600)
committerSerge Hallyn <serge.hallyn@ubuntu.com>
Tue, 17 Nov 2015 18:33:13 +0000 (12:33 -0600)
Taken from the justification in the launchpad bug:

To a task in freezer cgroup /a/b/c/d, it should appear that there are no
cgroups other than its descendents. Since this is a filesystem, we must have
the parent directories, but each parent cgroup should only contain the child
which the task can see.

So, when this task looks at /a/b, it should see only directory 'c' and no
files. Attempt to create /a/b/x should result in -EPERM, whether /a/b/x already
exists or not. Attempts to query /a/b/x should result in -ENOENT whether /a/b/x
exists or not. Opening /a/b/tasks should result in -ENOENT.

The caller_may_see_dir checks specifically whether a task may see a cgroup
directory - i.e. /a/b/x if opening /a/b/x/tasks, and /a/b/c/d if doing
opendir('/a/b/c/d').

caller_is_in_ancestor() will return true if the caller in /a/b/c/d looks at
/a/b/c/d/e. If the caller is in a child cgroup of the queried one - i.e. if the
task in /a/b/c/d queries /a/b, then *nextcg will container the next (the only)
directory which he can see in the path - 'c'.

Beyond this, regular DAC permissions should apply, with the
root-in-user-namespace privilege over its mapped uids being respected. The
fc_may_access check does this check for both directories and files.

This is CVE-2015-1342 (LP: #1508481)

Signed-off-by: Serge Hallyn <serge.hallyn@ubuntu.com>
Makefile.am
lxcfs.c
tests/test_confinement.sh [new file with mode: 0644]
tests/test_syscalls.c [new file with mode: 0644]

index 7dda776a2cf72282d1ef721cd4a5d6953297c5f1..cc19ce7c9826f8a56c31b28b3d25ff7abecf60b7 100644 (file)
@@ -29,11 +29,12 @@ endif
 
 TEST_READ: tests/test-read.c
        $(CC) -o tests/test-read tests/test-read.c
-
 TEST_CPUSET: tests/cpusetrange.c cpuset.c
        $(CC) -o tests/cpusetrange tests/cpusetrange.c cpuset.c
+TEST_SYSCALLS: tests/test_syscalls.c
+       $(CC) -o tests/test_syscalls tests/test_syscalls.c
 
-tests: TEST_READ TEST_CPUSET
+tests: TEST_READ TEST_CPUSET TEST_SYSCALLS
 
 distclean:
        rm -rf .deps/ \
@@ -60,4 +61,5 @@ distclean:
                lxcfs.o \
                m4/ \
                missing \
-               stamp-h1
+               stamp-h1 \
+               tests/test_syscalls
diff --git a/lxcfs.c b/lxcfs.c
index e2ccc5e855d5f72c2a38b81ac2934cf76cd87698..2c5aca4ece6482b6e20e70297cb7c654b1da059b 100644 (file)
--- a/lxcfs.c
+++ b/lxcfs.c
@@ -235,6 +235,12 @@ static bool perms_include(int fmode, mode_t req_mode)
        return ((fmode & r) == r);
 }
 
+
+/*
+ * taskcg is  a/b/c
+ * querycg is /a/b/c/d/e
+ * we return 'd'
+ */
 static char *get_next_cgroup_dir(const char *taskcg, const char *querycg)
 {
        char *start, *end;
@@ -378,53 +384,71 @@ static void prune_init_slice(char *cg)
  */
 static bool caller_is_in_ancestor(pid_t pid, const char *contrl, const char *cg, char **nextcg)
 {
-       char fnam[PROCLEN];
-       FILE *f;
        bool answer = false;
-       char *line = NULL;
-       size_t len = 0;
-       int ret;
+       char *c2 = get_pid_cgroup(pid, contrl);
+       char *linecmp;
 
-       ret = snprintf(fnam, PROCLEN, "/proc/%d/cgroup", pid);
-       if (ret < 0 || ret >= PROCLEN)
-               return false;
-       if (!(f = fopen(fnam, "r")))
+       if (!c2)
                return false;
+       prune_init_slice(c2);
 
-       while (getline(&line, &len, f) != -1) {
-               char *c1, *c2, *linecmp;
-               if (!line[0])
-                       continue;
-               c1 = strchr(line, ':');
-               if (!c1)
-                       goto out;
-               c1++;
-               c2 = strchr(c1, ':');
-               if (!c2)
-                       goto out;
-               *c2 = '\0';
-               if (strcmp(c1, contrl) != 0)
-                       continue;
-               c2++;
-               stripnewline(c2);
-               prune_init_slice(c2);
-               /*
-                * callers pass in '/' for root cgroup, otherwise they pass
-                * in a cgroup without leading '/'
-                */
-               linecmp = *cg == '/' ? c2 : c2+1;
-               if (strncmp(linecmp, cg, strlen(linecmp)) != 0) {
-                       if (nextcg)
-                               *nextcg = get_next_cgroup_dir(linecmp, cg);
-                       goto out;
+       /*
+        * callers pass in '/' for root cgroup, otherwise they pass
+        * in a cgroup without leading '/'
+        */
+       linecmp = *cg == '/' ? c2 : c2+1;
+       if (strncmp(linecmp, cg, strlen(linecmp)) != 0) {
+               if (nextcg) {
+                       *nextcg = get_next_cgroup_dir(linecmp, cg);
                }
+               goto out;
+       }
+       answer = true;
+
+out:
+       free(c2);
+       return answer;
+}
+
+/*
+ * If caller is in /a/b/c, he may see that /a exists, but not /b or /a/c.
+ */
+static bool caller_may_see_dir(pid_t pid, const char *contrl, const char *cg)
+{
+       bool answer = false;
+       char *c2, *task_cg;
+       size_t target_len, task_len;
+
+       if (strcmp(cg, "/") == 0)
+               return true;
+
+       c2 = get_pid_cgroup(pid, contrl);
+
+       if (!c2)
+               return false;
+
+       task_cg = c2 + 1;
+       target_len = strlen(cg);
+       task_len = strlen(task_cg);
+       if (strcmp(cg, task_cg) == 0) {
                answer = true;
                goto out;
        }
+       if (target_len < task_len) {
+               /* looking up a parent dir */
+               if (strncmp(task_cg, cg, target_len) == 0 && task_cg[target_len] == '/')
+                       answer = true;
+               goto out;
+       }
+       if (target_len > task_len) {
+               /* looking up a child dir */
+               if (strncmp(task_cg, cg, task_len) == 0 && cg[task_len] == '/')
+                       answer = true;
+               goto out;
+       }
 
 out:
-       fclose(f);
-       free(line);
+       free(c2);
        return answer;
 }
 
@@ -552,6 +576,10 @@ static int cg_getattr(const char *path, struct stat *sb)
         * cgroup, or cgdir if fpath is a file */
 
        if (is_child_cgroup(controller, path1, path2)) {
+               if (!caller_may_see_dir(fc->pid, controller, cgroup)) {
+                       ret = -ENOENT;
+                       goto out;
+               }
                if (!caller_is_in_ancestor(fc->pid, controller, cgroup, NULL)) {
                        /* this is just /cgroup/controller, return it as a dir */
                        sb->st_mode = S_IFDIR | 00555;
@@ -630,8 +658,11 @@ static int cg_opendir(const char *path, struct fuse_file_info *fi)
                }
        }
 
-       if (cgroup && !fc_may_access(fc, controller, cgroup, NULL, O_RDONLY)) {
-               return -EACCES;
+       if (cgroup) {
+               if (!caller_may_see_dir(fc->pid, controller, cgroup))
+                       return -ENOENT;
+               if (!fc_may_access(fc, controller, cgroup, NULL, O_RDONLY))
+                       return -EACCES;
        }
 
        /* we'll free this at cg_releasedir */
@@ -780,6 +811,10 @@ static int cg_open(const char *path, struct fuse_file_info *fi)
        }
        free_key(k);
 
+       if (!caller_may_see_dir(fc->pid, controller, path1)) {
+               ret = -ENOENT;
+               goto out;
+       }
        if (!fc_may_access(fc, controller, path1, path2, fi->flags)) {
                // should never get here
                ret = -EACCES;
@@ -1563,7 +1598,7 @@ out:
 int cg_mkdir(const char *path, mode_t mode)
 {
        struct fuse_context *fc = fuse_get_context();
-       char *fpath = NULL, *path1, *cgdir = NULL, *controller;
+       char *fpath = NULL, *path1, *cgdir = NULL, *controller, *next = NULL;
        const char *cgroup;
        int ret;
 
@@ -1585,6 +1620,14 @@ int cg_mkdir(const char *path, mode_t mode)
        else
                path1 = cgdir;
 
+       if (!caller_is_in_ancestor(fc->pid, controller, path1, &next)) {
+               if (fpath && strcmp(next, fpath) == 0)
+                       ret = -EEXIST;
+               else
+                       ret = -ENOENT;
+               goto out;
+       }
+
        if (!fc_may_access(fc, controller, path1, NULL, O_RDWR)) {
                ret = -EACCES;
                goto out;
@@ -1599,13 +1642,14 @@ int cg_mkdir(const char *path, mode_t mode)
 
 out:
        free(cgdir);
+       free(next);
        return ret;
 }
 
 static int cg_rmdir(const char *path)
 {
        struct fuse_context *fc = fuse_get_context();
-       char *fpath = NULL, *cgdir = NULL, *controller;
+       char *fpath = NULL, *cgdir = NULL, *controller, *next = NULL;
        const char *cgroup;
        int ret;
 
@@ -1626,8 +1670,14 @@ static int cg_rmdir(const char *path)
                goto out;
        }
 
-       fprintf(stderr, "rmdir: verifying access to %s:%s (req path %s)\n",
-                       controller, cgdir, path);
+       if (!caller_is_in_ancestor(fc->pid, controller, cgroup, &next)) {
+               if (!fpath || strcmp(next, fpath) == 0)
+                       ret = -EBUSY;
+               else
+                       ret = -ENOENT;
+               goto out;
+       }
+
        if (!fc_may_access(fc, controller, cgdir, NULL, O_WRONLY)) {
                ret = -EACCES;
                goto out;
@@ -1646,6 +1696,7 @@ static int cg_rmdir(const char *path)
 
 out:
        free(cgdir);
+       free(next);
        return ret;
 }
 
diff --git a/tests/test_confinement.sh b/tests/test_confinement.sh
new file mode 100644 (file)
index 0000000..8f5f484
--- /dev/null
@@ -0,0 +1,96 @@
+#!/bin/bash
+
+set -ex
+
+[ $(id -u) -eq 0 ]
+
+d=$(mktemp -t -d tmp.XXX)
+d2=$(mktemp -t -d tmp.XXX)
+
+pid=-1
+cleanup() {
+       [ $pid -ne -1 ] && kill -9 $pid
+       umount -l $d || true
+       umount -l $d2 || true
+       rm -rf $d $d2
+}
+
+cmdline=$(realpath $0)
+dirname=$(dirname ${cmdline})
+topdir=$(dirname ${dirname})
+
+trap cleanup EXIT HUP INT TERM
+
+${topdir}/lxcfs $d &
+pid=$!
+
+# put ourselves into x1
+cgm movepidabs freezer / $$
+cgm create freezer x1
+cgm movepid freezer x1 $$
+
+mount -t cgroup -o freezer freezer $d2
+sudo rmdir $d2/lxcfs_test_a1/lxcfs_test_a2 || true
+sudo rmdir $d2/lxcfs_test_a1 || true
+
+echo "Making sure root cannot mkdir"
+bad=0
+mkdir $d/cgroup/freezer/lxcfs_test_a1 && bad=1
+if [ "${bad}" -eq 1 ]; then
+       false
+fi
+
+echo "Making sure root cannot rmdir"
+mkdir $d2/lxcfs_test_a1
+mkdir $d2/lxcfs_test_a1/lxcfs_test_a2
+rmdir $d/cgroup/freezer/lxcfs_test_a1 && bad=1
+if [ "${bad}" -eq 1 ]; then
+       false
+fi
+[ -d $d2/lxcfs_test_a1 ]
+rmdir $d/cgroup/freezer/lxcfs_test_a1/lxcfs_test_a2 && bad=1
+if [ "${bad}" -eq 1 ]; then
+       false
+fi
+[ -d $d2/lxcfs_test_a1/lxcfs_test_a2 ]
+
+echo "Making sure root cannot read/write"
+sleep 200 &
+p=$!
+echo $p > $d/cgroup/freezer/lxcfs_test_a1/tasks && bad=1
+if [ "${bad}" -eq 1 ]; then
+       false
+fi
+cat $d/cgroup/freezer/lxcfs_test_a1/tasks && bad=1
+if [ "${bad}" -eq 1 ]; then
+       false
+fi
+echo $p > $d/cgroup/freezer/lxcfs_test_a1/lxcfs_test_a2/tasks && bad=1
+if [ "${bad}" -eq 1 ]; then
+       false
+fi
+cat $d/cgroup/freezer/lxcfs_test_a1/lxcfs_test_a2/tasks && bad=1
+if [ "${bad}" -eq 1 ]; then
+       false
+fi
+
+# make sure things like truncate and access don't leak info about
+# the /lxcfs_test_a1 cgroup which we shouldn't be able to reach
+echo "Testing other system calls"
+${dirname}/test_syscalls $d/cgroup/freezer/lxcfs_test_a1
+${dirname}/test_syscalls $d/cgroup/freezer/lxcfs_test_a1/lxcfs_test_a2
+
+echo "Making sure root can act on descendents"
+mycg=$(cgm getpidcgroupabs freezer $$)
+newcg=${mycg}/lxcfs_test_a1
+rmdir $d2/$newcg || true  # cleanup previosu run
+mkdir $d/cgroup/freezer/$newcg
+echo $p > $d/cgroup/freezer/$newcg/tasks
+cat $d/cgroup/freezer/$newcg/tasks
+kill -9 $p
+while [ `wc -l $d/cgroup/freezer/$newcg/tasks | awk '{ print $1 }'` -ne 0 ]; do
+       sleep 1
+done
+rmdir $d/cgroup/freezer/$newcg
+
+echo "All tests passed!"
diff --git a/tests/test_syscalls.c b/tests/test_syscalls.c
new file mode 100644 (file)
index 0000000..8561cfc
--- /dev/null
@@ -0,0 +1,451 @@
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <linux/un.h>
+#include <attr/xattr.h>
+#include <utime.h>
+#include <sys/stat.h>
+#include <sys/mount.h>
+#include <libgen.h>
+#include <fcntl.h>
+#include <errno.h>
+
+
+void test_open(const char *path)
+{
+       int fd = open(path, O_RDONLY);
+       if (fd >= 0) {
+               fprintf(stderr, "leak at open of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT) {
+               fprintf(stderr, "leak at open of %s: errno was %d\n", path, errno);
+               exit(1);
+       }
+}
+
+void test_stat(const char *path)
+{
+       struct stat sb;
+       if (stat(path, &sb) >= 0) {
+               fprintf(stderr, "leak at stat of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT) {
+               fprintf(stderr, "leak at stat of %s: errno was %d\n", path, errno);
+               exit(1);
+       }
+}
+
+void test_access(const char *path)
+{
+       if (access(path, O_RDONLY) >= 0) {
+               fprintf(stderr, "leak at access of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT) {
+               fprintf(stderr, "leak at access of %s: errno was %d\n", path, errno);
+               exit(1);
+       }
+}
+
+void test_bind(const char *path)
+{
+       int sfd;
+       struct sockaddr_un my_addr, peer_addr;
+
+       sfd = socket(AF_UNIX, SOCK_STREAM, 0);
+
+       if (sfd < 0) {
+               fprintf(stderr, "Failed to open a socket for bind test\n");
+               exit(1);
+       }
+       memset(&my_addr, 0, sizeof(struct sockaddr_un));
+       my_addr.sun_family = AF_UNIX;
+       strncpy(my_addr.sun_path, path,
+                       sizeof(my_addr.sun_path) - 1);
+       if (bind(sfd, (struct sockaddr *) &my_addr,
+                               sizeof(struct sockaddr_un)) != -1) {
+               fprintf(stderr, "leak at bind of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at bind of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+       close(sfd);
+}
+
+void test_bindmount(const char *path)
+{
+       if (mount(path, path, "none", MS_BIND, NULL) == 0) {
+               fprintf(stderr, "leak at bind mount of %s\n", path);
+               exit(1);
+       }
+}
+
+void test_truncate(const char *path)
+{
+       if (truncate(path, 0) == 0) {
+               fprintf(stderr, "leak at truncate of %s\n", path);
+               exit(1);
+       }
+}
+
+void test_chdir(const char *path)
+{
+       if (chdir(path) == 0) {
+               fprintf(stderr, "leak at chdir to %s\n", path);
+               exit(1);
+       }
+}
+
+void test_rename(const char *path)
+{
+       char *d = strdupa(path), *tmpname;
+       d = dirname(d);
+       size_t len = strlen(path) + 30;
+
+       tmpname = alloca(len);
+       snprintf(tmpname, len, "%s/%d", d, (int)getpid());
+       if (rename(path, tmpname) == 0 || errno != ENOENT) {
+               fprintf(stderr, "leak at rename of %s\n", path);
+               exit(1);
+       }
+}
+
+void test_mkdir(const char *path)
+{
+       size_t len = strlen(path) + 30;
+       char *tmpname = alloca(len);
+       snprintf(tmpname, len, "%s/%d", path, (int)getpid());
+
+       if (mkdir(path, 0755) == 0) {
+               fprintf(stderr, "leak at mkdir of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT) {
+               fprintf(stderr, "leak at mkdir of %s, errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+       if (mkdir(tmpname, 0755) == 0) {
+               fprintf(stderr, "leak at mkdir of %s\n", tmpname);
+               exit(1);
+       }
+       if (errno != ENOENT) {
+               fprintf(stderr, "leak at mkdir of %s, errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_rmdir(const char *path)
+{
+       size_t len = strlen(path) + 30;
+       char *tmpname = alloca(len);
+       snprintf(tmpname, len, "%s/%d", path, (int)getpid());
+
+       if (rmdir(path) == 0 || errno != ENOENT) {
+               fprintf(stderr, "leak at rmdir of %s\n", path);
+               exit(1);
+       }
+       if (rmdir(tmpname) == 0 || errno != ENOENT) {
+               fprintf(stderr, "leak at rmdir of %s\n", tmpname);
+               exit(1);
+       }
+}
+
+void test_creat(const char *path)
+{
+       if (creat(path, 0755) >= 0) {
+               fprintf(stderr, "leak at creat of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at creat of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_link(const char *path)
+{
+       char *d = strdupa(path), *tmpname;
+       d = dirname(d);
+       size_t len = strlen(path) + 30;
+       tmpname = alloca(len);
+       snprintf(tmpname, len, "%s/%d", d, (int)getpid());
+
+       if (link(path, tmpname) == 0) {
+               fprintf(stderr, "leak at link of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at link of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+
+       if (link(tmpname, path) == 0) {
+               fprintf(stderr, "leak at link (2) of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at link (2) of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_unlink(const char *path)
+{
+       if (unlink(path) == 0) {
+               fprintf(stderr, "leak at unlink of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at unlink of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_symlink(const char *path)
+{
+       char *d = strdupa(path), *tmpname;
+       d = dirname(d);
+       size_t len = strlen(path) + 30;
+       tmpname = alloca(len);
+       snprintf(tmpname, len, "%s/%d", d, (int)getpid());
+
+       if (symlink(tmpname, path) == 0) {
+               fprintf(stderr, "leak at symlink of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at symlink of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+       if (symlink(path, tmpname) == 0) {
+               fprintf(stderr, "leak at symlink (2) of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at symlink (2) of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_readlink(const char *path)
+{
+       char *dest = alloca(2 * strlen(path));
+
+       if (readlink(path, dest, 2 * strlen(path)) >= 0) {
+               fprintf(stderr, "leak at readlink of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at readlink of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_chmod(const char *path)
+{
+       if (chmod(path, 0755) == 0) {
+               fprintf(stderr, "leak at chmod of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at chmod of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_chown(const char *path)
+{
+       if (chown(path, 0, 0) == 0) {
+               fprintf(stderr, "leak at chown of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at chown of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_lchown(const char *path)
+{
+       if (lchown(path, 0, 0) == 0) {
+               fprintf(stderr, "leak at lchown of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at lchown of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_mknod(const char *path)
+{
+       if (mknod(path, 0755, makedev(0, 0)) == 0) {
+               fprintf(stderr, "leak at mknod of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at mknod of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_chroot(const char *path)
+{
+       if (chroot(path) == 0) {
+               fprintf(stderr, "leak at chroot of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at chroot of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_xattrs(const char *path)
+{
+       /*
+        * might consider doing all of:
+        *  setxattr
+        *  lsetxattr
+        *  getxattr
+        *  lgetxattr
+        *  listxattr
+        *  llistxattr
+        *  removexattr
+        *  lremovexattr
+        */
+        char value[200];
+        if (getxattr(path, "security.selinux", value, 200) >= 0) {
+               fprintf(stderr, "leak at getxattr of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at getxattr of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_utimes(const char *path)
+{
+       struct utimbuf times;
+       times.actime = 0;
+       times.modtime = 0;
+
+       if (utime(path, &times) == 0) {
+               fprintf(stderr, "leak at utime of %s\n", path);
+               exit(1);
+       }
+       if (errno != ENOENT && errno != ENOSYS) {
+               fprintf(stderr, "leak at utime of %s: errno was %s\n", path, strerror(errno));
+               exit(1);
+       }
+}
+
+void test_openat(const char *path)
+{
+       char *d = strdupa(path), *f, *tmpname;
+       int fd, fd2;
+       f = basename(d);
+       d = dirname(d);
+       fd = open(d, O_RDONLY);
+       if (fd < 0) {
+               fprintf(stderr, "Error in openat test: could not open parent dir\n");
+               fprintf(stderr, "(this is expected on the second run)\n");
+               return;
+       }
+       fd2 = openat(fd, f, O_RDONLY);
+       if (fd2 >= 0 || errno != ENOENT) {
+               fprintf(stderr, "leak at openat of %s\n", f);
+               exit(1);
+       }
+       size_t len = strlen(path) + strlen("/cgroup.procs") + 1;
+       tmpname = alloca(len);
+       snprintf(tmpname, len, "%s/cgroup.procs", f);
+       fd2 = openat(fd, tmpname, O_RDONLY);
+       if (fd2 >= 0 || errno != ENOENT) {
+               fprintf(stderr, "leak at openat of %s\n", tmpname);
+               exit(1);
+       }
+       close(fd);
+}
+
+int main(int argc, char *argv[])
+{
+       char *procspath;
+       size_t len;
+
+       if (geteuid() != 0) {
+               fprintf(stderr, "Run me as root\n");
+               exit(1);
+       }
+
+       if (argc != 2)  {
+               fprintf(stderr, "Usage: %s [lxcfs_test_cgroup_path]\n", argv[0]);
+               exit(1);
+       }
+
+       /* Try syscalls on the directory and on $directory/cgroup.procs */
+       len = strlen(argv[1]) + strlen("/cgroup.procs") + 1;
+       procspath = alloca(len);
+       snprintf(procspath, len, "%s/cgroup.procs", argv[1]);
+
+       test_open(argv[1]);
+       test_open(procspath);
+       test_stat(argv[1]);
+       test_stat(procspath);
+       test_access(argv[1]);
+       test_access(procspath);
+
+       test_bind(argv[1]);
+       test_bind(procspath);
+       test_bindmount(argv[1]);
+       test_bindmount(procspath);
+       test_truncate(argv[1]);
+       test_truncate(procspath);
+       test_chdir(argv[1]);
+       test_chdir(procspath);
+       test_rename(argv[1]);
+       test_rename(procspath);
+       test_mkdir(argv[1]);
+       test_mkdir(procspath);
+       test_rmdir(argv[1]);
+       test_rmdir(procspath);
+       test_creat(argv[1]);
+       test_creat(procspath);
+       test_link(argv[1]);
+       test_link(procspath);
+       test_unlink(argv[1]);
+       test_unlink(procspath);
+       test_symlink(argv[1]);
+       test_symlink(procspath);
+       test_readlink(argv[1]);
+       test_readlink(procspath);
+       test_chmod(argv[1]);
+       test_chmod(procspath);
+       test_chown(argv[1]);
+       test_chown(procspath);
+       test_lchown(argv[1]);
+       test_lchown(procspath);
+       test_mknod(argv[1]);
+       test_mknod(procspath);
+       test_chroot(argv[1]);
+       test_chroot(procspath);
+       test_xattrs(argv[1]);
+       test_xattrs(procspath);
+       test_utimes(argv[1]);
+       test_utimes(procspath);
+
+       test_openat(argv[1]);
+       // meh...  linkat etc?
+
+       printf("All tests passed\n");
+       return 0;
+}