golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/buildlet/buildlet.go (about)

     1  // Copyright 2014 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // The buildlet is an HTTP server that untars content to disk and runs
     6  // commands it has untarred, streaming their output back over HTTP.
     7  // It is part of Go's continuous build system.
     8  //
     9  // This program intentionally allows remote code execution, and
    10  // provides no security of its own. It is assumed that any user uses
    11  // it with an appropriately-configured firewall between their VM
    12  // instances.
    13  package main // import "golang.org/x/build/cmd/buildlet"
    14  
    15  import (
    16  	"archive/tar"
    17  	"bytes"
    18  	"compress/gzip"
    19  	"context"
    20  	"crypto/sha1"
    21  	"crypto/tls"
    22  	"encoding/json"
    23  	"errors"
    24  	"flag"
    25  	"fmt"
    26  	"io"
    27  	"io/fs"
    28  	"log"
    29  	"net"
    30  	"net/http"
    31  	"net/url"
    32  	"os"
    33  	"os/exec"
    34  	"path"
    35  	"path/filepath"
    36  	"runtime"
    37  	"strconv"
    38  	"strings"
    39  	"sync"
    40  	"time"
    41  
    42  	"cloud.google.com/go/compute/metadata"
    43  	"github.com/aws/aws-sdk-go/aws"
    44  	"github.com/aws/aws-sdk-go/aws/ec2metadata"
    45  	"github.com/aws/aws-sdk-go/aws/session"
    46  	"github.com/gliderlabs/ssh"
    47  	"golang.org/x/build/buildlet"
    48  	"golang.org/x/build/internal/cloud"
    49  	"golang.org/x/build/internal/envutil"
    50  	"golang.org/x/build/pargzip"
    51  )
    52  
    53  var (
    54  	haltEntireOS     = flag.Bool("halt", true, "halt OS in /halt handler. If false, the buildlet process just ends.")
    55  	rebootOnHalt     = flag.Bool("reboot", false, "reboot system in /halt handler.")
    56  	workDir          = flag.String("workdir", "", "Temporary directory to use. The contents of this directory may be deleted at any time. If empty, TempDir is used to create one.")
    57  	listenAddr       = flag.String("listen", "AUTO", "address to listen on. Unused in reverse mode. Warning: this service is inherently insecure and offers no protection of its own. Do not expose this port to the world.")
    58  	reverseType      = flag.String("reverse-type", "", "if non-empty, go into reverse mode where the buildlet dials the coordinator instead of listening for connections. The value is the dashboard/builders.go Hosts map key, naming a HostConfig. This buildlet will receive work for any BuildConfig specifying this named HostConfig.")
    59  	coordinator      = flag.String("coordinator", "localhost:8119", "address of coordinator, in production use farmer.golang.org. Only used in reverse mode.")
    60  	hostname         = flag.String("hostname", "", "hostname to advertise to coordinator for reverse mode; default is actual hostname")
    61  	healthAddr       = flag.String("health-addr", "0.0.0.0:8080", "For reverse buildlets, address to listen for /healthz requests separately from the reverse dialer to the coordinator.")
    62  	version          = flag.Bool("version", false, "print buildlet version and exit")
    63  	gomoteServerAddr = flag.String("gomote-server-addr", "gomotessh.golang.org:443", "Gomote server address and port")
    64  	swarmingBot      = flag.Bool("swarming-bot", false, "start the buildlet on a swarming bot")
    65  )
    66  
    67  // Bump this whenever something notable happens, or when another
    68  // component needs a certain feature. This shows on the coordinator
    69  // per reverse client, and is also accessible via the buildlet
    70  // package's client API (via the Status method).
    71  //
    72  // Notable versions:
    73  //
    74  //	 3: switched to revdial protocol
    75  //	 5: reverse dialing uses timeouts+tcp keepalives, pargzip fix
    76  //	 7: version bumps while debugging revdial hang (Issue 12816)
    77  //	 8: mac screensaver disabled
    78  //	11: move from self-signed cert to LetsEncrypt (Issue 16442)
    79  //	15: ssh support
    80  //	16: make macstadium builders always haltEntireOS
    81  //	17: make macstadium halts use sudo
    82  //	18: set TMPDIR and GOCACHE
    83  //	21: GO_BUILDER_SET_GOPROXY=coordinator support
    84  //	22: TrimSpace the reverse buildlet's gobuildkey contents
    85  //	23: revdial v2
    86  //	24: removeAllIncludingReadonly
    87  //	25: use removeAllIncludingReadonly for all work area cleanup
    88  //	26: clean up path validation and normalization
    89  //	27: export GOPLSCACHE=$workdir/goplscache
    90  //	28: add support for gomote server
    91  const buildletVersion = 28
    92  
    93  func defaultListenAddr() string {
    94  	if runtime.GOOS == "darwin" {
    95  		// Darwin will never run on GCE, so let's always
    96  		// listen on a high port (so we don't need to be
    97  		// root).
    98  		return ":5936"
    99  	}
   100  	// check if env is dev
   101  	if !metadata.OnGCE() && !onEC2() {
   102  		return "localhost:5936"
   103  	}
   104  	// In production, default to port 80 or 443, depending on
   105  	// whether TLS is configured.
   106  	if metadataValue(metaKeyTLSCert) != "" {
   107  		return ":443"
   108  	}
   109  	return ":80"
   110  }
   111  
   112  // Functionality set non-nil by some platforms:
   113  var (
   114  	configureSerialLogOutput func()
   115  	setOSRlimit              func() error
   116  )
   117  
   118  // If non-empty, the $TMPDIR, $GOCACHE, and $GOPLSCACHE environment
   119  // variables to use for child processes.
   120  var (
   121  	processTmpDirEnv     string
   122  	processGoCacheEnv    string
   123  	processGoplsCacheEnv string
   124  )
   125  
   126  const (
   127  	metaKeyPassword = "password"
   128  	metaKeyTLSCert  = "tls-cert"
   129  	metaKeyTLSkey   = "tls-key"
   130  )
   131  
   132  func main() {
   133  	builderEnv := os.Getenv("GO_BUILDER_ENV")
   134  	defer teardownOnce()
   135  	onGCE := metadata.OnGCE()
   136  	switch runtime.GOOS {
   137  	case "plan9":
   138  		if onGCE {
   139  			log.SetOutput(&gcePlan9LogWriter{w: os.Stderr})
   140  		}
   141  	case "linux":
   142  		if onGCE && !inKube {
   143  			if w, err := os.OpenFile("/dev/console", os.O_WRONLY, 0); err == nil {
   144  				log.SetOutput(w)
   145  			}
   146  		}
   147  	case "windows":
   148  		if onGCE {
   149  			configureSerialLogOutput()
   150  		}
   151  	}
   152  
   153  	flag.Parse()
   154  	if *version {
   155  		fmt.Printf("buildlet version %v (%s-%s)\n", buildletVersion, runtime.GOOS, runtime.GOARCH)
   156  		fmt.Printf("built with %v\n", runtime.Version())
   157  		os.Exit(0)
   158  	}
   159  	log.Printf("buildlet starting.")
   160  
   161  	if builderEnv == "android-amd64-emu" {
   162  		startAndroidEmulator()
   163  	}
   164  
   165  	// Optimize emphemeral filesystems. Prefer speed over safety,
   166  	// since these VMs only last for the duration of one build.
   167  	switch runtime.GOOS {
   168  	case "openbsd", "freebsd", "netbsd":
   169  		makeBSDFilesystemFast()
   170  	}
   171  	if setOSRlimit != nil {
   172  		err := setOSRlimit()
   173  		if err != nil {
   174  			log.Fatalf("setOSRLimit: %v", err)
   175  		}
   176  		log.Printf("set OS rlimits.")
   177  	}
   178  
   179  	isReverse := *reverseType != ""
   180  
   181  	if *listenAddr == "AUTO" && !isReverse {
   182  		v := defaultListenAddr()
   183  		log.Printf("Will listen on %s", v)
   184  		*listenAddr = v
   185  	}
   186  
   187  	if !onGCE && !isReverse && !onEC2() && !strings.HasPrefix(*listenAddr, "localhost:") {
   188  		log.Printf("** WARNING ***  This server is unsafe and offers no security. Be careful.")
   189  	}
   190  	if onGCE {
   191  		fixMTU()
   192  	}
   193  	if *workDir == "" && setWorkdirToTmpfs != nil {
   194  		setWorkdirToTmpfs()
   195  	}
   196  	if *workDir == "" {
   197  		switch runtime.GOOS {
   198  		case "windows":
   199  			// We want a short path on Windows, due to
   200  			// Windows issues with maximum path lengths.
   201  			*workDir = `C:\workdir`
   202  			if err := os.MkdirAll(*workDir, 0755); err != nil {
   203  				log.Fatalf("error creating workdir: %v", err)
   204  			}
   205  		default:
   206  			wdName := "workdir"
   207  			if *reverseType != "" {
   208  				wdName += "-" + *reverseType
   209  			}
   210  			dir := filepath.Join(os.TempDir(), wdName)
   211  			removeAllAndMkdir(dir)
   212  			*workDir = dir
   213  		}
   214  	}
   215  
   216  	os.Setenv("WORKDIR", *workDir) // mostly for demos
   217  
   218  	if _, err := os.Lstat(*workDir); err != nil {
   219  		log.Fatalf("invalid --workdir %q: %v", *workDir, err)
   220  	}
   221  
   222  	// Set up and clean $TMPDIR and $GOCACHE directories.
   223  	if runtime.GOOS != "plan9" { // go.dev/cl/207283 seems to indicate plan9 should work, but someone needs to test it.
   224  		processTmpDirEnv = filepath.Join(*workDir, "tmp")
   225  		removeAllAndMkdir(processTmpDirEnv)
   226  
   227  		processGoCacheEnv = filepath.Join(*workDir, "gocache")
   228  		removeAllAndMkdir(processGoCacheEnv)
   229  
   230  		processGoplsCacheEnv = filepath.Join(*workDir, "goplscache")
   231  		removeAllAndMkdir(processGoplsCacheEnv)
   232  	}
   233  
   234  	http.HandleFunc("/", handleRoot)
   235  	http.HandleFunc("/debug/x", handleX)
   236  
   237  	var password string
   238  	if !isReverse {
   239  		password = metadataValue(metaKeyPassword)
   240  	}
   241  	requireAuth := func(handler func(w http.ResponseWriter, r *http.Request)) http.Handler {
   242  		return requirePasswordHandler{http.HandlerFunc(handler), password}
   243  	}
   244  	http.Handle("/debug/goroutines", requireAuth(handleGoroutines))
   245  	http.Handle("/writetgz", requireAuth(handleWriteTGZ))
   246  	http.Handle("/write", requireAuth(handleWrite))
   247  	http.Handle("/exec", requireAuth(handleExec))
   248  	http.Handle("/halt", requireAuth(handleHalt))
   249  	http.Handle("/tgz", requireAuth(handleGetTGZ))
   250  	http.Handle("/removeall", requireAuth(handleRemoveAll))
   251  	http.Handle("/workdir", requireAuth(handleWorkDir))
   252  	http.Handle("/status", requireAuth(handleStatus))
   253  	http.Handle("/ls", requireAuth(handleLs))
   254  	http.Handle("/connect-ssh", requireAuth(handleConnectSSH))
   255  	http.HandleFunc("/healthz", handleHealthz)
   256  
   257  	if !isReverse && !*swarmingBot {
   258  		listenForCoordinator()
   259  	} else {
   260  		go func() {
   261  			if err := serveReverseHealth(); err != nil {
   262  				log.Printf("Error in serveReverseHealth: %v", err)
   263  			}
   264  		}()
   265  		ln, err := dialServer()
   266  		if err != nil {
   267  			log.Fatalf("Error dialing server: %v", err)
   268  		}
   269  		srv := &http.Server{}
   270  		err = srv.Serve(ln)
   271  		log.Printf("http.Serve on reverse connection complete: %v", err)
   272  		log.Printf("buildlet reverse mode exiting.")
   273  		if *haltEntireOS {
   274  			// The coordinator disconnects before doHalt has time to
   275  			// execute. handleHalt has a 1s delay.
   276  			time.Sleep(5 * time.Second)
   277  		}
   278  		os.Exit(0)
   279  	}
   280  }
   281  
   282  type teardownFunc func()
   283  
   284  var (
   285  	tdOnce        sync.Once
   286  	teardownOnce  func() = func() { tdOnce.Do(teardown) }
   287  	teardownFuncs []teardownFunc
   288  )
   289  
   290  func teardown() {
   291  	for _, f := range teardownFuncs {
   292  		f()
   293  	}
   294  }
   295  
   296  func dialServer() (net.Listener, error) {
   297  	if *swarmingBot {
   298  		return dialGomoteServer()
   299  	}
   300  	return dialCoordinator()
   301  }
   302  
   303  func listenForCoordinator() {
   304  	tlsCert, tlsKey := metadataValue(metaKeyTLSCert), metadataValue(metaKeyTLSkey)
   305  	if (tlsCert == "") != (tlsKey == "") {
   306  		log.Fatalf("tls-cert and tls-key must both be supplied, or neither.")
   307  	}
   308  
   309  	log.Printf("Listening on %s ...", *listenAddr)
   310  	ln, err := net.Listen("tcp", *listenAddr)
   311  	if err != nil {
   312  		log.Fatalf("Failed to listen on %s: %v", *listenAddr, err)
   313  	}
   314  	ln = tcpKeepAliveListener{ln.(*net.TCPListener)}
   315  
   316  	var srv http.Server
   317  	if tlsCert != "" {
   318  		cert, err := tls.X509KeyPair([]byte(tlsCert), []byte(tlsKey))
   319  		if err != nil {
   320  			log.Fatalf("TLS cert error: %v", err)
   321  		}
   322  		tlsConf := &tls.Config{
   323  			Certificates: []tls.Certificate{cert},
   324  		}
   325  		ln = tls.NewListener(ln, tlsConf)
   326  	}
   327  
   328  	serveErr := make(chan error, 1)
   329  	go func() {
   330  		serveErr <- srv.Serve(ln)
   331  	}()
   332  
   333  	signalChan := make(chan os.Signal, 1)
   334  	if registerSignal != nil {
   335  		registerSignal(signalChan)
   336  	}
   337  	select {
   338  	case sig := <-signalChan:
   339  		log.Printf("received signal %v; shutting down gracefully.", sig)
   340  	case err := <-serveErr:
   341  		log.Fatalf("Serve: %v", err)
   342  	}
   343  	time.AfterFunc(5*time.Second, func() {
   344  		log.Printf("timeout shutting down gracefully; exiting immediately")
   345  		os.Exit(1)
   346  	})
   347  	if err := srv.Shutdown(context.Background()); err != nil {
   348  		log.Printf("Graceful shutdown error: %v; exiting immediately instead", err)
   349  		os.Exit(1)
   350  	}
   351  	log.Printf("graceful shutdown complete.")
   352  	os.Exit(0)
   353  }
   354  
   355  // registerSignal if non-nil registers shutdown signals with the provided chan.
   356  var registerSignal func(chan<- os.Signal)
   357  
   358  var inKube = os.Getenv("KUBERNETES_SERVICE_HOST") != ""
   359  
   360  var (
   361  	// ec2UD contains a copy of the EC2 vm user data retrieved from the metadata.
   362  	ec2UD *cloud.EC2UserData
   363  	// ec2MdC is an EC2 metadata client.
   364  	ec2MdC *ec2metadata.EC2Metadata
   365  )
   366  
   367  // onEC2 evaluates if the buildlet is running on an EC2 instance.
   368  func onEC2() bool {
   369  	if ec2MdC != nil {
   370  		return ec2MdC.Available()
   371  	}
   372  	cfg := aws.NewConfig()
   373  	// TODO(golang/go#42604) - Improve detection of our qemu forwarded
   374  	// metadata service for Windows ARM VMs running on EC2.
   375  	if runtime.GOOS == "windows" && runtime.GOARCH == "arm64" {
   376  		cfg = cfg.WithEndpoint("http://10.0.2.100:8173/latest")
   377  	}
   378  	ses, err := session.NewSession(cfg)
   379  	if err != nil {
   380  		log.Printf("unable to create aws session: %s", err)
   381  		return false
   382  	}
   383  	ec2MdC = ec2metadata.New(ses, cfg)
   384  	return ec2MdC.Available()
   385  }
   386  
   387  // mdValueFromUserData maps a metadata key value into the corresponding
   388  // EC2UserData value. If a mapping is not found, an empty string is returned.
   389  func mdValueFromUserData(ud *cloud.EC2UserData, key string) string {
   390  	switch key {
   391  	case metaKeyTLSCert:
   392  		return ud.TLSCert
   393  	case metaKeyTLSkey:
   394  		return ud.TLSKey
   395  	case metaKeyPassword:
   396  		return ud.TLSPassword
   397  	default:
   398  		return ""
   399  	}
   400  }
   401  
   402  // metadataValue returns the GCE metadata instance value for the given key.
   403  // If the instance is on EC2 the corresponding value will be extracted from
   404  // the user data available via the metadata.
   405  // If the metadata is not defined, the returned string is empty.
   406  //
   407  // If not running on GCE or EC2, it falls back to using environment variables
   408  // for local development.
   409  func metadataValue(key string) string {
   410  	// The common case (on GCE, but not in Kubernetes):
   411  	if metadata.OnGCE() && !inKube {
   412  		v, err := metadata.InstanceAttributeValue(key)
   413  		if _, notDefined := err.(metadata.NotDefinedError); notDefined {
   414  			return ""
   415  		}
   416  		if err != nil {
   417  			log.Fatalf("metadata.InstanceAttributeValue(%q): %v", key, err)
   418  		}
   419  		return v
   420  	}
   421  
   422  	if onEC2() {
   423  		if ec2UD != nil {
   424  			return mdValueFromUserData(ec2UD, key)
   425  		}
   426  		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   427  		defer cancel()
   428  		ec2MetaJson, err := ec2MdC.GetUserDataWithContext(ctx)
   429  		if err != nil {
   430  			log.Fatalf("unable to retrieve EC2 user data: %v", err)
   431  		}
   432  		ec2UD = &cloud.EC2UserData{}
   433  		err = json.Unmarshal([]byte(ec2MetaJson), ec2UD)
   434  		if err != nil {
   435  			log.Fatalf("unable to unmarshal user data json: %v", err)
   436  		}
   437  		return mdValueFromUserData(ec2UD, key)
   438  	}
   439  
   440  	// Else allow use of environment variables to fake
   441  	// metadata keys, for Kubernetes pods or local testing.
   442  	envKey := "META_" + strings.Replace(key, "-", "_", -1)
   443  	v := os.Getenv(envKey)
   444  	// Respect curl-style '@' prefix to mean the rest is a filename.
   445  	if strings.HasPrefix(v, "@") {
   446  		slurp, err := os.ReadFile(v[1:])
   447  		if err != nil {
   448  			log.Fatalf("Error reading file for GCEMETA_%v: %v", key, err)
   449  		}
   450  		return string(slurp)
   451  	}
   452  	if v == "" {
   453  		log.Printf("Warning: not running on GCE, and no %v environment variable defined", envKey)
   454  	}
   455  	return v
   456  }
   457  
   458  // tcpKeepAliveListener is a net.Listener that sets TCP keep-alive
   459  // timeouts on accepted connections.
   460  type tcpKeepAliveListener struct {
   461  	*net.TCPListener
   462  }
   463  
   464  func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
   465  	tc, err := ln.AcceptTCP()
   466  	if err != nil {
   467  		return
   468  	}
   469  	tc.SetKeepAlive(true)
   470  	tc.SetKeepAlivePeriod(3 * time.Minute)
   471  	return tc, nil
   472  }
   473  
   474  func fixMTU_freebsd() error { return fixMTU_ifconfig("vtnet0") }
   475  func fixMTU_openbsd() error { return fixMTU_ifconfig("vio0") }
   476  func fixMTU_ifconfig(iface string) error {
   477  	out, err := exec.Command("/sbin/ifconfig", iface, "mtu", "1460").CombinedOutput()
   478  	if err != nil {
   479  		return fmt.Errorf("/sbin/ifconfig %s mtu 1460: %v, %s", iface, err, out)
   480  	}
   481  	return nil
   482  }
   483  
   484  func fixMTU_plan9() error {
   485  	f, err := os.OpenFile("/net/ipifc/0/ctl", os.O_WRONLY, 0)
   486  	if err != nil {
   487  		return err
   488  	}
   489  	if _, err := io.WriteString(f, "mtu 1460\n"); err != nil {
   490  		f.Close()
   491  		return err
   492  	}
   493  	return f.Close()
   494  }
   495  
   496  func fixMTU() {
   497  	fn, ok := map[string]func() error{
   498  		"openbsd": fixMTU_openbsd,
   499  		"freebsd": fixMTU_freebsd,
   500  		"plan9":   fixMTU_plan9,
   501  	}[runtime.GOOS]
   502  	if ok {
   503  		if err := fn(); err != nil {
   504  			log.Printf("Failed to set MTU: %v", err)
   505  		} else {
   506  			log.Printf("Adjusted MTU.")
   507  		}
   508  	}
   509  }
   510  
   511  // flushWriter is an io.Writer that Flushes after each Write if the
   512  // underlying Writer implements http.Flusher.
   513  type flushWriter struct {
   514  	rw http.ResponseWriter
   515  }
   516  
   517  func (fw flushWriter) Write(p []byte) (n int, err error) {
   518  	n, err = fw.rw.Write(p)
   519  	if f, ok := fw.rw.(http.Flusher); ok {
   520  		f.Flush()
   521  	}
   522  	return
   523  }
   524  
   525  func handleRoot(w http.ResponseWriter, r *http.Request) {
   526  	if r.URL.Path != "/" {
   527  		http.NotFound(w, r)
   528  		return
   529  	}
   530  	fmt.Fprintf(w, "buildlet running on %s-%s\n", runtime.GOOS, runtime.GOARCH)
   531  }
   532  
   533  func handleGoroutines(w http.ResponseWriter, r *http.Request) {
   534  	log.Printf("Dumping goroutines.")
   535  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   536  	buf := make([]byte, 2<<20)
   537  	buf = buf[:runtime.Stack(buf, true)]
   538  	w.Write(buf)
   539  	log.Printf("Dumped goroutines.")
   540  }
   541  
   542  // unauthenticated /debug/x handler, to test MTU settings.
   543  func handleX(w http.ResponseWriter, r *http.Request) {
   544  	n, _ := strconv.Atoi(r.FormValue("n"))
   545  	if n > 1<<20 {
   546  		n = 1 << 20
   547  	}
   548  	log.Printf("Dumping %d X.", n)
   549  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   550  	buf := make([]byte, n)
   551  	for i := range buf {
   552  		buf[i] = 'X'
   553  	}
   554  	w.Write(buf)
   555  	log.Printf("Dumped X.")
   556  }
   557  
   558  func handleGetTGZ(w http.ResponseWriter, r *http.Request) {
   559  	if r.Method != "GET" {
   560  		http.Error(w, "requires GET method", http.StatusBadRequest)
   561  		return
   562  	}
   563  	if !mkdirAllWorkdirOr500(w) {
   564  		return
   565  	}
   566  	dir, err := nativeRelPath(r.FormValue("dir"))
   567  	if err != nil {
   568  		http.Error(w, "invalid 'dir' parameter: "+err.Error(), http.StatusBadRequest)
   569  		return
   570  	}
   571  
   572  	zw := pargzip.NewWriter(w)
   573  	tw := tar.NewWriter(zw)
   574  	base := filepath.Join(*workDir, dir)
   575  	err = filepath.Walk(base, func(path string, fi os.FileInfo, err error) error {
   576  		if err != nil {
   577  			return err
   578  		}
   579  		rel := strings.TrimPrefix(filepath.ToSlash(strings.TrimPrefix(path, base)), "/")
   580  		var linkName string
   581  		if fi.Mode()&os.ModeSymlink != 0 {
   582  			linkName, err = os.Readlink(path)
   583  			if err != nil {
   584  				return err
   585  			}
   586  		}
   587  		th, err := tar.FileInfoHeader(fi, linkName)
   588  		if err != nil {
   589  			return err
   590  		}
   591  		th.Name = rel
   592  		if fi.IsDir() && !strings.HasSuffix(th.Name, "/") {
   593  			th.Name += "/"
   594  		}
   595  		if th.Name == "/" {
   596  			return nil
   597  		}
   598  		if err := tw.WriteHeader(th); err != nil {
   599  			return err
   600  		}
   601  		if fi.Mode().IsRegular() {
   602  			f, err := os.Open(path)
   603  			if err != nil {
   604  				return err
   605  			}
   606  			defer f.Close()
   607  			if _, err := io.Copy(tw, f); err != nil {
   608  				return err
   609  			}
   610  		}
   611  		return nil
   612  	})
   613  	if err != nil {
   614  		log.Printf("Walk error: %v", err)
   615  		panic(http.ErrAbortHandler)
   616  	}
   617  	tw.Close()
   618  	zw.Close()
   619  }
   620  
   621  func handleWriteTGZ(w http.ResponseWriter, r *http.Request) {
   622  	if !mkdirAllWorkdirOr500(w) {
   623  		return
   624  	}
   625  	urlParam, _ := url.ParseQuery(r.URL.RawQuery)
   626  	baseDir := *workDir
   627  	if dir := urlParam.Get("dir"); dir != "" {
   628  		var err error
   629  		dir, err = nativeRelPath(dir)
   630  		if err != nil {
   631  			log.Printf("writetgz: bogus dir %q", dir)
   632  			http.Error(w, "invalid 'dir' parameter: "+err.Error(), http.StatusBadRequest)
   633  			return
   634  		}
   635  		baseDir = filepath.Join(baseDir, dir)
   636  
   637  		// Special case: if the directory is "go1.4" and it already exists, do nothing.
   638  		// This lets clients do a blind write to it and not do extra work.
   639  		if r.Method == "POST" && dir == "go1.4" {
   640  			if fi, err := os.Stat(baseDir); err == nil && fi.IsDir() {
   641  				log.Printf("writetgz: skipping URL puttar to go1.4 dir; already exists")
   642  				io.WriteString(w, "SKIP")
   643  				return
   644  			}
   645  		}
   646  
   647  		if err := os.MkdirAll(baseDir, 0755); err != nil {
   648  			log.Printf("writetgz: %v", err)
   649  			http.Error(w, "mkdir of base: "+err.Error(), http.StatusInternalServerError)
   650  			return
   651  		}
   652  	}
   653  
   654  	var tgz io.Reader
   655  	var urlStr string
   656  	switch r.Method {
   657  	case "PUT":
   658  		tgz = r.Body
   659  		log.Printf("writetgz: untarring Request.Body into %s", baseDir)
   660  	case "POST":
   661  		urlStr = r.FormValue("url")
   662  		if urlStr == "" {
   663  			log.Printf("writetgz: missing url POST param")
   664  			http.Error(w, "missing url POST param", http.StatusBadRequest)
   665  			return
   666  		}
   667  		t0 := time.Now()
   668  		res, err := http.Get(urlStr)
   669  		if err != nil {
   670  			log.Printf("writetgz: failed to fetch tgz URL %s: %v", urlStr, err)
   671  			http.Error(w, fmt.Sprintf("fetching URL %s: %v", urlStr, err), http.StatusInternalServerError)
   672  			return
   673  		}
   674  		defer res.Body.Close()
   675  		if res.StatusCode != http.StatusOK {
   676  			log.Printf("writetgz: failed to fetch tgz URL %s: status=%v", urlStr, res.Status)
   677  			http.Error(w, fmt.Sprintf("writetgz: fetching provided URL %q: %s", urlStr, res.Status), http.StatusInternalServerError)
   678  			return
   679  		}
   680  		tgz = res.Body
   681  		log.Printf("writetgz: untarring %s (got headers in %v) into %s", urlStr, time.Since(t0), baseDir)
   682  	default:
   683  		log.Printf("writetgz: invalid method %q", r.Method)
   684  		http.Error(w, "requires PUT or POST method", http.StatusBadRequest)
   685  		return
   686  	}
   687  
   688  	err := untar(tgz, baseDir)
   689  	if err != nil {
   690  		http.Error(w, err.Error(), httpStatus(err))
   691  		return
   692  	}
   693  	io.WriteString(w, "OK")
   694  }
   695  
   696  func handleWrite(w http.ResponseWriter, r *http.Request) {
   697  	if r.Method != "PUT" {
   698  		http.Error(w, "requires POST method", http.StatusBadRequest)
   699  		return
   700  	}
   701  
   702  	param, _ := url.ParseQuery(r.URL.RawQuery)
   703  
   704  	path := param.Get("path")
   705  	if _, err := nativeRelPath(path); err != nil {
   706  		http.Error(w, "invalid 'path' parameter: "+err.Error(), http.StatusBadRequest)
   707  		return
   708  	}
   709  	path = filepath.FromSlash(path)
   710  	path = filepath.Join(*workDir, path)
   711  
   712  	modeInt, err := strconv.ParseInt(param.Get("mode"), 10, 64)
   713  	mode := os.FileMode(modeInt)
   714  	if err != nil || !mode.IsRegular() {
   715  		http.Error(w, "bad mode", http.StatusBadRequest)
   716  		return
   717  	}
   718  
   719  	// Make the parent directory, along with any necessary parents, if needed.
   720  	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
   721  		http.Error(w, err.Error(), http.StatusInternalServerError)
   722  		return
   723  	}
   724  
   725  	if err := writeFile(r.Body, path, mode); err != nil {
   726  		http.Error(w, err.Error(), http.StatusInternalServerError)
   727  		return
   728  	}
   729  
   730  	io.WriteString(w, "OK")
   731  }
   732  
   733  func writeFile(r io.Reader, path string, mode os.FileMode) error {
   734  	if runtime.GOOS == "darwin" && mode&0111 != 0 {
   735  		// The darwin kernel caches binary signatures and SIGKILLs
   736  		// binaries with mismatched signatures. Overwriting a binary
   737  		// with O_TRUNC does not clear the cache, rendering the new
   738  		// copy unusable. Removing the original file first does clear
   739  		// the cache. See #54132.
   740  		err := os.Remove(path)
   741  		if err != nil && !errors.Is(err, fs.ErrNotExist) {
   742  			return err
   743  		}
   744  	}
   745  	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
   746  	if err != nil {
   747  		return err
   748  	}
   749  	if _, err := io.Copy(f, r); err != nil {
   750  		f.Close()
   751  		return err
   752  	}
   753  	// Try to set the mode again, in case the file already existed.
   754  	if runtime.GOOS != "windows" {
   755  		if err := f.Chmod(mode); err != nil {
   756  			f.Close()
   757  			return err
   758  		}
   759  	}
   760  	return f.Close()
   761  }
   762  
   763  // untar reads the gzip-compressed tar file from r and writes it into dir.
   764  func untar(r io.Reader, dir string) (err error) {
   765  	t0 := time.Now()
   766  	nFiles := 0
   767  	madeDir := map[string]bool{}
   768  	defer func() {
   769  		td := time.Since(t0)
   770  		if err == nil {
   771  			log.Printf("extracted tarball into %s: %d files, %d dirs (%v)", dir, nFiles, len(madeDir), td)
   772  		} else {
   773  			log.Printf("error extracting tarball into %s after %d files, %d dirs, %v: %v", dir, nFiles, len(madeDir), td, err)
   774  		}
   775  	}()
   776  	zr, err := gzip.NewReader(r)
   777  	if err != nil {
   778  		return badRequestf("requires gzip-compressed body: %w", err)
   779  	}
   780  	tr := tar.NewReader(zr)
   781  	loggedChtimesError := false
   782  	for {
   783  		f, err := tr.Next()
   784  		if err == io.EOF {
   785  			break
   786  		}
   787  		if err != nil {
   788  			log.Printf("tar reading error: %v", err)
   789  			return badRequestf("tar error: %w", err)
   790  		}
   791  		if f.Typeflag == tar.TypeXGlobalHeader {
   792  			// golang.org/issue/22748: git archive exports
   793  			// a global header ('g') which after Go 1.9
   794  			// (for a bit?) contained an empty filename.
   795  			// Ignore it.
   796  			continue
   797  		}
   798  		rel, err := nativeRelPath(f.Name)
   799  		if err != nil {
   800  			return badRequestf("tar file contained invalid name %q: %v", f.Name, err)
   801  		}
   802  		abs := filepath.Join(dir, rel)
   803  
   804  		fi := f.FileInfo()
   805  		mode := fi.Mode()
   806  		switch {
   807  		case mode.IsRegular():
   808  			// Make the directory. This is redundant because it should
   809  			// already be made by a directory entry in the tar
   810  			// beforehand. Thus, don't check for errors; the next
   811  			// write will fail with the same error.
   812  			dir := filepath.Dir(abs)
   813  			if !madeDir[dir] {
   814  				if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
   815  					return err
   816  				}
   817  				madeDir[dir] = true
   818  			}
   819  			if runtime.GOOS == "darwin" && mode&0111 != 0 {
   820  				// See comment in writeFile.
   821  				err := os.Remove(abs)
   822  				if err != nil && !errors.Is(err, fs.ErrNotExist) {
   823  					return err
   824  				}
   825  			}
   826  			wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
   827  			if err != nil {
   828  				return err
   829  			}
   830  			n, err := io.Copy(wf, tr)
   831  			if closeErr := wf.Close(); closeErr != nil && err == nil {
   832  				err = closeErr
   833  			}
   834  			if err != nil {
   835  				return fmt.Errorf("error writing to %s: %v", abs, err)
   836  			}
   837  			if n != f.Size {
   838  				return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
   839  			}
   840  			modTime := f.ModTime
   841  			if modTime.After(t0) {
   842  				// Clamp modtimes at system time. See
   843  				// golang.org/issue/19062 when clock on
   844  				// buildlet was behind the gitmirror server
   845  				// doing the git-archive.
   846  				modTime = t0
   847  			}
   848  			if !modTime.IsZero() {
   849  				if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError {
   850  					// benign error. Gerrit doesn't even set the
   851  					// modtime in these, and we don't end up relying
   852  					// on it anywhere (the gomote push command relies
   853  					// on digests only), so this is a little pointless
   854  					// for now.
   855  					log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err)
   856  					loggedChtimesError = true // once is enough
   857  				}
   858  			}
   859  			nFiles++
   860  		case mode.IsDir():
   861  			if err := os.MkdirAll(abs, 0755); err != nil {
   862  				return err
   863  			}
   864  			madeDir[abs] = true
   865  		case mode&os.ModeSymlink != 0:
   866  			// TODO: ignore these for now. They were breaking x/build tests.
   867  			// Implement these if/when we ever have a test that needs them.
   868  			// But maybe we'd have to skip creating them on Windows for some builders
   869  			// without permissions.
   870  		default:
   871  			return badRequestf("tar file entry %s contained unsupported file type %v", f.Name, mode)
   872  		}
   873  	}
   874  	return nil
   875  }
   876  
   877  // Process-State is an HTTP Trailer set in the /exec handler to "ok"
   878  // on success, or os.ProcessState.String() on failure.
   879  const hdrProcessState = "Process-State"
   880  
   881  func handleExec(w http.ResponseWriter, r *http.Request) {
   882  	cn := w.(http.CloseNotifier)
   883  	clientGone := cn.CloseNotify()
   884  	handlerDone := make(chan bool)
   885  	defer close(handlerDone)
   886  
   887  	if r.Method != "POST" {
   888  		http.Error(w, "requires POST method", http.StatusBadRequest)
   889  		return
   890  	}
   891  	if r.ProtoMajor*10+r.ProtoMinor < 11 {
   892  		// We need trailers, only available in HTTP/1.1 or HTTP/2.
   893  		http.Error(w, "HTTP/1.1 or higher required", http.StatusBadRequest)
   894  		return
   895  	}
   896  	// Create *workDir and any needed temporary subdirectories.
   897  	if !mkdirAllWorkdirOr500(w) {
   898  		return
   899  	}
   900  	for _, dir := range []string{processTmpDirEnv, processGoCacheEnv, processGoplsCacheEnv} {
   901  		if dir == "" {
   902  			continue
   903  		}
   904  		if err := os.MkdirAll(dir, 0755); err != nil {
   905  			http.Error(w, err.Error(), http.StatusInternalServerError)
   906  			return
   907  		}
   908  	}
   909  	if err := checkAndroidEmulator(); err != nil {
   910  		http.Error(w, "android emulator not running: "+err.Error(), http.StatusInternalServerError)
   911  		return
   912  	}
   913  
   914  	w.Header().Set("Trailer", hdrProcessState) // declare it so we can set it
   915  
   916  	sysMode := r.FormValue("mode") == "sys"
   917  	debug, _ := strconv.ParseBool(r.FormValue("debug"))
   918  
   919  	absCmd, err := absExecCmd(r.FormValue("cmd"), sysMode) // required
   920  	if err != nil {
   921  		http.Error(w, "invalid 'cmd' parameter: "+err.Error(), httpStatus(err))
   922  		return
   923  	}
   924  
   925  	absDir, err := absExecDir(r.FormValue("dir"), sysMode, filepath.Dir(absCmd)) // optional
   926  	if err != nil {
   927  		http.Error(w, "invalid 'dir' parameter: "+err.Error(), httpStatus(err))
   928  		return
   929  	}
   930  
   931  	if f, ok := w.(http.Flusher); ok {
   932  		f.Flush()
   933  	}
   934  
   935  	postEnv := r.PostForm["env"]
   936  
   937  	goarch := "amd64" // unless we find otherwise
   938  	if v := envutil.Get(runtime.GOOS, postEnv, "GOARCH"); v != "" {
   939  		goarch = v
   940  	}
   941  	if v, _ := strconv.ParseBool(envutil.Get(runtime.GOOS, postEnv, "GO_DISABLE_OUTBOUND_NETWORK")); v {
   942  		disableOutboundNetwork()
   943  	}
   944  
   945  	env := append(baseEnv(goarch), postEnv...)
   946  	if v := processTmpDirEnv; v != "" {
   947  		env = append(env, "TMPDIR="+v)
   948  	}
   949  	if v := processGoCacheEnv; v != "" {
   950  		env = append(env, "GOCACHE="+v)
   951  	}
   952  	if v := processGoplsCacheEnv; v != "" {
   953  		env = append(env, "GOPLSCACHE="+v)
   954  	}
   955  	if path := r.PostForm["path"]; len(path) > 0 {
   956  		if kv, ok := pathEnv(runtime.GOOS, env, path, *workDir); ok {
   957  			env = append(env, kv)
   958  		}
   959  	}
   960  	env = envutil.Dedup(runtime.GOOS, env)
   961  
   962  	var cmd *exec.Cmd
   963  	if needsBashWrapper(absCmd) {
   964  		cmd = exec.Command("bash", absCmd)
   965  	} else {
   966  		cmd = exec.Command(absCmd)
   967  	}
   968  	cmd.Args = append(cmd.Args, r.PostForm["cmdArg"]...)
   969  	cmd.Env = env
   970  	envutil.SetDir(cmd, absDir)
   971  	cmdOutput := flushWriter{w}
   972  	cmd.Stdout = cmdOutput
   973  	cmd.Stderr = cmdOutput
   974  
   975  	log.Printf("[%p] Running %s with args %q and env %q in dir %s",
   976  		cmd, cmd.Path, cmd.Args, cmd.Env, cmd.Dir)
   977  
   978  	if debug {
   979  		fmt.Fprintf(cmdOutput, ":: Running %s with args %q and env %q in dir %s\n\n",
   980  			cmd.Path, cmd.Args, cmd.Env, cmd.Dir)
   981  	}
   982  
   983  	t0 := time.Now()
   984  	err = cmd.Start()
   985  	if err == nil {
   986  		go func() {
   987  			select {
   988  			case <-clientGone:
   989  				err := killProcessTree(cmd.Process)
   990  				if err != nil {
   991  					log.Printf("Kill failed: %v", err)
   992  				}
   993  			case <-handlerDone:
   994  				return
   995  			}
   996  		}()
   997  		err = cmd.Wait()
   998  	}
   999  	state := "ok"
  1000  	if err != nil {
  1001  		if ps := cmd.ProcessState; ps != nil {
  1002  			state = ps.String()
  1003  		} else {
  1004  			state = err.Error()
  1005  		}
  1006  	}
  1007  	w.Header().Set(hdrProcessState, state)
  1008  	log.Printf("[%p] Run = %s, after %v", cmd, state, time.Since(t0))
  1009  }
  1010  
  1011  // absExecCmd returns the native, absolute path corresponding to the "cmd"
  1012  // argument passed to the "exec" endpoint.
  1013  func absExecCmd(cmdArg string, sysMode bool) (absCmd string, err error) {
  1014  	if cmdArg == "" {
  1015  		return "", badRequestf("requires 'cmd' parameter")
  1016  	}
  1017  
  1018  	if filepath.IsAbs(cmdArg) {
  1019  		return filepath.Clean(cmdArg), nil
  1020  	}
  1021  
  1022  	relCmd, err := nativeRelPath(cmdArg)
  1023  	if err != nil {
  1024  		return "", badRequestf("invalid 'cmd' parameter: %w", err)
  1025  	}
  1026  
  1027  	if strings.Contains(relCmd, string(filepath.Separator)) {
  1028  		if sysMode {
  1029  			return "", badRequestf("'sys' mode requires absolute or system 'cmd' path")
  1030  		}
  1031  		return filepath.Join(*workDir, filepath.FromSlash(cmdArg)), nil
  1032  	}
  1033  
  1034  	if !sysMode {
  1035  		absCmd, err = exec.LookPath(filepath.Join(*workDir, cmdArg))
  1036  		if err == nil {
  1037  			return absCmd, nil
  1038  		}
  1039  		// Not found in workdir; treat as a system command even if sysMode is false.
  1040  	}
  1041  
  1042  	absCmd, err = exec.LookPath(cmdArg)
  1043  	if err != nil {
  1044  		return "", httpError{http.StatusUnprocessableEntity, fmt.Errorf("command %q not found", cmdArg)}
  1045  	}
  1046  	return absCmd, nil
  1047  }
  1048  
  1049  // absExecDir returns the native, absolute path corresponding to the "dir"
  1050  // argument passed to the "exec" endpoint.
  1051  func absExecDir(dirArg string, sysMode bool, cmdDir string) (absDir string, err error) {
  1052  	if dirArg == "" {
  1053  		if sysMode {
  1054  			return *workDir, nil
  1055  		}
  1056  		return cmdDir, nil
  1057  	}
  1058  
  1059  	if filepath.IsAbs(dirArg) {
  1060  		return filepath.Clean(dirArg), nil
  1061  	}
  1062  
  1063  	relDir, err := nativeRelPath(dirArg)
  1064  	if err != nil {
  1065  		return "", badRequestf("invalid 'dir' parameter: %w", err)
  1066  	}
  1067  	return filepath.Join(*workDir, relDir), nil
  1068  }
  1069  
  1070  // needsBashWrapper reports whether the given command needs to
  1071  // run through bash.
  1072  func needsBashWrapper(cmd string) bool {
  1073  	if !strings.HasSuffix(cmd, ".bash") {
  1074  		return false
  1075  	}
  1076  	// The mobile platforms can't execute shell scripts directly.
  1077  	ismobile := runtime.GOOS == "android" || runtime.GOOS == "ios"
  1078  	return ismobile
  1079  }
  1080  
  1081  // pathNotExist reports whether path does not exist.
  1082  func pathNotExist(path string) bool {
  1083  	_, err := os.Stat(path)
  1084  	return os.IsNotExist(err)
  1085  }
  1086  
  1087  // pathEnv returns a key=value string for the system path variable
  1088  // (either PATH or path depending on the platform) with values
  1089  // substituted from env:
  1090  //   - the string "$PATH" expands to the original value of the path variable
  1091  //   - the string "$WORKDIR" expands to the provided workDir
  1092  //   - the string "$EMPTY" expands to the empty string
  1093  //
  1094  // The "ok" result reports whether kv differs from the path found in env.
  1095  func pathEnv(goos string, env, path []string, workDir string) (kv string, ok bool) {
  1096  	pathVar := "PATH"
  1097  	if goos == "plan9" {
  1098  		pathVar = "path"
  1099  	}
  1100  
  1101  	orig := envutil.Get(goos, env, pathVar)
  1102  	r := strings.NewReplacer(
  1103  		"$PATH", orig,
  1104  		"$WORKDIR", workDir,
  1105  		"$EMPTY", "",
  1106  	)
  1107  
  1108  	// Apply substitutions to a copy of the path argument.
  1109  	subst := make([]string, 0, len(path))
  1110  	for _, elem := range path {
  1111  		if s := r.Replace(elem); s != "" {
  1112  			subst = append(subst, s)
  1113  		}
  1114  	}
  1115  	kv = pathVar + "=" + strings.Join(subst, pathListSeparator(goos))
  1116  	v := kv[len(pathVar)+1:]
  1117  	return kv, v != orig
  1118  }
  1119  
  1120  func pathListSeparator(goos string) string {
  1121  	switch goos {
  1122  	case "windows":
  1123  		return ";"
  1124  	case "plan9":
  1125  		return "\x00"
  1126  	default:
  1127  		return ":"
  1128  	}
  1129  }
  1130  
  1131  var (
  1132  	defaultBootstrap     string
  1133  	defaultBootstrapOnce sync.Once
  1134  )
  1135  
  1136  func baseEnv(goarch string) []string {
  1137  	var env []string
  1138  	if runtime.GOOS == "windows" {
  1139  		env = windowsBaseEnv(goarch)
  1140  	} else {
  1141  		env = os.Environ()
  1142  	}
  1143  
  1144  	defaultBootstrapOnce.Do(func() {
  1145  		defaultBootstrap = filepath.Join(*workDir, "go1.4")
  1146  
  1147  		// Prefer buildlet process's inherited GOROOT_BOOTSTRAP if
  1148  		// there was one and our default doesn't exist.
  1149  		if v := os.Getenv("GOROOT_BOOTSTRAP"); v != "" && v != defaultBootstrap {
  1150  			if pathNotExist(defaultBootstrap) {
  1151  				defaultBootstrap = v
  1152  			}
  1153  		}
  1154  	})
  1155  	env = append(env, "GOROOT_BOOTSTRAP="+defaultBootstrap)
  1156  
  1157  	return env
  1158  }
  1159  
  1160  func windowsBaseEnv(goarch string) (e []string) {
  1161  	e = append(e, "GOBUILDEXIT=1") // exit all.bat with completion status
  1162  
  1163  	for _, pair := range os.Environ() {
  1164  		const pathEq = "PATH="
  1165  		if hasPrefixFold(pair, pathEq) {
  1166  			e = append(e, "PATH="+windowsPath(pair[len(pathEq):], goarch))
  1167  		} else {
  1168  			e = append(e, pair)
  1169  		}
  1170  	}
  1171  	return e
  1172  }
  1173  
  1174  // hasPrefixFold is a case-insensitive strings.HasPrefix.
  1175  func hasPrefixFold(s, prefix string) bool {
  1176  	return len(s) >= len(prefix) && strings.EqualFold(s[:len(prefix)], prefix)
  1177  }
  1178  
  1179  // windowsPath cleans the windows %PATH% environment.
  1180  // is64Bit is whether this is a windows-amd64-* builder.
  1181  // The PATH is assumed to be that of the image described in env/windows/README.
  1182  func windowsPath(old string, goarch string) string {
  1183  	vv := filepath.SplitList(old)
  1184  	newPath := make([]string, 0, len(vv))
  1185  	is64Bit := goarch != "386"
  1186  
  1187  	// for windows-buildlet-v2 images
  1188  	for _, v := range vv {
  1189  		// The base VM image has both the 32-bit and 64-bit gcc installed.
  1190  		// They're both in the environment, so scrub the one
  1191  		// we don't want (TDM-GCC-64 or TDM-GCC-32).
  1192  		//
  1193  		// This is not present in arm64 images.
  1194  		if strings.Contains(v, "TDM-GCC-") {
  1195  			gcc64 := strings.Contains(v, "TDM-GCC-64")
  1196  			if is64Bit != gcc64 {
  1197  				continue
  1198  			}
  1199  		}
  1200  		newPath = append(newPath, v)
  1201  	}
  1202  
  1203  	switch goarch {
  1204  	case "arm64":
  1205  		newPath = append(newPath, `C:\godep\llvm-aarch64\bin`)
  1206  	case "386":
  1207  		newPath = append(newPath, `C:\godep\gcc32\bin`)
  1208  	default:
  1209  		newPath = append(newPath, `C:\godep\gcc64\bin`)
  1210  	}
  1211  
  1212  	return strings.Join(newPath, string(filepath.ListSeparator))
  1213  }
  1214  
  1215  func handleHalt(w http.ResponseWriter, r *http.Request) {
  1216  	if r.Method != "POST" {
  1217  		http.Error(w, "requires POST method", http.StatusBadRequest)
  1218  		return
  1219  	}
  1220  
  1221  	// Do the halt in 1 second, to give the HTTP response time to
  1222  	// complete.
  1223  	//
  1224  	// TODO(bradfitz): maybe prevent any (unlikely) future HTTP
  1225  	// requests from doing anything from this point on in the
  1226  	// remaining second.
  1227  	log.Printf("Halting in 1 second.")
  1228  	time.AfterFunc(1*time.Second, func() {
  1229  		teardownOnce()
  1230  		if *rebootOnHalt {
  1231  			doReboot()
  1232  		}
  1233  		if *haltEntireOS {
  1234  			doHalt()
  1235  		}
  1236  		log.Printf("Ending buildlet process due to halt.")
  1237  		os.Exit(0)
  1238  		return
  1239  	})
  1240  }
  1241  
  1242  func doHalt() {
  1243  	log.Printf("Halting machine.")
  1244  	// Backup mechanism, if exec hangs for any reason:
  1245  	time.AfterFunc(5*time.Second, func() { os.Exit(0) })
  1246  	var err error
  1247  	switch runtime.GOOS {
  1248  	case "openbsd":
  1249  		// Quick, no fs flush, and power down:
  1250  		err = exec.Command("halt", "-q", "-n", "-p").Run()
  1251  	case "freebsd":
  1252  		// Power off (-p), via halt (-o), now.
  1253  		err = exec.Command("shutdown", "-p", "-o", "now").Run()
  1254  	case "linux":
  1255  		// Don't sync (-n), force without shutdown (-f), and power off (-p).
  1256  		err = exec.Command("/bin/halt", "-n", "-f", "-p").Run()
  1257  	case "plan9":
  1258  		err = exec.Command("fshalt").Run()
  1259  	case "darwin":
  1260  		switch os.Getenv("GO_BUILDER_ENV") {
  1261  		case "macstadium_vm", "qemu_vm":
  1262  			// Fast, sloppy, unsafe, because we're never reusing this VM again.
  1263  			err = exec.Command("/usr/bin/sudo", "/sbin/halt", "-n", "-q", "-l").Run()
  1264  		default:
  1265  			err = errors.New("not respecting -halt flag on macOS in unknown environment")
  1266  		}
  1267  	case "windows":
  1268  		err = errors.New("not respecting -halt flag on Windows in unknown environment")
  1269  		if runtime.GOARCH == "arm64" {
  1270  			err = exec.Command("shutdown", "/s").Run()
  1271  		}
  1272  	default:
  1273  		err = errors.New("no system-specific halt command run; will just end buildlet process")
  1274  	}
  1275  	log.Printf("Shutdown: %v", err)
  1276  	log.Printf("Ending buildlet process post-halt")
  1277  	os.Exit(0)
  1278  }
  1279  
  1280  func doReboot() {
  1281  	log.Printf("Rebooting machine.")
  1282  	var err error
  1283  	switch runtime.GOOS {
  1284  	case "windows":
  1285  		err = exec.Command("shutdown", "/r").Run()
  1286  	default:
  1287  		err = exec.Command("reboot").Run()
  1288  	}
  1289  	log.Printf("Reboot: %v", err)
  1290  	log.Printf("Ending buildlet process post-halt")
  1291  	os.Exit(0)
  1292  }
  1293  
  1294  func handleRemoveAll(w http.ResponseWriter, r *http.Request) {
  1295  	if r.Method != "POST" {
  1296  		http.Error(w, "requires POST method", http.StatusBadRequest)
  1297  		return
  1298  	}
  1299  	if err := r.ParseForm(); err != nil {
  1300  		http.Error(w, err.Error(), http.StatusBadRequest)
  1301  		return
  1302  	}
  1303  	paths := r.Form["path"]
  1304  	if len(paths) == 0 {
  1305  		http.Error(w, "requires 'path' parameter", http.StatusBadRequest)
  1306  		return
  1307  	}
  1308  	for _, p := range paths {
  1309  		if _, err := nativeRelPath(p); err != nil {
  1310  			http.Error(w, "invalid 'path' parameter: "+err.Error(), http.StatusBadRequest)
  1311  			return
  1312  		}
  1313  	}
  1314  	for _, p := range paths {
  1315  		log.Printf("Removing %s", p)
  1316  		fullDir := filepath.Join(*workDir, filepath.FromSlash(p))
  1317  		err := removeAllIncludingReadonly(fullDir)
  1318  		if p == "." && err != nil {
  1319  			// If workDir is a mountpoint and/or contains a binary
  1320  			// using it, we can get a "Device or resource busy" error.
  1321  			// See if it's now empty and ignore the error.
  1322  			if f, oerr := os.Open(*workDir); oerr == nil {
  1323  				if all, derr := f.Readdirnames(-1); derr == nil && len(all) == 0 {
  1324  					log.Printf("Ignoring fail of RemoveAll(.)")
  1325  					err = nil
  1326  				} else {
  1327  					log.Printf("Readdir = %q, %v", all, derr)
  1328  				}
  1329  				f.Close()
  1330  			} else {
  1331  				log.Printf("Failed to open workdir: %v", oerr)
  1332  			}
  1333  		}
  1334  		if err != nil {
  1335  			http.Error(w, err.Error(), http.StatusInternalServerError)
  1336  			return
  1337  		}
  1338  	}
  1339  }
  1340  
  1341  // mkdirAllWorkdirOr500 reports whether *workDir either exists or was created.
  1342  // If it returns false, it also writes an HTTP 500 error to w.
  1343  // This is used by callers to verify *workDir exists, even if it might've been
  1344  // deleted previously.
  1345  func mkdirAllWorkdirOr500(w http.ResponseWriter) bool {
  1346  	if err := os.MkdirAll(*workDir, 0755); err != nil {
  1347  		http.Error(w, err.Error(), http.StatusInternalServerError)
  1348  		return false
  1349  	}
  1350  	return true
  1351  }
  1352  
  1353  func handleWorkDir(w http.ResponseWriter, r *http.Request) {
  1354  	if r.Method != "GET" {
  1355  		http.Error(w, "requires GET method", http.StatusBadRequest)
  1356  		return
  1357  	}
  1358  	fmt.Fprint(w, *workDir)
  1359  }
  1360  
  1361  func handleStatus(w http.ResponseWriter, r *http.Request) {
  1362  	if r.Method != "GET" {
  1363  		http.Error(w, "requires GET method", http.StatusBadRequest)
  1364  		return
  1365  	}
  1366  	status := buildlet.Status{
  1367  		Version: buildletVersion,
  1368  	}
  1369  	b, err := json.Marshal(status)
  1370  	if err != nil {
  1371  		http.Error(w, err.Error(), http.StatusInternalServerError)
  1372  		return
  1373  	}
  1374  	w.Header().Set("Content-Type", "application/json; charset=utf-8")
  1375  	w.Write(b)
  1376  }
  1377  
  1378  func handleLs(w http.ResponseWriter, r *http.Request) {
  1379  	if r.Method != "GET" {
  1380  		http.Error(w, "requires GET method", http.StatusBadRequest)
  1381  		return
  1382  	}
  1383  
  1384  	dir := r.FormValue("dir")
  1385  	if dir != "" {
  1386  		var err error
  1387  		dir, err = nativeRelPath(dir)
  1388  		if err != nil {
  1389  			http.Error(w, "invalid 'dir' parameter: "+err.Error(), http.StatusBadRequest)
  1390  			return
  1391  		}
  1392  	}
  1393  
  1394  	recursive, _ := strconv.ParseBool(r.FormValue("recursive"))
  1395  	digest, _ := strconv.ParseBool(r.FormValue("digest"))
  1396  	skip := r.Form["skip"] // '/'-separated relative dirs
  1397  
  1398  	if !mkdirAllWorkdirOr500(w) {
  1399  		return
  1400  	}
  1401  
  1402  	base := filepath.Join(*workDir, filepath.FromSlash(dir))
  1403  	anyOutput := false
  1404  	err := filepath.Walk(base, func(path string, fi os.FileInfo, err error) error {
  1405  		if err != nil {
  1406  			return err
  1407  		}
  1408  		rel := strings.TrimPrefix(filepath.ToSlash(strings.TrimPrefix(path, base)), "/")
  1409  		if rel == "" && fi.IsDir() {
  1410  			return nil
  1411  		}
  1412  		if fi.IsDir() {
  1413  			for _, v := range skip {
  1414  				if rel == v {
  1415  					return filepath.SkipDir
  1416  				}
  1417  			}
  1418  		}
  1419  		anyOutput = true
  1420  		fmt.Fprintf(w, "%s\t%s", fi.Mode(), rel)
  1421  		if fi.Mode().IsRegular() {
  1422  			fmt.Fprintf(w, "\t%d\t%s", fi.Size(), fi.ModTime().UTC().Format(time.RFC3339))
  1423  			if digest {
  1424  				if sha1, err := fileSHA1(path); err != nil {
  1425  					return err
  1426  				} else {
  1427  					io.WriteString(w, "\t"+sha1)
  1428  				}
  1429  			}
  1430  		} else if fi.Mode().IsDir() {
  1431  			io.WriteString(w, "/")
  1432  		}
  1433  		io.WriteString(w, "\n")
  1434  		if fi.IsDir() && !recursive {
  1435  			return filepath.SkipDir
  1436  		}
  1437  		return nil
  1438  	})
  1439  	if err != nil {
  1440  		log.Printf("Walk error: %v", err)
  1441  		if anyOutput {
  1442  			// Decent way to signal failure to the caller, since it'll break
  1443  			// the chunked response, rather than have a valid EOF.
  1444  			conn, _, _ := w.(http.Hijacker).Hijack()
  1445  			conn.Close()
  1446  			return
  1447  		}
  1448  		http.Error(w, "Walk error: "+err.Error(), 500)
  1449  		return
  1450  	}
  1451  }
  1452  
  1453  func useBuildletSSHServer() bool {
  1454  	return *swarmingBot && runtime.GOOS != "plan9"
  1455  }
  1456  
  1457  func handleConnectSSH(w http.ResponseWriter, r *http.Request) {
  1458  	if r.Method != "POST" {
  1459  		http.Error(w, "requires POST method", http.StatusBadRequest)
  1460  		return
  1461  	}
  1462  	if r.ContentLength != 0 {
  1463  		http.Error(w, "requires zero Content-Length", http.StatusBadRequest)
  1464  		return
  1465  	}
  1466  	sshUser := r.Header.Get("X-Go-Ssh-User")
  1467  	authKey := r.Header.Get("X-Go-Authorized-Key")
  1468  	if sshUser != "" && authKey != "" {
  1469  		if err := appendSSHAuthorizedKey(sshUser, authKey); err != nil {
  1470  			http.Error(w, "adding ssh authorized key: "+err.Error(), http.StatusBadRequest)
  1471  			return
  1472  		}
  1473  	}
  1474  
  1475  	sshServerOnce.Do(startSSHServer)
  1476  
  1477  	var sshConn net.Conn
  1478  	var err error
  1479  
  1480  	// In theory we shouldn't need retries here at all, but the
  1481  	// startSSHServerLinux's use of sshd -D is kinda sketchy and
  1482  	// restarts the process whenever we connect to it, so in case
  1483  	// it's just down between restarts, try a few times. 5 tries
  1484  	// and 5 seconds seems plenty.
  1485  	const maxTries = 5
  1486  	for try := 1; try <= maxTries; try++ {
  1487  		sshConn, err = net.Dial("tcp", "localhost:"+sshPort())
  1488  		if err == nil {
  1489  			break
  1490  		}
  1491  		if try == maxTries {
  1492  			http.Error(w, err.Error(), http.StatusBadGateway)
  1493  			return
  1494  		}
  1495  		time.Sleep(time.Second)
  1496  	}
  1497  	defer sshConn.Close()
  1498  	hj, ok := w.(http.Hijacker)
  1499  	if !ok {
  1500  		log.Printf("conn can't hijack for ssh proxy; HTTP/2 enabled by default?")
  1501  		http.Error(w, "conn can't hijack", http.StatusInternalServerError)
  1502  		return
  1503  	}
  1504  	conn, _, err := hj.Hijack()
  1505  	if err != nil {
  1506  		log.Printf("ssh hijack error: %v", err)
  1507  		http.Error(w, "ssh hijack error: "+err.Error(), http.StatusInternalServerError)
  1508  		return
  1509  	}
  1510  	defer conn.Close()
  1511  	fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: ssh\r\nConnection: Upgrade\r\n\r\n")
  1512  	errc := make(chan error, 1)
  1513  	go func() {
  1514  		_, err := io.Copy(sshConn, conn)
  1515  		errc <- err
  1516  	}()
  1517  	go func() {
  1518  		_, err := io.Copy(conn, sshConn)
  1519  		errc <- err
  1520  	}()
  1521  	<-errc
  1522  }
  1523  
  1524  var buildletSSHServer *ssh.Server
  1525  var buldletAuthKeys []byte
  1526  
  1527  // sshPort returns the port to use for the local SSH server.
  1528  func sshPort() string {
  1529  	// use port 2222 regardless of where the buildlet is running.
  1530  	if useBuildletSSHServer() {
  1531  		return "2222"
  1532  	}
  1533  
  1534  	// runningInCOS is whether we're running under GCE's Container-Optimized OS (COS).
  1535  	const runningInCOS = runtime.GOOS == "linux" && runtime.GOARCH == "amd64"
  1536  
  1537  	if runningInCOS {
  1538  		// If running in COS, we can't use port 22, as the system's sshd is already using it.
  1539  		// Our container runs in the system network namespace, not isolated as is typical
  1540  		// in Docker or Kubernetes. So use another high port. See https://golang.org/issue/26969.
  1541  		return "2200"
  1542  	}
  1543  	return "22"
  1544  }
  1545  
  1546  var sshServerOnce sync.Once
  1547  
  1548  // startSSHServer starts an SSH server.
  1549  func startSSHServer() {
  1550  	if useBuildletSSHServer() {
  1551  		startSSHServerSwarming()
  1552  		return
  1553  	}
  1554  	if inLinuxContainer() {
  1555  		startSSHServerLinux()
  1556  		return
  1557  	}
  1558  	if runtime.GOOS == "netbsd" {
  1559  		startSSHServerNetBSD()
  1560  		return
  1561  	}
  1562  
  1563  	log.Printf("start ssh server: don't know how to start SSH server on this host type")
  1564  }
  1565  
  1566  // inLinuxContainer reports whether it looks like we're on Linux running inside a container.
  1567  func inLinuxContainer() bool {
  1568  	if runtime.GOOS != "linux" {
  1569  		return false
  1570  	}
  1571  	if numProcs() >= 4 {
  1572  		// There should 1 process running (this buildlet
  1573  		// binary) if we're in Docker. Maybe 2 if something
  1574  		// else is happening. But if there are 4 or more,
  1575  		// we'll be paranoid and assuming we're running on a
  1576  		// user or host system and don't want to start an ssh
  1577  		// server.
  1578  		return false
  1579  	}
  1580  	// TODO: use a more explicit env variable or on-disk signal
  1581  	// that we're in a Go buildlet Docker image. But for now, this
  1582  	// seems to be consistently true:
  1583  	fi, err := os.Stat("/usr/local/bin/stage0")
  1584  	return err == nil && fi.Mode().IsRegular()
  1585  }
  1586  
  1587  // startSSHServerLinux starts an SSH server on a Linux system.
  1588  func startSSHServerLinux() {
  1589  	log.Printf("start ssh server for linux")
  1590  
  1591  	// First, create the privsep directory, otherwise we get a successful cmd.Start,
  1592  	// but this error message and then an exit:
  1593  	//    Missing privilege separation directory: /var/run/sshd
  1594  	if err := os.MkdirAll("/var/run/sshd", 0700); err != nil {
  1595  		log.Printf("creating /var/run/sshd: %v", err)
  1596  		return
  1597  	}
  1598  
  1599  	// The AWS Docker images don't have ssh host keys in
  1600  	// their image, at least as of 2017-07-23. So make them first.
  1601  	// These are the types sshd -D complains about currently.
  1602  	if runtime.GOARCH == "arm" {
  1603  		for _, keyType := range []string{"rsa", "dsa", "ed25519", "ecdsa"} {
  1604  			file := "/etc/ssh/ssh_host_" + keyType + "_key"
  1605  			if _, err := os.Stat(file); err == nil {
  1606  				continue
  1607  			}
  1608  			out, err := exec.Command("/usr/bin/ssh-keygen", "-f", file, "-N", "", "-t", keyType).CombinedOutput()
  1609  			log.Printf("ssh-keygen of type %s: err=%v, %s\n", keyType, err, out)
  1610  		}
  1611  	}
  1612  
  1613  	go func() {
  1614  		for {
  1615  			// TODO: using sshd -D isn't great as it only
  1616  			// handles a single connection and exits.
  1617  			// Maybe run in sshd -i (inetd) mode instead,
  1618  			// and hook that up to the buildlet directly?
  1619  			t0 := time.Now()
  1620  			cmd := exec.Command("/usr/sbin/sshd", "-D", "-p", sshPort(), "-d", "-d")
  1621  			cmd.Stderr = os.Stderr
  1622  			err := cmd.Start()
  1623  			if err != nil {
  1624  				log.Printf("starting sshd: %v", err)
  1625  				return
  1626  			}
  1627  			log.Printf("sshd started.")
  1628  			log.Printf("sshd exited: %v; restarting", cmd.Wait())
  1629  			if d := time.Since(t0); d < time.Second {
  1630  				time.Sleep(time.Second - d)
  1631  			}
  1632  		}
  1633  	}()
  1634  	waitLocalSSH()
  1635  }
  1636  
  1637  func startSSHServerNetBSD() {
  1638  	cmd := exec.Command("/etc/rc.d/sshd", "start")
  1639  	err := cmd.Start()
  1640  	if err != nil {
  1641  		log.Printf("starting sshd: %v", err)
  1642  		return
  1643  	}
  1644  	log.Printf("sshd started.")
  1645  	waitLocalSSH()
  1646  }
  1647  
  1648  // waitLocalSSH waits for sshd to start accepting connections.
  1649  func waitLocalSSH() {
  1650  	for i := 0; i < 40; i++ {
  1651  		time.Sleep(10 * time.Millisecond * time.Duration(i+1))
  1652  		c, err := net.Dial("tcp", "localhost:"+sshPort())
  1653  		if err == nil {
  1654  			c.Close()
  1655  			log.Printf("sshd connected.")
  1656  			return
  1657  		}
  1658  	}
  1659  	log.Printf("timeout waiting for sshd to come up")
  1660  }
  1661  
  1662  func numProcs() int {
  1663  	n := 0
  1664  	fis, _ := os.ReadDir("/proc")
  1665  	for _, fi := range fis {
  1666  		if _, err := strconv.Atoi(fi.Name()); err == nil {
  1667  			n++
  1668  		}
  1669  	}
  1670  	return n
  1671  }
  1672  
  1673  func fileSHA1(path string) (string, error) {
  1674  	f, err := os.Open(path)
  1675  	if err != nil {
  1676  		return "", err
  1677  	}
  1678  	defer f.Close()
  1679  	s1 := sha1.New()
  1680  	if _, err := io.Copy(s1, f); err != nil {
  1681  		return "", err
  1682  	}
  1683  	return fmt.Sprintf("%x", s1.Sum(nil)), nil
  1684  }
  1685  
  1686  // nativeRelPath verifies that p is a non-empty relative path
  1687  // using either slashes or the buildlet's native path separator,
  1688  // and returns it canonicalized to the native path separator.
  1689  func nativeRelPath(p string) (string, error) {
  1690  	if p == "" {
  1691  		return "", errors.New("path not provided")
  1692  	}
  1693  
  1694  	if filepath.Separator != '/' && strings.Contains(p, string(filepath.Separator)) {
  1695  		clean := filepath.Clean(p)
  1696  		if filepath.IsAbs(clean) {
  1697  			return "", fmt.Errorf("path %q is not relative", p)
  1698  		}
  1699  		if clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
  1700  			return "", fmt.Errorf("path %q refers to a parent directory", p)
  1701  		}
  1702  		if strings.HasPrefix(p, string(filepath.Separator)) || filepath.VolumeName(clean) != "" {
  1703  			// On Windows, this catches semi-relative paths like "C:" (meaning “the
  1704  			// current working directory on volume C:”) and "\windows" (meaning “the
  1705  			// windows subdirectory of the current drive letter”).
  1706  			return "", fmt.Errorf("path %q is relative to volume", p)
  1707  		}
  1708  		return p, nil
  1709  	}
  1710  
  1711  	clean := path.Clean(p)
  1712  	if path.IsAbs(clean) {
  1713  		return "", fmt.Errorf("path %q is not relative", p)
  1714  	}
  1715  	if clean == ".." || strings.HasPrefix(clean, "../") {
  1716  		return "", fmt.Errorf("path %q refers to a parent directory", p)
  1717  	}
  1718  	canon := filepath.FromSlash(p)
  1719  	if filepath.VolumeName(canon) != "" {
  1720  		return "", fmt.Errorf("path %q begins with a native volume name", p)
  1721  	}
  1722  	return canon, nil
  1723  }
  1724  
  1725  // An httpError wraps an error with a corresponding HTTP status code.
  1726  type httpError struct {
  1727  	statusCode int
  1728  	err        error
  1729  }
  1730  
  1731  func (he httpError) Error() string   { return he.err.Error() }
  1732  func (he httpError) Unwrap() error   { return he.err }
  1733  func (he httpError) httpStatus() int { return he.statusCode }
  1734  
  1735  // badRequestf returns an httpError with status 400 and an error constructed by
  1736  // formatting the given arguments.
  1737  func badRequestf(format string, args ...interface{}) error {
  1738  	return httpError{http.StatusBadRequest, fmt.Errorf(format, args...)}
  1739  }
  1740  
  1741  // httpStatus returns the httpStatus of err if it is or wraps an httpError,
  1742  // or StatusInternalServerError otherwise.
  1743  func httpStatus(err error) int {
  1744  	var he httpError
  1745  	if !errors.As(err, &he) {
  1746  		return http.StatusInternalServerError
  1747  	}
  1748  	return he.statusCode
  1749  }
  1750  
  1751  // requirePasswordHandler is an http.Handler auth wrapper that enforces an
  1752  // HTTP Basic password. The username is ignored.
  1753  type requirePasswordHandler struct {
  1754  	h        http.Handler
  1755  	password string // empty means no password
  1756  }
  1757  
  1758  func (h requirePasswordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  1759  	_, gotPass, _ := r.BasicAuth()
  1760  	if h.password != "" && h.password != gotPass {
  1761  		http.Error(w, "invalid password", http.StatusForbidden)
  1762  		return
  1763  	}
  1764  	h.h.ServeHTTP(w, r)
  1765  }
  1766  
  1767  // gcePlan9LogWriter truncates log writes to 128 bytes,
  1768  // to work around a GCE serial port bug affecting Plan 9.
  1769  type gcePlan9LogWriter struct {
  1770  	w   io.Writer
  1771  	buf []byte
  1772  }
  1773  
  1774  func (pw *gcePlan9LogWriter) Write(p []byte) (n int, err error) {
  1775  	const max = 128 - len("\n\x00")
  1776  	if len(p) < max {
  1777  		return pw.w.Write(p)
  1778  	}
  1779  	if pw.buf == nil {
  1780  		pw.buf = make([]byte, max+1)
  1781  	}
  1782  	n = copy(pw.buf[:max], p)
  1783  	pw.buf[n] = '\n'
  1784  	return pw.w.Write(pw.buf[:n+1])
  1785  }
  1786  
  1787  var killProcessTree = killProcessTreeUnix
  1788  
  1789  func killProcessTreeUnix(p *os.Process) error {
  1790  	return p.Kill()
  1791  }
  1792  
  1793  func vmwareGetInfo(key string) string {
  1794  	cmd := exec.Command("/Library/Application Support/VMware Tools/vmware-tools-daemon",
  1795  		"--cmd",
  1796  		"info-get "+key)
  1797  	var stdout, stderr bytes.Buffer
  1798  	cmd.Stdout = &stdout
  1799  	cmd.Stderr = &stderr
  1800  	err := cmd.Run()
  1801  	if err != nil {
  1802  		if strings.Contains(stderr.String(), "No value found") {
  1803  			return ""
  1804  		}
  1805  		log.Fatalf("Error running vmware-tools-daemon --cmd 'info-get %s': %v, %s\n%s", key, err, stderr.Bytes(), stdout.Bytes())
  1806  	}
  1807  	return strings.TrimSpace(stdout.String())
  1808  }
  1809  
  1810  func makeBSDFilesystemFast() {
  1811  	if !metadata.OnGCE() {
  1812  		log.Printf("Not on GCE; not remounting root filesystem.")
  1813  		return
  1814  	}
  1815  	btype, err := metadata.InstanceAttributeValue("buildlet-host-type")
  1816  	if _, ok := err.(metadata.NotDefinedError); ok && len(btype) == 0 {
  1817  		log.Printf("Not remounting root filesystem due to missing buildlet-host-type metadata.")
  1818  		return
  1819  	}
  1820  	if err != nil {
  1821  		log.Printf("Not remounting root filesystem due to failure getting builder type instance metadata: %v", err)
  1822  		return
  1823  	}
  1824  	// Tested on OpenBSD, FreeBSD, and NetBSD:
  1825  	out, err := exec.Command("/sbin/mount", "-u", "-o", "async,noatime", "/").CombinedOutput()
  1826  	if err != nil {
  1827  		log.Printf("Warning: failed to remount %s root filesystem with async,noatime: %v, %s", runtime.GOOS, err, out)
  1828  		return
  1829  	}
  1830  	log.Printf("Remounted / with async,noatime.")
  1831  }
  1832  
  1833  func appendSSHAuthorizedKey(sshUser, authKey string) error {
  1834  	if *swarmingBot {
  1835  		buldletAuthKeys = append(buldletAuthKeys, []byte(fmt.Sprintf("%s\n%s\n", sshUser, authKey))...)
  1836  		return nil
  1837  	}
  1838  	var homeRoot string
  1839  	switch runtime.GOOS {
  1840  	case "darwin":
  1841  		homeRoot = "/Users"
  1842  	case "plan9":
  1843  		return fmt.Errorf("ssh not supported on %v", runtime.GOOS)
  1844  	case "windows":
  1845  		homeRoot = `C:\Users`
  1846  	default:
  1847  		homeRoot = "/home"
  1848  		if runtime.GOOS == "freebsd" {
  1849  			if fi, err := os.Stat("/usr/home/" + sshUser); err == nil && fi.IsDir() {
  1850  				homeRoot = "/usr/home"
  1851  			}
  1852  		}
  1853  		if sshUser == "root" {
  1854  			homeRoot = "/"
  1855  		}
  1856  	}
  1857  	sshDir := filepath.Join(homeRoot, sshUser, ".ssh")
  1858  	if err := os.MkdirAll(sshDir, 0700); err != nil {
  1859  		return err
  1860  	}
  1861  	if err := os.Chmod(sshDir, 0700); err != nil {
  1862  		return err
  1863  	}
  1864  	authFile := filepath.Join(sshDir, "authorized_keys")
  1865  	exist, err := os.ReadFile(authFile)
  1866  	if err != nil && !os.IsNotExist(err) {
  1867  		return err
  1868  	}
  1869  	if strings.Contains(string(exist), authKey) {
  1870  		return nil
  1871  	}
  1872  	f, err := os.OpenFile(authFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
  1873  	if err != nil {
  1874  		return err
  1875  	}
  1876  	if _, err := fmt.Fprintf(f, "%s\n", authKey); err != nil {
  1877  		f.Close()
  1878  		return err
  1879  	}
  1880  	if err := f.Close(); err != nil {
  1881  		return err
  1882  	}
  1883  	if runtime.GOOS == "freebsd" {
  1884  		exec.Command("/usr/sbin/chown", "-R", sshUser, sshDir).Run()
  1885  	}
  1886  	if runtime.GOOS == "windows" {
  1887  		if res, err := exec.Command("icacls.exe", authFile, "/grant", `NT SERVICE\sshd:(R)`).CombinedOutput(); err != nil {
  1888  			return fmt.Errorf("setting permissions on authorized_keys with: %v\n%s", err, res)
  1889  		}
  1890  	}
  1891  	return nil
  1892  }
  1893  
  1894  // setWorkdirToTmpfs sets the *workDir (--workdir) flag to /workdir
  1895  // if the flag is empty and /workdir is a tmpfs mount, as it is on the various
  1896  // hosts that use rundockerbuildlet.
  1897  //
  1898  // It is set non-nil on operating systems where the functionality is
  1899  // needed & available. Currently we only use it on Linux.
  1900  var setWorkdirToTmpfs func()
  1901  
  1902  func initBaseUnixEnv() {
  1903  	if os.Getenv("USER") == "" {
  1904  		os.Setenv("USER", "root")
  1905  	}
  1906  	if os.Getenv("HOME") == "" {
  1907  		os.Setenv("HOME", "/root")
  1908  	}
  1909  }
  1910  
  1911  // removeAllAndMkdir calls removeAllIncludingReadonly and then os.Mkdir on the given
  1912  // dir, failing the process if either step fails.
  1913  func removeAllAndMkdir(dir string) {
  1914  	if err := removeAllIncludingReadonly(dir); err != nil {
  1915  		log.Fatal(err)
  1916  	}
  1917  	if err := os.Mkdir(dir, 0755); err != nil {
  1918  		log.Fatal(err)
  1919  	}
  1920  }
  1921  
  1922  // removeAllIncludingReadonly is like os.RemoveAll except that it'll
  1923  // also try to change permissions to work around permission errors
  1924  // when deleting.
  1925  func removeAllIncludingReadonly(dir string) error {
  1926  	err := os.RemoveAll(dir)
  1927  	if err == nil || !os.IsPermission(err) {
  1928  		return err
  1929  	}
  1930  	// Make a best effort (ignoring errors) attempt to make all
  1931  	// files and directories writable before we try to delete them
  1932  	// all again.
  1933  	filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
  1934  		const ownerWritable = 0200
  1935  		if err != nil || fi.Mode().Perm()&ownerWritable != 0 {
  1936  			return nil
  1937  		}
  1938  		os.Chmod(path, fi.Mode().Perm()|ownerWritable)
  1939  		return nil
  1940  	})
  1941  	return os.RemoveAll(dir)
  1942  }
  1943  
  1944  var (
  1945  	androidEmuDead = make(chan error) // closed on death
  1946  	androidEmuErr  error              // set prior to channel close
  1947  )
  1948  
  1949  func startAndroidEmulator() {
  1950  	cmd := exec.Command("/android/sdk/emulator/emulator",
  1951  		"@android-avd",
  1952  		"-no-audio",
  1953  		"-no-window",
  1954  		"-no-boot-anim",
  1955  		"-no-snapshot-save",
  1956  		"-wipe-data", // required to prevent a hang with -no-window when recovering from a snapshot?
  1957  	)
  1958  	log.Printf("running Android emulator: %v", cmd.Args)
  1959  	cmd.Stdout = os.Stdout
  1960  	cmd.Stderr = os.Stderr
  1961  	if err := cmd.Start(); err != nil {
  1962  		log.Fatalf("failed to start Android emulator: %v", err)
  1963  	}
  1964  	go func() {
  1965  		err := cmd.Wait()
  1966  		if err == nil {
  1967  			err = errors.New("exited without error")
  1968  		}
  1969  		androidEmuErr = err
  1970  		close(androidEmuDead)
  1971  	}()
  1972  }
  1973  
  1974  // checkAndroidEmulator returns an error if this machine is an Android builder
  1975  // and the Android emulator process has exited.
  1976  func checkAndroidEmulator() error {
  1977  	select {
  1978  	case <-androidEmuDead:
  1979  		return androidEmuErr
  1980  	default:
  1981  		return nil
  1982  	}
  1983  }
  1984  
  1985  var disableNetOnce sync.Once
  1986  
  1987  func disableOutboundNetwork() {
  1988  	if runtime.GOOS != "linux" {
  1989  		return
  1990  	}
  1991  	disableNetOnce.Do(disableOutboundNetworkLinux)
  1992  }
  1993  
  1994  func disableOutboundNetworkLinux() {
  1995  	iptables, err := exec.LookPath("iptables-legacy")
  1996  	if err != nil {
  1997  		// Some older distributions, such as Debian Stretch, don't yet have nftables,
  1998  		// so "iptables" gets us the legacy version whose rules syntax is used below.
  1999  		iptables, err = exec.LookPath("iptables")
  2000  		if err != nil {
  2001  			log.Println("disableOutboundNetworkLinux failed to find iptables:", err)
  2002  			return
  2003  		}
  2004  	}
  2005  	runOrLog(exec.Command(iptables, "-I", "OUTPUT", "2", "-m", "state", "--state", "NEW", "-d", "10.0.0.0/8", "-p", "tcp", "-j", "ACCEPT"))
  2006  	runOrLog(exec.Command(iptables, "-I", "OUTPUT", "3", "-m", "state", "--state", "NEW", "-p", "tcp", "--dport", "443", "-j", "REJECT", "--reject-with", "icmp-host-prohibited"))
  2007  	runOrLog(exec.Command(iptables, "-I", "OUTPUT", "3", "-m", "state", "--state", "NEW", "-p", "tcp", "--dport", "80", "-j", "REJECT", "--reject-with", "icmp-host-prohibited"))
  2008  	runOrLog(exec.Command(iptables, "-I", "OUTPUT", "3", "-m", "state", "--state", "NEW", "-p", "tcp", "--dport", "22", "-j", "REJECT", "--reject-with", "icmp-host-prohibited"))
  2009  }
  2010  
  2011  func runOrLog(cmd *exec.Cmd) {
  2012  	out, err := cmd.CombinedOutput()
  2013  	if err != nil {
  2014  		log.Printf("failed to run %s: %v, %s", cmd.Args, err, out)
  2015  	}
  2016  }
  2017  
  2018  // handleHealthz always returns 200 OK.
  2019  func handleHealthz(w http.ResponseWriter, _ *http.Request) {
  2020  	fmt.Fprintln(w, "ok")
  2021  }
  2022  
  2023  // serveReverseHealth serves /healthz requests on healthAddr for
  2024  // reverse buildlets.
  2025  //
  2026  // This can be used to monitor the health of guest buildlets, such as
  2027  // the Windows ARM64 qemu guest buildlet.
  2028  func serveReverseHealth() error {
  2029  	m := &http.ServeMux{}
  2030  	m.HandleFunc("/healthz", handleHealthz)
  2031  	return http.ListenAndServe(*healthAddr, m)
  2032  }
  2033  
  2034  func shell() string {
  2035  	switch runtime.GOOS {
  2036  	case "linux":
  2037  		return "bash"
  2038  	case "windows":
  2039  		return `C:\Windows\System32\cmd.exe`
  2040  	default:
  2041  		return os.Getenv("SHELL")
  2042  	}
  2043  }