CVE-2015-1342: Permissions, Privileges, and Access Controls (CWE-264)
value: LXCFS before 0.12 does not properly enforce directory escapes, which might allow local users to gain privileges by (1) querying or (2) updating a cgroup.
CWE description: Weaknesses in this category are related to the management of permissions, privileges, and other security features that are used to perform access control.
Published: 2015-12-07T20:59Z
Repository: https://github.com/lxc/lxcfs
Language: C
Files changed: Makefile.am, lxcfs.c, tests/test_confinement.sh, tests/test_syscalls.c
Commit date: 2015-11-17 12:33:13 -0600
Commit message: Fix checking of parent directories

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>

Diff: diff --git a/Makefile.am b/Makefile.am
index 7dda776..cc19ce7 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -27,15 +27,16 @@ lxcfs.1: lxcfs lxcfs.man.add
 	$(HELP2MAN) -n "Set up cgroup fs for containers" --no-discard-stderr -s 1 -I lxcfs.man.add -N ./lxcfs > lxcfs.1
 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/ \
 		INSTALL \
 		Makefile \
@@ -58,6 +59,7 @@ distclean:
 		lxcfs \
 		lxcfs.1 \
 		lxcfs.o \
 		m4/ \
 		missing \
-		stamp-h1
+		stamp-h1 \
+		tests/test_syscalls
diff --git a/lxcfs.c b/lxcfs.c
index e2ccc5e..2c5aca4 100644
--- a/lxcfs.c
+++ b/lxcfs.c
@@ -233,10 +233,16 @@ static bool perms_include(int fmode, mode_t req_mode)
 		return false;
 	}
 	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;
 
 	if (strlen(taskcg) <= strlen(querycg)) {
@@ -376,57 +382,75 @@ static void prune_init_slice(char *cg)
  * to a string containing the next cgroup directory under cg, which must be
  * freed by the caller.
  */
 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;
 }
 
 /*
  * given /cgroup/freezer/a/b, return "freezer".
@@ -550,10 +574,14 @@ static int cg_getattr(const char *path, struct stat *sb)
 	/* check that cgcopy is either a child cgroup of cgdir, or listed in its keys.
 	 * Then check that caller's cgroup is under path if fpath is a child
 	 * 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;
 			sb->st_nlink = 2;
 			ret = 0;
@@ -628,12 +656,15 @@ static int cg_opendir(const char *path, struct fuse_file_info *fi)
 			/* this is just /cgroup/controller, return its contents */
 			cgroup = "/";
 		}
 	}
 
-	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 */
 	dir_info = malloc(sizeof(*dir_info));
 	if (!dir_info)
@@ -778,10 +809,14 @@ static int cg_open(const char *path, struct fuse_file_info *fi)
 		ret = -EINVAL;
 		goto out;
 	}
 	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;
 		goto out;
 	}
@@ -1561,11 +1596,11 @@ 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;
 
 	if (!fc)
 		return -EIO;
@@ -1583,10 +1618,18 @@ int cg_mkdir(const char *path, mode_t mode)
 	if (!fpath)
 		path1 = "/";
 	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;
 	}
 	if (!caller_is_in_ancestor(fc->pid, controller, path1, NULL)) {
@@ -1597,17 +1640,18 @@ int cg_mkdir(const char *path, mode_t mode)
 	ret = cgfs_create(controller, cgroup, fc->uid, fc->gid);
 	printf("cgfs_create returned %d for %s %s\n", ret, controller, cgroup);
 
 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;
 
 	if (!fc)
 		return -EIO;
@@ -1624,12 +1668,18 @@ static int cg_rmdir(const char *path)
 	if (!fpath) {
 		ret = -EINVAL;
 		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;
 	}
 	if (!caller_is_in_ancestor(fc->pid, controller, cgroup, NULL)) {
@@ -1644,10 +1694,11 @@ static int cg_rmdir(const char *path)
 
 	ret = 0;
 
 out:
 	free(cgdir);
+	free(next);
 	return ret;
 }
 
 static bool startswith(const char *line, const char *pref)
 {
diff --git a/tests/test_confinement.sh b/tests/test_confinement.sh
new file mode 100644
index 0000000..8f5f484
--- /dev/null
+++ b/tests/test_confinement.sh
@@ -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
index 0000000..8561cfc
--- /dev/null
+++ b/tests/test_syscalls.c
@@ -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;
+}

Vulnerable code: 
tests: TEST_READ TEST_CPUSET
		stamp-h1
	char fnam[PROCLEN];
	FILE *f;
	char *line = NULL;
	size_t len = 0;
	int ret;
	ret = snprintf(fnam, PROCLEN, "/proc/%d/cgroup", pid);
	if (ret < 0 || ret >= PROCLEN)
		return false;
	if (!(f = fopen(fnam, "r")))
	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;
	fclose(f);
	free(line);
	if (cgroup && !fc_may_access(fc, controller, cgroup, NULL, O_RDONLY)) {
		return -EACCES;
	char *fpath = NULL, *path1, *cgdir = NULL, *controller;
	char *fpath = NULL, *cgdir = NULL, *controller;
	fprintf(stderr, "rmdir: verifying access to %s:%s (req path %s)\n",
			controller, cgdir, path);
Fixed code: TEST_SYSCALLS: tests/test_syscalls.c
	$(CC) -o tests/test_syscalls tests/test_syscalls.c
tests: TEST_READ TEST_CPUSET TEST_SYSCALLS
		stamp-h1 \
		tests/test_syscalls

/*
 * taskcg is  a/b/c
 * querycg is /a/b/c/d/e
 * we return 'd'
 */
	char *c2 = get_pid_cgroup(pid, contrl);
	char *linecmp;
	if (!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;
	}
	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) {
	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;
	}
	free(c2);
		if (!caller_may_see_dir(fc->pid, controller, cgroup)) {
			ret = -ENOENT;
			goto out;
		}
	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;
	if (!caller_may_see_dir(fc->pid, controller, path1)) {
		ret = -ENOENT;
		goto out;
	}
	char *fpath = NULL, *path1, *cgdir = NULL, *controller, *next = NULL;
	if (!caller_is_in_ancestor(fc->pid, controller, path1, &next)) {
		if (fpath && strcmp(next, fpath) == 0)
			ret = -EEXIST;
		else
			ret = -ENOENT;
		goto out;
	}

	free(next);
	char *fpath = NULL, *cgdir = NULL, *controller, *next = NULL;
	if (!caller_is_in_ancestor(fc->pid, controller, cgroup, &next)) {
		if (!fpath || strcmp(next, fpath) == 0)
			ret = -EBUSY;
		else
			ret = -ENOENT;
		goto out;
	}

	free(next);
#!/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!"
#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;
}