CVE-2015-1342 (C, published 2015-12-07T20:59Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: )
Weakness: CWE-264 \u2014 Permissions, Privileges, and Access Controls
Weaknesses in this category are related to the management of permissions, privileges, and other security features that are used to perform access control.
Repository: https://github.com/lxc/lxcfs
Files changed:
Makefile.am
lxcfs.c
tests/test_confinement.sh
tests/test_syscalls.c
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>


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;
}