Skip to content

Commit 4721f95

Browse files
committed
cmd/compile, cmd/link: disallow linkname of some newly added internal functions
Go API is defined through exported symbols. When a package is imported, the compiler ensures that only exported symbols can be accessed, and the go command ensures that internal packages cannot be imported. This ensures API integrity. But there is a hole: using linkname, one can access internal or non-exported symbols. Linkname is a mechanism to give access of a symbol to a package without adding it to the public API. It is intended for coupled packages to share some implementation details, or to break circular dependencies, and both "push" (definition) and "pull" (reference) sides are controlled, so they can be updated in sync. Nevertheless, it is abused as a mechanism to reach into internal details of other packages uncontrolled by the user, notably the runtime. As the other package evolves, the code often breaks, because the linknamed symbol may no longer exist, or change its signature or semantics. This CL adds a mechanism to enforce the integrity of linknames. Generally, "push" linkname is allowed, as the package defining the symbol explicitly opt in for access outside of the package. "Pull" linkname is checked and only allowed in some circumstances. Given that there are existing code that use "pull"-only linkname to access other package's internals, disallowing it completely is too much a change at this point in the release cycle. For a start, implement a hard-coded blocklist, which contains some newly added internal functions that, if used inappropriately, may break memory safety or runtime integrity. All blocked symbols are newly added in Go 1.23. So existing code that builds with Go 1.22 will continue to build. For the implementation, when compiling a package, we mark linknamed symbols in the current package with an attribute. At link time, marked linknamed symbols are checked against the blocklist. Care is taken so it distinguishes a linkname reference in the current package vs. a reference of a linkname from another package and propagated to the current package (e.g. through inlining or instantiation). Symbol references in assembly code are similar to linknames, and are treated similarly. Change-Id: I8067efe29c122740cd4f1effd2dec2d839147d5d Reviewed-on: https://go-review.googlesource.com/c/go/+/584598 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Than McIntosh <[email protected]> Reviewed-by: Cuong Manh Le <[email protected]>
1 parent df4f40b commit 4721f95

File tree

14 files changed

+284
-2
lines changed

14 files changed

+284
-2
lines changed

src/cmd/compile/internal/noder/reader.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -1176,7 +1176,18 @@ func (r *reader) linkname(name *ir.Name) {
11761176
lsym.SymIdx = int32(idx)
11771177
lsym.Set(obj.AttrIndexed, true)
11781178
} else {
1179-
name.Sym().Linkname = r.String()
1179+
linkname := r.String()
1180+
sym := name.Sym()
1181+
sym.Linkname = linkname
1182+
if sym.Pkg == types.LocalPkg && linkname != "" {
1183+
// Mark linkname in the current package. We don't mark the
1184+
// ones that are imported and propagated (e.g. through
1185+
// inlining or instantiation, which are marked in their
1186+
// corresponding packages). So we can tell in which package
1187+
// the linkname is used (pulled), and the linker can
1188+
// make a decision for allowing or disallowing it.
1189+
sym.Linksym().Set(obj.AttrLinkname, true)
1190+
}
11801191
}
11811192
}
11821193

src/cmd/internal/goobj/objfile.go

+2
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ const (
303303
SymFlagItab
304304
SymFlagDict
305305
SymFlagPkgInit
306+
SymFlagLinkname
306307
)
307308

308309
// Returns the length of the name of the symbol.
@@ -334,6 +335,7 @@ func (s *Sym) UsedInIface() bool { return s.Flag2()&SymFlagUsedInIface != 0 }
334335
func (s *Sym) IsItab() bool { return s.Flag2()&SymFlagItab != 0 }
335336
func (s *Sym) IsDict() bool { return s.Flag2()&SymFlagDict != 0 }
336337
func (s *Sym) IsPkgInit() bool { return s.Flag2()&SymFlagPkgInit != 0 }
338+
func (s *Sym) IsLinkname() bool { return s.Flag2()&SymFlagLinkname != 0 }
337339

