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