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