338340
func (s *Sym) SetName(x string, w *Writer) {
339341
binary.LittleEndian.PutUint32(s[:], uint32(len(x)))

src/cmd/internal/obj/link.go

+5
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,9 @@ const (
836836
// PkgInit indicates this is a compiler-generated package init func.
837837
AttrPkgInit
838838

839+
// Linkname indicates this is a go:linkname'd symbol.
840+
AttrLinkname
841+
839842
// attrABIBase is the value at which the ABI is encoded in
840843
// Attribute. This must be last; all bits after this are
841844
// assumed to be an ABI value.
@@ -865,6 +868,7 @@ func (a *Attribute) ContentAddressable() bool { return a.load()&AttrContentAddre
865868
func (a *Attribute) ABIWrapper() bool { return a.load()&AttrABIWrapper != 0 }
866869
func (a *Attribute) IsPcdata() bool { return a.load()&AttrPcdata != 0 }
867870
func (a *Attribute) IsPkgInit() bool { return a.load()&AttrPkgInit != 0 }
871+
func (a *Attribute) IsLinkname() bool { return a.load()&AttrLinkname != 0 }
868872

869873
func (a *Attribute) Set(flag Attribute, value bool) {
870874
for {
@@ -914,6 +918,7 @@ var textAttrStrings = [...]struct {
914918
{bit: AttrContentAddressable, s: ""},
915919
{bit: AttrABIWrapper, s: "ABIWRAPPER"},
916920
{bit: AttrPkgInit, s: "PKGINIT"},
921+
{bit: AttrLinkname, s: "LINKNAME"},
917922
}
918923

919924
// String formats a for printing in as part of a TEXT prog.

src/cmd/internal/obj/objfile.go

+3
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,9 @@ func (w *writer) Sym(s *LSym) {
348348
if s.IsPkgInit() {
349349
flag2 |= goobj.SymFlagPkgInit
350350
}
351+
if s.IsLinkname() || w.ctxt.IsAsm { // assembly reference is treated the same as linkname
352+
flag2 |= goobj.SymFlagLinkname
353+
}
351354
name := s.Name
352355
if strings.HasPrefix(name, "gofile..") {
353356
name = filepath.ToSlash(name)

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

+60-1
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,16 @@ func (st *loadState) addSym(name string, ver int, r *oReader, li uint32, kind in
420420
return i
421421
}
422422

423-
// Non-package (named) symbol. Check if it already exists.
423+
// Non-package (named) symbol.
424+
if osym.IsLinkname() && r.DataSize(li) == 0 {
425+
// This is a linknamed "var" "reference" (var x T with no data and //go:linkname x).
426+
// Check if a linkname reference is allowed.
427+
// Only check references (pull), not definitions (push, with non-zero size),
428+
// so push is always allowed.
429+
// Linkname is always a non-package reference.
430+
checkLinkname(r.unit.Lib.Pkg, name)
431+
}
432+
// Check if it already exists.
424433
oldi, existed := l.symsByName[ver][name]
425434
if !existed {
426435
l.symsByName[ver][name] = i
@@ -2243,6 +2252,13 @@ func loadObjRefs(l *Loader, r *oReader, arch *sys.Arch) {
22432252
osym := r.Sym(ndef + i)
22442253
name := osym.Name(r.Reader)
22452254
v := abiToVer(osym.ABI(), r.version)
2255+
if osym.IsLinkname() {
2256+
// Check if a linkname reference is allowed.
2257+
// Only check references (pull), not definitions (push),
2258+
// so push is always allowed.
2259+
// Linkname is always a non-package reference.
2260+
checkLinkname(r.unit.Lib.Pkg, name)
2261+
}
22462262
r.syms[ndef+i] = l.LookupOrCreateSym(name, v)
22472263
gi := r.syms[ndef+i]
22482264
if osym.Local() {
@@ -2289,6 +2305,49 @@ func abiToVer(abi uint16, localSymVersion int) int {
22892305
return v
22902306
}
22912307

2308+
// A list of blocked linknames. Some linknames are allowed only
2309+
// in specific packages. This maps symbol names to allowed packages.
2310+
// If a name is not in this map, and not with a blocked prefix (see
2311+
// blockedLinknamePrefixes), it is allowed everywhere.
2312+
// If a name is in this map, it is allowed only in listed packages.
2313+
var blockedLinknames = map[string][]string{
2314+
// coroutines
2315+
"runtime.coroexit": nil,
2316+
"runtime.corostart": nil,
2317+
"runtime.coroswitch": {"iter"},
2318+
"runtime.newcoro": {"iter"},
2319+
// weak references
2320+
"internal/weak.runtime_registerWeakPointer": {"internal/weak"},
2321+
"internal/weak.runtime_makeStrongFromWeak": {"internal/weak"},
2322+
"runtime.getOrAddWeakHandle": nil,
2323+
}
2324+
2325+
// A list of blocked linkname prefixes (packages).
2326+
var blockedLinknamePrefixes = []string{
2327+
"internal/weak.",
2328+
"internal/concurrent.",
2329+
}
2330+
2331+
func checkLinkname(pkg, name string) {
2332+
error := func() {
2333+
log.Fatalf("linkname or assembly reference of %s is not allowed in package %s", name, pkg)
2334+
}
2335+
pkgs, ok := blockedLinknames[name]
2336+
if ok {
2337+
for _, p := range pkgs {
2338+
if pkg == p {
2339+
return // pkg is allowed
2340+
}
2341+
}
2342+
error()
2343+
}
2344+
for _, p := range blockedLinknamePrefixes {
2345+
if strings.HasPrefix(name, p) {
2346+
error()
2347+
}
2348+
}
2349+
}
2350+
22922351
// TopLevelSym tests a symbol (by name and kind) to determine whether
22932352
// the symbol first class sym (participating in the link) or is an
22942353
// anonymous aux or sub-symbol containing some sub-part or payload of

src/cmd/link/link_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -1415,3 +1415,43 @@ func TestRandLayout(t *testing.T) {
14151415
t.Errorf("randlayout with different seeds produced same layout:\n%s\n===\n\n%s", syms[0], syms[1])
14161416
}
14171417
}
1418+
1419+
func TestBlockedLinkname(t *testing.T) {
1420+
// Test that code containing blocked linknames does not build.
1421+
testenv.MustHaveGoBuild(t)
1422+
t.Parallel()
1423+
1424+
tmpdir := t.TempDir()
1425+
1426+
tests := []struct {
1427+
src string
1428+
ok bool
1429+
}{
1430+
// use (instantiation) of public API is ok
1431+
{"ok.go", true},
1432+
// push linkname is ok
1433+
{"push.go", true},
1434+
// pull linkname of blocked symbol is not ok
1435+
{"coro.go", false},
1436+
{"weak.go", false},
1437+
{"coro_var.go", false},
1438+
// assembly reference is not ok
1439+
{"coro_asm", false},
1440+
}
1441+
for _, test := range tests {
1442+
test := test
1443+
t.Run(test.src, func(t *testing.T) {
1444+
t.Parallel()
1445+
src := filepath.Join("testdata", "linkname", test.src)
1446+
exe := filepath.Join(tmpdir, test.src+".exe")
1447+
cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", exe, src)
1448+
out, err := cmd.CombinedOutput()
1449+
if test.ok && err != nil {
1450+
t.Errorf("build failed unexpectedly: %v:\n%s", err, out)
1451+
}
1452+
if !test.ok && err == nil {
1453+
t.Errorf("build succeeded unexpectedly: %v:\n%s", err, out)
1454+
}
1455+
})
1456+
}
1457+
}
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2024 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+
// Linkname coroswitch is not allowed, even if iter.Pull
6+
// is instantiated in the same package.
7+
8+
package main
9+
10+
import (
11+
"iter"
12+
"unsafe"
13+
)
14+
15+
func seq(yield func(int) bool) {
16+
yield(123)
17+
}
18+
19+
func main() {
20+
next, stop := iter.Pull(seq)
21+
next()
22+
stop()
23+
coroswitch(nil)
24+
}
25+
26+
//go:linkname coroswitch runtime.coroswitch
27+
func coroswitch(unsafe.Pointer)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright 2024 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+
TEXT ·newcoro(SB),0,$0-0
6+
CALL runtime·newcoro(SB)
7+
RET
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2024 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+
// Assembly reference of newcoro is not allowed.
6+
7+
package main
8+
9+
func main() {
10+
newcoro()
11+
}
12+
13+
func newcoro()
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2024 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+
// Linkname "var" to reference newcoro is not allowed.
6+
7+
package main
8+
9+
import "unsafe"
10+
11+
func main() {
12+
call(&newcoro)
13+
}
14+
15+
//go:linkname newcoro runtime.newcoro
16+
var newcoro unsafe.Pointer
17+
18+
//go:noinline
19+
func call(*unsafe.Pointer) {
20+
// not implemented
21+
}

src/cmd/link/testdata/linkname/ok.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2024 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+
// Use of public API is ok.
6+
7+
package main
8+
9+
import (
10+
"iter"
11+
"unique"
12+
)
13+
14+
func seq(yield func(int) bool) {
15+
yield(123)
16+
}
17+
18+
var s = "hello"
19+
20+
func main() {
21+
h := unique.Make(s)
22+
next, stop := iter.Pull(seq)
23+
defer stop()
24+
println(h.Value())
25+
println(next())
26+
println(next())
27+
}

src/cmd/link/testdata/linkname/p/p.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2024 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 p
6+
7+
import _ "unsafe"
8+
9+
// f1 is pushed from main.
10+
//
11+
//go:linkname f1
12+
func f1()
13+
14+
// Push f2 to main.
15+
//
16+
//go:linkname f2 main.f2
17+
func f2() {}
18+
19+
func F() { f1() }
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2024 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+
// "Push" linknames are ok.
6+
7+
package main
8+
9+
import (
10+
"cmd/link/testdata/linkname/p"
11+
_ "unsafe"
12+
)
13+
14+
// Push f1 to p.
15+
//
16+
//go:linkname f1 cmd/link/testdata/linkname/p.f1
17+
func f1() { f2() }
18+
19+
// f2 is pushed from p.
20+
//
21+
//go:linkname f2
22+
func f2()
23+
24+
func main() {
25+
p.F()
26+
}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2024 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+
// Linkname generic functions in internal/weak is not
6+
// allowed; legitimate instantiation is ok.
7+
8+
package main
9+
10+
import (
11+
"unique"
12+
"unsafe"
13+
)
14+
15+
//go:linkname weakMake internal/weak.Make[string]
16+
func weakMake(string) unsafe.Pointer
17+
18+
func main() {
19+
h := unique.Make("xxx")
20+
println(h.Value())
21+
weakMake("xxx")
22+
}

0 commit comments

Comments
 (0)