sandbox: restrict run time

Assuming that wall time is a reasonable proxy for cpu time.

Change-Id: If6a3fe141f16d4b400047154d1513c6d32c945c5
Reviewed-on: https://go-review.googlesource.com/2851
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go
index 6f10f16..dbca0b1 100644
--- a/sandbox/sandbox.go
+++ b/sandbox/sandbox.go
@@ -3,6 +3,8 @@
 // license that can be found in the LICENSE file.
 
 // TODO(adg): add logging
+// TODO(proppy): restrict memory use
+// TODO(adg): send exit code to user
 
 // Command sandbox is an HTTP server that takes requests containing go
 // source files, and builds and executes them in a NaCl sanbox.
@@ -10,6 +12,7 @@
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"log"
@@ -17,8 +20,11 @@
 	"os"
 	"os/exec"
 	"path/filepath"
+	"time"
 )
 
+const maxRunTime = 500 * time.Millisecond
+
 type Request struct {
 	Body string
 }
@@ -31,16 +37,16 @@
 func compileHandler(w http.ResponseWriter, r *http.Request) {
 	var req Request
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
-		http.Error(w, fmt.Sprintf("request error: %v", err), http.StatusBadRequest)
+		http.Error(w, fmt.Sprintf("error decoding request: %v", err), http.StatusBadRequest)
 		return
 	}
 	resp, err := compileAndRun(&req)
 	if err != nil {
-		http.Error(w, fmt.Sprintf("sandbox error: %v", err), http.StatusInternalServerError)
+		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
 	if err := json.NewEncoder(w).Encode(resp); err != nil {
-		http.Error(w, fmt.Sprintf("response error: %v", err), http.StatusInternalServerError)
+		http.Error(w, fmt.Sprintf("error encoding response: %v", err), http.StatusInternalServerError)
 		return
 	}
 }
@@ -66,12 +72,14 @@
 		}
 		return nil, fmt.Errorf("error building go source: %v", err)
 	}
-	// TODO(proppy): restrict run time and memory use.
 	cmd = exec.Command("sel_ldr_x86_64", "-l", "/dev/null", "-S", "-e", exe)
 	rec := new(Recorder)
 	cmd.Stdout = rec.Stdout()
 	cmd.Stderr = rec.Stderr()
-	if err := cmd.Run(); err != nil {
+	if err := runTimeout(cmd, maxRunTime); err != nil {
+		if err == timeoutErr {
+			return &Response{Errors: "process took too long"}, nil
+		}
 		if _, ok := err.(*exec.ExitError); !ok {
 			return nil, fmt.Errorf("error running sandbox: %v", err)
 		}
@@ -83,6 +91,27 @@
 	return &Response{Events: events}, nil
 }
 
+var timeoutErr = errors.New("process timed out")
+
+func runTimeout(cmd *exec.Cmd, d time.Duration) error {
+	if err := cmd.Start(); err != nil {
+		return err
+	}
+	errc := make(chan error, 1)
+	go func() {
+		errc <- cmd.Wait()
+	}()
+	t := time.NewTimer(d)
+	select {
+	case err := <-errc:
+		t.Stop()
+		return err
+	case <-t.C:
+		cmd.Process.Kill()
+		return timeoutErr
+	}
+}
+
 func healthHandler(w http.ResponseWriter, r *http.Request) {
 	if err := healthCheck(); err != nil {
 		http.Error(w, "Health check failed: "+err.Error(), http.StatusInternalServerError)