1// Copyright 2018 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package build 16 17import ( 18 "fmt" 19 "io/ioutil" 20 "os" 21 "os/exec" 22 "path/filepath" 23 "runtime" 24 "strings" 25 26 "github.com/google/blueprint/microfactory" 27 28 "android/soong/ui/build/paths" 29 "android/soong/ui/metrics" 30) 31 32// parsePathDir returns the list of filenames of readable files in a directory. 33// This does not recurse into subdirectories, and does not contain subdirectory 34// names in the list. 35func parsePathDir(dir string) []string { 36 f, err := os.Open(dir) 37 if err != nil { 38 return nil 39 } 40 defer f.Close() 41 42 if s, err := f.Stat(); err != nil || !s.IsDir() { 43 return nil 44 } 45 46 infos, err := f.Readdir(-1) 47 if err != nil { 48 return nil 49 } 50 51 ret := make([]string, 0, len(infos)) 52 for _, info := range infos { 53 if m := info.Mode(); !m.IsDir() && m&0111 != 0 { 54 ret = append(ret, info.Name()) 55 } 56 } 57 return ret 58} 59 60// SetupLitePath is the "lite" version of SetupPath used for dumpvars, or other 61// places that does not need the full logging capabilities of path_interposer, 62// wants the minimal performance overhead, and still get the benefits of $PATH 63// hermeticity. 64func SetupLitePath(ctx Context, config Config, tmpDir string) { 65 // Don't replace the path twice. 66 if config.pathReplaced { 67 return 68 } 69 70 ctx.BeginTrace(metrics.RunSetupTool, "litepath") 71 defer ctx.EndTrace() 72 73 origPath, _ := config.Environment().Get("PATH") 74 75 // If tmpDir is empty, the default TMPDIR is used from config. 76 if tmpDir == "" { 77 tmpDir, _ = config.Environment().Get("TMPDIR") 78 } 79 myPath := filepath.Join(tmpDir, "path") 80 ensureEmptyDirectoriesExist(ctx, myPath) 81 82 os.Setenv("PATH", origPath) 83 // Iterate over the ACL configuration of host tools for this build. 84 for name, pathConfig := range paths.Configuration { 85 if !pathConfig.Symlink { 86 // Excludes 'Forbidden' and 'LinuxOnlyPrebuilt' PathConfigs. 87 continue 88 } 89 90 origExec, err := exec.LookPath(name) 91 if err != nil { 92 continue 93 } 94 origExec, err = filepath.Abs(origExec) 95 if err != nil { 96 continue 97 } 98 99 // Symlink allowed host tools into a directory for hermeticity. 100 err = os.Symlink(origExec, filepath.Join(myPath, name)) 101 if err != nil { 102 ctx.Fatalln("Failed to create symlink:", err) 103 } 104 } 105 106 myPath, _ = filepath.Abs(myPath) 107 108 // Set up the checked-in prebuilts path directory for the current host OS. 109 prebuiltsPath, _ := filepath.Abs("prebuilts/build-tools/path/" + runtime.GOOS + "-x86") 110 myPath = prebuiltsPath + string(os.PathListSeparator) + myPath 111 112 if value, _ := config.Environment().Get("BUILD_BROKEN_PYTHON_IS_PYTHON2"); value == "true" { 113 py2Path, _ := filepath.Abs("prebuilts/build-tools/path/" + runtime.GOOS + "-x86/py2") 114 if info, err := os.Stat(py2Path); err == nil && info.IsDir() { 115 myPath = py2Path + string(os.PathListSeparator) + myPath 116 } 117 } else if value != "" { 118 ctx.Fatalf("BUILD_BROKEN_PYTHON_IS_PYTHON2 can only be set to 'true' or an empty string, but got %s\n", value) 119 } 120 121 // Set $PATH to be the directories containing the host tool symlinks, and 122 // the prebuilts directory for the current host OS. 123 config.Environment().Set("PATH", myPath) 124 config.pathReplaced = true 125} 126 127// SetupPath uses the path_interposer to intercept calls to $PATH binaries, and 128// communicates with the interposer to validate allowed $PATH binaries at 129// runtime, using logs as a medium. 130// 131// This results in hermetic directories in $PATH containing only allowed host 132// tools for the build, and replaces $PATH to contain *only* these directories, 133// and enables an incremental restriction of tools allowed in the $PATH without 134// breaking existing use cases. 135func SetupPath(ctx Context, config Config) { 136 // Don't replace $PATH twice. 137 if config.pathReplaced { 138 return 139 } 140 141 ctx.BeginTrace(metrics.RunSetupTool, "path") 142 defer ctx.EndTrace() 143 144 origPath, _ := config.Environment().Get("PATH") 145 // The directory containing symlinks from binaries in $PATH to the interposer. 146 myPath := filepath.Join(config.OutDir(), ".path") 147 interposer := myPath + "_interposer" 148 149 // Bootstrap the path_interposer Go binary with microfactory. 150 var cfg microfactory.Config 151 cfg.Map("android/soong", "build/soong") 152 cfg.TrimPath, _ = filepath.Abs(".") 153 if _, err := microfactory.Build(&cfg, interposer, "android/soong/cmd/path_interposer"); err != nil { 154 ctx.Fatalln("Failed to build path interposer:", err) 155 } 156 157 // Save the original $PATH in a file. 158 if err := ioutil.WriteFile(interposer+"_origpath", []byte(origPath), 0777); err != nil { 159 ctx.Fatalln("Failed to write original path:", err) 160 } 161 162 // Communication with the path interposer works over log entries. Set up the 163 // listener channel for the log entries here. 164 entries, err := paths.LogListener(ctx.Context, interposer+"_log") 165 if err != nil { 166 ctx.Fatalln("Failed to listen for path logs:", err) 167 } 168 169 // Loop over all log entry listener channels to validate usage of only 170 // allowed PATH tools at runtime. 171 go func() { 172 for log := range entries { 173 curPid := os.Getpid() 174 for i, proc := range log.Parents { 175 if proc.Pid == curPid { 176 log.Parents = log.Parents[i:] 177 break 178 } 179 } 180 // Compute the error message along with the process tree, including 181 // parents, for this log line. 182 procPrints := []string{ 183 "See https://android.googlesource.com/platform/build/+/master/Changes.md#PATH_Tools for more information.", 184 } 185 if len(log.Parents) > 0 { 186 procPrints = append(procPrints, "Process tree:") 187 for i, proc := range log.Parents { 188 procPrints = append(procPrints, fmt.Sprintf("%s→ %s", strings.Repeat(" ", i), proc.Command)) 189 } 190 } 191 192 // Validate usage against disallowed or missing PATH tools. 193 config := paths.GetConfig(log.Basename) 194 if config.Error { 195 ctx.Printf("Disallowed PATH tool %q used: %#v", log.Basename, log.Args) 196 for _, line := range procPrints { 197 ctx.Println(line) 198 } 199 } else { 200 ctx.Verbosef("Unknown PATH tool %q used: %#v", log.Basename, log.Args) 201 for _, line := range procPrints { 202 ctx.Verboseln(line) 203 } 204 } 205 } 206 }() 207 208 // Create the .path directory. 209 ensureEmptyDirectoriesExist(ctx, myPath) 210 211 // Compute the full list of binaries available in the original $PATH. 212 var execs []string 213 for _, pathEntry := range filepath.SplitList(origPath) { 214 if pathEntry == "" { 215 // Ignore the current directory 216 continue 217 } 218 // TODO(dwillemsen): remove path entries under TOP? or anything 219 // that looks like an android source dir? They won't exist on 220 // the build servers, since they're added by envsetup.sh. 221 // (Except for the JDK, which is configured in ui/build/config.go) 222 223 execs = append(execs, parsePathDir(pathEntry)...) 224 } 225 226 if config.Environment().IsEnvTrue("TEMPORARY_DISABLE_PATH_RESTRICTIONS") { 227 ctx.Fatalln("TEMPORARY_DISABLE_PATH_RESTRICTIONS was a temporary migration method, and is now obsolete.") 228 } 229 230 // Create symlinks from the path_interposer binary to all binaries for each 231 // directory in the original $PATH. This ensures that during the build, 232 // every call to a binary that's expected to be in the $PATH will be 233 // intercepted by the path_interposer binary, and validated with the 234 // LogEntry listener above at build time. 235 for _, name := range execs { 236 if !paths.GetConfig(name).Symlink { 237 // Ignore host tools that shouldn't be symlinked. 238 continue 239 } 240 241 err := os.Symlink("../.path_interposer", filepath.Join(myPath, name)) 242 // Intentionally ignore existing files -- that means that we 243 // just created it, and the first one should win. 244 if err != nil && !os.IsExist(err) { 245 ctx.Fatalln("Failed to create symlink:", err) 246 } 247 } 248 249 myPath, _ = filepath.Abs(myPath) 250 251 // We put some prebuilts in $PATH, since it's infeasible to add dependencies 252 // for all of them. 253 prebuiltsPath, _ := filepath.Abs("prebuilts/build-tools/path/" + runtime.GOOS + "-x86") 254 myPath = prebuiltsPath + string(os.PathListSeparator) + myPath 255 256 if value, _ := config.Environment().Get("BUILD_BROKEN_PYTHON_IS_PYTHON2"); value == "true" { 257 py2Path, _ := filepath.Abs("prebuilts/build-tools/path/" + runtime.GOOS + "-x86/py2") 258 if info, err := os.Stat(py2Path); err == nil && info.IsDir() { 259 myPath = py2Path + string(os.PathListSeparator) + myPath 260 } 261 } else if value != "" { 262 ctx.Fatalf("BUILD_BROKEN_PYTHON_IS_PYTHON2 can only be set to 'true' or an empty string, but got %s\n", value) 263 } 264 265 // Replace the $PATH variable with the path_interposer symlinks, and 266 // checked-in prebuilts. 267 config.Environment().Set("PATH", myPath) 268 config.pathReplaced = true 269} 270