summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.buildkite/hooks/post-command8
-rw-r--r--Makefile2
-rw-r--r--WORKSPACE4
-rw-r--r--website/cmd/server/BUILD3
-rw-r--r--website/cmd/server/main.go153
5 files changed, 154 insertions, 16 deletions
diff --git a/.buildkite/hooks/post-command b/.buildkite/hooks/post-command
index 5cd974002..8af1369a6 100644
--- a/.buildkite/hooks/post-command
+++ b/.buildkite/hooks/post-command
@@ -27,10 +27,14 @@ make -s testlogs 2>/dev/null | grep // | sort | uniq | (
# Upload all profiles, and include in an annotation.
declare profile_output=$(mktemp --tmpdir)
for file in $(find /tmp/profile -name \*.pprof -print 2>/dev/null | sort); do
- # Generate a link to the profile file at the top.
+ # Generate a link to the profile parsing function in gvisor.dev, which
+ # implicitly uses a prefix of https://storage.googleapis.com. Note that
+ # this relies on the specific BuildKite bucket location, and will break if
+ # this changes (although the artifacts will still exist and be just fine).
profile_name="${file#/tmp/profile/}"
+ profile_url="https://gvisor.dev/profile/gvisor-buildkite/${BUILDKITE_BUILD_ID}/${BUILDKITE_JOB_ID}/${file#/}/"
buildkite-agent artifact upload "${file}"
- echo "<li><a href='artifact://${file#/}'>${profile_name}</a></li>" >> "${profile_output}"
+ echo "<li><a href='${profile_url}'>${profile_name}</a></li>" >> "${profile_output}"
done
# Upload if we had outputs.
diff --git a/Makefile b/Makefile
index e7d4434f3..9f3bd6c88 100644
--- a/Makefile
+++ b/Makefile
@@ -389,7 +389,7 @@ website-push: website-build ## Push a new image and update the service.
.PHONY: website-push
website-deploy: website-push ## Deploy a new version of the website.
- @gcloud run deploy $(WEBSITE_SERVICE) --platform=managed --region=$(WEBSITE_REGION) --project=$(WEBSITE_PROJECT) --image=$(WEBSITE_IMAGE)
+ @gcloud run deploy $(WEBSITE_SERVICE) --platform=managed --region=$(WEBSITE_REGION) --project=$(WEBSITE_PROJECT) --image=$(WEBSITE_IMAGE) --memory 1Gi
.PHONY: website-deploy
##
diff --git a/WORKSPACE b/WORKSPACE
index 031c21163..72f9a89cc 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -866,8 +866,8 @@ go_repository(
go_repository(
name = "com_github_google_pprof",
importpath = "github.com/google/pprof",
- sum = "h1:DLpL8pWq0v4JYoRpEhDfsJhhJyGKCcQM2WPW2TJs31c=",
- version = "v0.0.0-20191218002539-d4f498aebedc",
+ sum = "h1:LR89qFljJ48s990kEKGsk213yIJDPI4205OKOzbURK8=",
+ version = "v0.0.0-20201218002935-b9804c9f04c2",
)
go_repository(
diff --git a/website/cmd/server/BUILD b/website/cmd/server/BUILD
index 6b5a08f0d..e4cf91e07 100644
--- a/website/cmd/server/BUILD
+++ b/website/cmd/server/BUILD
@@ -7,4 +7,7 @@ go_binary(
srcs = ["main.go"],
pure = True,
visibility = ["//website:__pkg__"],
+ deps = [
+ "@com_github_google_pprof//driver:go_default_library",
+ ],
)
diff --git a/website/cmd/server/main.go b/website/cmd/server/main.go
index ac09550a9..84a576858 100644
--- a/website/cmd/server/main.go
+++ b/website/cmd/server/main.go
@@ -21,8 +21,11 @@ import (
"log"
"net/http"
"os"
+ "path"
"regexp"
"strings"
+
+ "github.com/google/pprof/driver"
)
var redirects = map[string]string{
@@ -170,28 +173,155 @@ func redirectHandler(target string) http.Handler {
// redirectRedirects registers redirect http handlers.
func registerRedirects(mux *http.ServeMux) {
- if mux == nil {
- mux = http.DefaultServeMux
- }
-
for prefix, baseURL := range prefixHelpers {
p := "/" + prefix + "/"
mux.Handle(p, hostRedirectHandler(wrappedHandler(prefixRedirectHandler(p, baseURL))))
}
-
for path, redirect := range redirects {
mux.Handle(path, hostRedirectHandler(wrappedHandler(redirectHandler(redirect))))
}
}
-// registerStatic registers static file handlers
+// registerStatic registers static file handlers.
func registerStatic(mux *http.ServeMux, staticDir string) {
- if mux == nil {
- mux = http.DefaultServeMux
- }
mux.Handle("/", hostRedirectHandler(wrappedHandler(http.FileServer(http.Dir(staticDir)))))
}
+// profileMeta implements synthetic flags for pprof.
+type profileMeta struct {
+ // Mux is the mux to register on.
+ Mux *http.ServeMux
+
+ // SourceURL is the source of the profile.
+ SourceURL string
+}
+
+func (*profileMeta) ExtraUsage() string { return "" }
+func (*profileMeta) AddExtraUsage(string) {}
+func (*profileMeta) Bool(_ string, def bool, _ string) *bool { return &def }
+func (*profileMeta) Int(_ string, def int, _ string) *int { return &def }
+func (*profileMeta) Float64(_ string, def float64, _ string) *float64 { return &def }
+func (*profileMeta) StringList(_ string, def string, _ string) *[]*string { return new([]*string) }
+func (*profileMeta) String(option string, def string, _ string) *string {
+ switch option {
+ case "http":
+ // Only http is specified. Other options may be accessible via
+ // the web interface, so we just need to spoof a valid option
+ // here. The server is actually bound by HTTPServer, below.
+ value := "localhost:80"
+ return &value
+ case "symbolize":
+ // Don't attempt symbolization. Most profiles should come with
+ // mappings built-in to the profile itself.
+ value := "none"
+ return &value
+ default:
+ return &def // Default.
+ }
+}
+
+// Parse implements plugin.FlagSet.Parse.
+func (p *profileMeta) Parse(usage func()) []string {
+ // Just return the SourceURL. This is interpreted as the profile to
+ // download. We validate that the URL corresponds to a Google Cloud
+ // Storage URL below.
+ return []string{p.SourceURL}
+}
+
+// pprofFixedPrefix is used to limit the exposure to SSRF.
+//
+// See registerProfile below.
+const pprofFixedPrefix = "https://storage.googleapis.com/"
+
+// Target returns the URL target.
+func (p *profileMeta) Target() string {
+ return fmt.Sprintf("/profile/%s/", p.SourceURL[len(pprofFixedPrefix):])
+}
+
+// HTTPServer is a function passed to driver.PProf.
+func (p *profileMeta) HTTPServer(args *driver.HTTPServerArgs) error {
+ target := p.Target()
+ for subpath, handler := range args.Handlers {
+ handlerPath := path.Join(target, subpath)
+ if len(handlerPath) < len(target) {
+ // Don't clean the target, match only as the literal
+ // directory path in order to keep relative links
+ // working in the profile. E.g. /profile/foo/ is the
+ // base URL for the profile at https://.../foo.
+ handlerPath = target
+ }
+ p.Mux.Handle(handlerPath, handler)
+ }
+ return nil
+}
+
+// registerProfile registers the profile handler.
+//
+// Note that this has a security surface worth considering.
+//
+// We are passed effectively a URL, which we fetch and parse,
+// then display the profile output. We limit the possibility of
+// SSRF by interpreting the URL strictly as a part to an object
+// in Google Cloud Storage, but we allow the user to specify any
+// bucket (since this may change with the CI system).
+//
+// We additionally must consider the possibility that users may
+// craft malicious profile objects (somehow) and pass those URLs
+// here as well. It seems feasible that we could parse a profile
+// that causes a crash (DOS), but this would be automatically
+// handled without a blip. It seems unlikely that we could parse a
+// profile that gives full code execution, but even so there is
+// nothing in this image except this code and CA certs. At worst,
+// code execution would enable someone to serve up content under the
+// web domain. This would be ephemeral with the specific instance,
+// and persisting such an attack would require constantly crashing
+// instances in whatever way gives remote code execution. Even if
+// this were possible, it's unlikely that exploiting such a crash
+// could be done so constantly and consistently.
+//
+// The user can also fill the "disk" of this container instance,
+// causing an OOM and a crash. This has similar semantics to the
+// DOS scenario above, and would just be handled by Cloud Run.
+//
+// Finally, a malicious user could cause us to repeatedly fetch
+// extremely large objects. However, since we fetch objects via
+// the unauthenticated URL, such accesses would always be charged
+// to the object owner. Downloading large objects can lead to the
+// filling of the "disk" scenario above, but this is similarly a
+// minor issue and immediately mitigated.
+func registerProfile(mux *http.ServeMux) {
+ const urlPrefix = "/profile/"
+ mux.Handle(urlPrefix, hostRedirectHandler(wrappedHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Extract the URL; this is everything except the final /.
+ parts := strings.Split(r.URL.Path[len(urlPrefix):], "/")
+ url := pprofFixedPrefix + strings.Join(parts[:len(parts)-1], "/")
+ if url == pprofFixedPrefix {
+ http.Error(w, "Invalid URL: no path provided.", http.StatusNotFound)
+ return
+ }
+
+ // Set up the meta handler. This will modify the original mux
+ // accordingly, and we ultimately return a redirect that
+ // includes all the original arguments. This means that if we
+ // ever hit a server that does not have this profile loaded, it
+ // will load and redirect again.
+ meta := &profileMeta{
+ Mux: mux,
+ SourceURL: url,
+ }
+ if err := driver.PProf(&driver.Options{
+ Flagset: meta,
+ HTTPServer: meta.HTTPServer,
+ }); err != nil {
+ http.Error(w, fmt.Sprintf("Invalid profile: %v", err), http.StatusNotImplemented)
+ return
+ }
+
+ // Serve the path directly.
+ mux.ServeHTTP(w, r)
+ }))))
+}
+
func envFlagString(name, def string) string {
if val := os.Getenv(name); val != "" {
return val
@@ -211,8 +341,9 @@ var (
func main() {
flag.Parse()
- registerRedirects(nil)
- registerStatic(nil, *staticDir)
+ registerRedirects(http.DefaultServeMux)
+ registerStatic(http.DefaultServeMux, *staticDir)
+ registerProfile(http.DefaultServeMux)
log.Printf("Listening on %s...", *addr)
log.Fatal(http.ListenAndServe(*addr, nil))