golang.org/x/playground@v0.0.0-20230418134305-14ebe15bcd59/sandbox.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 // TODO(andybons): add logging 6 // TODO(andybons): restrict memory use 7 8 package main 9 10 import ( 11 "bytes" 12 "context" 13 "crypto/sha256" 14 "encoding/json" 15 "errors" 16 "fmt" 17 "go/ast" 18 "go/doc" 19 "go/parser" 20 "go/token" 21 "io" 22 "net" 23 "net/http" 24 "os" 25 "os/exec" 26 "path/filepath" 27 "runtime" 28 "strconv" 29 "strings" 30 "sync" 31 "time" 32 "unicode" 33 "unicode/utf8" 34 35 "cloud.google.com/go/compute/metadata" 36 "github.com/bradfitz/gomemcache/memcache" 37 "go.opencensus.io/stats" 38 "go.opencensus.io/tag" 39 "golang.org/x/playground/internal" 40 "golang.org/x/playground/internal/gcpdial" 41 "golang.org/x/playground/sandbox/sandboxtypes" 42 ) 43 44 const ( 45 // Time for 'go build' to download 3rd-party modules and compile. 46 maxBuildTime = 10 * time.Second 47 maxRunTime = 5 * time.Second 48 49 // progName is the implicit program name written to the temp 50 // dir and used in compiler and vet errors. 51 progName = "prog.go" 52 progTestName = "prog_test.go" 53 ) 54 55 const ( 56 goBuildTimeoutError = "timeout running go build" 57 runTimeoutError = "timeout running program" 58 ) 59 60 // internalErrors are strings found in responses that will not be cached 61 // due to their non-deterministic nature. 62 var internalErrors = []string{ 63 "out of memory", 64 "cannot allocate memory", 65 } 66 67 type request struct { 68 Body string 69 WithVet bool // whether client supports vet response in a /compile request (Issue 31970) 70 } 71 72 type response struct { 73 Errors string 74 Events []Event 75 Status int 76 IsTest bool 77 TestsFailed int 78 79 // VetErrors, if non-empty, contains any vet errors. It is 80 // only populated if request.WithVet was true. 81 VetErrors string `json:",omitempty"` 82 // VetOK reports whether vet ran & passed. It is only 83 // populated if request.WithVet was true. Only one of 84 // VetErrors or VetOK can be non-zero. 85 VetOK bool `json:",omitempty"` 86 } 87 88 // commandHandler returns an http.HandlerFunc. 89 // This handler creates a *request, assigning the "Body" field a value 90 // from the "body" form parameter or from the HTTP request body. 91 // If there is no cached *response for the combination of cachePrefix and request.Body, 92 // handler calls cmdFunc and in case of a nil error, stores the value of *response in the cache. 93 // The handler returned supports Cross-Origin Resource Sharing (CORS) from any domain. 94 func (s *server) commandHandler(cachePrefix string, cmdFunc func(context.Context, *request) (*response, error)) http.HandlerFunc { 95 return func(w http.ResponseWriter, r *http.Request) { 96 cachePrefix := cachePrefix // so we can modify it below 97 w.Header().Set("Access-Control-Allow-Origin", "*") 98 if r.Method == "OPTIONS" { 99 // This is likely a pre-flight CORS request. 100 return 101 } 102 103 var req request 104 // Until programs that depend on golang.org/x/tools/godoc/static/playground.js 105 // are updated to always send JSON, this check is in place. 106 if b := r.FormValue("body"); b != "" { 107 req.Body = b 108 req.WithVet, _ = strconv.ParseBool(r.FormValue("withVet")) 109 } else if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 110 s.log.Errorf("error decoding request: %v", err) 111 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 112 return 113 } 114 115 if req.WithVet { 116 cachePrefix += "_vet" // "prog" -> "prog_vet" 117 } 118 119 resp := &response{} 120 key := cacheKey(cachePrefix, req.Body) 121 if err := s.cache.Get(key, resp); err != nil { 122 if !errors.Is(err, memcache.ErrCacheMiss) { 123 s.log.Errorf("s.cache.Get(%q, &response): %v", key, err) 124 } 125 resp, err = cmdFunc(r.Context(), &req) 126 if err != nil { 127 s.log.Errorf("cmdFunc error: %v", err) 128 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 129 return 130 } 131 if strings.Contains(resp.Errors, goBuildTimeoutError) || strings.Contains(resp.Errors, runTimeoutError) { 132 // TODO(golang.org/issue/38576) - This should be a http.StatusBadRequest, 133 // but the UI requires a 200 to parse the response. It's difficult to know 134 // if we've timed out because of an error in the code snippet, or instability 135 // on the playground itself. Either way, we should try to show the user the 136 // partial output of their program. 137 s.writeJSONResponse(w, resp, http.StatusOK) 138 return 139 } 140 for _, e := range internalErrors { 141 if strings.Contains(resp.Errors, e) { 142 s.log.Errorf("cmdFunc compilation error: %q", resp.Errors) 143 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 144 return 145 } 146 } 147 for _, el := range resp.Events { 148 if el.Kind != "stderr" { 149 continue 150 } 151 for _, e := range internalErrors { 152 if strings.Contains(el.Message, e) { 153 s.log.Errorf("cmdFunc runtime error: %q", el.Message) 154 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 155 return 156 } 157 } 158 } 159 if err := s.cache.Set(key, resp); err != nil { 160 s.log.Errorf("cache.Set(%q, resp): %v", key, err) 161 } 162 } 163 164 s.writeJSONResponse(w, resp, http.StatusOK) 165 } 166 } 167 168 func cacheKey(prefix, body string) string { 169 h := sha256.New() 170 io.WriteString(h, body) 171 return fmt.Sprintf("%s-%s-%x", prefix, runtime.Version(), h.Sum(nil)) 172 } 173 174 // isTestFunc tells whether fn has the type of a testing, or fuzz function, or a TestMain func. 175 func isTestFunc(fn *ast.FuncDecl) bool { 176 if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 || 177 fn.Type.Params.List == nil || 178 len(fn.Type.Params.List) != 1 || 179 len(fn.Type.Params.List[0].Names) > 1 { 180 return false 181 } 182 ptr, ok := fn.Type.Params.List[0].Type.(*ast.StarExpr) 183 if !ok { 184 return false 185 } 186 // We can't easily check that the type is *testing.T or *testing.F 187 // because we don't know how testing has been imported, 188 // but at least check that it's *T (or *F) or *something.T (or *something.F). 189 if name, ok := ptr.X.(*ast.Ident); ok && (name.Name == "T" || name.Name == "F" || name.Name == "M") { 190 return true 191 } 192 if sel, ok := ptr.X.(*ast.SelectorExpr); ok && (sel.Sel.Name == "T" || sel.Sel.Name == "F" || sel.Sel.Name == "M") { 193 return true 194 } 195 return false 196 } 197 198 // isTest tells whether name looks like a test (or benchmark, or fuzz, according to prefix). 199 // It is a Test (say) if there is a character after Test that is not a lower-case letter. 200 // We don't want mistaken Testimony or erroneous Benchmarking. 201 func isTest(name, prefix string) bool { 202 if !strings.HasPrefix(name, prefix) { 203 return false 204 } 205 if len(name) == len(prefix) { // "Test" is ok 206 return true 207 } 208 r, _ := utf8.DecodeRuneInString(name[len(prefix):]) 209 return !unicode.IsLower(r) 210 } 211 212 // getTestProg returns source code that executes all valid tests and examples in src. 213 // If the main function is present or there are no tests or examples, it returns nil. 214 // getTestProg emulates the "go test" command as closely as possible. 215 // Benchmarks are not supported because of sandboxing. 216 func isTestProg(src []byte) bool { 217 fset := token.NewFileSet() 218 // Early bail for most cases. 219 f, err := parser.ParseFile(fset, progName, src, parser.ImportsOnly) 220 if err != nil || f.Name.Name != "main" { 221 return false 222 } 223 224 // Parse everything and extract test names. 225 f, err = parser.ParseFile(fset, progName, src, parser.ParseComments) 226 if err != nil { 227 return false 228 } 229 230 var hasTest bool 231 var hasFuzz bool 232 for _, d := range f.Decls { 233 n, ok := d.(*ast.FuncDecl) 234 if !ok { 235 continue 236 } 237 name := n.Name.Name 238 switch { 239 case name == "main": 240 // main declared as a method will not obstruct creation of our main function. 241 if n.Recv == nil { 242 return false 243 } 244 case name == "TestMain" && isTestFunc(n): 245 hasTest = true 246 case isTest(name, "Test") && isTestFunc(n): 247 hasTest = true 248 case isTest(name, "Fuzz") && isTestFunc(n): 249 hasFuzz = true 250 } 251 } 252 253 if hasTest || hasFuzz { 254 return true 255 } 256 257 return len(doc.Examples(f)) > 0 258 } 259 260 var failedTestPattern = "--- FAIL" 261 262 // compileAndRun tries to build and run a user program. 263 // The output of successfully ran program is returned in *response.Events. 264 // If a program cannot be built or has timed out, 265 // *response.Errors contains an explanation for a user. 266 func compileAndRun(ctx context.Context, req *request) (*response, error) { 267 // TODO(andybons): Add semaphore to limit number of running programs at once. 268 tmpDir, err := os.MkdirTemp("", "sandbox") 269 if err != nil { 270 return nil, fmt.Errorf("error creating temp directory: %v", err) 271 } 272 defer os.RemoveAll(tmpDir) 273 274 br, err := sandboxBuild(ctx, tmpDir, []byte(req.Body), req.WithVet) 275 if err != nil { 276 return nil, err 277 } 278 if br.errorMessage != "" { 279 return &response{Errors: removeBanner(br.errorMessage)}, nil 280 } 281 282 execRes, err := sandboxRun(ctx, br.exePath, br.testParam) 283 if err != nil { 284 return nil, err 285 } 286 if execRes.Error != "" { 287 return &response{Errors: execRes.Error}, nil 288 } 289 290 rec := new(Recorder) 291 rec.Stdout().Write(execRes.Stdout) 292 rec.Stderr().Write(execRes.Stderr) 293 events, err := rec.Events() 294 if err != nil { 295 log.Printf("error decoding events: %v", err) 296 return nil, fmt.Errorf("error decoding events: %v", err) 297 } 298 var fails int 299 if br.testParam != "" { 300 // In case of testing the TestsFailed field contains how many tests have failed. 301 for _, e := range events { 302 fails += strings.Count(e.Message, failedTestPattern) 303 } 304 } 305 return &response{ 306 Events: events, 307 Status: execRes.ExitCode, 308 IsTest: br.testParam != "", 309 TestsFailed: fails, 310 VetErrors: br.vetOut, 311 VetOK: req.WithVet && br.vetOut == "", 312 }, nil 313 } 314 315 // buildResult is the output of a sandbox build attempt. 316 type buildResult struct { 317 // goPath is a temporary directory if the binary was built with module support. 318 // TODO(golang.org/issue/25224) - Why is the module mode built so differently? 319 goPath string 320 // exePath is the path to the built binary. 321 exePath string 322 // testParam is set if tests should be run when running the binary. 323 testParam string 324 // errorMessage is an error message string to be returned to the user. 325 errorMessage string 326 // vetOut is the output of go vet, if requested. 327 vetOut string 328 } 329 330 // cleanup cleans up the temporary goPath created when building with module support. 331 func (b *buildResult) cleanup() error { 332 if b.goPath != "" { 333 return os.RemoveAll(b.goPath) 334 } 335 return nil 336 } 337 338 // sandboxBuild builds a Go program and returns a build result that includes the build context. 339 // 340 // An error is returned if a non-user-correctable error has occurred. 341 func sandboxBuild(ctx context.Context, tmpDir string, in []byte, vet bool) (br *buildResult, err error) { 342 start := time.Now() 343 defer func() { 344 status := "success" 345 if err != nil { 346 status = "error" 347 } 348 // Ignore error. The only error can be invalid tag key or value 349 // length, which we know are safe. 350 stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kGoBuildSuccess, status)}, 351 mGoBuildLatency.M(float64(time.Since(start))/float64(time.Millisecond))) 352 }() 353 354 files, err := splitFiles(in) 355 if err != nil { 356 return &buildResult{errorMessage: err.Error()}, nil 357 } 358 359 br = new(buildResult) 360 defer br.cleanup() 361 var buildPkgArg = "." 362 if files.Num() == 1 && len(files.Data(progName)) > 0 { 363 src := files.Data(progName) 364 if isTestProg(src) { 365 br.testParam = "-test.v" 366 files.MvFile(progName, progTestName) 367 } 368 } 369 370 if !files.Contains("go.mod") { 371 files.AddFile("go.mod", []byte("module play\n")) 372 } 373 374 for f, src := range files.m { 375 // Before multi-file support we required that the 376 // program be in package main, so continue to do that 377 // for now. But permit anything in subdirectories to have other 378 // packages. 379 if !strings.Contains(f, "/") { 380 fset := token.NewFileSet() 381 f, err := parser.ParseFile(fset, f, src, parser.PackageClauseOnly) 382 if err == nil && f.Name.Name != "main" { 383 return &buildResult{errorMessage: "package name must be main"}, nil 384 } 385 } 386 387 in := filepath.Join(tmpDir, f) 388 if strings.Contains(f, "/") { 389 if err := os.MkdirAll(filepath.Dir(in), 0755); err != nil { 390 return nil, err 391 } 392 } 393 if err := os.WriteFile(in, src, 0644); err != nil { 394 return nil, fmt.Errorf("error creating temp file %q: %v", in, err) 395 } 396 } 397 398 br.exePath = filepath.Join(tmpDir, "a.out") 399 goCache := filepath.Join(tmpDir, "gocache") 400 401 // Copy the gocache directory containing .a files for std, so that we can 402 // avoid recompiling std during this build. Using -al (hard linking) is 403 // faster than actually copying the bytes. 404 // 405 // This is necessary as .a files are no longer included in GOROOT following 406 // https://go.dev/cl/432535. 407 if err := exec.Command("cp", "-al", "/gocache", goCache).Run(); err != nil { 408 return nil, fmt.Errorf("error copying GOCACHE: %v", err) 409 } 410 411 var goArgs []string 412 if br.testParam != "" { 413 goArgs = append(goArgs, "test", "-c") 414 } else { 415 goArgs = append(goArgs, "build") 416 } 417 goArgs = append(goArgs, "-o", br.exePath, "-tags=faketime") 418 419 cmd := exec.Command("/usr/local/go-faketime/bin/go", goArgs...) 420 cmd.Dir = tmpDir 421 cmd.Env = []string{"GOOS=linux", "GOARCH=amd64", "GOROOT=/usr/local/go-faketime"} 422 cmd.Env = append(cmd.Env, "GOCACHE="+goCache) 423 cmd.Env = append(cmd.Env, "CGO_ENABLED=0") 424 // Create a GOPATH just for modules to be downloaded 425 // into GOPATH/pkg/mod. 426 cmd.Args = append(cmd.Args, "-modcacherw") 427 cmd.Args = append(cmd.Args, "-mod=mod") 428 br.goPath, err = os.MkdirTemp("", "gopath") 429 if err != nil { 430 log.Printf("error creating temp directory: %v", err) 431 return nil, fmt.Errorf("error creating temp directory: %v", err) 432 } 433 cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY="+playgroundGoproxy()) 434 cmd.Args = append(cmd.Args, buildPkgArg) 435 cmd.Env = append(cmd.Env, "GOPATH="+br.goPath) 436 out := &bytes.Buffer{} 437 cmd.Stderr, cmd.Stdout = out, out 438 439 if err := cmd.Start(); err != nil { 440 return nil, fmt.Errorf("error starting go build: %v", err) 441 } 442 ctx, cancel := context.WithTimeout(ctx, maxBuildTime) 443 defer cancel() 444 if err := internal.WaitOrStop(ctx, cmd, os.Interrupt, 250*time.Millisecond); err != nil { 445 if errors.Is(err, context.DeadlineExceeded) { 446 br.errorMessage = fmt.Sprintln(goBuildTimeoutError) 447 } else if ee := (*exec.ExitError)(nil); !errors.As(err, &ee) { 448 log.Printf("error building program: %v", err) 449 return nil, fmt.Errorf("error building go source: %v", err) 450 } 451 // Return compile errors to the user. 452 // Rewrite compiler errors to strip the tmpDir name. 453 br.errorMessage = br.errorMessage + strings.Replace(string(out.Bytes()), tmpDir+"/", "", -1) 454 455 // "go build", invoked with a file name, puts this odd 456 // message before any compile errors; strip it. 457 br.errorMessage = strings.Replace(br.errorMessage, "# command-line-arguments\n", "", 1) 458 459 return br, nil 460 } 461 const maxBinarySize = 100 << 20 // copied from sandbox backend; TODO: unify? 462 if fi, err := os.Stat(br.exePath); err != nil || fi.Size() == 0 || fi.Size() > maxBinarySize { 463 if err != nil { 464 return nil, fmt.Errorf("failed to stat binary: %v", err) 465 } 466 return nil, fmt.Errorf("invalid binary size %d", fi.Size()) 467 } 468 if vet { 469 // TODO: do this concurrently with the execution to reduce latency. 470 br.vetOut, err = vetCheckInDir(ctx, tmpDir, br.goPath) 471 if err != nil { 472 return nil, fmt.Errorf("running vet: %v", err) 473 } 474 } 475 return br, nil 476 } 477 478 // sandboxRun runs a Go binary in a sandbox environment. 479 func sandboxRun(ctx context.Context, exePath string, testParam string) (execRes sandboxtypes.Response, err error) { 480 start := time.Now() 481 defer func() { 482 status := "success" 483 if err != nil { 484 status = "error" 485 } 486 // Ignore error. The only error can be invalid tag key or value 487 // length, which we know are safe. 488 stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kGoBuildSuccess, status)}, 489 mGoRunLatency.M(float64(time.Since(start))/float64(time.Millisecond))) 490 }() 491 exeBytes, err := os.ReadFile(exePath) 492 if err != nil { 493 return execRes, err 494 } 495 ctx, cancel := context.WithTimeout(ctx, maxRunTime) 496 defer cancel() 497 sreq, err := http.NewRequestWithContext(ctx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes)) 498 if err != nil { 499 return execRes, fmt.Errorf("NewRequestWithContext %q: %w", sandboxBackendURL(), err) 500 } 501 sreq.Header.Add("Idempotency-Key", "1") // lets Transport do retries with a POST 502 if testParam != "" { 503 sreq.Header.Add("X-Argument", testParam) 504 } 505 sreq.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(exeBytes)), nil } 506 res, err := sandboxBackendClient().Do(sreq) 507 if err != nil { 508 if errors.Is(ctx.Err(), context.DeadlineExceeded) { 509 execRes.Error = runTimeoutError 510 return execRes, nil 511 } 512 return execRes, fmt.Errorf("POST %q: %w", sandboxBackendURL(), err) 513 } 514 defer res.Body.Close() 515 if res.StatusCode != http.StatusOK { 516 log.Printf("unexpected response from backend: %v", res.Status) 517 return execRes, fmt.Errorf("unexpected response from backend: %v", res.Status) 518 } 519 if err := json.NewDecoder(res.Body).Decode(&execRes); err != nil { 520 log.Printf("JSON decode error from backend: %v", err) 521 return execRes, errors.New("error parsing JSON from backend") 522 } 523 return execRes, nil 524 } 525 526 // playgroundGoproxy returns the GOPROXY environment config the playground should use. 527 // It is fetched from the environment variable PLAY_GOPROXY. A missing or empty 528 // value for PLAY_GOPROXY returns the default value of https://proxy.golang.org. 529 func playgroundGoproxy() string { 530 proxypath := os.Getenv("PLAY_GOPROXY") 531 if proxypath != "" { 532 return proxypath 533 } 534 return "https://proxy.golang.org" 535 } 536 537 // healthCheck attempts to build a binary from the source in healthProg. 538 // It returns any error returned from sandboxBuild, or nil if none is returned. 539 func (s *server) healthCheck(ctx context.Context) error { 540 tmpDir, err := os.MkdirTemp("", "sandbox") 541 if err != nil { 542 return fmt.Errorf("error creating temp directory: %v", err) 543 } 544 defer os.RemoveAll(tmpDir) 545 br, err := sandboxBuild(ctx, tmpDir, []byte(healthProg), false) 546 if err != nil { 547 return err 548 } 549 if br.errorMessage != "" { 550 return errors.New(br.errorMessage) 551 } 552 return nil 553 } 554 555 // sandboxBackendURL returns the URL of the sandbox backend that 556 // executes binaries. This backend is required for Go 1.14+ (where it 557 // executes using gvisor, since Native Client support is removed). 558 // 559 // This function either returns a non-empty string or it panics. 560 func sandboxBackendURL() string { 561 if v := os.Getenv("SANDBOX_BACKEND_URL"); v != "" { 562 return v 563 } 564 id, _ := metadata.ProjectID() 565 switch id { 566 case "golang-org": 567 return "http://sandbox.play-sandbox-fwd.il4.us-central1.lb.golang-org.internal/run" 568 } 569 panic(fmt.Sprintf("no SANDBOX_BACKEND_URL environment and no default defined for project %q", id)) 570 } 571 572 var sandboxBackendOnce struct { 573 sync.Once 574 c *http.Client 575 } 576 577 func sandboxBackendClient() *http.Client { 578 sandboxBackendOnce.Do(initSandboxBackendClient) 579 return sandboxBackendOnce.c 580 } 581 582 // initSandboxBackendClient runs from a sync.Once and initializes 583 // sandboxBackendOnce.c with the *http.Client we'll use to contact the 584 // sandbox execution backend. 585 func initSandboxBackendClient() { 586 id, _ := metadata.ProjectID() 587 switch id { 588 case "golang-org": 589 // For production, use a funky Transport dialer that 590 // contacts backend directly, without going through an 591 // internal load balancer, due to internal GCP 592 // reasons, which we might resolve later. This might 593 // be a temporary hack. 594 tr := http.DefaultTransport.(*http.Transport).Clone() 595 rigd := gcpdial.NewRegionInstanceGroupDialer("golang-org", "us-central1", "play-sandbox-rigm") 596 tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) { 597 if addr == "sandbox.play-sandbox-fwd.il4.us-central1.lb.golang-org.internal:80" { 598 ip, err := rigd.PickIP(ctx) 599 if err != nil { 600 return nil, err 601 } 602 addr = net.JoinHostPort(ip, "80") // and fallthrough 603 } 604 var d net.Dialer 605 return d.DialContext(ctx, netw, addr) 606 } 607 sandboxBackendOnce.c = &http.Client{Transport: tr} 608 default: 609 sandboxBackendOnce.c = http.DefaultClient 610 } 611 } 612 613 // removeBanner remove package name banner 614 func removeBanner(output string) string { 615 if strings.HasPrefix(output, "#") { 616 if nl := strings.Index(output, "\n"); nl != -1 { 617 output = output[nl+1:] 618 } 619 } 620 return output 621 } 622 623 const healthProg = ` 624 package main 625 626 import "fmt" 627 628 func main() { fmt.Print("ok") } 629 `