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	// Set $PATH to be the directories containing the host tool symlinks, and
113	// the prebuilts directory for the current host OS.
114	config.Environment().Set("PATH", myPath)
115	config.pathReplaced = true
116}
117
118// SetupPath uses the path_interposer to intercept calls to $PATH binaries, and
119// communicates with the interposer to validate allowed $PATH binaries at
120// runtime, using logs as a medium.
121//
122// This results in hermetic directories in $PATH containing only allowed host
123// tools for the build, and replaces $PATH to contain *only* these directories,
124// and enables an incremental restriction of tools allowed in the $PATH without
125// breaking existing use cases.
126func SetupPath(ctx Context, config Config) {
127	// Don't replace $PATH twice.
128	if config.pathReplaced {
129		return
130	}
131
132	ctx.BeginTrace(metrics.RunSetupTool, "path")
133	defer ctx.EndTrace()
134
135	origPath, _ := config.Environment().Get("PATH")
136	// The directory containing symlinks from binaries in $PATH to the interposer.
137	myPath := filepath.Join(config.OutDir(), ".path")
138	interposer := myPath + "_interposer"
139
140	// Bootstrap the path_interposer Go binary with microfactory.
141	var cfg microfactory.Config
142	cfg.Map("android/soong", "build/soong")
143	cfg.TrimPath, _ = filepath.Abs(".")
144	if _, err := microfactory.Build(&cfg, interposer, "android/soong/cmd/path_interposer"); err != nil {
145		ctx.Fatalln("Failed to build path interposer:", err)
146	}
147
148	// Save the original $PATH in a file.
149	if err := ioutil.WriteFile(interposer+"_origpath", []byte(origPath), 0777); err != nil {
150		ctx.Fatalln("Failed to write original path:", err)
151	}
152
153	// Communication with the path interposer works over log entries. Set up the
154	// listener channel for the log entries here.
155	entries, err := paths.LogListener(ctx.Context, interposer+"_log")
156	if err != nil {
157		ctx.Fatalln("Failed to listen for path logs:", err)
158	}
159
160	// Loop over all log entry listener channels to validate usage of only
161	// allowed PATH tools at runtime.
162	go func() {
163		for log := range entries {
164			curPid := os.Getpid()
165			for i, proc := range log.Parents {
166				if proc.Pid == curPid {
167					log.Parents = log.Parents[i:]
168					break
169				}
170			}
171			// Compute the error message along with the process tree, including
172			// parents, for this log line.
173			procPrints := []string{
174				"See https://android.googlesource.com/platform/build/+/master/Changes.md#PATH_Tools for more information.",
175			}
176			if len(log.Parents) > 0 {
177				procPrints = append(procPrints, "Process tree:")
178				for i, proc := range log.Parents {
179					procPrints = append(procPrints, fmt.Sprintf("%s→ %s", strings.Repeat(" ", i), proc.Command))
180				}
181			}
182
183			// Validate usage against disallowed or missing PATH tools.
184			config := paths.GetConfig(log.Basename)
185			if config.Error {
186				ctx.Printf("Disallowed PATH tool %q used: %#v", log.Basename, log.Args)
187				for _, line := range procPrints {
188					ctx.Println(line)
189				}
190			} else {
191				ctx.Verbosef("Unknown PATH tool %q used: %#v", log.Basename, log.Args)
192				for _, line := range procPrints {
193					ctx.Verboseln(line)
194				}
195			}
196		}
197	}()
198
199	// Create the .path directory.
200	ensureEmptyDirectoriesExist(ctx, myPath)
201
202	// Compute the full list of binaries available in the original $PATH.
203	var execs []string
204	for _, pathEntry := range filepath.SplitList(origPath) {
205		if pathEntry == "" {
206			// Ignore the current directory
207			continue
208		}
209		// TODO(dwillemsen): remove path entries under TOP? or anything
210		// that looks like an android source dir? They won't exist on
211		// the build servers, since they're added by envsetup.sh.
212		// (Except for the JDK, which is configured in ui/build/config.go)
213
214		execs = append(execs, parsePathDir(pathEntry)...)
215	}
216
217	if config.Environment().IsEnvTrue("TEMPORARY_DISABLE_PATH_RESTRICTIONS") {
218		ctx.Fatalln("TEMPORARY_DISABLE_PATH_RESTRICTIONS was a temporary migration method, and is now obsolete.")
219	}
220
221	// Create symlinks from the path_interposer binary to all binaries for each
222	// directory in the original $PATH. This ensures that during the build,
223	// every call to a binary that's expected to be in the $PATH will be
224	// intercepted by the path_interposer binary, and validated with the
225	// LogEntry listener above at build time.
226	for _, name := range execs {
227		if !paths.GetConfig(name).Symlink {
228			// Ignore host tools that shouldn't be symlinked.
229			continue
230		}
231
232		err := os.Symlink("../.path_interposer", filepath.Join(myPath, name))
233		// Intentionally ignore existing files -- that means that we
234		// just created it, and the first one should win.
235		if err != nil && !os.IsExist(err) {
236			ctx.Fatalln("Failed to create symlink:", err)
237		}
238	}
239
240	myPath, _ = filepath.Abs(myPath)
241
242	// We put some prebuilts in $PATH, since it's infeasible to add dependencies
243	// for all of them.
244	prebuiltsPath, _ := filepath.Abs("prebuilts/build-tools/path/" + runtime.GOOS + "-x86")
245	myPath = prebuiltsPath + string(os.PathListSeparator) + myPath
246
247	// Replace the $PATH variable with the path_interposer symlinks, and
248	// checked-in prebuilts.
249	config.Environment().Set("PATH", myPath)
250	config.pathReplaced = true
251}
252