From a8b6c3e0537e90fba3c55910fd1b7229d54a60a7 Mon Sep 17 00:00:00 2001 From: Serge Hallyn Date: Fri, 13 Nov 2015 17:07:36 -0600 Subject: [PATCH] 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 --- Makefile.am | 8 +- lxcfs.c | 137 ++++++++---- tests/test_confinement.sh | 96 ++++++++ tests/test_syscalls.c | 451 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 646 insertions(+), 46 deletions(-) create mode 100644 tests/test_confinement.sh create mode 100644 tests/test_syscalls.c diff --git a/Makefile.am b/Makefile.am index 7dda776a..cc19ce7c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 e2ccc5e8..2c5aca4e 100644 --- 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 @@ int cg_chmod(const char *path, mode_t mode) 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 index 00000000..8f5f484b --- /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 00000000..8561cfcd --- /dev/null +++ b/tests/test_syscalls.c @@ -0,0 +1,451 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +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, ×) == 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; +}