xref: /aosp_15_r20/build/soong/ui/build/path.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
1// Copyright 2018 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	"fmt"
19	"io/ioutil"
20	"os"
21	"os/exec"
22	"path/filepath"
23	"runtime"
24	"strings"
25
26	"github.com/google/blueprint/microfactory"
27
28	"android/soong/ui/build/paths"
29	"android/soong/ui/metrics"
30)
31
32// parsePathDir returns the list of filenames of readable files in a directory.
33// This does not recurse into subdirectories, and does not contain subdirectory
34// names in the list.
35func parsePathDir(dir string) []string {
36	f, err := os.Open(dir)
37	if err != nil {
38		return nil
39	}
40	defer f.Close()
41
42	if s, err := f.Stat(); err != nil || !s.IsDir() {
43		return nil
44	}
45
46	infos, err := f.Readdir(-1)
47	if err != nil {
48		return nil
49	}
50
51	ret := make([]string, 0, len(infos))
52	for _, info := range infos {
53		if m := info.Mode(); !m.IsDir() && m&0111 != 0 {
54			ret = append(ret, info.Name())
55		}
56	}
57	return ret
58}
59
60func updatePathForSandbox(config Config) {
61	wd, err := os.Getwd()
62	if err != nil {
63		return
64	}
65
66	var newPath []string
67	if path, ok := config.Environment().Get("PATH"); ok && path != "" {
68		entries := strings.Split(path, string(filepath.ListSeparator))
69		for _, ent := range entries {
70			newPath = append(newPath, config.sandboxPath(wd, ent))
71		}
72	}
73	config.Environment().Set("PATH", strings.Join(newPath, string(filepath.ListSeparator)))
74}
75
76// SetupLitePath is the "lite" version of SetupPath used for dumpvars, or other
77// places that does not need the full logging capabilities of path_interposer,
78// wants the minimal performance overhead, and still get the benefits of $PATH
79// hermeticity.
80func SetupLitePath(ctx Context, config Config, tmpDir string) {
81	// Don't replace the path twice.
82	if config.pathReplaced {
83		return
84	}
85
86	ctx.BeginTrace(metrics.RunSetupTool, "litepath")
87	defer ctx.EndTrace()
88
89	origPath, _ := config.Environment().Get("PATH")
90
91	// If tmpDir is empty, the default TMPDIR is used from config.
92	if tmpDir == "" {
93		tmpDir, _ = config.Environment().Get("TMPDIR")
94	}
95	myPath := filepath.Join(tmpDir, "path")
96	ensureEmptyDirectoriesExist(ctx, myPath)
97
98	os.Setenv("PATH", origPath)
99	// Iterate over the ACL configuration of host tools for this build.
100	for name, pathConfig := range paths.Configuration {
101		if !pathConfig.Symlink {
102			// Excludes 'Forbidden' and 'LinuxOnlyPrebuilt' PathConfigs.
103			continue
104		}
105
106		origExec, err := exec.LookPath(name)
107		if err != nil {
108			continue
109		}
110		origExec, err = filepath.Abs(origExec)
111		if err != nil {
112			continue
113		}
114
115		// Symlink allowed host tools into a directory for hermeticity.
116		err = os.Symlink(origExec, filepath.Join(myPath, name))
117		if err != nil {
118			ctx.Fatalln("Failed to create symlink:", err)
119		}
120	}
121
122	myPath, _ = filepath.Abs(myPath)
123
124	// Set up the checked-in prebuilts path directory for the current host OS.
125	prebuiltsPath, _ := filepath.Abs("prebuilts/build-tools/path/" + runtime.GOOS + "-x86")
126	myPath = prebuiltsPath + string(os.PathListSeparator) + myPath
127
128	// Set $PATH to be the directories containing the host tool symlinks, and
129	// the prebuilts directory for the current host OS.
130	config.Environment().Set("PATH", myPath)
131	updatePathForSandbox(config)
132	config.pathReplaced = true
133}
134
135// SetupPath uses the path_interposer to intercept calls to $PATH binaries, and
136// communicates with the interposer to validate allowed $PATH binaries at
137// runtime, using logs as a medium.
138//
139// This results in hermetic directories in $PATH containing only allowed host
140// tools for the build, and replaces $PATH to contain *only* these directories,
141// and enables an incremental restriction of tools allowed in the $PATH without
142// breaking existing use cases.
143func SetupPath(ctx Context, config Config) {
144	// Don't replace $PATH twice.
145	if config.pathReplaced {
146		return
147	}
148
149	ctx.BeginTrace(metrics.RunSetupTool, "path")
150	defer ctx.EndTrace()
151
152	origPath, _ := config.Environment().Get("PATH")
153	// The directory containing symlinks from binaries in $PATH to the interposer.
154	myPath := filepath.Join(config.OutDir(), ".path")
155	interposer := myPath + "_interposer"
156
157	// Bootstrap the path_interposer Go binary with microfactory.
158	var cfg microfactory.Config
159	cfg.Map("android/soong", "build/soong")
160	cfg.TrimPath, _ = filepath.Abs(".")
161	if _, err := microfactory.Build(&cfg, interposer, "android/soong/cmd/path_interposer"); err != nil {
162		ctx.Fatalln("Failed to build path interposer:", err)
163	}
164
165	// Save the original $PATH in a file.
166	if err := ioutil.WriteFile(interposer+"_origpath", []byte(origPath), 0777); err != nil {
167		ctx.Fatalln("Failed to write original path:", err)
168	}
169
170	// Communication with the path interposer works over log entries. Set up the
171	// listener channel for the log entries here.
172	entries, err := paths.LogListener(ctx.Context, interposer+"_log")
173	if err != nil {
174		ctx.Fatalln("Failed to listen for path logs:", err)
175	}
176
177	// Loop over all log entry listener channels to validate usage of only
178	// allowed PATH tools at runtime.
179	go func() {
180		for log := range entries {
181			curPid := os.Getpid()
182			for i, proc := range log.Parents {
183				if proc.Pid == curPid {
184					log.Parents = log.Parents[i:]
185					break
186				}
187			}
188			// Compute the error message along with the process tree, including
189			// parents, for this log line.
190			procPrints := []string{
191				"See https://android.googlesource.com/platform/build/+/main/Changes.md#PATH_Tools for more information.",
192			}
193			if len(log.Parents) > 0 {
194				procPrints = append(procPrints, "Process tree:")
195				for i, proc := range log.Parents {
196					procPrints = append(procPrints, fmt.Sprintf("%s→ %s", strings.Repeat(" ", i), proc.Command))
197				}
198			}
199
200			// Validate usage against disallowed or missing PATH tools.
201			config := paths.GetConfig(log.Basename)
202			if config.Error {
203				ctx.Printf("Disallowed PATH tool %q used: %#v", log.Basename, log.Args)
204				for _, line := range procPrints {
205					ctx.Println(line)
206				}
207			} else {
208				ctx.Verbosef("Unknown PATH tool %q used: %#v", log.Basename, log.Args)
209				for _, line := range procPrints {
210					ctx.Verboseln(line)
211				}
212			}
213		}
214	}()
215
216	// Create the .path directory.
217	ensureEmptyDirectoriesExist(ctx, myPath)
218
219	// Compute the full list of binaries available in the original $PATH.
220	var execs []string
221	for _, pathEntry := range filepath.SplitList(origPath) {
222		if pathEntry == "" {
223			// Ignore the current directory
224			continue
225		}
226		// TODO(dwillemsen): remove path entries under TOP? or anything
227		// that looks like an android source dir? They won't exist on
228		// the build servers, since they're added by envsetup.sh.
229		// (Except for the JDK, which is configured in ui/build/config.go)
230
231		execs = append(execs, parsePathDir(pathEntry)...)
232	}
233
234	if config.Environment().IsEnvTrue("TEMPORARY_DISABLE_PATH_RESTRICTIONS") {
235		ctx.Fatalln("TEMPORARY_DISABLE_PATH_RESTRICTIONS was a temporary migration method, and is now obsolete.")
236	}
237
238	// Create symlinks from the path_interposer binary to all binaries for each
239	// directory in the original $PATH. This ensures that during the build,
240	// every call to a binary that's expected to be in the $PATH will be
241	// intercepted by the path_interposer binary, and validated with the
242	// LogEntry listener above at build time.
243	for _, name := range execs {
244		if !paths.GetConfig(name).Symlink {
245			// Ignore host tools that shouldn't be symlinked.
246			continue
247		}
248
249		err := os.Symlink("../.path_interposer", filepath.Join(myPath, name))
250		// Intentionally ignore existing files -- that means that we
251		// just created it, and the first one should win.
252		if err != nil && !os.IsExist(err) {
253			ctx.Fatalln("Failed to create symlink:", err)
254		}
255	}
256
257	myPath, _ = filepath.Abs(myPath)
258
259	// We put some prebuilts in $PATH, since it's infeasible to add dependencies
260	// for all of them.
261	prebuiltsPath, _ := filepath.Abs("prebuilts/build-tools/path/" + runtime.GOOS + "-x86")
262	myPath = prebuiltsPath + string(os.PathListSeparator) + myPath
263
264	// Replace the $PATH variable with the path_interposer symlinks, and
265	// checked-in prebuilts.
266	config.Environment().Set("PATH", myPath)
267	updatePathForSandbox(config)
268	config.pathReplaced = true
269}
270