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
15// This tool tries to prohibit access to tools on the system on which the build
16// is run.
17//
18// The rationale is that if the build uses a binary that is not shipped in the
19// source tree, it is unknowable which version of that binary will be installed
20// and therefore the output of the build will be unpredictable. Therefore, we
21// should make every effort to use only tools under our control.
22//
23// This is currently implemented by a "sandbox" that sets $PATH to a specific,
24// single directory and creates a symlink for every binary in $PATH in it. That
25// symlink will point to path_interposer, which then uses an embedded
26// configuration to determine whether to allow access to the binary (in which
27// case it calls the original executable) or not (in which case it fails). It
28// can also optionally log invocations.
29//
30// This, of course, does not help if one invokes the tool in question with its
31// full path.
32package main
33
34import (
35	"bytes"
36	"fmt"
37	"io"
38	"io/ioutil"
39	"os"
40	"os/exec"
41	"path/filepath"
42	"strconv"
43	"syscall"
44
45	"android/soong/ui/build/paths"
46)
47
48func main() {
49	interposer, err := os.Executable()
50	if err != nil {
51		fmt.Fprintln(os.Stderr, "Unable to locate interposer executable:", err)
52		os.Exit(1)
53	}
54
55	if fi, err := os.Lstat(interposer); err == nil {
56		if fi.Mode()&os.ModeSymlink != 0 {
57			link, err := os.Readlink(interposer)
58			if err != nil {
59				fmt.Fprintln(os.Stderr, "Unable to read link to interposer executable:", err)
60				os.Exit(1)
61			}
62			if filepath.IsAbs(link) {
63				interposer = link
64			} else {
65				interposer = filepath.Join(filepath.Dir(interposer), link)
66			}
67		}
68	} else {
69		fmt.Fprintln(os.Stderr, "Unable to stat interposer executable:", err)
70		os.Exit(1)
71	}
72
73	exitCode, err := Main(os.Stdout, os.Stderr, interposer, os.Args, mainOpts{
74		sendLog:       paths.SendLog,
75		config:        paths.GetConfig,
76		lookupParents: lookupParents,
77	})
78	if err != nil {
79		fmt.Fprintln(os.Stderr, err.Error())
80	}
81	os.Exit(exitCode)
82}
83
84var usage = fmt.Errorf(`To use the PATH interposer:
85 * Write the original PATH variable to <interposer>_origpath
86 * Set up a directory of symlinks to the PATH interposer, and use that in PATH
87
88If a tool isn't in the allowed list, a log will be posted to the unix domain
89socket at <interposer>_log.`)
90
91type mainOpts struct {
92	sendLog       func(logSocket string, entry *paths.LogEntry, done chan interface{})
93	config        func(name string) paths.PathConfig
94	lookupParents func() []paths.LogProcess
95}
96
97func Main(stdout, stderr io.Writer, interposer string, args []string, opts mainOpts) (int, error) {
98	base := filepath.Base(args[0])
99
100	origPathFile := interposer + "_origpath"
101	if base == filepath.Base(interposer) {
102		return 1, usage
103	}
104
105	origPath, err := ioutil.ReadFile(origPathFile)
106	if err != nil {
107		if os.IsNotExist(err) {
108			return 1, usage
109		} else {
110			return 1, fmt.Errorf("Failed to read original PATH: %v", err)
111		}
112	}
113
114	cmd := &exec.Cmd{
115		Args: args,
116		Env:  os.Environ(),
117
118		Stdin:  os.Stdin,
119		Stdout: stdout,
120		Stderr: stderr,
121	}
122
123	if err := os.Setenv("PATH", string(origPath)); err != nil {
124		return 1, fmt.Errorf("Failed to set PATH env: %v", err)
125	}
126
127	if config := opts.config(base); config.Log || config.Error {
128		var procs []paths.LogProcess
129		if opts.lookupParents != nil {
130			procs = opts.lookupParents()
131		}
132
133		if opts.sendLog != nil {
134			waitForLog := make(chan interface{})
135			opts.sendLog(interposer+"_log", &paths.LogEntry{
136				Basename: base,
137				Args:     args,
138				Parents:  procs,
139			}, waitForLog)
140			defer func() { <-waitForLog }()
141		}
142		if config.Error {
143			return 1, fmt.Errorf("%q is not allowed to be used. See https://android.googlesource.com/platform/build/+/main/Changes.md#PATH_Tools for more information.", base)
144		}
145	}
146
147	cmd.Path, err = exec.LookPath(base)
148	if err != nil {
149		return 1, err
150	}
151
152	if err = cmd.Run(); err != nil {
153		if exitErr, ok := err.(*exec.ExitError); ok {
154			if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
155				if status.Exited() {
156					return status.ExitStatus(), nil
157				} else if status.Signaled() {
158					exitCode := 128 + int(status.Signal())
159					return exitCode, nil
160				} else {
161					return 1, exitErr
162				}
163			} else {
164				return 1, nil
165			}
166		}
167	}
168
169	return 0, nil
170}
171
172type procEntry struct {
173	Pid     int
174	Ppid    int
175	Command string
176}
177
178func readProcs() map[int]procEntry {
179	cmd := exec.Command("ps", "-o", "pid,ppid,command")
180	data, err := cmd.Output()
181	if err != nil {
182		return nil
183	}
184
185	return parseProcs(data)
186}
187
188func parseProcs(data []byte) map[int]procEntry {
189	lines := bytes.Split(data, []byte("\n"))
190	if len(lines) < 2 {
191		return nil
192	}
193	// Remove the header
194	lines = lines[1:]
195
196	ret := make(map[int]procEntry, len(lines))
197	for _, line := range lines {
198		fields := bytes.SplitN(line, []byte(" "), 2)
199		if len(fields) != 2 {
200			continue
201		}
202
203		pid, err := strconv.Atoi(string(fields[0]))
204		if err != nil {
205			continue
206		}
207
208		line = bytes.TrimLeft(fields[1], " ")
209
210		fields = bytes.SplitN(line, []byte(" "), 2)
211		if len(fields) != 2 {
212			continue
213		}
214
215		ppid, err := strconv.Atoi(string(fields[0]))
216		if err != nil {
217			continue
218		}
219
220		ret[pid] = procEntry{
221			Pid:     pid,
222			Ppid:    ppid,
223			Command: string(bytes.TrimLeft(fields[1], " ")),
224		}
225	}
226
227	return ret
228}
229
230func lookupParents() []paths.LogProcess {
231	procs := readProcs()
232	if procs == nil {
233		return nil
234	}
235
236	list := []paths.LogProcess{}
237	pid := os.Getpid()
238	for {
239		entry, ok := procs[pid]
240		if !ok {
241			break
242		}
243
244		list = append([]paths.LogProcess{
245			{
246				Pid:     pid,
247				Command: entry.Command,
248			},
249		}, list...)
250
251		pid = entry.Ppid
252	}
253
254	return list
255}
256