When should you distrust a Go vulnerability report that says the vulnerable symbol is never called?
answer
- It is a statement about a graph
- Some edges are not in the source
- Names resolved at runtime, not at compile time
- One build, one GOOS, one set of tags
- Prioritiser, not clearance authority
basics
~20 sWhenever the real call is invisible to a static call graph: dispatch through reflect, symbols loaded with the plugin package, code entered through cgo or assembly, and go:linkname. Not called means no path the analysis could see.
solid answer
~50 s"Not called" is a statement about a graph, not about your program. The analysis builds a static call graph from the packages in one build, so it misses any edge that does not exist in the source: a method invoked through `reflect.Value.Call` or found by name, a symbol resolved at runtime with the `plugin` package, code entered through cgo or hand-written assembly, and functions wired up with `//go:linkname`. It is also scoped to that build - files excluded by build constraints or by the target `GOOS`/`GOARCH` were never analysed, so scanning on one platform and shipping three leaves two unscanned. Interface dispatch is the opposite case: the analysis over-approximates it, which yields false positives rather than misses. The posture with a two-hundred-row report is to use "not called" to order the queue, then override it for reflection-heavy, plugin-loading or cgo-bound components and upgrade anyway.
code
go · 3 linesm := reflect.ValueOf(p).MethodByName("Parse")
res := m.Call([]reflect.Value{reflect.ValueOf(input)})
_ = resgo deeper
Understand that a not-called result means the tool found no path in the code it analysed, and that some ways of calling a function - by name at runtime, for instance - are not visible to it.
Be able to list the mechanisms that hide an edge from a static call graph: reflection, the plugin package, cgo and assembly, go:linkname, and files excluded by build constraints or the target platform.
An interviewer expects the operational posture: read the trace on called rows, segment the not-called rows by whether the component uses those mechanisms, re-scan per build target, and write down the reasoning behind anything you close.
Own where the organisation draws the line between prioritisation and clearance - which services may close a finding on a not-called result, what evidence must accompany that decision, and how much analysis cost the fleet report is worth.
## What the claim actually means A reachability result is the output of a static analysis over one build: load the packages, pick entry points, build a call graph, intersect it with the advisory's symbols. "Not called" therefore means *no path was found from the entry points of this build, using the edges the analysis could construct*. That is a genuinely useful signal, and it is not a proof. The question that matters operationally is: which edges can Go's source have that a static call graph does not? ## The false-negative sources **Reflection.** `reflect.Value.Call`, and especially `reflect.Value.MethodByName`, dispatch on a string that may come from configuration or from data. There is no syntactic call to the target, so no edge. Reflection-heavy code - serialisation, ORMs, dependency-injection wiring, generic dispatch tables built by name - is where this bites hardest, and it is exactly where parsing vulnerabilities live. **Plugins.** `plugin.Open` loads a shared object at runtime and `(*plugin.Plugin).Lookup` resolves a symbol by name. The loaded code was not part of the analysed build at all. **cgo and assembly.** A call that leaves Go for C and comes back, or a function implemented in a `.s` file, is opaque to a Go call graph. So is any callback registered with C code. **`//go:linkname`.** This directive binds a local declaration to a symbol in another package, sometimes an unexported one. The edge exists at link time and not in the source. **External processes.** If your service shells out with `os/exec` to a tool that is itself vulnerable, that tool is not in your call graph or your module graph. Its advisories will never appear. **Code not in the analysed build.** Build constraints and the target `GOOS`/`GOARCH` decide which files are loaded. A vulnerable path that only compiles on Windows is invisible to a scan run on Linux, and vice versa. Generated code that is produced at build time but not committed is another version of the same gap. **Artefacts you did not scan.** Vendored dependencies you did not include, dependencies shipped as prebuilt binaries, and vulnerabilities in a data file or template rather than in code all sit outside the model. ## The direction that is safe Interface dispatch is frequently miscited as a false-negative source. It is not. A whole-program analysis propagates the concrete types that can flow into each interface variable and adds an edge for every method those types could provide, which is an **over**-approximation: it can report a path that never executes. So an interface-heavy codebase tends to produce findings you cannot actually trigger, and the correct scepticism there runs the other way - verify before you scramble. ## Working the two-hundred-row report The realistic scenario is a fleet-wide digest landing in an owner's inbox with a deadline attached. A defensible way to work it: 1. **Called rows first.** Read the trace, not the row. Where the path starts decides urgency. 2. **Segment the not-called rows by mechanism, not by severity.** Ask which components in this service use reflection, load plugins, link against C, or use `//go:linkname` in a dependency. For those components, downgrade the analysis to version matching and upgrade regardless. 3. **Check the build matrix.** If you ship more than one `GOOS`/`GOARCH` or use build tags to select implementations, the scan you ran covers one of them. Re-run per target or accept the gap explicitly. 4. **Cross-check the artefact.** Scanning the compiled binary is a different lens on the same question: it sees what was actually linked, including code paths chosen by build tags, and it catches the case where the source you scanned is not the source that was shipped. 5. **Write the reasoning down.** "Not called, and this component uses no reflection or cgo" is an auditable decision. "The tool said not called" is not. ## The pathology to avoid The failure mode this guards against is a **false negative you never revisit**: an advisory closed because a tool printed "not called", where the actual path into the vulnerable parser runs through a `MethodByName` lookup in a serialisation helper three modules down. Nothing about the report is wrong - the graph really had no edge - but the conclusion drawn from it was. The correct mental model is that reachability analysis is a **prioritiser**, not a **clearance authority**. It buys you the right to fix twenty things this week instead of two hundred. It does not buy you the right to declare the other hundred and eighty safe forever.
- Does dispatch through an interface cause missed findings?No - it causes the opposite. The analysis propagates which concrete types can reach each interface variable and adds an edge for every method they could supply, so interface-heavy code produces paths that may never execute. Interfaces are a false-positive source; reflection, plugins, cgo and `//go:linkname` are the false-negative sources.
- You own a service with a reflection-heavy serialisation layer and a not-called advisory against a parser. What do you do?Upgrade anyway. The one mechanism that reliably hides edges is exactly the one this service uses, so the not-called result carries little weight here. Record that the decision overrode the analysis and why, so the next person reading the report does not re-litigate it.
- Why can scanning the compiled binary be worth doing alongside a source scan?It answers a different question: what was actually linked into the artefact you ship, including whichever build-tag and platform variants were selected. It also catches the case where the scanned source and the deployed binary have drifted. It is coarser than source analysis, so it complements rather than replaces it.
- How do build constraints change what a scan covers?Only files selected for the target build are loaded and type-checked, so a path that compiles only under another build tag or another `GOOS`/`GOARCH` is not in the graph at all. If you ship several platform builds, one scan covers one of them; either run the scan per target or state the gap explicitly.
A static call graph is a map of the roads it can see. Reflection, plugins and cgo are helicopters: the destination is reachable, just not by any road on the map.
saying these in an interview costs you the question
- Treats not called as proof the service is safe
- Never considers reflection or plugin dispatch
- Scans one platform and ships several
- Claims interface calls cause missed findings
- Closes advisories without recording the reasoning