CVE-2015-1342 (published 2015-12-07T20:59Z)

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.

Severity: MEDIUM
CVSS v2 base score: 4.6 (AV:L/AC:L/Au:N/C:P/I:P/A:P)

Weakness information:
cwe_id: CWE-264
cwe_name: Permissions, Privileges, and Access Controls
description: Weaknesses in this category are related to the management of permissions, privileges, and other security features that are used to perform access control.

Fix information:
repo_url: https://github.com/lxc/lxcfs
commit_details: repo_url: https://github.com/lxc/lxcfs
author: Serge Hallyn
author_date: 2015-11-13 17:07:36-06:00
msg: 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>
repository: repo_url: https://github.com/lxc/lxcfs
repo_name: lxc/lxcfs
description: FUSE filesystem for LXC
repo_language: C
owner: lxc
file_changes: filename: Makefile.am
change_type: ModificationType.MODIFY
diff: @@ -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

programming_language: Shell
filename: lxcfs.c
change_type: ModificationType.MODIFY
diff: @@ -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;
 }
 

programming_language: C
filename: test_confinement.sh
change_type: ModificationType.ADD
diff: @@ -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!"

programming_language: Shell
filename: test_syscalls.c
change_type: ModificationType.ADD
diff: @@ -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;
+}

programming_language: C