xref: /aosp_15_r20/build/soong/ui/build/sandbox_linux.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
1// Copyright 2017 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package build
16
17import (
18	"bytes"
19	"os"
20	"os/exec"
21	"os/user"
22	"path/filepath"
23	"strings"
24	"sync"
25)
26
27type Sandbox struct {
28	Enabled              bool
29	DisableWhenUsingGoma bool
30
31	AllowBuildBrokenUsesNetwork bool
32}
33
34var (
35	noSandbox    = Sandbox{}
36	basicSandbox = Sandbox{
37		Enabled: true,
38	}
39
40	dumpvarsSandbox = basicSandbox
41	katiSandbox     = basicSandbox
42	soongSandbox    = basicSandbox
43	ninjaSandbox    = Sandbox{
44		Enabled:              true,
45		DisableWhenUsingGoma: true,
46
47		AllowBuildBrokenUsesNetwork: true,
48	}
49)
50
51const (
52	nsjailPath = "prebuilts/build-tools/linux-x86/bin/nsjail"
53)
54
55var sandboxConfig struct {
56	once sync.Once
57
58	working bool
59	group   string
60	srcDir  string
61	outDir  string
62	distDir string
63}
64
65func (c *Cmd) sandboxSupported() bool {
66	if !c.Sandbox.Enabled {
67		return false
68	}
69
70	// Goma is incompatible with PID namespaces and Mount namespaces. b/122767582
71	if c.Sandbox.DisableWhenUsingGoma && c.config.UseGoma() {
72		return false
73	}
74
75	sandboxConfig.once.Do(func() {
76		sandboxConfig.group = "nogroup"
77		if _, err := user.LookupGroup(sandboxConfig.group); err != nil {
78			sandboxConfig.group = "nobody"
79		}
80
81		// These directories will be bind mounted
82		// so we need full non-symlink paths
83		sandboxConfig.srcDir = absPath(c.ctx, ".")
84		if derefPath, err := filepath.EvalSymlinks(sandboxConfig.srcDir); err == nil {
85			sandboxConfig.srcDir = absPath(c.ctx, derefPath)
86		}
87		sandboxConfig.outDir = absPath(c.ctx, c.config.OutDir())
88		if derefPath, err := filepath.EvalSymlinks(sandboxConfig.outDir); err == nil {
89			sandboxConfig.outDir = absPath(c.ctx, derefPath)
90		}
91		sandboxConfig.distDir = absPath(c.ctx, c.config.DistDir())
92		if derefPath, err := filepath.EvalSymlinks(sandboxConfig.distDir); err == nil {
93			sandboxConfig.distDir = absPath(c.ctx, derefPath)
94		}
95
96		sandboxArgs := []string{
97			"-H", "android-build",
98			"-e",
99			"-u", "nobody",
100			"-g", sandboxConfig.group,
101			"-R", "/",
102			// Mount tmp before srcDir
103			// srcDir is /tmp/.* in integration tests, which is a child dir of /tmp
104			// nsjail throws an error if a child dir is mounted before its parent
105			"-B", "/tmp",
106			c.config.sandboxConfig.SrcDirMountFlag(), sandboxConfig.srcDir,
107			"-B", sandboxConfig.outDir,
108		}
109
110		if _, err := os.Stat(sandboxConfig.distDir); !os.IsNotExist(err) {
111			//Mount dist dir as read-write if it already exists
112			sandboxArgs = append(sandboxArgs, "-B",
113				sandboxConfig.distDir)
114		}
115
116		sandboxArgs = append(sandboxArgs,
117			"--disable_clone_newcgroup",
118			"--",
119			"/bin/bash", "-c", `if [ $(hostname) == "android-build" ]; then echo "Android" "Success"; else echo Failure; fi`)
120
121		cmd := exec.CommandContext(c.ctx.Context, nsjailPath, sandboxArgs...)
122
123		cmd.Env = c.config.Environment().Environ()
124
125		c.ctx.Verboseln(cmd.Args)
126		data, err := cmd.CombinedOutput()
127		if err == nil && bytes.Contains(data, []byte("Android Success")) {
128			sandboxConfig.working = true
129			return
130		}
131
132		c.ctx.Println("Build sandboxing disabled due to nsjail error.")
133
134		for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") {
135			c.ctx.Verboseln(line)
136		}
137
138		if err == nil {
139			c.ctx.Verboseln("nsjail exited successfully, but without the correct output")
140		} else if e, ok := err.(*exec.ExitError); ok {
141			c.ctx.Verbosef("nsjail failed with %v", e.ProcessState.String())
142		} else {
143			c.ctx.Verbosef("nsjail failed with %v", err)
144		}
145	})
146
147	return sandboxConfig.working
148}
149
150// Assumes input path is absolute, clean, and if applicable, an evaluated
151// symlink. If path is not a subdirectory of src dir or relative path
152// cannot be determined, return the input untouched.
153func (c *Cmd) relFromSrcDir(path string) string {
154	if !strings.HasPrefix(path, sandboxConfig.srcDir) {
155		return path
156	}
157
158	rel, err := filepath.Rel(sandboxConfig.srcDir, path)
159	if err != nil {
160		return path
161	}
162
163	return rel
164}
165
166func (c *Cmd) dirArg(path string) string {
167	if !c.config.UseABFS() {
168		return path
169	}
170
171	rel := c.relFromSrcDir(path)
172
173	return path + ":" + filepath.Join(abfsSrcDir, rel)
174}
175
176func (c *Cmd) srcDirArg() string {
177	return c.dirArg(sandboxConfig.srcDir)
178}
179
180func (c *Cmd) outDirArg() string {
181	return c.dirArg(sandboxConfig.outDir)
182}
183
184func (c *Cmd) distDirArg() string {
185	return c.dirArg(sandboxConfig.distDir)
186}
187
188// When configured to use ABFS, we need to allow the creation of the /src
189// directory. Therefore, we cannot mount the root "/" directory as read-only.
190// Instead, we individually mount the children of "/" as RO.
191func (c *Cmd) readMountArgs() []string {
192	if !c.config.UseABFS() {
193		// For now, just map everything. Make most things readonly.
194		return []string{"-R", "/"}
195	}
196
197	entries, err := os.ReadDir("/")
198	if err != nil {
199		// If we can't read "/", just use the default non-ABFS behavior.
200		return []string{"-R", "/"}
201	}
202
203	args := make([]string, 0, 2*len(entries))
204	for _, ent := range entries {
205		args = append(args, "-R", "/"+ent.Name())
206	}
207
208	return args
209}
210
211func (c *Cmd) workDir() string {
212	if !c.config.UseABFS() {
213		wd, _ := os.Getwd()
214		return wd
215	}
216
217	return abfsSrcDir
218}
219
220func (c *Cmd) wrapSandbox() {
221	wd := c.workDir()
222
223	var sandboxArgs []string
224	sandboxArgs = append(sandboxArgs,
225		// The executable to run
226		"-x", c.Path,
227
228		// Set the hostname to something consistent
229		"-H", "android-build",
230
231		// Use the current working dir
232		"--cwd", wd,
233
234		// No time limit
235		"-t", "0",
236
237		// Keep all environment variables, we already filter them out
238		// in soong_ui
239		"-e",
240
241		// Mount /proc read-write, necessary to run a nested nsjail or minijail0
242		"--proc_rw",
243
244		// Use a consistent user & group.
245		// Note that these are mapped back to the real UID/GID when
246		// doing filesystem operations, so they're rather arbitrary.
247		"-u", "nobody",
248		"-g", sandboxConfig.group,
249
250		// Set high values, as nsjail uses low defaults.
251		"--rlimit_as", "soft",
252		"--rlimit_core", "soft",
253		"--rlimit_cpu", "soft",
254		"--rlimit_fsize", "soft",
255		"--rlimit_nofile", "soft",
256	)
257
258	sandboxArgs = append(sandboxArgs,
259		c.readMountArgs()...,
260	)
261
262	sandboxArgs = append(sandboxArgs,
263		// Mount a writable tmp dir
264		"-B", "/tmp",
265
266		// Mount source
267		c.config.sandboxConfig.SrcDirMountFlag(), c.srcDirArg(),
268
269		//Mount out dir as read-write
270		"-B", c.outDirArg(),
271
272		// Disable newcgroup for now, since it may require newer kernels
273		// TODO: try out cgroups
274		"--disable_clone_newcgroup",
275
276		// Only log important warnings / errors
277		"-q",
278	)
279	if c.config.UseABFS() {
280		sandboxArgs = append(sandboxArgs, "-B", "{ABFS_DIR}")
281	}
282
283	// Mount srcDir RW allowlists as Read-Write
284	if len(c.config.sandboxConfig.SrcDirRWAllowlist()) > 0 && !c.config.sandboxConfig.SrcDirIsRO() {
285		errMsg := `Product source tree has been set as ReadWrite, RW allowlist not necessary.
286			To recover, either
287			1. Unset BUILD_BROKEN_SRC_DIR_IS_WRITABLE #or
288			2. Unset BUILD_BROKEN_SRC_DIR_RW_ALLOWLIST`
289		c.ctx.Fatalln(errMsg)
290	}
291	for _, srcDirChild := range c.config.sandboxConfig.SrcDirRWAllowlist() {
292		sandboxArgs = append(sandboxArgs, "-B", srcDirChild)
293	}
294
295	if _, err := os.Stat(sandboxConfig.distDir); !os.IsNotExist(err) {
296		//Mount dist dir as read-write if it already exists
297		sandboxArgs = append(sandboxArgs, "-B", c.distDirArg())
298	}
299
300	if c.Sandbox.AllowBuildBrokenUsesNetwork && c.config.BuildBrokenUsesNetwork() {
301		c.ctx.Printf("AllowBuildBrokenUsesNetwork: %v", c.Sandbox.AllowBuildBrokenUsesNetwork)
302		c.ctx.Printf("BuildBrokenUsesNetwork: %v", c.config.BuildBrokenUsesNetwork())
303		sandboxArgs = append(sandboxArgs, "-N")
304	} else if dlv, _ := c.config.Environment().Get("SOONG_DELVE"); dlv != "" {
305		// The debugger is enabled and soong_build will pause until a remote delve process connects, allow
306		// network connections.
307		sandboxArgs = append(sandboxArgs, "-N")
308	}
309
310	// Stop nsjail from parsing arguments
311	sandboxArgs = append(sandboxArgs, "--")
312
313	c.Args = append(sandboxArgs, c.Args[1:]...)
314	c.Path = nsjailPath
315
316	env := Environment(c.Env)
317	if _, hasUser := env.Get("USER"); hasUser {
318		env.Set("USER", "nobody")
319	}
320	c.Env = []string(env)
321}
322