cmd/link: check linkname access to assembly symbols

With the introduction of checklinkname, using linkname to
reference a symbol from a different symbol is checked, and is
permitted only when there is a push linkname. One exception is
that linkname reference to an assembly symbol is always permitted,
for now. This CL addresses this exception, and let checklinkname
handle assembly symbols as well.

The rule is similar to Go functions: linkname access is permitted
if there is a push linkname annotation. One trickiness is that
the assembly function is defined in assembly, whereas the
annotation is in Go. They are different objects. To connect the
two, we apply the annotation to the ABI wrapper, and let the
linker to check the ABI wrapper symbol. A further complication is
that on non-regABI platforms there is no ABI wrapper. In this
case, we just allow the access for now. As most popular platforms
are register ABI platforms, this shouldn't leave too big a hole.

To allow one package pushes assembly functions to another, like,
package a defines an assembly symbol b.F, it is permitted to
reference directly from the target package (b in the example)
based on the name.

This change also makes it handle linkname references to ABI
wrappers more strict. Previously it was always permitted. Now
we treat the ABI wrapper the same as the underlying symbol.
With this, we can migrate runtime.newcoro to linknamestd and
remove it from the blocklist, and the coro_var and coro_asm test
cases in cmd/link.TestCheckLinkname still pass.

Change-Id: I6c03467d3eaaa536663e52ce289e3c1c23079aa6
Reviewed-on: https://go-review.googlesource.com/c/go/+/761481
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Cherry Mui 2026-02-23 12:38:03 -05:00
parent ad46b4815e
commit aee6009ba5
9 changed files with 72 additions and 14 deletions

View file

