从Go调用setns为mnt命名空间返回EINVAL

ilo*_*owe 10 c linux system-calls go cgo

C代码工作正常并正确进入命名空间,但Go代码似乎总是从setns调用返回EINVAL 进入mnt命名空间.我已经尝试了数置换(包括嵌入的C代码与CGO和外部的.so)上的Go 1.2,1.3和当前的尖端.

单步执行代码gdb表明两个序列都setnslibc完全相同的方式调用(或者它在我看来).

我已经把下面的代码中的问题煮成了问题.我究竟做错了什么?

建立

我有一个用于启动快速bu​​sybox容器的shell别名:

alias startbb='docker inspect --format "{{ .State.Pid }}" $(docker run -d busybox sleep 1000000)'
Run Code Online (Sandbox Code Playgroud)

运行之后,startbb将启动一个容器并输出它的PID.

lxc-checkconfig 输出:

Found kernel config file /boot/config-3.8.0-44-generic
--- Namespaces ---
Namespaces: enabled
Utsname namespace: enabled
Ipc namespace: enabled
Pid namespace: enabled
User namespace: missing
Network namespace: enabled
Multiple /dev/pts instances: enabled

--- Control groups ---
Cgroup: enabled
Cgroup clone_children flag: enabled
Cgroup device: enabled
Cgroup sched: enabled
Cgroup cpu account: enabled
Cgroup memory controller: missing
Cgroup cpuset: enabled

--- Misc ---
Veth pair device: enabled
Macvlan: enabled
Vlan: enabled
File capabilities: enabled
Run Code Online (Sandbox Code Playgroud)

uname -a 生产:

Linux gecko 3.8.0-44-generic #66~precise1-Ubuntu SMP Tue Jul 15 04:01:04 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
Run Code Online (Sandbox Code Playgroud)

工作C代码

以下C代码工作正常:

#include <errno.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

main(int argc, char* argv[]) {
    int i;
    char nspath[1024];
    char *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" };

    if (geteuid()) { fprintf(stderr, "%s\n", "abort: you want to run this as root"); exit(1); }

    if (argc != 2) { fprintf(stderr, "%s\n", "abort: you must provide a PID as the sole argument"); exit(2); }

    for (i=0; i<5; i++) {
        sprintf(nspath, "/proc/%s/ns/%s", argv[1], namespaces[i]);
        int fd = open(nspath, O_RDONLY);

        if (setns(fd, 0) == -1) { 
            fprintf(stderr, "setns on %s namespace failed: %s\n", namespaces[i], strerror(errno));
        } else {
            fprintf(stdout, "setns on %s namespace succeeded\n", namespaces[i]);
        }

        close(fd);
    }
}
Run Code Online (Sandbox Code Playgroud)

编译后gcc -o checkns checkns.c,输出sudo ./checkns <PID>为:

setns on ipc namespace succeeded
setns on uts namespace succeeded
setns on net namespace succeeded
setns on pid namespace succeeded
setns on mnt namespace succeeded
Run Code Online (Sandbox Code Playgroud)

失败的Go代码

相反,以下Go代码(应该是相同的)不能很好地工作:

package main

import (
    "fmt"
    "os"
    "path/filepath"
    "syscall"
)

func main() {
    if syscall.Geteuid() != 0 {
        fmt.Println("abort: you want to run this as root")
        os.Exit(1)
    }

    if len(os.Args) != 2 {
        fmt.Println("abort: you must provide a PID as the sole argument")
        os.Exit(2)
    }

    namespaces := []string{"ipc", "uts", "net", "pid", "mnt"}

    for i := range namespaces {
        fd, _ := syscall.Open(filepath.Join("/proc", os.Args[1], "ns", namespaces[i]), syscall.O_RDONLY, 0644)
        err, _, msg := syscall.RawSyscall(308, uintptr(fd), 0, 0) // 308 == setns

        if err != 0 {
            fmt.Println("setns on", namespaces[i], "namespace failed:", msg)
        } else {
            fmt.Println("setns on", namespaces[i], "namespace succeeded")
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

相反,运行sudo go run main.go <PID>产生:

setns on ipc namespace succeeded
setns on uts namespace succeeded
setns on net namespace succeeded
setns on pid namespace succeeded
setns on mnt namespace failed: invalid argument
Run Code Online (Sandbox Code Playgroud)

ilo*_*owe 6

(Go项目存在问题)

所以,这个问题的答案是你必须setns从单线程上下文调用.这是有道理的,因为setns应该将当前线程加入命名空间.由于Go是多线程的,因此您需要setns在Go运行时线程启动之前进行调用.

这是因为线程中调用syscall.RawSyscall执行不是主要的话题-甚至 runtime.LockOSThread结果不是你所期望的(即该够程被"锁定"在主要的C线程,因此相当于.构造函数技巧解释如下).

我在提交问题后得到的回复建议使用" cgo构造函数".我找不到关于这个"技巧"的任何"正确"文档,但它被nsinitDocker/Michael Crosby使用,即使我逐行检查了该代码,我也没有尝试以这种方式运行它(见下文)沮丧).

"技巧"基本上是你可以cgo在启动Go运行时之前执行C函数.

要执行此操作,请添加__attribute__((constructor))宏以装饰要在Go启动之前运行的功能:

/*
__attribute__((constructor)) void init() {
    // this code will execute before Go starts up
    // in runs in a single-threaded C context
    // before Go's threads start running
}
*/
import "C"
Run Code Online (Sandbox Code Playgroud)

使用它作为模板,我修改checkns.go如下:

/*
#include <sched.h>
#include <stdio.h>
#include <fcntl.h>

__attribute__((constructor)) void enter_namespace(void) {
   setns(open("/proc/<PID>/ns/mnt", O_RDONLY, 0644), 0);
}
*/
import "C"

... rest of file is unchanged ...
Run Code Online (Sandbox Code Playgroud)

这段代码有效,但要求PID硬编码,因为它没有从命令行输入中正确读取,但它说明了这个想法(如果你PID从如上所述的容器中提供一个容器,则可以使用).

这令人沮丧,因为我想setns多次调用,但由于这个C代码在Go运行时启动之前执行,因此没有可用的Go代码.

更新:在内核邮件列表中进行翻转,提供此链接以记录此文档.我似乎无法找到它在任何实际公布的联机帮助,但这里是从一个补丁报价setns(2),由埃里克·比德曼证实:

如果进程是多线程的,则可能无法将该进程与新的mount命名空间重新关联.更改装入命名空间要求调用者在其自己的用户命名空间中拥有CAP_SYS_CHROOT和CAP_SYS_ADMIN功能,并在目标装入命名空间中拥有CAP_SYS_ADMIN.