skip to content

Advisories and Reachability

The Go vulnerability database records affected symbols, so a vulnerable module you never call need not be a finding, and the standard library gets advisories fixed by upgrading the toolchain itself.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

Why does Go's symbol-level vulnerability analysis report fewer findings than a go.mod manifest scan?

level: middleimportance: must knowfreq 55%

answer

  1. Two different questions, two different counts
  2. The advisory names functions, not just versions
  3. Entry points, then a graph of calls
  4. Called, imported-not-called, required-not-imported
  5. It prints the chain into the bug

basics

~20 s

Because it matches called symbols, not module versions. Go's analysis builds a static call graph and reports an advisory only when a path in your program reaches one of the vulnerable functions. A manifest scan flags every affected version you require.

solid answer

~50 s

A manifest scan compares the versions in `go.mod` against advisory ranges, so requiring an affected version is enough to produce a finding. Go's analysis goes further because the vulnerability database names the **exported symbols** that carry each bug. It loads and type-checks your packages, builds a static call graph rooted at your entry points - `main` and package `init` functions for a program, every exported function for a library - and asks whether any path in that graph reaches a vulnerable symbol. Only those become actionable findings; advisories against modules you require or even import but never call are reported separately as informational. It also prints the trace - the chain of calls from your own function into the vulnerable one - which tells you whether the fix is an upgrade or a code change. The result is far fewer rows: a big module usually has one vulnerable package.

code

go · 3 lines
go
func encode(v any) ([]byte, error) {
	return json.Marshal(v)
}

go deeper

for a junior

Know that a Go vulnerability report distinguishes advisories your code actually calls from ones that merely affect a module you depend on, and that the called ones are the rows to act on first.

for a middle

Be ready to describe the mechanism: load and type-check the packages, pick entry points, build a static call graph, intersect it with the symbols the advisory names. Name the three result classes.

for a senior

Show that you read the printed call chain rather than the row count - where the path starts decides whether this is a page or a scheduled upgrade, and sometimes reveals a remedy other than upgrading.

for a principal

Own the tradeoff between cheap version matching everywhere and expensive reachability analysis on the services that matter, including who pays the build time and how a fleet-wide report is scoped so owners get rows they can act on.