@ -280,6 +280,10 @@ func makeABIWrapper(f *ir.Func, wrapperABI obj.ABI) {
fn.SetABIWrapper(true)
fn.SetDupok(true)
// Propagate linkname attribute.
fn.LinksymABI(fn.ABI).Set(obj.AttrLinkname, f.Linksym().IsLinkname())
fn.LinksymABI(fn.ABI).Set(obj.AttrLinknameStd, f.Linksym().IsLinknameStd())
// ABI0-to-ABIInternal wrappers will be mainly loading params from
// stack into registers (and/or storing stack locations back to
// registers after the wrapped call); in most cases they won't

View file

@ -371,9 +371,7 @@ func (w *writer) Sym(s *LSym) {
if s.IsPkgInit() {
flag2 |= goobj.SymFlagPkgInit
}
if s.IsLinkname() || (w.ctxt.IsAsm && name != "") || name == "main.main" {
// Assembly reference is treated the same as linkname,
// but not for unnamed (aux) symbols.
if s.IsLinkname() || name == "main.main" {
// The runtime linknames main.main.
flag2 |= goobj.SymFlagLinkname
}

View file

@ -2265,7 +2265,16 @@ func instinit(ctxt *obj.Link) {
switch ctxt.Headtype {
case objabi.Hplan9:
// _privates is a special symbol on Plan 9 that
// points to perprocess private data (like TLS area).
// See https://9p.io/magic/man2html/2/exec .
// The assembler inserts a reference to this symbol
// for accessing the G. Mark it as linkname so it is
// allowed to access from anywhere. (Would be nice to
// mark it external, but we don't have a mechanism for
// that.)
plan9privates = ctxt.Lookup("_privates")
plan9privates.Set(obj.AttrLinkname, true)
}
for i := range avxOptab {

View file

@ -15,6 +15,7 @@ import (
"debug/elf"
"fmt"
"internal/abi"
"internal/buildcfg"
"io"
"iter"
"log"
@ -2368,7 +2369,7 @@ func loadObjRefs(l *Loader, r *oReader, arch *sys.Arch) {
v := abiToVer(osym.ABI(), r.version)
gi := l.LookupOrCreateSym(name, v)
r.syms[ndef+i] = gi
if osym.IsLinkname() || osym.IsLinknameStd() {
if osym.IsLinkname() || osym.IsLinknameStd() || r.FromAssembly() {
// Check if a linkname reference is allowed.
// Only check references (pull), not definitions (push),
// so push is always allowed.
@ -2426,8 +2427,6 @@ func abiToVer(abi uint16, localSymVersion int) int {
// If a name is in this map, it is allowed only in listed packages,
// even if it has a linknamed definition.
var blockedLinknames = map[string][]string{
// coroutines
"runtime.newcoro": {"iter"},
// fips info
"go:fipsinfo": {"crypto/internal/fips140/check"},
// New internal linknames in Go 1.24
@ -2553,6 +2552,27 @@ func (l *Loader) checkLinkname(refpkg *oReader, name string, s Sym) {
return
}
osym := r.Sym(li)
if r.FromAssembly() && !osym.IsLinknameStd() && !osym.IsLinkname() {
if strings.HasPrefix(name, pkg) {
// Allow if by name it is pushed to pkg, e.g. in package a,
// an assembly function is defined as b.F, then it is allowed
// to be used in package b.
return
}
// For an assembly symbol, check if there is a linkname applied
// to its ABI wrapper.
if !buildcfg.Experiment.RegabiWrappers {
// If ABI wrapper is not enabled (i.e. non-regabi platform),
// permit for now, as there is no good way to check.
return
}
otherABI := 1 - abiToVer(osym.ABI(), r.version) // for now, we only have ABI 0 and 1
w := l.Lookup(name, otherABI) // TODO: use an aux symbol instead of name lookup?
if w != 0 {
r, li = l.toLocal(w)
osym = r.Sym(li)
}
}
if osym.IsLinknameStd() {
// It is pushed with linknamestd. Allow only pulls from the
// standard library.
@ -2560,12 +2580,8 @@ func (l *Loader) checkLinkname(refpkg *oReader, name string, s Sym) {
return
}
}
if osym.IsLinkname() || osym.ABIWrapper() {
if osym.IsLinkname() {
// Allow if the def has a linkname (push).
// ABI wrapper usually wraps an assembly symbol, a linknamed symbol,
// or an external symbol, or provide access of a Go symbol to assembly.
// For now, allow ABI wrappers.
// TODO: check the wrapped symbol?
return
}
error()

View file

@ -12,6 +12,7 @@ import (
"debug/pe"
"errors"
"internal/abi"
"internal/buildcfg"
"internal/platform"
"internal/testenv"
"internal/xcoff"
@ -1715,6 +1716,10 @@ func TestCheckLinkname(t *testing.T) {
{"coro2.go", false},
// pull linkname of a builtin symbol is not ok
{"builtin.go", false},
// using a linkname to reference a runtime assembly
// function is not ok (except on non-regabi platforms)
{"systemstack.go", !buildcfg.Experiment.RegabiWrappers},
// misc
{"addmoduledata.go", false},
{"freegc.go", false},
// legacy bad linkname is ok, for now

View file

@ -3,7 +3,8 @@
// license that can be found in the LICENSE file.
// Existing pull linknames in the wild are allowed _for now_,
// for legacy reason. Test a function and a method.
// for legacy reason. Test a function, a method, and an
// assembly symbol.
// NOTE: this may not be allowed in the future. Don't do this!
package main
@ -19,6 +20,12 @@ func noescape(unsafe.Pointer) unsafe.Pointer
//go:linkname rtype_String reflect.(*rtype).String
func rtype_String(unsafe.Pointer) string
//go:linkname memmove runtime.memmove
func memmove(to, from unsafe.Pointer, n uintptr)
var n uintptr // use a global to prevent compiler optimize out memmove call
func main() {
println(rtype_String(noescape(nil)))
memmove(nil, nil, n)
}

View file

@ -0,0 +1,19 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Linkname systemstack is not allowed, even if it is
// defined in assembly.
package main
import _ "unsafe"
func f() {}
func main() {
systemstack(f)
}
//go:linkname systemstack runtime.systemstack
func systemstack(func())

View file

@ -230,7 +230,7 @@ type Seq2[K, V any] func(yield func(K, V) bool)
type coro struct{}
//go:linkname newcoro runtime.newcoro
//go:linknamestd newcoro runtime.newcoro
func newcoro(func(*coro)) *coro
//go:linknamestd coroswitch runtime.coroswitch

View file

@ -34,7 +34,7 @@ type coro struct {
lockedInt uint32 // mp's internal lockOSThread counter at coro creation time.
}
//go:linkname newcoro
//go:linknamestd newcoro
// newcoro creates a new coro containing a
// goroutine blocked waiting to run f