linuxgoptraceseccomp

Install seccomp filter in child


I would like to know if it is possible to install a seccomp filter in a child process in a Go program. Currently I am spawning the child process as follows:

cmd := exec.Command(target)
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &unix.SysProcAttr{
    Ptrace: true,
}

cmd.Start()
cmd.Wait()

This works fine, and SysProcAttr provides a mechanism to enable Ptrace in the child process. However, there doesn't seem to be any support for also installing seccomp filters in the child. I know about https://github.com/elastic/go-seccomp-bpf and https://github.com/seccomp/libseccomp-golang but these libraries appear to only support installing seccomp filters in the current process (and any children that are spawned from the current process). I only want to install the filters in the child, not the parent, because I don't want the parent to be sandboxed as well.

In C, the mechanism to install a seccomp filter in a child process would be to fork, install the filter in the current process (from in the child), and then exec. However, it doesn't seem like there is support for separating fork and exec like this in Go. Is there any way around this, or do I need to use C to do this properly?

Could SysProcAttr be extended to allow seccomp filters, or allow running arbitrary code after the fork and before the exec (at some point I assume it calls fork and then exec)? How would I go about implementing this?


Solution

  • I solved this issue by using the forkexec package from here: https://godoc.org/github.com/criyle/go-sandbox/pkg/forkexec and elastic/go-seccomp-bpf. Seccomp filters can be installed like so:

    import bseccomp "github.com/elastic/go-seccomp-bpf"
    import "github.com/criyle/go-sandbox/pkg/forkexec"
    
    // ...
    
    policy := bseccomp.Policy{
        DefaultAction: bseccomp.ActionAllow,
        Syscalls: []bseccomp.SyscallGroup{
            {
                Action: bseccomp.ActionTrace,
                Names: []string{
                    "write",
                },
            },
        },
    }
    
    program, err := policy.Assemble()
    must(err)
    filter, err := ExportBPF(program)
    must(err)
    
    bin, err := os.Open(args[0])
    must(err)
    cmd := forkexec.Runner{
        Args:              args[0:],
        ExecFile:          bin.Fd(),
        Seccomp:           filter.SockFprog(),
        StopBeforeSeccomp: true,
        Ptrace:            true,
    }
    
    pid, err := cmd.Start()
    must(err)
    var ws unix.WaitStatus
    _, err = unix.Wait4(pid, &ws, 0, nil)
    must(err)
    

    This requires some extra functions using the criyle/go-sandbox/pkg/seccomp package.

    // ExportBPF convert libseccomp filter to kernel readable BPF content
    func ExportBPF(filter []bpf.Instruction) (seccomp.Filter, error) {
        raw, err := bpf.Assemble(filter)
        if err != nil {
            return nil, err
        }
        return sockFilter(raw), nil
    }
    
    func sockFilter(raw []bpf.RawInstruction) []syscall.SockFilter {
        filter := make([]syscall.SockFilter, 0, len(raw))
        for _, instruction := range raw {
            filter = append(filter, syscall.SockFilter{
                Code: instruction.Op,
                Jt:   instruction.Jt,
                Jf:   instruction.Jf,
                K:    instruction.K,
            })
        }
        return filter
    }