← 1.0.0 backlogs ISS-18

A linker stamp aimed at a symbol that does not exist is accepted in silence

Type
Debt — a toolchain failure mode, currently guarded
Found while
SAPE-1
Triage
Defer — because a guard exists, not because it is small
Status
Open
Severity
Medium — the failure is silent, total, and lands on a release artifact

Description

The product version is written into the binary by the linker: go build -ldflags "-X github.com/sapedb/sapedb/internal/build.Version=1.0.0". internal/build.Version is a variable with the default "dev", and nothing in the program ever assigns it.

-X aimed at a symbol that does not exist does nothing, and says nothing. No warning, no error, no non-zero exit. The build succeeds, the binary runs, and it reports dev. So version stamping does not fail loudly on the day somebody renames the package or the variable — it stops happening, and every artifact built after that claims to be an unstamped development build.

That day is not hypothetical, because the path has to be spelled by hand in two places that cannot read a Go constant: Makefile:20 and Dockerfile:32. Go source has one spelling of it, build.Path (internal/build/build.go, the last line), and make and docker have two more. Three spellings, two of which no compiler checks.

Evidence

Measured at sapedb 248fa02, Go 1.27.1, darwin/arm64 — by building three binaries and running each:

StampBuildsapedb version says
…/internal/build.Version=9.9.9exit 0, silent sapedb 9.9.9
…/internal/build.Verzion=9.9.9exit 0, silent sapedb dev
…/internal/buildinfo.Version=9.9.9exit 0, silent sapedb dev

A misspelled variable and a wrong package path are indistinguishable from a correct build at build time. The only observable difference is what the binary says afterwards, which is exactly what nobody checks on a release artifact until somebody asks.

The failure mode is documented in three places, in the same words, by people who hit it:

What guards it today — two tests, deliberately approaching from different directions:

Triage

Does it lose data, break a promise on a public surface, or stop somebody installing and running? No. A binary saying dev serves correctly; nothing depends on the string, and a stranger installs and runs it either way.

Defer — and the reason matters more than the label. This is deferred because it is already guarded, not because it is unimportant. Those are different deferrals and they age differently. An unimportant issue can be left alone; a guarded one is only safe while the guard holds, and the thing to keep is the guard, not the calm.

So the standing condition is explicit: the four tests above must stay green and must stay non-vacuous. Specifically — if build_test.go's file reads ever start failing soft, if Path is ever inlined back into a string literal at its use sites, or if TestAStampedBuildSaysWhatItWasStampedWith ever becomes conditional on a toolchain being present, this issue is promoted, because at that point nothing is watching the two hand-written spellings.

The residual risk the guard does not cover is worth naming rather than implying: it checks the two build files in this repository. Any other place that stamps a sapedb binary — a downstream packaging script, a CI workflow, a distro recipe — carries a fourth and fifth spelling that nothing here can see.

If deferred, what it costs to wait