| // 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/+/main/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 |
| } |