本文所有内容均在 linux 环境下(Ubuntu 18.04)。
Go 提倡使用 Gorotuine 来替代系统所提供的 线程、进程,所以在语言中并没有提供直接 fork 或类似的方法。 只有提供了 ForkExec 及类似(包装后)的方法。 为了限制 syscall 的调用,go 语言只能使用 ptrace 进行捕捉后判断,这样使得效率会低下,(可以使用 time strace 做些测试)。那必然就要使用 seccomp,关于 seccomp 的一些东西可以看 开发OJ之沙箱 — syscall 限制;那问题来了,怎么把seccomp 的规则 给即将运行的命令呢?
这是个坑吧
最开始,想着在某一阶段 对本进程 load Seccomp filter 后直接调用 FrokExec 函数,但问题来了,ForkExec中有些系统调用会被继承到的的 seccomp 所限制导致无法执行。 所以 seccomp 只能在 执行命令前被载入。
这就是个坑
最初把go内部的ForkAndExec实现复制了一份,用 go:linkname 连接了所需函数,之后在最后Exec前添加 scmp.load 函数,但这样一运行就会 panic ,报 栈生长 错误。。。
花了大半天的时间看了一下 go 中关于 ForkExec 那部分的源码。
分析一下
只看了 Linux 下的源代码,毕竟 seccomp 只在 linux 下才能使用。 大致代码思路其实并没有什么不同,就是在 execve 前调用 Syscall 这个汇编函数去加载各种限制。只不过在fork开始前有一个 runtime_BeforeFork 的函数,父进程返回后之后会执行 runtime_AfterFork,子进程则会执行 runtime_AfterForkInChild,就是这么几个函数,导致这篇文章拖了这么久。
这三个函数,都只是link过来而已,函数的定义则都是在runtime包里。runtime_BeforeFork, runtime_AfterFork,[runtime_AfterForkInChild](https://github.com/golang/go/blob/master/src/runtime/proc.go#L3238]; 这三个函数是为了 修复 这个 issue 而提交的 commit的。之后已经经过多次修改才成为现在看到的这样。
先说说这三个函数都干了啥。
BeforeFork
会在系统栈上进行操作, 通过别的资料可以了解到,golang协程必然也是有栈的,否则将无法记录函数的调用以及返回值。协程最先的栈是 2k ,之后会根据实际情况进行增长,所以这部分是语言自己维护的。BeforeFork 会先获取当前协程运行 M 的 G (GMP,之后锁定 G 绑定的 M 里的锁,将信号存到别的地方,当前 M 将不再处理信号,接着破坏当前栈,导致其在增长的时候会失败,接着抛出错误。
之后我们就可以调用 fork 了,这时候为了避免栈增长,只能调用汇编函数,好在 syscall 包里提供了一些方法来调用syscall。
AfterFork
fork之后,父线程将立刻返回,返回后才会父进程里会执行 afterFork, 这个函数也会在系统栈上操作。还是先获取当前 M 上的 G,之后将栈恢复,恢复刚刚存到别的地方的信号,继续接下去的操作。最后将 M 里的锁解除。
这样父进程就没有任何问题
AfterForkInChild
fork之后,子进程将会执行这个函数,与之前不停,它不会在系统栈上执行,这个函数只做两件事,清除原有的信号处理,恢复父进程继承下来的信号。
但是没有恢复之前被破坏的栈,所以,在这之后,都不能执行任何非汇编的函数调用。否则会直接因为栈空间增长直接 panic。
PS. 最先开始解决issue只会限制M里的信号处理防止信号冲突而fork失败,之后为了解决issue #7511 等问题才引入的,有兴趣可以去了解。
这样以来,我们如果使用 seccomp-golang 包的函数加载seccomp 由于涉及到函数调用,必然会出现栈增长,这时整个程序就会被破坏。 那如何解决呢?
solved it
查看了 libseccomp-golang 的包中提供了一个函数 ExportBpf, 可以将 seccomp 的规则导出成 bpf 规则链,之后使用 prctl 载入规则。
示例代码如下:
scmp, err := seccomp.NewFilter(seccomp.ActAllow)
scmpBpfFile, err := ioutil.TempFile("", "scmpBpfTempFile-")
if err != nil {
panic(err)
}
err = scmp.ExportBPF(scmpBpfFile)
if err != nil {
panic(err)
}
scmp.Release()
_, err = scmpBpfFile.Seek(0, os.SEEK_SET)
if err != nil {
panic(err)
}
scmpBpfFileStat, _ := scmpBpfFile.Stat()
sockFilterFileSize := scmpBpfFileStat.Size()
if sockFilterFileSize % 8 != 0 {
panic("sockFilterFileSize error " + string(sockFilterFileSize) )
}
sockFilters := make([]syscall.SockFilter, sockFilterFileSize / 8)
sockFilterFileContent, err := ioutil.ReadAll(scmpBpfFile)
bytesBuffer := bytes.NewBuffer(sockFilterFileContent)
err = binary.Read(bytesBuffer, binary.LittleEndian, &sockFilters)
bpf := syscall.SockFprog{
Len: uint16(len(sockFilters)),
Filter: &sockFilters[0],
}
r1, _, err1 := syscall.RawSyscall6(syscall.SYS_CLONE, uintptr(syscall.SIGCHID), 0, 0, 0, 0, 0)
if err1 != 0 || r1 != 0 {
return
}
_, _, err1 = syscall.RawSyscall6(syscall.SYS_PRCTL, syscall.PR_SET_SECCOMP, 2, uintptr(unsafe.Pointer(&bpf)), 0, 0, 0)
// ... exec code
大概就是这样的代码, 就能实现父进程不套 seccomp 限制,而子进程在 exec 前加上 seccomp 限制;如果想使用 seccomp 的 ACT_TRACE 功能,go 的syscall包中并没有 PTRACE_EVENT_SECCOMP
和 PTRACE_O_TRACESECCOMP
的定义,需要手动定义
const PTRACE_EVENT_SECCOMP = 7
const PTRACE_O_TRACESECCOMP = 1 << PTRACE_EVENT_SECCOMP
至于 golang-seccomp 的使用,可以看文档,相比C语言来的更繁琐了,可以手动添加函数使得方便一些。包中的 ExportBPF 是写入文件,但是实现是写入一个 fd,可以手动加个函数 ExportBPF2Fd,直接写到文件,这样就可以使用 pipe 等更方便的方式传递,不用操作文件,相比之下效率会块很多。
以上~
-- EOF --
comments