diff options
-rw-r--r-- | cmd/path_interposer/Android.bp | 20 | ||||
-rw-r--r-- | cmd/path_interposer/main.go | 247 | ||||
-rw-r--r-- | cmd/path_interposer/main_test.go | 196 | ||||
-rw-r--r-- | python/scripts/stub_template_host.txt | 2 | ||||
-rw-r--r-- | ui/build/Android.bp | 14 | ||||
-rw-r--r-- | ui/build/build.go | 2 | ||||
-rw-r--r-- | ui/build/config.go | 2 | ||||
-rw-r--r-- | ui/build/path.go | 149 | ||||
-rw-r--r-- | ui/build/paths/config.go | 160 | ||||
-rw-r--r-- | ui/build/paths/logs.go | 211 | ||||
-rw-r--r-- | ui/build/paths/logs_test.go | 154 | ||||
-rw-r--r-- | ui/build/sandbox/darwin/global.sb | 6 |
12 files changed, 1161 insertions, 2 deletions
diff --git a/cmd/path_interposer/Android.bp b/cmd/path_interposer/Android.bp new file mode 100644 index 00000000..41a219f9 --- /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 00000000..cd28b960 --- /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 00000000..4b25c446 --- /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 b90a28b5..386298eb 100644 --- a/python/scripts/stub_template_host.txt +++ b/python/scripts/stub_template_host.txt @@ -18,8 +18,6 @@ def SearchPathEnv(name): 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 58098943..1fe5b6f2 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 @@ bootstrap_go_package { "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 78eb6a32..66dbf03f 100644 --- a/ui/build/build.go +++ b/ui/build/build.go @@ -140,6 +140,8 @@ func Build(ctx Context, config Config, what int) { 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 5622dff6..6f2d24a5 100644 --- a/ui/build/config.go +++ b/ui/build/config.go @@ -51,6 +51,8 @@ type configImpl struct { 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 00000000..52658ef1 --- /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 00000000..548b0385 --- /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 00000000..6c24968b --- /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 00000000..3b1005fb --- /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 47d0c434..e32b64b6 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) |