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