11

C 代码工作正常并正确进入命名空间,但 Go 代码似乎总是从setns调用中返回 EINVAL 以进入mnt命名空间。我.so在 Go 上尝试了许多排列(包括带有 cgo 和 external 的嵌入式 C 代码)1.21.3以及当前的提示。

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

我已经将似乎是问题的问题归结为下面的代码。我究竟做错了什么?

设置

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

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

运行后,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

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

工作 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);
    }
}

用 编译后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

失败的 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")
        }

    }
}

相反,运行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
4

1 回答 1

8

( Go 项目有一个问题)

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

认为这是因为syscall.RawSyscall执行调用的线程不是主线程——即使结果 runtime.LockOSThread不是你所期望的(即 goroutine 被“锁定”到主 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"

使用它作为模板,我修改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 ...

此代码有效,但需要PID硬编码,因为它没有从命令行输入正确读取,但它说明了这个想法(并且如果您提供一个PID从如上所述启动的容器中工作)。

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

更新:在内核邮件列表中闲逛提供了此链接到记录此内容的对话。我似乎无法在任何实际发布的联机帮助页中找到它,但这是来自补丁的引用setns(2),由 Eric Biederman 确认:

如果一个进程是多线程的,它可能不会与新的挂载命名空间重新关联。更改挂载命名空间要求调用者在其自己的用户命名空间中拥有 CAP_SYS_CHROOT 和 CAP_SYS_ADMIN 能力,并在目标挂载命名空间中拥有 CAP_SYS_ADMIN。

于 2014-09-07T04:12:40.103 回答