## Two different questions There are two questions you can ask about a dependency advisory, and they give wildly different answer counts. - **Version matching**: *does my build require a version of module M inside the affected range?* This is what a manifest scan does. It needs only `go.mod` (and the module graph) and it is essentially free. - **Reachability**: *can execution in my program get from an entry point into one of the vulnerable functions?* This needs the source, a type-checker and a call graph. Go can ask the second question because its vulnerability database records the affected exported symbols, not just the affected versions. ## How the call graph gets built In source mode the analysis: 1. **Loads and type-checks the packages you name** - the same package loading the compiler does, honouring build constraints and the target `GOOS`/`GOARCH`. 2. **Chooses entry points.** For a `main` package these are `func main` plus the `init` functions of every package in the build. When you scan a library, every exported function and method of the analysed packages is treated as an entry point, because a caller you cannot see may call any of them. 3. **Builds a static call graph.** Direct calls are trivial. Calls through interface values and function values need a whole-program analysis - Go's tooling uses a variable-type analysis (VTA) style algorithm from `golang.org/x/tools`, which propagates the set of concrete types that can flow into each interface variable and therefore the set of methods a dynamic call site can dispatch to. 4. **Intersects the graph with the advisory symbols.** For every advisory whose module and version match, it checks whether any of the named symbols is reachable from an entry point. ## The three classes of result Findings sort into a hierarchy of decreasing concern: - **Called** - a path exists into the vulnerable symbol. This is the actionable row. - **Imported but not called** - you compile the vulnerable package in but never reach the affected function. - **Required but not imported** - the module is in your module graph, possibly pulled in transitively, but no package of yours imports it, so no code from it is even linked into your binary. Only the first class gets full treatment; the rest are reported as informational context. Go's own linker also drops unreferenced code, so "required but not imported" usually means the vulnerable machine code is not in your binary at all. ## The trace is the deliverable When a symbol is reachable, the report prints the chain of calls - your function, then the intermediate frames, then the vulnerable function. That trace is what turns an advisory into a work item: - If the chain starts in your request-handling path, the exposure is real and immediate. - If it starts in a debug command or a code path behind a feature flag, you can schedule instead of paging. - If the vulnerable function is called from a helper you own, you sometimes have a second remedy besides upgrading: stop calling it, or validate the input before you do. None of that is visible from a version match. ## Why this matters at scale Imagine a fleet report with two hundred rows produced by version matching. Most of those rows are modules pulled in transitively for one utility function. Symbol-level analysis typically collapses that to a handful of genuinely reachable findings plus a long informational tail. The difference is not cosmetic - it is the difference between a report that an owner can act on before a deadline and one that gets ignored wholesale. ## The cost side Reachability is not free. It needs to build the package graph, which means the code must compile for the target platform, the module cache must be populated, and the analysis is orders of magnitude slower than parsing `go.mod`. It also produces results that are specific to *this* build: change a build tag, a `GOOS`, or which packages you scan, and the reachable set changes. ## The honest caveat "Not called" is the analysis's best answer given the edges it could see. A static call graph cannot see every dynamic dispatch mechanism Go offers, so a not-called result is a strong prioritisation signal rather than a proof of safety. Treat it as ordering your work, not as closing the ticket.

  • What does the analysis use as entry points when you scan a library rather than a main package?
    Every exported function and method of the analysed packages, because any importer could call any of them. That makes library scans noisier than service scans by design - a service has one real entry point, `main`, so most of a dependency's surface is genuinely unreachable, while a library must assume its whole exported surface is live.
  • How does the analysis handle a call made through an interface variable?
    It over-approximates. A whole-program analysis propagates which concrete types can flow into each interface variable and treats every method those types provide as a possible callee. That may add edges that never fire at runtime, so interface dispatch tends to produce false positives - a finding you cannot actually reach - rather than missed findings.
  • You get a finding with a trace that starts inside a code path behind a disabled feature flag. Does that change what you do?
    It changes urgency, not the fix. The reachable path exists in the shipped binary and anyone who flips the flag exposes it, so the upgrade still happens - but it can go in the normal release rather than a hotfix. Record the reasoning, because the flag can be enabled by someone who never saw the advisory.

A manifest scan asks whether a recalled part sits somewhere in the warehouse. Symbol-level analysis asks whether that part was actually bolted into the car you shipped.

saying these in an interview costs you the question

  • Says the scan just diffs go.mod against a feed
  • Thinks importing a package is enough to be called
  • Believes reachability is measured by running tests
  • Claims unreachable advisories are dropped silently
  • Cannot say what the printed call chain is for
open as a page

What is Go's vulnerability database at vuln.go.dev, and what does a GO-2024-1234 entry add over a CVE record?

level: juniorimportance: should knowfreq 40%

basics

~20 s

Go's vulnerability database at vuln.go.dev is a curated feed of advisories for Go modules and the standard library. Each GO-YYYY-NNNN entry names the affected module, its packages and the exported symbols inside them, the fixed version, and any CVE alias.

open as a page

Why did a CI step running govulncheck stop failing on known vulnerabilities after it switched to -format sarif, and how do you restore the gate?

level: middleimportance: should knowfreq 28%

basics

~20 s

govulncheck exits 0 with -json, -format sarif or -format openvex regardless of findings; only the default text output exits 3 when vulnerabilities are found. Gate on a text-mode run, or parse the report and fail on findings yourself.

open as a page

Why does a Go advisory against the stdlib module require a toolchain upgrade rather than go get?

level: middleimportance: should knowfreq 45%

basics

~20 s

Because a stdlib advisory is about the Go release that compiled your binary, not about anything your go.mod requires. There is no dependency version to bump; you fix it by building with a patched Go toolchain and redeploying every affected binary.

open as a page

When should you distrust a Go vulnerability report that says the vulnerable symbol is never called?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Whenever 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.

open as a page