From 25cbdb24df9107ef9cf7e4c18a0fb31385e9d573 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 1/4] 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. --- 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 From c2590f9506e431dc1552bf1e84cc0dd481a306f1 Mon Sep 17 00:00:00 2001 From: Alexey Alexandrov Date: Sat, 1 Aug 2026 17:38:55 -0700 Subject: [PATCH 2/4] Update go versions, remove go tip testing. (#1013) Testing against the Go tip has not been useful, but it requires maintenance. It seems low ROI overall, so remove the go tip testing. Also add a permission fix to address the Zizmor failed checks. --- .github/workflows/ci.yaml | 58 +++++---------------------------------- browsertests/go.mod | 4 +-- go.mod | 4 +-- 3 files changed, 9 insertions(+), 57 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 56df7f125f..fec07a88a2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,6 +6,10 @@ on: pull_request: schedule: - cron: '0 2 * * *' # Run every day, at 2AM UTC. + +permissions: + contents: read + env: GOPATH: ${{ github.workspace }} WORKING_DIR: ./src/github.com/google/pprof/ @@ -18,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - go: ['1.24', '1.25', 'tip'] + go: ['1.25', '1.26'] # Supported macOS versions can be found in # https://github.com/actions/virtual-environments#available-environments. os: ['macos-14', 'macos-15'] @@ -54,7 +58,6 @@ jobs: - name: Update Go version using setup-go uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - if: matrix.go != 'tip' with: # Include cache directives to allow proper caching. Without them, we # get setup-go "Restore cache failed" warnings. @@ -62,29 +65,6 @@ jobs: cache: true cache-dependency-path: '**/go.sum' - - name: Install Go bootstrap compiler - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - if: matrix.go == 'tip' - with: - # Bootstrapping go tip requires 1.24 - # Include cache directives to allow proper caching. Without them, we - # get setup-go "Restore cache failed" warnings. - go-version: 1.24 - cache: true - cache-dependency-path: '**/go.sum' - - - name: Update Go version manually - if: matrix.go == 'tip' - working-directory: ${{ github.workspace }} - run: | - git clone https://go.googlesource.com/go $HOME/gotip - cd $HOME/gotip/src - ./make.bash - echo "GOROOT=$HOME/gotip" >> $GITHUB_ENV - echo "RUN_STATICCHECK=false" >> $GITHUB_ENV - echo "RUN_GOLANGCI_LINTER=false" >> $GITHUB_ENV - echo "$HOME/gotip/bin:$PATH" >> $GITHUB_PATH - - name: Set up Xcode uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: @@ -120,7 +100,7 @@ jobs: strategy: fail-fast: false matrix: - go: ['1.24', '1.25', 'tip'] + go: ['1.25', '1.26'] os: ['ubuntu-24.04', 'ubuntu-22.04'] steps: - name: Checkout the repo @@ -130,7 +110,6 @@ jobs: - name: Update Go version using setup-go uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - if: matrix.go != 'tip' with: # Include cache directives to allow proper caching. Without them, we # get setup-go "Restore cache failed" warnings. @@ -138,29 +117,6 @@ jobs: cache: true cache-dependency-path: '**/go.sum' - - name: Install Go bootstrap compiler - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - if: matrix.go == 'tip' - with: - # Bootstrapping go tip requires 1.24 - # Include cache directives to allow proper caching. Without them, we - # get setup-go "Restore cache failed" warnings. - go-version: 1.24 - cache: true - cache-dependency-path: '**/go.sum' - - - name: Update Go version manually - if: matrix.go == 'tip' - working-directory: ${{ github.workspace }} - run: | - git clone https://go.googlesource.com/go $HOME/gotip - cd $HOME/gotip/src - ./make.bash - echo "GOROOT=$HOME/gotip" >> $GITHUB_ENV - echo "RUN_STATICCHECK=false" >> $GITHUB_ENV - echo "RUN_GOLANGCI_LINTER=false" >> $GITHUB_ENV - echo "$HOME/gotip/bin" >> $GITHUB_PATH - - name: Check chrome for browser tests run: | google-chrome --version @@ -214,7 +170,7 @@ jobs: strategy: fail-fast: false matrix: - go: ['1.24', '1.25'] + go: ['1.25', '1.26'] steps: - name: Checkout the repo uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/browsertests/go.mod b/browsertests/go.mod index 475c481489..e3b94583b9 100644 --- a/browsertests/go.mod +++ b/browsertests/go.mod @@ -1,8 +1,6 @@ module github.com/google/pprof/browsertests -go 1.24.0 - -toolchain go1.24.9 +go 1.25.0 // Use the version of pprof in this directory tree. replace github.com/google/pprof => ../ diff --git a/go.mod b/go.mod index 97386cfe13..09b01517d1 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/google/pprof -go 1.24.0 - -toolchain go1.24.9 +go 1.25.0 require ( github.com/chzyer/readline v1.5.1 From a8c34b9dd1c876a9454ca99a30edf1aa40709f81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 00:45:07 +0000 Subject: [PATCH 3/4] Bump actions/setup-go from 6.5.0 to 7.0.0 (#1012) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.5.0 to 7.0.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/924ae3a1cded613372ab5595356fb5720e22ba16...b7ad1dad31e06c5925ef5d2fc7ad053ef454303e) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fec07a88a2..0512ecffde 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -57,7 +57,7 @@ jobs: path: ${{ env.WORKING_DIR }} - name: Update Go version using setup-go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: # Include cache directives to allow proper caching. Without them, we # get setup-go "Restore cache failed" warnings. @@ -109,7 +109,7 @@ jobs: path: ${{ env.WORKING_DIR }} - name: Update Go version using setup-go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: # Include cache directives to allow proper caching. Without them, we # get setup-go "Restore cache failed" warnings. @@ -178,7 +178,7 @@ jobs: path: ${{ env.WORKING_DIR }} - name: Update Go version using setup-go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: # Include cache directives to allow proper caching. Without them, we # get setup-go "Restore cache failed" warnings. From 7686969aae1f690d805df1fc912783621c4c0a11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:15:13 +0000 Subject: [PATCH 4/4] Bump actions/checkout from 7.0.0 to 7.0.1 (#1011) Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alexey Alexandrov --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0512ecffde..6c6606952a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -52,7 +52,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: ${{ env.WORKING_DIR }} @@ -104,7 +104,7 @@ jobs: os: ['ubuntu-24.04', 'ubuntu-22.04'] steps: - name: Checkout the repo - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: ${{ env.WORKING_DIR }} @@ -173,7 +173,7 @@ jobs: go: ['1.25', '1.26'] steps: - name: Checkout the repo - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: ${{ env.WORKING_DIR }}