From 3f2f7cad335f2cfa3f4ccafdf5446c5a6f725ed0 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 24 Apr 2026 10:08:45 +0200 Subject: [PATCH] builder,loader: fix -ldflags -X not overriding variables with default values When a string variable already had a source-level default value, the -ldflags "-X" flag was silently ignored and the variable remained the default value at runtime. Fix by stripping the InitOrder entry for each -X variable before LoadSSA() is called. With no entry in InitOrder, go/ssa emits no init store, so the global stays zero-valued in the IR. makeGlobalsModule still injects the actual -X values at final link time. The -X values remain out of the per-package build cache with only the variable names appearing in the cache key. Signed-off-by: deadprogram --- builder/build.go | 12 ++++++++++++ loader/loader.go | 23 +++++++++++++++++++++++ main_test.go | 13 +++++++++++++ testdata/ldflags-initialized.go | 9 +++++++++ testdata/ldflags-initialized.txt | 1 + testdata/ldflags.go | 3 +-- 6 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 testdata/ldflags-initialized.go create mode 100644 testdata/ldflags-initialized.txt diff --git a/builder/build.go b/builder/build.go index 714a331a39..325f6b4906 100644 --- a/builder/build.go +++ b/builder/build.go @@ -255,6 +255,18 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe result.PackagePathMap[pkg.OriginalDir()] = pkg.Pkg.Path() } + // Strip default initializers for -X globals from the type info before + // building SSA. This prevents go/ssa from emitting init stores for them, + // so that makeGlobalsModule can supply the correct values at final link + // time without any runtime init overwriting them. The -X values themselves + // are kept out of the per-package build cache; only the variable names + // appear in the cache key. + for _, pkg := range lprogram.Sorted() { + for name := range globalValues[pkg.Pkg.Path()] { + pkg.StripVarInitializer(name) + } + } + // Create the *ssa.Program. This does not yet build the entire SSA of the // program so it's pretty fast and doesn't need to be parallelized. program := lprogram.LoadSSA() diff --git a/loader/loader.go b/loader/loader.go index 1ca1b6679d..dd7dd2dd8e 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -304,6 +304,29 @@ func (p *Program) Sorted() []*Package { return p.sorted } +// StripVarInitializer removes the package-level initializer for the named +// variable from the type info. This prevents go/ssa from emitting an init +// store for it, leaving the global zero-initialized in the IR. An external +// value (e.g. from -ldflags -X via makeGlobalsModule) can then be linked in +// at final link time without any runtime init overwriting it. +// +// Must be called after Parse() (typechecking populates InitOrder) and before +// LoadSSA() (which consumes InitOrder to build the package init function). +// +// Only 1:1 var initializers (var x = expr) are matched. Multi-variable +// initializers (var x, y = f()) are left untouched. +func (p *Package) StripVarInitializer(name string) { + n := 0 + for _, init := range p.info.InitOrder { + if len(init.Lhs) == 1 && init.Lhs[0].Name() == name { + continue // drop this initializer + } + p.info.InitOrder[n] = init + n++ + } + p.info.InitOrder = p.info.InitOrder[:n] +} + // MainPkg returns the last package in the Sorted() slice. This is the main // package of the program. func (p *Program) MainPkg() *Package { diff --git a/main_test.go b/main_test.go index 55eb678910..6ac0d0f596 100644 --- a/main_test.go +++ b/main_test.go @@ -155,6 +155,19 @@ func TestBuild(t *testing.T) { } runTestWithConfig("ldflags.go", t, opts, nil, nil) }) + + // Same as ldflags, but the global has a default value in source. + // -ldflags -X must override it, matching standard Go behaviour. + t.Run("ldflags-initialized", func(t *testing.T) { + t.Parallel() + opts := optionsFromTarget("", sema) + opts.GlobalValues = map[string]map[string]string{ + "main": { + "someGlobal": "foobar", + }, + } + runTestWithConfig("ldflags-initialized.go", t, opts, nil, nil) + }) }) if testing.Short() { diff --git a/testdata/ldflags-initialized.go b/testdata/ldflags-initialized.go new file mode 100644 index 0000000000..44b0e85358 --- /dev/null +++ b/testdata/ldflags-initialized.go @@ -0,0 +1,9 @@ +package main + +// This global has a default value. It should be overridable via +// -ldflags="-X main.someGlobal=value" just like an uninitialized global. +var someGlobal = "default" + +func main() { + println("someGlobal:", someGlobal) +} diff --git a/testdata/ldflags-initialized.txt b/testdata/ldflags-initialized.txt new file mode 100644 index 0000000000..0f39abf054 --- /dev/null +++ b/testdata/ldflags-initialized.txt @@ -0,0 +1 @@ +someGlobal: foobar diff --git a/testdata/ldflags.go b/testdata/ldflags.go index 94db0dcb12..b4c8a43106 100644 --- a/testdata/ldflags.go +++ b/testdata/ldflags.go @@ -1,7 +1,6 @@ package main -// These globals can be changed using -ldflags="-X main.someGlobal=value". -// At the moment, only globals without an initializer can be replaced this way. +// This global can be changed using -ldflags="-X main.someGlobal=value". var someGlobal string func main() {