Skip to content

x/tools/cmd/callgraph: calls from unreachable unexported methods not reported in callgraph #66251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
thesilentg opened this issue Mar 11, 2024 · 7 comments
Assignees
Labels
Tools This label describes issues relating to any tools in the x/tools repository.
Milestone

Comments

@thesilentg
Copy link

Go version

go version go1.22.0 darwin/amd64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='amd64'
GOBIN='/Users/aggnolek/go/bin'
GOCACHE='/Users/aggnolek/Library/Caches/go-build'
GOENV='/Users/aggnolek/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/aggnolek/go/pkg/mod'
GONOPROXY='*'
GONOSUMDB='*'
GOOS='darwin'
GOPATH='/Users/aggnolek/go'
GOPRIVATE='*'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/aggnolek/sdk/go1.22.0'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/aggnolek/sdk/go1.22.0/pkg/tool/darwin_amd64'
GOVCS=''
GOVERSION='go1.22.0'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='cc'
CXX='c++'
CGO_ENABLED='1'
GOMOD='/Users/aggnolek/gorepos/aggnolek/scratchpad/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/x7/2f8ynt3954s4y78yt_54v9fr0000gs/T/go-build886972198=/tmp/go-build -gno-record-gcc-switches -fno-common'

What did you do?

go.mod

module scratchpad

go 1.21

example2/main.go

package main

func called() {}

type unexported struct{}

func (u unexported) Func1() {
	called()
}

type Exported struct{}

func (e Exported) Func1() {
	called()
}

func main() {
	
}

Ran callgraph -algo={algo} ./example2 for algo in [static, cha, rta, vta]

What did you see happen?

callgraph -algo=static ./example2

(scratchpad/example2.Exported).Func1	--static-14:8-->	scratchpad/example2.called

callgraph -algo=cha ./example2

(scratchpad/example2.Exported).Func1	--static-14:8-->	scratchpad/example2.called

callgraph -algo=rta ./example2

{Empty Output}

callgraph -algo=vta ./example2

(scratchpad/example2.Exported).Func1	--static-14:8-->	scratchpad/example2.called

What did you expect to see?

callgraph -algo=static ./example2

(scratchpad/example2.Exported).Func1	--static-14:8-->	scratchpad/example2.called
(scratchpad/example2.unexported).Func1	--static-8:8-->	scratchpad/example2.called

callgraph -algo=cha ./example2

(scratchpad/example2.Exported).Func1	--static-14:8-->	scratchpad/example2.called
(scratchpad/example2.unexported).Func1	--static-8:8-->	scratchpad/example2.called

callgraph -algo=rta ./example2

{Empty Output}

This is because rta only includes reachable funcs by design

callgraph -algo=vta ./example2

(scratchpad/example2.Exported).Func1	--static-14:8-->	scratchpad/example2.called
(scratchpad/example2.unexported).Func1	--static-8:8-->	scratchpad/example2.called

Note that the link from example2.unexported).Func1 to called is present when example2.unexported).Func1 is forced to be reachable. For example:

example2/main.go

func main() {
	unexported{}.Func1()
}

callgraph -algo=static ./example2

(scratchpad/example2.unexported).Func1	--static-8:8-->	scratchpad/example2.called
(scratchpad/example2.Exported).Func1	--static-14:8-->	scratchpad/example2.called
scratchpad/example2.main	--static-18:20-->	(scratchpad/example2.unexported).Func1

callgraph -algo=cha ./example2

(scratchpad/example2.unexported).Func1	--static-8:8-->	scratchpad/example2.called
(scratchpad/example2.Exported).Func1	--static-14:8-->	scratchpad/example2.called
scratchpad/example2.main	--static-18:20-->	(scratchpad/example2.unexported).Func1

callgraph -algo=rta ./example2

(scratchpad/example2.unexported).Func1	--static-8:8-->	scratchpad/example2.called
scratchpad/example2.main	--static-18:20-->	(scratchpad/example2.unexported).Func1

