platform_build_soong/ui/build/sandbox_linux.go
Spandan Das 0506361a60 Add errorHints to stdout when read-only file system errors are detected
The source tree will eventually be made ReadOnly, and recipes that write
directly to the source tree will fail. Use a pattern-match approach on
the results of stdout/stderr to provide hints to the user in such a
scenario.

If multiple patterns are found in raw output, print error hint
corresponding to first pattern match. first pattern match is chosen
since the failing function will be at the top of the stack, and hence
will be logged first

Test: Wrote a unit test to assert errorhint is added to output.
Wrote an integration test that writes to a file in the source tree
1. When source_tree is RO, the recipe fails and an error hint is printed
to stdout
2. When source tree is RW, the recipe succeeds and no error hint is
printed

Bug: 174726238
Change-Id: Id67b48f8094cdf8a571c239ae469d60464a1e89c
2021-06-29 00:20:21 +00:00

249 lines
6.7 KiB
Go

// Copyright 2017 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package build
import (
"bytes"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"sync"
)
type Sandbox struct {
Enabled bool
DisableWhenUsingGoma bool
AllowBuildBrokenUsesNetwork bool
}
var (
noSandbox = Sandbox{}
basicSandbox = Sandbox{
Enabled: true,
}
dumpvarsSandbox = basicSandbox
katiSandbox = basicSandbox
soongSandbox = basicSandbox
ninjaSandbox = Sandbox{
Enabled: true,
DisableWhenUsingGoma: true,
AllowBuildBrokenUsesNetwork: true,
}
)
const nsjailPath = "prebuilts/build-tools/linux-x86/bin/nsjail"
var sandboxConfig struct {
once sync.Once
working bool
group string
srcDir string
outDir string
distDir string
}
func (c *Cmd) sandboxSupported() bool {
if !c.Sandbox.Enabled {
return false
}
// Goma is incompatible with PID namespaces and Mount namespaces. b/122767582
if c.Sandbox.DisableWhenUsingGoma && c.config.UseGoma() {
return false
}
sandboxConfig.once.Do(func() {
sandboxConfig.group = "nogroup"
if _, err := user.LookupGroup(sandboxConfig.group); err != nil {
sandboxConfig.group = "nobody"
}
// These directories will be bind mounted
// so we need full non-symlink paths
sandboxConfig.srcDir = absPath(c.ctx, ".")
if derefPath, err := filepath.EvalSymlinks(sandboxConfig.srcDir); err == nil {
sandboxConfig.srcDir = absPath(c.ctx, derefPath)
}
sandboxConfig.outDir = absPath(c.ctx, c.config.OutDir())
if derefPath, err := filepath.EvalSymlinks(sandboxConfig.outDir); err == nil {
sandboxConfig.outDir = absPath(c.ctx, derefPath)
}
sandboxConfig.distDir = absPath(c.ctx, c.config.DistDir())
if derefPath, err := filepath.EvalSymlinks(sandboxConfig.distDir); err == nil {
sandboxConfig.distDir = absPath(c.ctx, derefPath)
}
sandboxArgs := []string{
"-H", "android-build",
"-e",
"-u", "nobody",
"-g", sandboxConfig.group,
"-R", "/",
// Mount tmp before srcDir
// srcDir is /tmp/.* in integration tests, which is a child dir of /tmp
// nsjail throws an error if a child dir is mounted before its parent
"-B", "/tmp",
"-B", sandboxConfig.srcDir,
"-B", sandboxConfig.outDir,
}
if _, err := os.Stat(sandboxConfig.distDir); !os.IsNotExist(err) {
//Mount dist dir as read-write if it already exists
sandboxArgs = append(sandboxArgs, "-B",
sandboxConfig.distDir)
}
sandboxArgs = append(sandboxArgs,
"--disable_clone_newcgroup",
"--",
"/bin/bash", "-c", `if [ $(hostname) == "android-build" ]; then echo "Android" "Success"; else echo Failure; fi`)
cmd := exec.CommandContext(c.ctx.Context, nsjailPath, sandboxArgs...)
cmd.Env = c.config.Environment().Environ()
c.ctx.Verboseln(cmd.Args)
data, err := cmd.CombinedOutput()
if err == nil && bytes.Contains(data, []byte("Android Success")) {
sandboxConfig.working = true
return
}
c.ctx.Println("Build sandboxing disabled due to nsjail error.")
for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") {
c.ctx.Verboseln(line)
}
if err == nil {
c.ctx.Verboseln("nsjail exited successfully, but without the correct output")
} else if e, ok := err.(*exec.ExitError); ok {
c.ctx.Verbosef("nsjail failed with %v", e.ProcessState.String())
} else {
c.ctx.Verbosef("nsjail failed with %v", err)
}
})
return sandboxConfig.working
}
func (c *Cmd) wrapSandbox() {
wd, _ := os.Getwd()
var srcDirMountFlag string
if c.config.sandboxConfig.SrcDirIsRO() {
srcDirMountFlag = "-R"
} else {
srcDirMountFlag = "-B" //Read-Write
}
sandboxArgs := []string{
// The executable to run
"-x", c.Path,
// Set the hostname to something consistent
"-H", "android-build",
// Use the current working dir
"--cwd", wd,
// No time limit
"-t", "0",
// Keep all environment variables, we already filter them out
// in soong_ui
"-e",
// Mount /proc read-write, necessary to run a nested nsjail or minijail0
"--proc_rw",
// Use a consistent user & group.
// Note that these are mapped back to the real UID/GID when
// doing filesystem operations, so they're rather arbitrary.
"-u", "nobody",
"-g", sandboxConfig.group,
// Set high values, as nsjail uses low defaults.
"--rlimit_as", "soft",
"--rlimit_core", "soft",
"--rlimit_cpu", "soft",
"--rlimit_fsize", "soft",
"--rlimit_nofile", "soft",
// For now, just map everything. Make most things readonly.
"-R", "/",
// Mount a writable tmp dir
"-B", "/tmp",
// Mount source
srcDirMountFlag, sandboxConfig.srcDir,
//Mount out dir as read-write
"-B", sandboxConfig.outDir,
// Disable newcgroup for now, since it may require newer kernels
// TODO: try out cgroups
"--disable_clone_newcgroup",
// Only log important warnings / errors
"-q",
}
// Mount srcDir RW allowlists as Read-Write
if len(c.config.sandboxConfig.SrcDirRWAllowlist()) > 0 && !c.config.sandboxConfig.SrcDirIsRO() {
errMsg := `Product source tree has been set as ReadWrite, RW allowlist not necessary.
To recover, either
1. Unset BUILD_BROKEN_SRC_DIR_IS_WRITABLE #or
2. Unset BUILD_BROKEN_SRC_DIR_RW_ALLOWLIST`
c.ctx.Fatalln(errMsg)
}
for _, srcDirChild := range c.config.sandboxConfig.SrcDirRWAllowlist() {
sandboxArgs = append(sandboxArgs, "-B", srcDirChild)
}
if _, err := os.Stat(sandboxConfig.distDir); !os.IsNotExist(err) {
//Mount dist dir as read-write if it already exists
sandboxArgs = append(sandboxArgs, "-B", sandboxConfig.distDir)
}
if c.Sandbox.AllowBuildBrokenUsesNetwork && c.config.BuildBrokenUsesNetwork() {
c.ctx.Printf("AllowBuildBrokenUsesNetwork: %v", c.Sandbox.AllowBuildBrokenUsesNetwork)
c.ctx.Printf("BuildBrokenUsesNetwork: %v", c.config.BuildBrokenUsesNetwork())
sandboxArgs = append(sandboxArgs, "-N")
} else if dlv, _ := c.config.Environment().Get("SOONG_DELVE"); dlv != "" {
// The debugger is enabled and soong_build will pause until a remote delve process connects, allow
// network connections.
sandboxArgs = append(sandboxArgs, "-N")
}
// Stop nsjail from parsing arguments
sandboxArgs = append(sandboxArgs, "--")
c.Args = append(sandboxArgs, c.Args[1:]...)
c.Path = nsjailPath
env := Environment(c.Env)
if _, hasUser := env.Get("USER"); hasUser {
env.Set("USER", "nobody")
}
c.Env = []string(env)
}