2388f64e76
Test: Presubmits. Change-Id: I22c08f282019050da1198cce1f92f5d825ee649f
255 lines
6 KiB
Go
255 lines
6 KiB
Go
// 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
|
|
}
|