Merge "Revert "Revert "Revert "Revert "Add path interposer"""""
diff --git a/cmd/path_interposer/Android.bp b/cmd/path_interposer/Android.bp
new file mode 100644
index 0000000..41a219f
--- /dev/null
+++ b/cmd/path_interposer/Android.bp
@@ -0,0 +1,20 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+blueprint_go_binary {
+    name: "path_interposer",
+    deps: ["soong-ui-build-paths"],
+    srcs: ["main.go"],
+    testSrcs: ["main_test.go"],
+}
diff --git a/cmd/path_interposer/main.go b/cmd/path_interposer/main.go
new file mode 100644
index 0000000..cd28b96
--- /dev/null
+++ b/cmd/path_interposer/main.go
@@ -0,0 +1,247 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"syscall"
+
+	"android/soong/ui/build/paths"
+)
+
+func main() {
+	interposer, err := os.Executable()
+	if err != nil {
+		fmt.Fprintln(os.Stderr, "Unable to locate interposer executable:", err)
+		os.Exit(1)
+	}
+
+	if fi, err := os.Lstat(interposer); err == nil {
+		if fi.Mode()&os.ModeSymlink != 0 {
+			link, err := os.Readlink(interposer)
+			if err != nil {
+				fmt.Fprintln(os.Stderr, "Unable to read link to interposer executable:", err)
+				os.Exit(1)
+			}
+			if filepath.IsAbs(link) {
+				interposer = link
+			} else {
+				interposer = filepath.Join(filepath.Dir(interposer), link)
+			}
+		}
+	} else {
+		fmt.Fprintln(os.Stderr, "Unable to stat interposer executable:", err)
+		os.Exit(1)
+	}
+
+	disableError := false
+	if e, ok := os.LookupEnv("TEMPORARY_DISABLE_PATH_RESTRICTIONS"); ok {
+		disableError = e == "1" || e == "y" || e == "yes" || e == "on" || e == "true"
+	}
+
+	exitCode, err := Main(os.Stdout, os.Stderr, interposer, os.Args, mainOpts{
+		disableError: disableError,
+
+		sendLog:       paths.SendLog,
+		config:        paths.GetConfig,
+		lookupParents: lookupParents,
+	})
+	if err != nil {
+		fmt.Fprintln(os.Stderr, err.Error())
+	}
+	os.Exit(exitCode)
+}
+
+var usage = fmt.Errorf(`To use the PATH interposer:
+ * Write the original PATH variable to <interposer>_origpath
+ * Set up a directory of symlinks to the PATH interposer, and use that in PATH
+
+If a tool isn't in the allowed list, a log will be posted to the unix domain
+socket at <interposer>_log.`)
+
+type mainOpts struct {
+	disableError bool
+
+	sendLog       func(logSocket string, entry *paths.LogEntry, done chan interface{})
+	config        func(name string) paths.PathConfig
+	lookupParents func() []paths.LogProcess
+}
+
+func Main(stdout, stderr io.Writer, interposer string, args []string, opts mainOpts) (int, error) {
+	base := filepath.Base(args[0])
+
+	origPathFile := interposer + "_origpath"
+	if base == filepath.Base(interposer) {
+		return 1, usage
+	}
+
+	origPath, err := ioutil.ReadFile(origPathFile)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return 1, usage
+		} else {
+			return 1, fmt.Errorf("Failed to read original PATH: %v", err)
+		}
+	}
+
+	cmd := &exec.Cmd{
+		Args: args,
+		Env:  os.Environ(),
+
+		Stdin:  os.Stdin,
+		Stdout: stdout,
+		Stderr: stderr,
+	}
+
+	if err := os.Setenv("PATH", string(origPath)); err != nil {
+		return 1, fmt.Errorf("Failed to set PATH env: %v", err)
+	}
+
+	if config := opts.config(base); config.Log || config.Error {
+		var procs []paths.LogProcess
+		if opts.lookupParents != nil {
+			procs = opts.lookupParents()
+		}
+
+		if opts.sendLog != nil {
+			waitForLog := make(chan interface{})
+			opts.sendLog(interposer+"_log", &paths.LogEntry{
+				Basename: base,
+				Args:     args,
+				Parents:  procs,
+			}, waitForLog)
+			defer func() { <-waitForLog }()
+		}
+		if config.Error && !opts.disableError {
+			return 1, fmt.Errorf("%q is not allowed to be used. See https://android.googlesource.com/platform/build/+/master/Changes.md#PATH_Tools for more information.", base)
+		}
+	}
+
+	cmd.Path, err = exec.LookPath(base)
+	if err != nil {
+		return 1, err
+	}
+
+	if err = cmd.Run(); err != nil {
+		if exitErr, ok := err.(*exec.ExitError); ok {
+			if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
+				if status.Exited() {
+					return status.ExitStatus(), nil
+				} else if status.Signaled() {
+					exitCode := 128 + int(status.Signal())
+					return exitCode, nil
+				} else {
+					return 1, exitErr
+				}
+			} else {
+				return 1, nil
+			}
+		}
+	}
+
+	return 0, nil
+}
+
+type procEntry struct {
+	Pid     int
+	Ppid    int
+	Command string
+}
+
+func readProcs() map[int]procEntry {
+	cmd := exec.Command("ps", "-o", "pid,ppid,command")
+	data, err := cmd.Output()
+	if err != nil {
+		return nil
+	}
+
+	return parseProcs(data)
+}
+
+func parseProcs(data []byte) map[int]procEntry {
+	lines := bytes.Split(data, []byte("\n"))
+	if len(lines) < 2 {
+		return nil
+	}
+	// Remove the header
+	lines = lines[1:]
+
+	ret := make(map[int]procEntry, len(lines))
+	for _, line := range lines {
+		fields := bytes.SplitN(line, []byte(" "), 2)
+		if len(fields) != 2 {
+			continue
+		}
+
+		pid, err := strconv.Atoi(string(fields[0]))
+		if err != nil {
+			continue
+		}
+
+		line = bytes.TrimLeft(fields[1], " ")
+
+		fields = bytes.SplitN(line, []byte(" "), 2)
+		if len(fields) != 2 {
+			continue
+		}
+
+		ppid, err := strconv.Atoi(string(fields[0]))
+		if err != nil {
+			continue
+		}
+
+		ret[pid] = procEntry{
+			Pid:     pid,
+			Ppid:    ppid,
+			Command: string(bytes.TrimLeft(fields[1], " ")),
+		}
+	}
+
+	return ret
+}
+
+func lookupParents() []paths.LogProcess {
+	procs := readProcs()
+	if procs == nil {
+		return nil
+	}
+
+	list := []paths.LogProcess{}
+	pid := os.Getpid()
+	for {
+		entry, ok := procs[pid]
+		if !ok {
+			break
+		}
+
+		list = append([]paths.LogProcess{
+			{
+				Pid:     pid,
+				Command: entry.Command,
+			},
+		}, list...)
+
+		pid = entry.Ppid
+	}
+
+	return list
+}
diff --git a/cmd/path_interposer/main_test.go b/cmd/path_interposer/main_test.go
new file mode 100644
index 0000000..4b25c44
--- /dev/null
+++ b/cmd/path_interposer/main_test.go
@@ -0,0 +1,196 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"android/soong/ui/build/paths"
+)
+
+var tmpDir string
+var origPATH string
+
+func TestMain(m *testing.M) {
+	os.Exit(func() int {
+		var err error
+		tmpDir, err = ioutil.TempDir("", "interposer_test")
+		if err != nil {
+			panic(err)
+		}
+		defer os.RemoveAll(tmpDir)
+
+		origPATH = os.Getenv("PATH")
+		err = os.Setenv("PATH", "")
+		if err != nil {
+			panic(err)
+		}
+
+		return m.Run()
+	}())
+}
+
+func setup(t *testing.T) string {
+	f, err := ioutil.TempFile(tmpDir, "interposer")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer f.Close()
+
+	err = ioutil.WriteFile(f.Name()+"_origpath", []byte(origPATH), 0666)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return f.Name()
+}
+
+func TestInterposer(t *testing.T) {
+	interposer := setup(t)
+
+	logConfig := func(name string) paths.PathConfig {
+		if name == "true" {
+			return paths.PathConfig{
+				Log:   false,
+				Error: false,
+			}
+		} else if name == "path_interposer_test_not_allowed" {
+			return paths.PathConfig{
+				Log:   false,
+				Error: true,
+			}
+		}
+		return paths.PathConfig{
+			Log:   true,
+			Error: false,
+		}
+	}
+
+	testCases := []struct {
+		name string
+		args []string
+
+		exitCode int
+		err      error
+		logEntry string
+	}{
+		{
+			name: "direct call",
+			args: []string{interposer},
+
+			exitCode: 1,
+			err:      usage,
+		},
+		{
+			name: "relative call",
+			args: []string{filepath.Base(interposer)},
+
+			exitCode: 1,
+			err:      usage,
+		},
+		{
+			name: "true",
+			args: []string{"/my/path/true"},
+		},
+		{
+			name: "relative true",
+			args: []string{"true"},
+		},
+		{
+			name: "exit code",
+			args: []string{"bash", "-c", "exit 42"},
+
+			exitCode: 42,
+			logEntry: "bash",
+		},
+		{
+			name: "signal",
+			args: []string{"bash", "-c", "kill -9 $$"},
+
+			exitCode: 137,
+			logEntry: "bash",
+		},
+		{
+			name: "does not exist",
+			args: []string{"path_interposer_test_does_not_exist"},
+
+			exitCode: 1,
+			err:      fmt.Errorf(`exec: "path_interposer_test_does_not_exist": executable file not found in $PATH`),
+			logEntry: "path_interposer_test_does_not_exist",
+		},
+		{
+			name: "not allowed",
+			args: []string{"path_interposer_test_not_allowed"},
+
+			exitCode: 1,
+			err:      fmt.Errorf(`"path_interposer_test_not_allowed" is not allowed to be used. See https://android.googlesource.com/platform/build/+/master/Changes.md#PATH_Tools for more information.`),
+			logEntry: "path_interposer_test_not_allowed",
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			logged := false
+			logFunc := func(logSocket string, entry *paths.LogEntry, done chan interface{}) {
+				defer close(done)
+
+				logged = true
+				if entry.Basename != testCase.logEntry {
+					t.Errorf("unexpected log entry:\nwant: %q\n got: %q", testCase.logEntry, entry.Basename)
+				}
+			}
+
+			exitCode, err := Main(ioutil.Discard, ioutil.Discard, interposer, testCase.args, mainOpts{
+				sendLog: logFunc,
+				config:  logConfig,
+			})
+
+			errstr := func(err error) string {
+				if err == nil {
+					return ""
+				}
+				return err.Error()
+			}
+			if errstr(testCase.err) != errstr(err) {
+				t.Errorf("unexpected error:\nwant: %v\n got: %v", testCase.err, err)
+			}
+			if testCase.exitCode != exitCode {
+				t.Errorf("expected exit code %d, got %d", testCase.exitCode, exitCode)
+			}
+			if !logged && testCase.logEntry != "" {
+				t.Errorf("no log entry, but expected %q", testCase.logEntry)
+			}
+		})
+	}
+}
+
+func TestMissingPath(t *testing.T) {
+	interposer := setup(t)
+	err := os.Remove(interposer + "_origpath")
+	if err != nil {
+		t.Fatalf("Failed to remove:", err)
+	}
+
+	exitCode, err := Main(ioutil.Discard, ioutil.Discard, interposer, []string{"true"}, mainOpts{})
+	if err != usage {
+		t.Errorf("Unexpected error:\n got: %v\nwant: %v", err, usage)
+	}
+	if exitCode != 1 {
+		t.Errorf("expected exit code %d, got %d", 1, exitCode)
+	}
+}
diff --git a/python/scripts/stub_template_host.txt b/python/scripts/stub_template_host.txt
index b90a28b..386298e 100644
--- a/python/scripts/stub_template_host.txt
+++ b/python/scripts/stub_template_host.txt
@@ -18,8 +18,6 @@
   for directory in search_path:
     if directory == '': continue
     path = os.path.join(directory, name)
-    if os.path.islink(path):
-      path = os.path.realpath(path)
     # Check if path is actual executable file.
     if os.path.isfile(path) and os.access(path, os.X_OK):
       return path
diff --git a/ui/build/Android.bp b/ui/build/Android.bp
index 5809894..1fe5b6f 100644
--- a/ui/build/Android.bp
+++ b/ui/build/Android.bp
@@ -13,9 +13,22 @@
 // limitations under the License.
 
 bootstrap_go_package {
+    name: "soong-ui-build-paths",
+    pkgPath: "android/soong/ui/build/paths",
+    srcs: [
+        "paths/config.go",
+        "paths/logs.go",
+    ],
+    testSrcs: [
+        "paths/logs_test.go",
+    ],
+}
+
+bootstrap_go_package {
     name: "soong-ui-build",
     pkgPath: "android/soong/ui/build",
     deps: [
+        "soong-ui-build-paths",
         "soong-ui-logger",
         "soong-ui-tracer",
         "soong-shared",
@@ -33,6 +46,7 @@
         "finder.go",
         "kati.go",
         "ninja.go",
+        "path.go",
         "proc_sync.go",
         "signal.go",
         "soong.go",
diff --git a/ui/build/build.go b/ui/build/build.go
index 78eb6a3..66dbf03 100644
--- a/ui/build/build.go
+++ b/ui/build/build.go
@@ -140,6 +140,8 @@
 
 	ensureEmptyDirectoriesExist(ctx, config.TempDir())
 
+	SetupPath(ctx, config)
+
 	if what&BuildProductConfig != 0 {
 		// Run make for product config
 		runMakeProductConfig(ctx, config)
diff --git a/ui/build/config.go b/ui/build/config.go
index 5622dff..6f2d24a 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -51,6 +51,8 @@
 	targetDeviceDir string
 
 	brokenDupRules bool
+
+	pathReplaced bool
 }
 
 const srcDirFileCheck = "build/soong/root.bp"
diff --git a/ui/build/path.go b/ui/build/path.go
new file mode 100644
index 0000000..52658ef
--- /dev/null
+++ b/ui/build/path.go
@@ -0,0 +1,149 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/google/blueprint/microfactory"
+
+	"android/soong/ui/build/paths"
+)
+
+func parsePathDir(dir string) []string {
+	f, err := os.Open(dir)
+	if err != nil {
+		return nil
+	}
+	defer f.Close()
+
+	if s, err := f.Stat(); err != nil || !s.IsDir() {
+		return nil
+	}
+
+	infos, err := f.Readdir(-1)
+	if err != nil {
+		return nil
+	}
+
+	ret := make([]string, 0, len(infos))
+	for _, info := range infos {
+		if m := info.Mode(); !m.IsDir() && m&0111 != 0 {
+			ret = append(ret, info.Name())
+		}
+	}
+	return ret
+}
+
+func SetupPath(ctx Context, config Config) {
+	if config.pathReplaced {
+		return
+	}
+
+	ctx.BeginTrace("path")
+	defer ctx.EndTrace()
+
+	origPath, _ := config.Environment().Get("PATH")
+	myPath := filepath.Join(config.OutDir(), ".path")
+	interposer := myPath + "_interposer"
+
+	var cfg microfactory.Config
+	cfg.Map("android/soong", "build/soong")
+	cfg.TrimPath, _ = filepath.Abs(".")
+	if _, err := microfactory.Build(&cfg, interposer, "android/soong/cmd/path_interposer"); err != nil {
+		ctx.Fatalln("Failed to build path interposer:", err)
+	}
+
+	if err := ioutil.WriteFile(interposer+"_origpath", []byte(origPath), 0777); err != nil {
+		ctx.Fatalln("Failed to write original path:", err)
+	}
+
+	entries, err := paths.LogListener(ctx.Context, interposer+"_log")
+	if err != nil {
+		ctx.Fatalln("Failed to listen for path logs:", err)
+	}
+
+	go func() {
+		for log := range entries {
+			curPid := os.Getpid()
+			for i, proc := range log.Parents {
+				if proc.Pid == curPid {
+					log.Parents = log.Parents[i:]
+					break
+				}
+			}
+			procPrints := []string{
+				"See https://android.googlesource.com/platform/build/+/master/Changes.md#PATH_Tools for more information.",
+			}
+			if len(log.Parents) > 0 {
+				procPrints = append(procPrints, "Process tree:")
+				for i, proc := range log.Parents {
+					procPrints = append(procPrints, fmt.Sprintf("%s→ %s", strings.Repeat(" ", i), proc.Command))
+				}
+			}
+
+			config := paths.GetConfig(log.Basename)
+			if config.Error {
+				ctx.Printf("Disallowed PATH tool %q used: %#v", log.Basename, log.Args)
+				for _, line := range procPrints {
+					ctx.Println(line)
+				}
+			} else {
+				ctx.Verbosef("Unknown PATH tool %q used: %#v", log.Basename, log.Args)
+				for _, line := range procPrints {
+					ctx.Verboseln(line)
+				}
+			}
+		}
+	}()
+
+	ensureEmptyDirectoriesExist(ctx, myPath)
+
+	var execs []string
+	for _, pathEntry := range filepath.SplitList(origPath) {
+		if pathEntry == "" {
+			// Ignore the current directory
+			continue
+		}
+		// TODO(dwillemsen): remove path entries under TOP? or anything
+		// that looks like an android source dir? They won't exist on
+		// the build servers, since they're added by envsetup.sh.
+		// (Except for the JDK, which is configured in ui/build/config.go)
+
+		execs = append(execs, parsePathDir(pathEntry)...)
+	}
+
+	allowAllSymlinks := config.Environment().IsEnvTrue("TEMPORARY_DISABLE_PATH_RESTRICTIONS")
+	for _, name := range execs {
+		if !paths.GetConfig(name).Symlink && !allowAllSymlinks {
+			continue
+		}
+
+		err := os.Symlink("../.path_interposer", filepath.Join(myPath, name))
+		// Intentionally ignore existing files -- that means that we
+		// just created it, and the first one should win.
+		if err != nil && !os.IsExist(err) {
+			ctx.Fatalln("Failed to create symlink:", err)
+		}
+	}
+
+	myPath, _ = filepath.Abs(myPath)
+	config.Environment().Set("PATH", myPath)
+	config.pathReplaced = true
+}
diff --git a/ui/build/paths/config.go b/ui/build/paths/config.go
new file mode 100644
index 0000000..548b038
--- /dev/null
+++ b/ui/build/paths/config.go
@@ -0,0 +1,160 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package paths
+
+import "runtime"
+
+type PathConfig struct {
+	// Whether to create the symlink in the new PATH for this tool.
+	Symlink bool
+
+	// Whether to log about usages of this tool to the soong.log
+	Log bool
+
+	// Whether to exit with an error instead of invoking the underlying tool.
+	Error bool
+}
+
+var Allowed = PathConfig{
+	Symlink: true,
+	Log:     false,
+	Error:   false,
+}
+
+var Forbidden = PathConfig{
+	Symlink: false,
+	Log:     true,
+	Error:   true,
+}
+
+// The configuration used if the tool is not listed in the config below.
+// Currently this will create the symlink, but log a warning. In the future,
+// I expect this to move closer to Forbidden.
+var Missing = PathConfig{
+	Symlink: true,
+	Log:     true,
+	Error:   false,
+}
+
+func GetConfig(name string) PathConfig {
+	if config, ok := Configuration[name]; ok {
+		return config
+	}
+	return Missing
+}
+
+var Configuration = map[string]PathConfig{
+	"awk":       Allowed,
+	"basename":  Allowed,
+	"bash":      Allowed,
+	"bzip2":     Allowed,
+	"cat":       Allowed,
+	"chmod":     Allowed,
+	"cmp":       Allowed,
+	"comm":      Allowed,
+	"cp":        Allowed,
+	"cut":       Allowed,
+	"date":      Allowed,
+	"dd":        Allowed,
+	"diff":      Allowed,
+	"dirname":   Allowed,
+	"echo":      Allowed,
+	"egrep":     Allowed,
+	"env":       Allowed,
+	"expr":      Allowed,
+	"find":      Allowed,
+	"getconf":   Allowed,
+	"getopt":    Allowed,
+	"git":       Allowed,
+	"grep":      Allowed,
+	"gzip":      Allowed,
+	"head":      Allowed,
+	"hexdump":   Allowed,
+	"hostname":  Allowed,
+	"jar":       Allowed,
+	"java":      Allowed,
+	"javap":     Allowed,
+	"ln":        Allowed,
+	"ls":        Allowed,
+	"m4":        Allowed,
+	"make":      Allowed,
+	"md5sum":    Allowed,
+	"mkdir":     Allowed,
+	"mktemp":    Allowed,
+	"mv":        Allowed,
+	"openssl":   Allowed,
+	"patch":     Allowed,
+	"perl":      Allowed,
+	"pstree":    Allowed,
+	"python":    Allowed,
+	"python2.7": Allowed,
+	"python3":   Allowed,
+	"readlink":  Allowed,
+	"realpath":  Allowed,
+	"rm":        Allowed,
+	"rsync":     Allowed,
+	"runalarm":  Allowed,
+	"sed":       Allowed,
+	"setsid":    Allowed,
+	"sh":        Allowed,
+	"sha256sum": Allowed,
+	"sha512sum": Allowed,
+	"sort":      Allowed,
+	"stat":      Allowed,
+	"sum":       Allowed,
+	"tar":       Allowed,
+	"tail":      Allowed,
+	"touch":     Allowed,
+	"tr":        Allowed,
+	"true":      Allowed,
+	"uname":     Allowed,
+	"uniq":      Allowed,
+	"unzip":     Allowed,
+	"wc":        Allowed,
+	"which":     Allowed,
+	"whoami":    Allowed,
+	"xargs":     Allowed,
+	"xmllint":   Allowed,
+	"xz":        Allowed,
+	"zip":       Allowed,
+	"zipinfo":   Allowed,
+
+	// Host toolchain is removed. In-tree toolchain should be used instead.
+	// GCC also can't find cc1 with this implementation.
+	"ar":         Forbidden,
+	"as":         Forbidden,
+	"cc":         Forbidden,
+	"clang":      Forbidden,
+	"clang++":    Forbidden,
+	"gcc":        Forbidden,
+	"g++":        Forbidden,
+	"ld":         Forbidden,
+	"ld.bfd":     Forbidden,
+	"ld.gold":    Forbidden,
+	"pkg-config": Forbidden,
+
+	// We've got prebuilts of these
+	//"dtc":  Forbidden,
+	//"lz4":  Forbidden,
+	//"lz4c": Forbidden,
+}
+
+func init() {
+	if runtime.GOOS == "darwin" {
+		Configuration["md5"] = Allowed
+		Configuration["sw_vers"] = Allowed
+		Configuration["xcrun"] = Allowed
+	}
+}
diff --git a/ui/build/paths/logs.go b/ui/build/paths/logs.go
new file mode 100644
index 0000000..6c24968
--- /dev/null
+++ b/ui/build/paths/logs.go
@@ -0,0 +1,211 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package paths
+
+import (
+	"context"
+	"encoding/gob"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"os"
+	"path/filepath"
+	"runtime"
+	"sync"
+	"syscall"
+	"time"
+)
+
+type LogProcess struct {
+	Pid     int
+	Command string
+}
+
+type LogEntry struct {
+	Basename string
+	Args     []string
+	Parents  []LogProcess
+}
+
+const timeoutDuration = time.Duration(100) * time.Millisecond
+
+type socketAddrFunc func(string) (string, func(), error)
+
+func procFallback(name string) (string, func(), error) {
+	d, err := os.Open(filepath.Dir(name))
+	if err != nil {
+		return "", func() {}, err
+	}
+
+	return fmt.Sprintf("/proc/self/fd/%d/%s", d.Fd(), filepath.Base(name)), func() {
+		d.Close()
+	}, nil
+}
+
+func tmpFallback(name string) (addr string, cleanup func(), err error) {
+	d, err := ioutil.TempDir("/tmp", "log_sock")
+	if err != nil {
+		cleanup = func() {}
+		return
+	}
+	cleanup = func() {
+		os.RemoveAll(d)
+	}
+
+	dir := filepath.Dir(name)
+
+	absDir, err := filepath.Abs(dir)
+	if err != nil {
+		return
+	}
+
+	err = os.Symlink(absDir, filepath.Join(d, "d"))
+	if err != nil {
+		return
+	}
+
+	addr = filepath.Join(d, "d", filepath.Base(name))
+
+	return
+}
+
+func getSocketAddr(name string) (string, func(), error) {
+	maxNameLen := len(syscall.RawSockaddrUnix{}.Path)
+
+	if len(name) < maxNameLen {
+		return name, func() {}, nil
+	}
+
+	if runtime.GOOS == "linux" {
+		addr, cleanup, err := procFallback(name)
+		if err == nil {
+			if len(addr) < maxNameLen {
+				return addr, cleanup, nil
+			}
+		}
+		cleanup()
+	}
+
+	addr, cleanup, err := tmpFallback(name)
+	if err == nil {
+		if len(addr) < maxNameLen {
+			return addr, cleanup, nil
+		}
+	}
+	cleanup()
+
+	return name, func() {}, fmt.Errorf("Path to socket is still over size limit, fallbacks failed.")
+}
+
+func dial(name string, lookup socketAddrFunc, timeout time.Duration) (net.Conn, error) {
+	socket, cleanup, err := lookup(name)
+	defer cleanup()
+	if err != nil {
+		return nil, err
+	}
+
+	dialer := &net.Dialer{
+		Timeout: timeout,
+	}
+	return dialer.Dial("unix", socket)
+}
+
+func listen(name string, lookup socketAddrFunc) (net.Listener, error) {
+	socket, cleanup, err := lookup(name)
+	defer cleanup()
+	if err != nil {
+		return nil, err
+	}
+
+	return net.Listen("unix", socket)
+}
+
+func SendLog(logSocket string, entry *LogEntry, done chan interface{}) {
+	sendLog(logSocket, getSocketAddr, timeoutDuration, entry, done)
+}
+
+func sendLog(logSocket string, lookup socketAddrFunc, timeout time.Duration, entry *LogEntry, done chan interface{}) {
+	defer close(done)
+
+	conn, err := dial(logSocket, lookup, timeout)
+	if err != nil {
+		return
+	}
+	defer conn.Close()
+
+	if timeout != 0 {
+		conn.SetDeadline(time.Now().Add(timeout))
+	}
+
+	enc := gob.NewEncoder(conn)
+	enc.Encode(entry)
+}
+
+func LogListener(ctx context.Context, logSocket string) (chan *LogEntry, error) {
+	return logListener(ctx, logSocket, getSocketAddr)
+}
+
+func logListener(ctx context.Context, logSocket string, lookup socketAddrFunc) (chan *LogEntry, error) {
+	ret := make(chan *LogEntry, 5)
+
+	if err := os.Remove(logSocket); err != nil && !os.IsNotExist(err) {
+		return nil, err
+	}
+
+	ln, err := listen(logSocket, lookup)
+	if err != nil {
+		return nil, err
+	}
+
+	go func() {
+		for {
+			select {
+			case <-ctx.Done():
+				ln.Close()
+			}
+		}
+	}()
+
+	go func() {
+		var wg sync.WaitGroup
+		defer func() {
+			wg.Wait()
+			close(ret)
+		}()
+
+		for {
+			conn, err := ln.Accept()
+			if err != nil {
+				ln.Close()
+				break
+			}
+			conn.SetDeadline(time.Now().Add(timeoutDuration))
+			wg.Add(1)
+
+			go func() {
+				defer wg.Done()
+				defer conn.Close()
+
+				dec := gob.NewDecoder(conn)
+				entry := &LogEntry{}
+				if err := dec.Decode(entry); err != nil {
+					return
+				}
+				ret <- entry
+			}()
+		}
+	}()
+	return ret, nil
+}
diff --git a/ui/build/paths/logs_test.go b/ui/build/paths/logs_test.go
new file mode 100644
index 0000000..3b1005f
--- /dev/null
+++ b/ui/build/paths/logs_test.go
@@ -0,0 +1,154 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package paths
+
+import (
+	"context"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"reflect"
+	"runtime"
+	"strings"
+	"testing"
+)
+
+func TestSendLog(t *testing.T) {
+	t.Run("Short name", func(t *testing.T) {
+		d, err := ioutil.TempDir("", "s")
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer os.RemoveAll(d)
+		f := filepath.Join(d, "s")
+
+		testSendLog(t, f, getSocketAddr)
+	})
+
+	testLongName := func(t *testing.T, lookup socketAddrFunc) {
+		d, err := ioutil.TempDir("", strings.Repeat("s", 150))
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer os.RemoveAll(d)
+		f := filepath.Join(d, strings.Repeat("s", 10))
+
+		testSendLog(t, f, lookup)
+	}
+
+	// Using a name longer than the ~100 limit of the underlying calls to bind, etc
+	t.Run("Long name", func(t *testing.T) {
+		testLongName(t, getSocketAddr)
+	})
+
+	if runtime.GOOS == "linux" {
+		t.Run("Long name proc fallback", func(t *testing.T) {
+			testLongName(t, procFallback)
+		})
+	}
+
+	t.Run("Long name tmp fallback", func(t *testing.T) {
+		testLongName(t, tmpFallback)
+	})
+}
+
+func testSendLog(t *testing.T, socket string, lookup socketAddrFunc) {
+	recv, err := logListener(context.Background(), socket, lookup)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	go func() {
+		for i := 0; i < 10; i++ {
+			sendLog(socket, lookup, 0, &LogEntry{
+				Basename: "test",
+				Args:     []string{"foo", "bar"},
+			}, make(chan interface{}))
+		}
+	}()
+
+	count := 0
+	for {
+		entry := <-recv
+		if entry == nil {
+			if count != 10 {
+				t.Errorf("Expected 10 logs, got %d", count)
+			}
+			return
+		}
+
+		ref := LogEntry{
+			Basename: "test",
+			Args:     []string{"foo", "bar"},
+		}
+		if !reflect.DeepEqual(ref, *entry) {
+			t.Fatalf("Bad log entry: %v", entry)
+		}
+		count++
+
+		if count == 10 {
+			return
+		}
+	}
+}
+
+func TestSendLogError(t *testing.T) {
+	d, err := ioutil.TempDir("", "log_socket")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(d)
+
+	// Missing log sockets should not block waiting for the timeout to elapse
+	t.Run("Missing file", func(t *testing.T) {
+		sendLog(filepath.Join(d, "missing"), getSocketAddr, 0, &LogEntry{}, make(chan interface{}))
+	})
+
+	// Non-sockets should not block waiting for the timeout to elapse
+	t.Run("Regular file", func(t *testing.T) {
+		f := filepath.Join(d, "file")
+		if fp, err := os.Create(f); err == nil {
+			fp.Close()
+		} else {
+			t.Fatal(err)
+		}
+
+		sendLog(f, getSocketAddr, 0, &LogEntry{}, make(chan interface{}))
+	})
+
+	// If the reader is stuck, we should be able to make progress
+	t.Run("Reader not reading", func(t *testing.T) {
+		f := filepath.Join(d, "sock1")
+
+		ln, err := listen(f, getSocketAddr)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer ln.Close()
+
+		done := make(chan bool, 1)
+		go func() {
+			for i := 0; i < 10; i++ {
+				sendLog(f, getSocketAddr, timeoutDuration, &LogEntry{
+					// Ensure a relatively large payload
+					Basename: strings.Repeat(" ", 100000),
+				}, make(chan interface{}))
+			}
+			done <- true
+		}()
+
+		<-done
+	})
+}
diff --git a/ui/build/sandbox/darwin/global.sb b/ui/build/sandbox/darwin/global.sb
index 47d0c43..e32b64b 100644
--- a/ui/build/sandbox/darwin/global.sb
+++ b/ui/build/sandbox/darwin/global.sb
@@ -35,6 +35,12 @@
     (global-name-regex #"^com\.apple\.distributed_notifications") ; xcodebuild in Soong
 )
 
+; Allow suid /bin/ps to function
+(allow process-exec (literal "/bin/ps") (with no-sandbox))
+
+; Allow path_interposer unix domain socket without logging
+(allow network-outbound (literal (string-append (param "OUT_DIR") "/.path_interposer_log")))
+
 ; Allow executing any file
 (allow process-exec*)
 (allow process-fork)