blob: fc8e68948608cd7756e960d2dd77dcab8675c78e [file] [log] [blame]
// Copyright 2019 The Goma Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package remoteexec
import (
"sort"
"strings"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
gomapb "go.chromium.org/goma/server/proto/api"
nsjailpb "go.chromium.org/goma/server/proto/nsjail"
)
const (
nsjailHardeningConfig = `
name: "hardening by nsjail (seccomp-bpf)"
mode: ONCE
# keep_env = true
mount_proc: true
# it runs in docker container, so ok to mount / as RO.
mount <
src: "/"
dst: "/"
is_bind: true
rw: false
is_dir: true
>
mount <
dst: "/tmp"
fstype: "tmpfs"
options: "size=5000000"
rw: true
is_dir: true
>
# input root is per request, so ok to mount it as RW.
# (does not affect to other requests).
mount <
prefix_src_env: "INPUT_ROOT"
src: ""
prefix_dst_env: "INPUT_ROOT"
dst: ""
is_bind: true
rw: true
is_dir: true
>
# default may fail with "File too large"
rlimit_fsize_type: INF
rlimit_as_type: INF
# syscalls used by clang.
seccomp_string: "ALLOW {"
seccomp_string: " access,"
seccomp_string: " alarm,"
seccomp_string: " arch_prctl,"
seccomp_string: " brk,"
seccomp_string: " clone,"
seccomp_string: " close,"
seccomp_string: " connect,"
seccomp_string: " dup2,"
seccomp_string: " execve,"
seccomp_string: " exit_group,"
seccomp_string: " fcntl,"
seccomp_string: " fstatfs,"
seccomp_string: " futex,"
seccomp_string: " getcwd,"
seccomp_string: " getdents,"
seccomp_string: " getdents64,"
seccomp_string: " getegid,"
seccomp_string: " geteuid,"
seccomp_string: " getgid,"
seccomp_string: " getpgrp,"
seccomp_string: " getpid,"
seccomp_string: " getppid,"
seccomp_string: " getrlimit,"
seccomp_string: " gettid,"
seccomp_string: " getuid,"
seccomp_string: " ioctl,"
seccomp_string: " lseek,"
seccomp_string: " mmap,"
seccomp_string: " mprotect,"
seccomp_string: " mremap,"
seccomp_string: " munmap,"
seccomp_string: " newfstat,"
seccomp_string: " newlstat,"
seccomp_string: " newstat,"
seccomp_string: " newuname,"
seccomp_string: " open,"
seccomp_string: " openat,"
seccomp_string: " pipe,"
seccomp_string: " pipe2,"
seccomp_string: " pread64,"
seccomp_string: " prlimit64,"
seccomp_string: " read,"
seccomp_string: " readlink,"
seccomp_string: " rename,"
seccomp_string: " rt_sigaction,"
seccomp_string: " rt_sigprocmask,"
seccomp_string: " rt_sigreturn, "
seccomp_string: " set_robust_list,"
seccomp_string: " set_tid_address,"
seccomp_string: " sigaltstack,"
seccomp_string: " socket,"
seccomp_string: " sysinfo,"
seccomp_string: " unlink,"
seccomp_string: " vfork,"
seccomp_string: " wait4,"
seccomp_string: " write,"
seccomp_string: " writev"
seccomp_string: "}"
seccomp_string: "DEFAULT KILL"
#seccomp_log: true
iface_no_lo: true
`
nsjailHardeningWrapperScript = `#!/bin/bash
export INPUT_ROOT="$(pwd)"
if [[ "$WORK_DIR" != "" ]]; then
cd "${WORK_DIR}"
fi
export PWD="$(pwd)"
# exit 159 -> seccomp violation
nsjail -q -C "./nsjail.cfg" --cwd "$PWD" \
-- \
"$@"
`
nsjailChrootRunWrapperScript = `#!/bin/bash
set -e
if [[ "$WORK_DIR" == "" ]]; then
echo "ERROR: WORK_DIR is not set" >&2
exit 1
fi
rundir="$(pwd)"
chroot_workdir="/tmp/goma_chroot"
#
# mount directories under $chroot_workdir and execute.
#
run_dirs=($(ls -1 "$rundir"))
sys_dirs=(dev proc)
# RBE server generates __action_home__XXXXXXXXXX directory in $rundir
# (note: XXXXXXXXXX is a random). Let's skip it because we do not use that.
# mount directories in the request.
for d in "${run_dirs[@]}"; do
if [[ "$d" == __action_home__* ]]; then
continue
fi
mkdir -p "$chroot_workdir/$d"
mount --bind "$rundir/$d" "$chroot_workdir/$d"
done
# mount directories not included in the request.
for d in "${sys_dirs[@]}"; do
# avoid to mount system directories if that exist in the user's request.
if [[ -d "$rundir/$d" ]]; then
continue
fi
# directory will be mounted by nsjail later.
mkdir -p "$chroot_workdir/$d"
done
# needed to make nsjail bind device files.
touch "$chroot_workdir/dev/urandom"
touch "$chroot_workdir/dev/null"
# currently running with root. run the command with nobody:nogroup with chroot.
# We use nsjail to chdir without running bash script inside chroot, and
# libc inside chroot can be different from libc outside.
nsjail --quiet --config "$WORK_DIR/nsjail.cfg" -- "$@"
`
)
// pathFromToolchainSpec returns ':'-joined directories of paths in toolchain spec.
// Since symlinks may point to executables, having directories with executables
// may not work, but it is a bit cumbersome to analyze symlinks.
// Also, having library directories in PATH should be harmless because
// the Goma client may not include multiple subprograms with the same name.
func pathFromToolchainSpec(cfp clientFilePath, ts []*gomapb.ToolchainSpec) string {
m := make(map[string]bool)
for _, e := range ts {
m[cfp.Dir(e.GetPath())] = true
}
var r []string
for k := range m {
if k == "" || k == "." {
continue
}
r = append(r, k)
}
// This function must return the same result for the same input, but go
// does not guarantee the iteration order.
sort.Strings(r)
return strings.Join(r, ":")
}
// nsjailConfig returns nsjail configuration.
// When you modify followings, please make sure it matches
// nsjailChrootRunWrapperScript above.
func nsjailChrootConfig(cwd string, cfp clientFilePath, ts []*gomapb.ToolchainSpec, envs []string) []byte {
chrootWorkdir := "/tmp/goma_chroot"
cfg := &nsjailpb.NsJailConfig{
Uidmap: []*nsjailpb.IdMap{
{
InsideId: proto.String("nobody"),
OutsideId: proto.String("nobody"),
},
},
Gidmap: []*nsjailpb.IdMap{
{
InsideId: proto.String("nogroup"),
OutsideId: proto.String("nogroup"),
},
},
Mount: []*nsjailpb.MountPt{
{
Src: proto.String(chrootWorkdir),
Dst: proto.String("/"),
IsBind: proto.Bool(true),
Rw: proto.Bool(true),
IsDir: proto.Bool(true),
},
{
Src: proto.String("/dev/null"),
Dst: proto.String("/dev/null"),
Rw: proto.Bool(true),
IsBind: proto.Bool(true),
},
{
Src: proto.String("/dev/urandom"),
Dst: proto.String("/dev/urandom"),
IsBind: proto.Bool(true),
},
},
Cwd: proto.String(cwd),
// TODO: use log file and print to server log.
LogLevel: nsjailpb.LogLevel_WARNING.Enum(),
MountProc: proto.Bool(true),
Envar: append(
[]string{
"PATH=" + pathFromToolchainSpec(cfp, ts),
// Dummy home directory is needed by pnacl-clang to
// import site.py to import user-defined python
// packages.
"HOME=/",
},
// Add client-side environemnt to execution environment.
envs...),
RlimitAsType: nsjailpb.RLimit_INF.Enum(),
RlimitFsizeType: nsjailpb.RLimit_INF.Enum(),
// TODO: relax RLimit from the default.
// Default size might be too strict, and not suitable for
// compiling.
}
return []byte(prototext.Format(cfg))
}