blob: 51ebff1178a5fd61d5a37ef940ee9a3ddbb65b88 [file] [log] [blame]
// 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"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/google/blueprint/microfactory"
"android/soong/ui/build/paths"
"android/soong/ui/metrics"
)
// parsePathDir returns the list of filenames of readable files in a directory.
// This does not recurse into subdirectories, and does not contain subdirectory
// names in the list.
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
}
// SetupLitePath is the "lite" version of SetupPath used for dumpvars, or other
// places that does not need the full logging capabilities of path_interposer,
// wants the minimal performance overhead, and still get the benefits of $PATH
// hermeticity.
func SetupLitePath(ctx Context, config Config, tmpDir string) {
// Don't replace the path twice.
if config.pathReplaced {
return
}
ctx.BeginTrace(metrics.RunSetupTool, "litepath")
defer ctx.EndTrace()
origPath, _ := config.Environment().Get("PATH")
// If tmpDir is empty, the default TMPDIR is used from config.
if tmpDir == "" {
tmpDir, _ = config.Environment().Get("TMPDIR")
}
myPath := filepath.Join(tmpDir, "path")
ensureEmptyDirectoriesExist(ctx, myPath)
os.Setenv("PATH", origPath)
// Iterate over the ACL configuration of host tools for this build.
for name, pathConfig := range paths.Configuration {
if !pathConfig.Symlink {
// Excludes 'Forbidden' and 'LinuxOnlyPrebuilt' PathConfigs.
continue
}
origExec, err := exec.LookPath(name)
if err != nil {
continue
}
origExec, err = filepath.Abs(origExec)
if err != nil {
continue
}
// Symlink allowed host tools into a directory for hermeticity.
err = os.Symlink(origExec, filepath.Join(myPath, name))
if err != nil {
ctx.Fatalln("Failed to create symlink:", err)
}
}
myPath, _ = filepath.Abs(myPath)
// Set up the checked-in prebuilts path directory for the current host OS.
prebuiltsPath, _ := filepath.Abs("prebuilts/build-tools/path/" + runtime.GOOS + "-x86")
myPath = prebuiltsPath + string(os.PathListSeparator) + myPath
if value, _ := config.Environment().Get("BUILD_BROKEN_PYTHON_IS_PYTHON2"); value == "true" {
py2Path, _ := filepath.Abs("prebuilts/build-tools/path/" + runtime.GOOS + "-x86/py2")
if info, err := os.Stat(py2Path); err == nil && info.IsDir() {
myPath = py2Path + string(os.PathListSeparator) + myPath
}
} else if value != "" {
ctx.Fatalf("BUILD_BROKEN_PYTHON_IS_PYTHON2 can only be set to 'true' or an empty string, but got %s\n", value)
}
// Set $PATH to be the directories containing the host tool symlinks, and
// the prebuilts directory for the current host OS.
config.Environment().Set("PATH", myPath)
config.pathReplaced = true
}
// SetupPath uses the path_interposer to intercept calls to $PATH binaries, and
// communicates with the interposer to validate allowed $PATH binaries at
// runtime, using logs as a medium.
//
// This results in hermetic directories in $PATH containing only allowed host
// tools for the build, and replaces $PATH to contain *only* these directories,
// and enables an incremental restriction of tools allowed in the $PATH without
// breaking existing use cases.
func SetupPath(ctx Context, config Config) {
// Don't replace $PATH twice.
if config.pathReplaced {
return
}
ctx.BeginTrace(metrics.RunSetupTool, "path")
defer ctx.EndTrace()
origPath, _ := config.Environment().Get("PATH")
// The directory containing symlinks from binaries in $PATH to the interposer.
myPath := filepath.Join(config.OutDir(), ".path")
interposer := myPath + "_interposer"
// Bootstrap the path_interposer Go binary with microfactory.
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)
}
// Save the original $PATH in a file.
if err := ioutil.WriteFile(interposer+"_origpath", []byte(origPath), 0777); err != nil {
ctx.Fatalln("Failed to write original path:", err)
}
// Communication with the path interposer works over log entries. Set up the
// listener channel for the log entries here.
entries, err := paths.LogListener(ctx.Context, interposer+"_log")
if err != nil {
ctx.Fatalln("Failed to listen for path logs:", err)
}
// Loop over all log entry listener channels to validate usage of only
// allowed PATH tools at runtime.
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
}
}
// Compute the error message along with the process tree, including
// parents, for this log line.
procPrints := []string{
"See https://android.googlesource.com/platform/build/+/main/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))
}
}
// Validate usage against disallowed or missing PATH tools.
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)
}
}
}
}()
// Create the .path directory.
ensureEmptyDirectoriesExist(ctx, myPath)
// Compute the full list of binaries available in the original $PATH.
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)...)
}
if config.Environment().IsEnvTrue("TEMPORARY_DISABLE_PATH_RESTRICTIONS") {
ctx.Fatalln("TEMPORARY_DISABLE_PATH_RESTRICTIONS was a temporary migration method, and is now obsolete.")
}
// Create symlinks from the path_interposer binary to all binaries for each
// directory in the original $PATH. This ensures that during the build,
// every call to a binary that's expected to be in the $PATH will be
// intercepted by the path_interposer binary, and validated with the
// LogEntry listener above at build time.
for _, name := range execs {
if !paths.GetConfig(name).Symlink {
// Ignore host tools that shouldn't be symlinked.
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)
// We put some prebuilts in $PATH, since it's infeasible to add dependencies
// for all of them.
prebuiltsPath, _ := filepath.Abs("prebuilts/build-tools/path/" + runtime.GOOS + "-x86")
myPath = prebuiltsPath + string(os.PathListSeparator) + myPath
if value, _ := config.Environment().Get("BUILD_BROKEN_PYTHON_IS_PYTHON2"); value == "true" {
py2Path, _ := filepath.Abs("prebuilts/build-tools/path/" + runtime.GOOS + "-x86/py2")
if info, err := os.Stat(py2Path); err == nil && info.IsDir() {
myPath = py2Path + string(os.PathListSeparator) + myPath
}
} else if value != "" {
ctx.Fatalf("BUILD_BROKEN_PYTHON_IS_PYTHON2 can only be set to 'true' or an empty string, but got %s\n", value)
}
// Replace the $PATH variable with the path_interposer symlinks, and
// checked-in prebuilts.
config.Environment().Set("PATH", myPath)
config.pathReplaced = true
}