Skip to content

Commit 8f6c083

Browse files
committed
cmd/link: choose one with larger size for duplicated BSS symbols
When two packages declare a variable with the same name (with linkname at least on one side), the linker will choose one as the actual definition of the symbol if one has content (i.e. a DATA symbol) and the other does not (i.e. a BSS symbol). When both have content, it is redefinition error. When neither has content, currently the choice is sort of arbitrary (depending on symbol loading order, etc. which are subject to change). One use case for that is that one wants to reference a symbol defined in another package, and the reference side just wants to see some of the fields, so it may be declared with a smaller type. In this case, we want to choose the one with the larger size as the true definition. Otherwise the code accessing the larger sized one may read/write out of bounds, corrupting the next variable. This CL makes the linker do so. Fixes #72032. Change-Id: I160aa9e0234702066cb8f141c186eaa89d0fcfed Reviewed-on: https://go-review.googlesource.com/c/go/+/660696 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: David Chase <[email protected]> Reviewed-by: Than McIntosh <[email protected]>
1 parent 26fdb07 commit 8f6c083

File tree

3 files changed

+90
-8
lines changed

3 files changed

+90
-8
lines changed

src/cmd/link/internal/loader/loader.go

+21-8
Original file line numberDiff line numberDiff line change
@@ -432,16 +432,16 @@ func (st *loadState) addSym(name string, ver int, r *oReader, li uint32, kind in
432432
return i
433433
}
434434
// symbol already exists
435+
// Fix for issue #47185 -- given two dupok or BSS symbols with
436+
// different sizes, favor symbol with larger size. See also
437+
// issue #46653 and #72032.
438+
oldsz := l.SymSize(oldi)
439+
sz := int64(r.Sym(li).Siz())
435440
if osym.Dupok() {
436441
if l.flags&FlagStrictDups != 0 {
437442
l.checkdup(name, r, li, oldi)
438443
}
439-
// Fix for issue #47185 -- given two dupok symbols with
440-
// different sizes, favor symbol with larger size. See
441-
// also issue #46653.
442-
szdup := l.SymSize(oldi)
443-
sz := int64(r.Sym(li).Siz())
444-
if szdup < sz {
444+
if oldsz < sz {
445445
// new symbol overwrites old symbol.
446446
l.objSyms[oldi] = objSym{r.objidx, li}
447447
}
@@ -452,11 +452,24 @@ func (st *loadState) addSym(name string, ver int, r *oReader, li uint32, kind in
452452
if oldsym.Dupok() {
453453
return oldi
454454
}
455-
overwrite := r.DataSize(li) != 0
455+
// If one is a DATA symbol (i.e. has content, DataSize != 0)
456+
// and the other is BSS, the one with content wins.
457+
// If both are BSS, the one with larger size wins.
458+
// Specifically, the "overwrite" variable and the final result are
459+
//
460+
// new sym old sym overwrite
461+
// ---------------------------------------------
462+
// DATA DATA true => ERROR
463+
// DATA lg/eq BSS sm/eq true => new wins
464+
// DATA small BSS large true => ERROR
465+
// BSS large DATA small true => ERROR
466+
// BSS large BSS small true => new wins
467+
// BSS sm/eq D/B lg/eq false => old wins
468+
overwrite := r.DataSize(li) != 0 || oldsz < sz
456469
if overwrite {
457470
// new symbol overwrites old symbol.
458471
oldtyp := sym.AbiSymKindToSymKind[objabi.SymKind(oldsym.Type())]
459-
if !(oldtyp.IsData() && oldr.DataSize(oldli) == 0) {
472+
if !(oldtyp.IsData() && oldr.DataSize(oldli) == 0) || oldsz > sz {
460473
log.Fatalf("duplicated definition of symbol %s, from %s and %s", name, r.unit.Lib.Pkg, oldr.unit.Lib.Pkg)
461474
}
462475
l.objSyms[oldi] = objSym{r.objidx, li}

src/cmd/link/link_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"testing"
2121

2222
imacho "cmd/internal/macho"
23+
"cmd/internal/objfile"
2324
"cmd/internal/sys"
2425
)
2526

@@ -1541,3 +1542,52 @@ func TestCheckLinkname(t *testing.T) {
15411542
})
15421543
}
15431544
}
1545+
1546+
func TestLinknameBSS(t *testing.T) {
1547+
// Test that the linker chooses the right one as the definition
1548+
// for linknamed variables. See issue #72032.
1549+
testenv.MustHaveGoBuild(t)
1550+
t.Parallel()
1551+
1552+
tmpdir := t.TempDir()
1553+
1554+
src := filepath.Join("testdata", "linkname", "sched.go")
1555+
exe := filepath.Join(tmpdir, "sched.exe")
1556+
cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", exe, src)
1557+
out, err := cmd.CombinedOutput()
1558+
if err != nil {
1559+
t.Fatalf("build failed unexpectedly: %v:\n%s", err, out)
1560+
}
1561+
1562+
// Check the symbol size.
1563+
f, err := objfile.Open(exe)
1564+
if err != nil {
1565+
t.Fatalf("fail to open executable: %v", err)
1566+
}
1567+
syms, err := f.Symbols()
1568+
if err != nil {
1569+
t.Fatalf("fail to get symbols: %v", err)
1570+
}
1571+
found := false
1572+
for _, s := range syms {
1573+
if s.Name == "runtime.sched" || s.Name == "_runtime.sched" {
1574+
found = true
1575+
if s.Size < 100 {
1576+
// As of Go 1.25 (Mar 2025), runtime.sched has 6848 bytes on
1577+
// darwin/arm64. It should always be larger than 100 bytes on
1578+
// all platforms.
1579+
t.Errorf("runtime.sched symbol size too small: want > 100, got %d", s.Size)
1580+
}
1581+
}
1582+
}
1583+
if !found {
1584+
t.Errorf("runtime.sched symbol not found")
1585+
}
1586+
1587+
// Executable should run.
1588+
cmd = testenv.Command(t, exe)
1589+
out, err = cmd.CombinedOutput()
1590+
if err != nil {
1591+
t.Errorf("executable failed to run: %v\n%s", err, out)
1592+
}
1593+
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import _ "unsafe"
8+
9+
type schedt struct{}
10+
11+
//go:linkname sched runtime.sched
12+
var sched schedt
13+
14+
func main() {
15+
select {
16+
default:
17+
println("hello")
18+
}
19+
}

0 commit comments

Comments
 (0)