From 1f00bf59232077112fac51d0853112460db66916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 16 Jul 2026 17:00:08 +0200 Subject: [PATCH] internal/report: fall back to $GOROOT/src when locating source files Profiles from Go programs refer to standard library sources under the GOROOT of the build machine, which breaks the source and weblist views when that path does not exist locally (e.g. a profile from another machine), or when the program was built with -trimpath and the files are recorded relative to $GOROOT/src. Teach openSourceFile to fall back to the local $GOROOT/src after the regular search fails: relative paths resolve against it directly, and absolute paths resolve by matching the components after a /src/ one. The fallback only runs when the existing lookup finds nothing, so previously resolved files are unaffected. Also introduce tryOpenFile, which rejects directories that os.Open would happily open, so a directory matching a candidate path no longer wins the search and then fails when its lines are read. Co-authored-by: Aliaksandr Valialkin --- internal/report/source.go | 98 ++++++++++++++++++++---- internal/report/source_test.go | 131 +++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 15 deletions(-) diff --git a/internal/report/source.go b/internal/report/source.go index 771f500ec1..8a8284f7ab 100644 --- a/internal/report/source.go +++ b/internal/report/source.go @@ -25,6 +25,7 @@ import ( "os" "path/filepath" "regexp" + "runtime" "slices" "sort" "strconv" @@ -1007,28 +1008,95 @@ func openSourceFile(path, searchPath, trim string) (*os.File, error) { path = trimPath(path, trim, searchPath) // If file is still absolute, require file to exist. if filepath.IsAbs(path) { - f, err := os.Open(path) - return f, err - } - // Scan each component of the path. - for _, dir := range filepath.SplitList(searchPath) { - // Search up for every parent of each possible path. - for { - filename := filepath.Join(dir, path) - if f, err := os.Open(filename); err == nil { - return f, nil - } - parent := filepath.Dir(dir) - if parent == dir { - break + if f, err := tryOpenFile(path); err == nil { + return f, nil + } + } else { + // Scan each component of the path. + for _, dir := range filepath.SplitList(searchPath) { + // Search up for every parent of each possible path. + for { + filename := filepath.Join(dir, path) + if f, err := tryOpenFile(filename); err == nil { + return f, nil + } + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent } - dir = parent } } + // Fall back to looking for Go standard library sources under the local + // $GOROOT/src, since profiles from Go programs refer to standard library + // files under the GOROOT of the machine where the program was built. + if f, err := openGorootSourceFile(path, gorootSrc); err == nil { + return f, nil + } return nil, fmt.Errorf("could not find file %s on path %s", path, searchPath) } +// tryOpenFile opens the file at filename if it exists and is not a directory, +// which os.Open would also happily open. +func tryOpenFile(filename string) (*os.File, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + stat, err := f.Stat() + if err != nil { + f.Close() + return nil, err + } + if stat.IsDir() { + f.Close() + return nil, fmt.Errorf("%s is a directory", filename) + } + return f, nil +} + +// gorootSrc is the directory holding the Go standard library sources, if +// known. runtime.GOROOT honors the $GOROOT environment variable and otherwise +// reports the GOROOT this binary was built with, which is the Go installation +// used to `go install` pprof in the common case. +var gorootSrc = func() string { + if goroot := runtime.GOROOT(); goroot != "" { + return filepath.Join(goroot, "src") + } + return "" +}() + +// openGorootSourceFile tries to open a Go standard library source file under +// gorootSrc. Binaries built without -trimpath record standard library files +// under the build machine's GOROOT (e.g. /usr/local/go/src/runtime/proc.go), +// which may not exist locally, so resolve the path components after a "/src/" +// component against gorootSrc. Binaries built with -trimpath record them +// relative to $GOROOT/src (e.g. runtime/proc.go), so resolve relative paths +// against gorootSrc directly. +func openGorootSourceFile(path, gorootSrc string) (*os.File, error) { + if gorootSrc == "" { + return nil, fmt.Errorf("GOROOT is not known") + } + if !filepath.IsAbs(path) { + if f, err := tryOpenFile(filepath.Join(gorootSrc, path)); err == nil { + return f, nil + } + } + sPath := filepath.ToSlash(path) + for { + i := strings.Index(sPath, "/src/") + if i == -1 { + return nil, fmt.Errorf("could not find file %s under %s", path, gorootSrc) + } + sPath = sPath[i+len("/src/"):] + if f, err := tryOpenFile(filepath.Join(gorootSrc, filepath.FromSlash(sPath))); err == nil { + return f, nil + } + } +} + // trimPath cleans up a path by removing prefixes that are commonly // found on profiles plus configured prefixes. // TODO(aalexand): Consider optimizing out the redundant work done in this diff --git a/internal/report/source_test.go b/internal/report/source_test.go index dce64bc438..5ef39aabeb 100644 --- a/internal/report/source_test.go +++ b/internal/report/source_test.go @@ -180,6 +180,12 @@ func TestOpenSourceFile(t *testing.T) { desc: "error when not found", path: "foo.cc", }, + { + desc: "directory is not matched", + searchPath: "$dir", + fs: []string{"foo/bar.cc"}, + path: "foo", + }, } { t.Run(tc.desc, func(t *testing.T) { defer func() { @@ -215,6 +221,131 @@ func TestOpenSourceFile(t *testing.T) { } } +func TestOpenGorootSourceFile(t *testing.T) { + gorootSrc := filepath.Join(t.TempDir(), "goroot", "src") + for _, f := range []string{"runtime/proc.go", "fmt/print.go"} { + path := filepath.Join(gorootSrc, filepath.FromSlash(f)) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatalf("failed to create dir for %q: %v", path, err) + } + if err := os.WriteFile(path, nil, 0644); err != nil { + t.Fatalf("failed to create file %q: %v", path, err) + } + } + for _, tc := range []struct { + desc string + path string + noGoroot bool + wantPath string // Relative to gorootSrc. If empty, error is wanted. + }{ + { + desc: "absolute path under a foreign GOROOT", + path: "/usr/local/go/src/runtime/proc.go", + wantPath: "runtime/proc.go", + }, + { + desc: "absolute path with multiple src components", + path: "/home/user/src/go/src/fmt/print.go", + wantPath: "fmt/print.go", + }, + { + desc: "relative path from a -trimpath build", + path: "runtime/proc.go", + wantPath: "runtime/proc.go", + }, + { + desc: "absolute path without a src component", + path: "/usr/local/go/runtime/proc.go", + }, + { + desc: "file missing from GOROOT", + path: "/usr/local/go/src/runtime/missing.go", + }, + { + desc: "unknown GOROOT", + path: "/usr/local/go/src/runtime/proc.go", + noGoroot: true, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + src := gorootSrc + if tc.noGoroot { + src = "" + } + path := filepath.FromSlash(tc.path) + f, err := openGorootSourceFile(path, src) + if tc.wantPath == "" { + if err == nil { + gotPath := f.Name() + f.Close() + t.Fatalf("openGorootSourceFile(%q) = %q, want error", path, gotPath) + } + return + } + if err != nil { + t.Fatalf("openGorootSourceFile(%q) = err %v, want path %q", path, err, tc.wantPath) + } + defer f.Close() + if want := filepath.Join(src, filepath.FromSlash(tc.wantPath)); f.Name() != want { + t.Errorf("openGorootSourceFile(%q) = %q, want %q", path, f.Name(), want) + } + }) + } +} + +func TestOpenSourceFileGorootFallback(t *testing.T) { + fakeGorootSrc := filepath.Join(t.TempDir(), "goroot", "src") + path := filepath.Join(fakeGorootSrc, "runtime", "proc.go") + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatalf("failed to create dir for %q: %v", path, err) + } + if err := os.WriteFile(path, nil, 0644); err != nil { + t.Fatalf("failed to create file %q: %v", path, err) + } + savedGorootSrc := gorootSrc + gorootSrc = fakeGorootSrc + defer func() { gorootSrc = savedGorootSrc }() + + for _, tc := range []struct { + desc string + path string + wantErr bool + }{ + { + desc: "absolute path under a foreign GOROOT", + path: "/usr/local/go/src/runtime/proc.go", + }, + { + desc: "relative path from a -trimpath build", + path: "runtime/proc.go", + }, + { + desc: "not found under GOROOT falls through to error", + path: "runtime/missing.go", + wantErr: true, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + f, err := openSourceFile(filepath.FromSlash(tc.path), "", "") + if tc.wantErr { + if err == nil { + gotPath := f.Name() + f.Close() + t.Fatalf("openSourceFile(%q) = %q, want error", tc.path, gotPath) + } + return + } + if err != nil { + t.Fatalf("openSourceFile(%q) = err %v, want path %q", tc.path, err, path) + } + defer f.Close() + if f.Name() != path { + t.Errorf("openSourceFile(%q) = %q, want %q", tc.path, f.Name(), path) + } + }) + } +} + func TestIndentation(t *testing.T) { for _, c := range []struct { str string