This is because rta only includes reachable funcs by design

callgraph -algo=vta ./example2

(scratchpad/example2.unexported).Func1	--static-8:8-->	scratchpad/example2.called
(scratchpad/example2.Exported).Func1	--static-14:8-->	scratchpad/example2.called
scratchpad/example2.main	--static-18:20-->	(scratchpad/example2.unexported).Func1

This likely has to do with the same usage of ssautil.AllFunctions (at least for vta) that resulted in this bug for the deadcode command.

@gopherbot gopherbot added the Tools This label describes issues relating to any tools in the x/tools repository. label Mar 11, 2024
@gopherbot gopherbot added this to the Unreleased milestone Mar 11, 2024
@thesilentg
Copy link
Author

@adonovan Given your closeness to the likely similar prior issue

@adonovan
Copy link
Member

Thanks. I expect a patch along these lines should be effective:

        case "vta":
-               cg = vta.CallGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog))
+               // Gather all source-level functions as entry points.
+               sourceFuncs := make(map[*ssa.Function]bool)
+               packages.Visit(initial, nil, func(p *packages.Package) {
+                       for _, file := range p.Syntax {
+                               for _, decl := range file.Decls {
+                                       if decl, ok := decl.(*ast.FuncDecl); ok {
+                                               obj := p.TypesInfo.Defs[decl.Name].(*types.Func)
+                                               fn := prog.FuncValue(obj)
+                                               sourceFuncs[fn] = true
+                                       }
+                               }
+                       }
+               })
+
+               cg = vta.CallGraph(sourceFuncs, cha.CallGraph(prog))

@adonovan adonovan self-assigned this Mar 11, 2024
@thesilentg
Copy link
Author

Agree with respect to rta, although based on my understanding both static and cha will require separate fixes as those don't take in sourceFuncs.

@thesilentg
Copy link
Author

Based on my early testing, I believe you'll also need to filter out nil results for fn, as prog.FuncValue returns nil for interface methods and those nils cause panics within vta.CallGraph

@adonovan
Copy link
Member

adonovan commented Sep 4, 2024

Agree with respect to rta, although based on my understanding both static and cha will require separate fixes as those don't take in sourceFuncs.

The bug in -algo=static was fixed yesterday by https://go.dev/cl/609280.

The fix for the bug in CHA (which is really a bug in AllFunctions) is something along the lines of https://go.dev/cl/609281, but as it stands that CL would cause CHA's analysis of main packages to no longer treat exported symbols as entry points, which is sound, but I need to think a little more about whether this is consistent with the expectations we have set for CHA. For example, that CL would cause the -algo=cha output on the example above to report the empty set (just like RTA), which is sound but is counter to the expectations you had when filing this bug report. I am inclined to think that the problem of executing the CHA analysis is really orthogonal to the problem of how one gathers up the initial set of functions it uses (and which of those functions are considered address-taken). We may need new API to do it justice.

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/609281 mentions this issue: go/callgraph/cha: make CHA, VTA faster and more precise

@adonovan
Copy link
Member

adonovan commented Sep 6, 2024

On further reflection, I think the callgraph tool is working as intended when it shows no call edge from unexported.Func1 to called in your first example, because the unexported type is unreachable according to the reachability algorithm implemented by AllFunctions. AllFunctions has certainly had a history of bugs (see #69291), but it has always pretended to use a linker-style reachability algorithm, and any such algorithm should report that unexported is not reachable.

If you were to use a different algorithm to gather up functions ("ReallyAllFunctions(prog)"), the CHA algorithm would analyze the dynamic calls within even the unreachable functions. (We don't have a test for that because cmd/callgraph and the CHA tests both use AllFunction, but I'm confident it would work.) So if you really need that behavior, that's something you could do.

My apologies for the various contradictory things I have said on this and other related callgraph issues this week.

@adonovan adonovan closed this as completed Sep 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Tools This label describes issues relating to any tools in the x/tools repository.
Projects
None yet
Development

No branches or pull requests

3 participants