| // 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)) |
| } |