diff --git a/test b/test new file mode 100755 index 0000000..ac2aec4 --- /dev/null +++ b/test @@ -0,0 +1,20 @@ +#!/bin/sh + +set -x + +NS_ROOT=${1?:Root not specified} +N=$2 + +python setup.py build || exit 1 +if [ ! -d ${NS_ROOT} ] ; then + sudo mkdir ${NS_ROOT} +fi +if [ "$(findmnt -n -o PROPAGATION ${NS_ROOT})" == "" ] ; then + sudo mount --bind ${NS_ROOT} ${NS_ROOT} +fi +if [ "$(findmnt -n -o PROPAGATION ${NS_ROOT})" == "shared" ] ; then + sudo mount --make-private ${NS_ROOT} +fi + +sudo PYTHONPATH=build/lib.linux-x86_64-2.7/ ./test.py ${NS_ROOT} ${N} +ls -li ${NS_ROOT} diff --git a/test.py b/test.py new file mode 100755 index 0000000..b4c1fe9 --- /dev/null +++ b/test.py @@ -0,0 +1,122 @@ +#!/usr/bin/python -u + +import unshare +import time +import sys +import os +import itertools +import pickle + +kw_ns = dict(cgroup=unshare.CLONE_NEWCGROUP, + ipc=unshare.CLONE_NEWIPC, + mount=unshare.CLONE_NEWNS, + net=unshare.CLONE_NEWNET, + pid=unshare.CLONE_NEWPID, + user=unshare.CLONE_NEWUSER, + uts=unshare.CLONE_NEWUTS) + +flag_ns = (unshare.CLONE_FILES, + unshare.CLONE_FS, + unshare.CLONE_NEWCGROUP, + unshare.CLONE_NEWIPC, + unshare.CLONE_NEWNET, + unshare.CLONE_NEWNS, + unshare.CLONE_NEWPID, + unshare.CLONE_NEWUSER, + unshare.CLONE_NEWUTS, + unshare.CLONE_SIGHAND, + unshare.CLONE_SYSVSEM, + unshare.CLONE_THREAD, + unshare.CLONE_VM) + +def test(v): + flags = 0 + kwpath = {} + for a in v: + if len(a) == 1: + flags |= a[0] + else: + kwpath[a[0]] = a[1]; + for k in kwpath: + if os.path.exists(kwpath[k]) and unshare.get_nstype(kwpath[k]) != None: + unshare.unbind(kwpath[k], kw_ns[k]) + open(kwpath[k], "w").close() + r,w = os.pipe() + r = os.fdopen(r) + w = os.fdopen(w, 'w') + pid = os.fork() + if pid != 0: + w.close() + ns1 = pickle.load(r) + ns2 = dict([ (k, os.stat("/proc/self/ns/%s" % (k)).st_ino) + for k in os.listdir("/proc/self/ns") ]) + pid, status = os.waitpid(pid, 0) + if status != 0: + print "%x %s failed" % (flags, kwpath) + raise(Exception) + else: + for k in ns1: + if k == 'pid': + continue + def getpath(): + if k in kwpath: + return kwpath[k] + if k == 'mnt' and 'mount' in kwpath: + return kwpath['mount'] + if k == 'pid_for_children' and 'pid' in kwpath: + return kwpath['pid'] + return None + def getflags(): + if k in kw_ns: + return kw_ns[k] + if k == 'mnt': + return kw_ns['mount'] + if k == 'pid_for_children': + return kw_ns['pid'] + raise Exception(k) + path = getpath() + def error(msg): + raise Exception("%s \n %s \n %s \n%x %s" % ( + msg, ns1, ns2, flags, kwpath)) + if path != None: + if ns1[k] != os.stat(path).st_ino: + error("Wrong namespace saved in %s %d" % ( + path, os.stat(path).st_ino)) + if ns1[k] == ns2[k]: + raise Exception("%s %s %s" % (k, ns1, ns2)) + elif getflags() & flags == 0: + if ns1[k] != ns2[k]: + error("Namespaces not equal %s" % (k)) + else: + if ns1[k] == ns2[k]: + error("Namespaces equal %s" % (k)) + else: + r.close() + try: + unshare.unshare(flags, **kwpath) + ns = dict([ (k, os.stat("/proc/self/ns/%s" % (k)).st_ino) + for k in os.listdir("/proc/self/ns") ]) + pickle.dump(ns, w) + w.close() + os._exit(0) + except Exception, e: + print>>sys.stderr, e + os._exit(1) + +if __name__ == '__main__': + args = list([ (n, os.path.join(sys.argv[1], n)) for n in kw_ns ] + + [ (n,) for n in flag_ns ]) + if len(sys.argv) >=3: + N = int(sys.argv[2]) + else: + N = len(args) + unshare.unshare(unshare.CLONE_NEWNS) # Isolate test from systemd + for n in range(1, N + 1): + combinations = list(itertools.combinations(args, n)) + print "n=%d/N=%d (%d combinations)" % (n, N, len(combinations)) + for v in combinations: + try: + test(v) + except: + print "test(%s) failed" % (str(v)) + raise diff --git a/unshare.c b/unshare.c index 81d0851..dd33670 100644 --- a/unshare.c +++ b/unshare.c @@ -23,14 +23,402 @@ # define _GNU_SOURCE #endif #include +#include +#include +#include +#include +#include +#include +#include +#include -static PyObject * _unshare(PyObject *self, PyObject *args) { - int flags, ret; - if (!PyArg_ParseTuple(args, "i", &flags)) +struct mapping { + char **path; + char *kw_name; + int mask; + char *mask_name; + char *format; + int is_mounted; + int ns_old; + int ns_new; +}; + +#ifndef CLONE_NEWCGROUP +# define CLONE_NEWCGROUP 0x02000000 /* New cgroup namespace. */ +#endif + +static struct mask_list_entry { + char *name; + int mask; + int bindable; +} mask_list[] = { + { "CLONE_FILES", CLONE_FILES, 0 }, + { "CLONE_FS", CLONE_FS, 0 }, + { "CLONE_NEWCGROUP", CLONE_NEWCGROUP, 1 }, + { "CLONE_NEWIPC", CLONE_NEWIPC, 1 }, + { "CLONE_NEWNET", CLONE_NEWNET, 1 }, + { "CLONE_NEWNS", CLONE_NEWNS, 1 }, + { "CLONE_NEWPID", CLONE_NEWPID, 1 }, + { "CLONE_NEWUSER", CLONE_NEWUSER, 1 }, + { "CLONE_NEWUTS", CLONE_NEWUTS, 1 }, + { "CLONE_SIGHAND", CLONE_SIGHAND, 0 }, + { "CLONE_SYSVSEM", CLONE_SYSVSEM, 0 }, + { "CLONE_THREAD", CLONE_THREAD, 0 }, + { "CLONE_VM", CLONE_VM, 0 }, + { NULL, 0, 0 } +}; + +static PyObject * _unshare(PyObject *self, PyObject *args, PyObject *keywds) +{ + PyObject *result = NULL; + int ret; + + int v_flags = 0; + static char *empty = ""; + char *v_cgroup = empty; + char *v_ipc = empty; + char *v_mount = empty; + char *v_net = empty; + char *v_pid = empty; + char *v_user = empty; + char *v_uts = empty; + static char *kwlist[] = { "flags", "cgroup", "ipc", "mount", "net", "pid", "user", "uts", NULL }; + struct mapping mapping[] = { + { NULL, kwlist[0], 0, NULL, NULL, 0 }, + { &v_cgroup, kwlist[1], CLONE_NEWCGROUP, "CLONE_NEWCGROUP", "/proc/%d/ns/cgroup", 0, -1, -1 }, + { &v_ipc, kwlist[2], CLONE_NEWIPC, "CLONE_NEWIPC", "/proc/%d/ns/ipc", 0, -1, -1 }, + { &v_mount, kwlist[3], CLONE_NEWNS, "CLONE_NEWNS", "/proc/%d/ns/mnt", 0, -1, -1 }, + { &v_net, kwlist[4], CLONE_NEWNET, "CLONE_NEWNET", "/proc/%d/ns/net", 0, -1, -1 }, + { &v_pid, kwlist[5], CLONE_NEWPID, "CLONE_NEWPID", "/proc/%d/ns/pid_for_children", 0, -1, -1 }, + { &v_user, kwlist[6], CLONE_NEWUSER, "CLONE_NEWUSER", "/proc/%d/ns/user", 0, -1, -1 }, + { &v_uts, kwlist[7], CLONE_NEWUTS, "CLONE_NEWUTS", "/proc/%d/ns/uts", 0, -1, -1 }, + { NULL, NULL, 0, NULL, NULL, 0 } + }; + + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|izzzzzzz", kwlist, + &v_flags, &v_cgroup, &v_ipc, &v_mount, + &v_net, &v_pid, &v_user, &v_uts)) { return NULL; - ret = unshare(flags); - if(ret == -1) - return PyErr_SetFromErrno(PyExc_OSError); + } + + int flags_child = 0; + int flags_parent = v_flags; + pid_t my_pid = getpid(); + + struct mapping *m; + for ( m = mapping + 1 ; m->path ; m++) { + if (*m->path != empty) { + if (*m->path != NULL) { + /* Bind mount needed, unshare in child */ + char ns[PATH_MAX]; + struct stat stat_buf; + snprintf(ns, sizeof(ns), m->format, my_pid); + ret = stat(ns, &stat_buf); + if (ret == -1) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, ns); + goto err_file_missing; + } + flags_child |= m->mask; + flags_parent &= ~m->mask; + } else { + /* Bind mount not needed, unshare in parent */ + flags_parent |= m->mask; + } + } + } + + int fd[2] = { -1, -1 }; + if (flags_child) { + ret = socketpair(AF_UNIX, SOCK_STREAM, 0, fd); + if(ret == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto err_socketpair; + } + pid_t pid = fork(); + if (pid < 0) { + PyErr_SetFromErrno(PyExc_OSError); + goto err_fork; + } else if (pid == 0) { + /* Child */ + close(fd[1]); + unsigned char result = 0; + ret = unshare(flags_child); + if (flags_child & CLONE_NEWPID) { + /* Force pid_for_children to appear */ + pid_t pid = fork(); + if (pid == 0) { + _exit(0); + } + waitpid(pid, NULL, 0); + } + if (ret != 0) { + result = errno < 255 ? errno : EINVAL; + } + if (write(fd[0], &result, 1) != 1) { + goto exit_1; + } + ret = read(fd[0], &result, 1); + if (ret != 1 || result != 'Q') { + goto exit_1; + } + close(fd[0]); + _exit(0); + exit_1: + close(fd[0]); + _exit(1); + } else { + /* Parent */ + unsigned char result; + + close(fd[0]); + fd[0] = -1; + ret = read(fd[1], &result, 1); + if (ret == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto out; + } + if (result != 0) { + errno = result; + PyErr_SetFromErrno(PyExc_OSError); + goto out; + } + for ( m = mapping + 1 ; m->path ; m++) {; + if (*m->path != NULL && *m->path != empty) { + char ns[PATH_MAX]; + /* Save a restoration namespace in case of error */ + + snprintf(ns, sizeof(ns), m->format, my_pid); + m->ns_old = open(ns, O_RDONLY); + if (m->ns_old == -1) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, ns); + goto err_ns_old_open; + } + snprintf(ns, sizeof(ns), m->format, pid); + m->ns_new = open(ns, O_RDONLY); + if (m->ns_new == -1) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, ns); + goto err_ns_new_open; + } + ret = mount(ns, *m->path, NULL, MS_BIND, NULL); + if (ret == -1) { + close(fd[1]); + PyErr_SetFromErrnoWithFilename(PyExc_OSError, *m->path); + goto err_ns_mount; + } else { + m->is_mounted = 1; + } + } + } + if (write(fd[1], "Q", 1) != 1) { + PyErr_SetFromErrno(PyExc_OSError); + goto err_child_terminate; + } + waitpid(pid, NULL, 0); + } + } + if (flags_parent & ~CLONE_NEWUSER) { + ret = unshare(flags_parent & ~CLONE_NEWUSER); + if (ret == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto err_unshare_failed; + } + } + for ( m = mapping + 1 ; m->path ; m++) { + if (m->ns_new != -1) { + ret = setns(m->ns_new, m->mask); + if (ret == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto err_setns_failed; + } + } + } + if (flags_parent & CLONE_NEWUSER) { + ret = unshare(CLONE_NEWUSER); + if (ret == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto err_unshare_newuser_failed; + } + } + if (flags_parent & CLONE_NEWPID) { + /* Force pid_for_children to appear */ + pid_t pid = fork(); + if (pid == 0) { + _exit(0); + } + waitpid(pid, NULL, 0); + } + result = Py_None; + goto out; + + +err_child_terminate: +err_unshare_newuser_failed: +err_setns_failed: + /* restore old namespaces */ + for ( m = mapping + 1 ; m->path ; m++) { + if (m->ns_old != -1) { + ret = setns(m->ns_old, m->mask); + } + } +err_unshare_failed: +err_ns_mount: +err_ns_new_open: +err_ns_old_open: + /* Remove (now invalid) ns mounts */ + for ( m = mapping + 1 ; m->path ; m++) {; + if (m->is_mounted) { + umount(*m->path); + m->is_mounted = 0; + } + } +err_fork: +err_socketpair: +err_file_missing: + +out: + for ( m = mapping + 1 ; m->path ; m++) {; + if (m->ns_old != -1) { + close(m->ns_old); + m->ns_old = -1; + } + if (m->ns_new != -1) { + close(m->ns_new); + m->ns_new = -1; + } + } + if (fd[0] != -1) { close(fd[0]); } + if (fd[1] != -1) { close(fd[1]); } + if (result == NULL) { + return NULL; + } else { + Py_RETURN_NONE; + } +} + +static int get_nstype_or_zero_or_errno(char *path) { + int result = 0; + int saved_errno = 0; + int fd[2] = { -1, -1 }; + int path_fd = open(path, O_RDONLY); + if (path_fd == -1) { + goto out_errno; + } + int path_nstype = 0; +#ifdef NS_GET_NSTYPE + /* Compiled on system that knows about ioctl_ns */ + path_nstype = ioctl(path_fd, NS_GET_NSTYPE); + if (path_nstype != -1) { + result = path_nstype; + goto out_close; + } else if (path_nstype == -1 && errno == ENOTTY) { + /* Not a namespace path */ + goto out_close; + } else if (path_nstype == -1 && errno != EINVAL) { + /* Unexpected, propagate errno */ + goto out_errno; + } + /* Running on Linux older than 4.11 */ +#else + /* Compiled on Linux older than 4.11 */ +#endif + /* Expensive pre Linux 4.11 path */ + if (pipe(fd) == -1) { + goto out_errno; + } + pid_t pid = fork(); + if (pid < 0) { + goto out_errno; + } else if (pid == 0) { + /* Child, test all possible known namespace kinds */ + close(fd[0]); + struct mask_list_entry *ml; + for (ml = mask_list ; ml->name ; ml++) { + if (ml-> bindable) { + int ret = setns(path_fd, ml->mask); + if (ret == 0) { + ret = write(fd[1], &ml->mask, sizeof(ml->mask)); + if (ret != sizeof(ml->mask)) { + _exit(1); + } + _exit(0); + } + } + } + int zero = 0; + int ret = write(fd[1], &zero, sizeof(zero)); + if (ret != sizeof(zero)) { + _exit(1); + } + _exit(0); + } else { + /* Parent */ + close(fd[1]); + int ret = read(fd[0], &result, sizeof(result)); + if (ret == -1) { + goto out_errno; + } else if (ret != sizeof(result)) { + errno = EIO; + goto out_errno; + } + int status; + ret = waitpid(pid, &status, 0); + if (ret == -1) { + goto out_errno; + } else if (status != 0) { + errno = EIO; + goto out_errno; + } + goto out; + } + +out_errno: + result = -1; + saved_errno = errno; +out_close: + if (path_fd != -1) { close(path_fd); } + if (fd[0] != -1) { close(fd[0]); } + if (fd[1] != -1) { close(fd[1]); } +out: + if (saved_errno != 0) { errno = saved_errno; } + return result; +} + +static PyObject * _get_nstype(PyObject *self, PyObject *args) { + char *path; + if (!PyArg_ParseTuple(args, "s", &path)) + return NULL; + + int path_nstype = get_nstype_or_zero_or_errno(path); + if (path_nstype == -1) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); + return NULL; + } else if (path_nstype == 0) { + Py_RETURN_NONE; + } else { + return Py_BuildValue("i", path_nstype); + } +} + +static PyObject * _unbind(PyObject *self, PyObject *args) { + char *path; + int nstype, res; + if (!PyArg_ParseTuple(args, "si", &path, &nstype)) + return NULL; + + int path_nstype = get_nstype_or_zero_or_errno(path); + if (path_nstype == -1) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); + return NULL; + } else if (path_nstype == 0 || nstype != path_nstype) { + PyErr_Format(PyExc_OSError, "%s does not refer to namspace of kind %x", + path, nstype); + return NULL; + } + res = umount2(path, MNT_DETACH); + if (res == -1) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); + return NULL; + } Py_RETURN_NONE; } @@ -45,61 +433,96 @@ static PyObject * _setns(PyObject *self, PyObject *args) { } static PyMethodDef methods[] = { - {"unshare", _unshare, METH_VARARGS, - "unshare(flags)\n\n" - "Disassociate parts of the process execution context.\n" - "flags is a bitmask that specifies which parts to unshare.\n\n" - "Possible values for flags:\n" - " CLONE_VM CLONE_FS CLONE_FILES CLONE_SIGHAND CLONE_THREAD " - "CLONE_NEWNS\n" - " CLONE_SYSVSEM CLONE_NEWUTS CLONE_NEWIPC CLONE_NEWUSER " - "CLONE_NEWPID\n" - " CLONE_NEWNET\n" + {"unshare", (PyCFunction)_unshare, METH_VARARGS | METH_KEYWORDS, + "unshare(flags, **kwargs)\n\n" + "Disassociate parts of the process execution context.\n" + "flags is a bitmask that specifies which parts to unshare.\n\n" + "Possible values for flags:\n" + " CLONE_VM CLONE_FS CLONE_FILES CLONE_SIGHAND CLONE_THREAD " + "CLONE_NEWNS\n" + " CLONE_SYSVSEM CLONE_NEWUTS CLONE_NEWIPC CLONE_NEWUSER " + "CLONE_NEWPID\n" + " CLONE_NEWNET\n" + "Possible values for kwargs are (PATH == None is equivalent to FLAG):\n" + " cgroup=PATH save new cgroup namespace to PATH (CLONE_NEWCGROUP)\n" + " ipc=PATH save new ipc namespace to PATH (CLONE_NEWIPC)\n" + " mount=PATH save new mount namespace to PATH (CLONE_NEWNS)\n" + " net=PATH save new net namespace to PATH (CLONE_NEWNET)\n" + " pid=PATH save new pid namespace to PATH (CLONE_NEWPID)\n" + " user=PATH save new user namespace to PATH (CLONE_NEWUSER)\n" + " uts=PATH save new uts namespa nstype\n\n" + "Return the nstype for path\n\n" + "None is returned if path does not refer to a namespace\n" + }, + {"unbind", _unbind, METH_VARARGS, + "unbind(path, nstype)\n\n" + "Unbind path from its current namespace.\n" + " path is bound to a namespace.\n" + " nstype specifies the type of namespace that should be unbound.\n" }, {"setns", _setns, METH_VARARGS, - "setns(fd, nstype)\n\n" - "Reassociate the calling thread with a new namespace.\n" - "fd is a filedescriptor referring to a namespace.\n" - "nstype specifies which type of namespace the calling thread\n" - "may be reassociated with.\n\n" - "Possible values for nstype:\n" - " 0 Allow any type of namespace to be joined.\n" - " CLONE_NEWIPC fd must refer to an IPC namespace.\n" - " CLONE_NEWNET fd must refer to a network namespace.\n" - " CLONE_NEWNS fd must refer to a mount namespace.\n" - " CLONE_NEWPID fd must refer to a descendant PID namespace.\n" - " CLONE_NEWUSER fd must refer to a user namespace.\n" - " CLONE_NEWUTS fd must refer to a UTS namespace.\n" + "setns(fd, nstype)\n\n" + "Reassociate the calling thread with a new namespace.\n" + "fd is a filedescriptor referring to a namespace.\n" + "nstype specifies which type of namespace the calling thread\n" + "may be reassociated with.\n\n" + "Possible values for nstype:\n" + " 0 Allow any type of namespace to be joined.\n" + " CLONE_NEWIPC fd must refer to an IPC namespace.\n" + " CLONE_NEWNET fd must refer to a network namespace.\n" + " CLONE_NEWNS fd must refer to a mount namespace.\n" + " CLONE_NEWPID fd must refer to a descendant PID namespace.\n" + " CLONE_NEWUSER fd must refer to a user namespace.\n" + " CLONE_NEWUTS fd must refer to a UTS namespace.\n" }, {NULL, NULL, 0, NULL} }; -PyMODINIT_FUNC initunshare(void) { - PyObject *m; - m = Py_InitModule("unshare", methods); +#if PY_MAJOR_VERSION >= 3 + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "unshare", + NULL, + 0, + methods, + NULL, + NULL, + NULL, + NULL +}; + +#define INITERROR return NULL + +PyMODINIT_FUNC +PyInit_unshare(void) + +#else + +#define INITERROR return + +PyMODINIT_FUNC initunshare(void) +#endif +{ + +#if PY_MAJOR_VERSION >= 3 + PyObject *m = PyModule_Create(&moduledef); +#else + PyObject *m = Py_InitModule("unshare", methods); +#endif if (m == NULL) - return; - - /* Currently (2.6.33) not-implemented: CLONE_VM, CLONE_SIGHAND, - * CLONE_THREAD, CLONE_NEWUSER, CLONE_NEWPID */ - - PyModule_AddIntConstant(m, "CLONE_VM", CLONE_VM); - /* No CAP_* needed */ - PyModule_AddIntConstant(m, "CLONE_FS", CLONE_FS); - /* No CAP_* needed */ - PyModule_AddIntConstant(m, "CLONE_FILES", CLONE_FILES); - PyModule_AddIntConstant(m, "CLONE_SIGHAND", CLONE_SIGHAND); - PyModule_AddIntConstant(m, "CLONE_THREAD", CLONE_THREAD); - /* CAP_SYS_ADMIN */ - PyModule_AddIntConstant(m, "CLONE_NEWNS", CLONE_NEWNS); - PyModule_AddIntConstant(m, "CLONE_SYSVSEM", CLONE_SYSVSEM); - /* CAP_SYS_ADMIN */ - PyModule_AddIntConstant(m, "CLONE_NEWUTS", CLONE_NEWUTS); - /* CAP_SYS_ADMIN */ - PyModule_AddIntConstant(m, "CLONE_NEWIPC", CLONE_NEWIPC); - PyModule_AddIntConstant(m, "CLONE_NEWUSER", CLONE_NEWUSER); - PyModule_AddIntConstant(m, "CLONE_NEWPID", CLONE_NEWPID); - /* CAP_SYS_ADMIN */ - PyModule_AddIntConstant(m, "CLONE_NEWNET", CLONE_NEWNET); + INITERROR; + + struct mask_list_entry *ml; + + for (ml = mask_list ; ml->name ; ml++) { + PyModule_AddIntConstant(m, ml->name, ml->mask); + } +#if PY_MAJOR_VERSION >= 3 + return m; +#endif }