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