| // 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. | |
| // This tool tries to prohibit access to tools on the system on which the build | |
| // is run. | |
| // | |
| // The rationale is that if the build uses a binary that is not shipped in the | |
| // source tree, it is unknowable which version of that binary will be installed | |
| // and therefore the output of the build will be unpredictable. Therefore, we | |
| // should make every effort to use only tools under our control. | |
| // | |
| // This is currently implemented by a "sandbox" that sets $PATH to a specific, | |
| // single directory and creates a symlink for every binary in $PATH in it. That | |
| // symlink will point to path_interposer, which then uses an embedded | |
| // configuration to determine whether to allow access to the binary (in which | |
| // case it calls the original executable) or not (in which case it fails). It | |
| // can also optionally log invocations. | |
| // | |
| // This, of course, does not help if one invokes the tool in question with its | |
| // full path. | |
| 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) | |
| } | |
| exitCode, err := Main(os.Stdout, os.Stderr, interposer, os.Args, mainOpts{ | |
| 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 { | |
| 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 { | |
| 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 | |
| } |