Skip to content

cmd/link: add option to enable full RELRO for ELF #58869

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/cmd/go/internal/work/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,7 @@ var validLinkerFlags = []*lazyregexp.Regexp{
re(`-Wl,-?-unresolved-symbols=[^,]+`),
re(`-Wl,--(no-)?warn-([^,]+)`),
re(`-Wl,-?-wrap[=,][^,@\-][^,]*`),
re(`-Wl,-z,(no)?execstack`),
re(`-Wl,-z,relro`),
re(`-Wl(,-z,(relro|now|(no)?execstack))+`),

re(`[a-zA-Z0-9_/].*\.(a|o|obj|dll|dylib|so|tbd)`), // direct linker inputs: x.o or libfoo.so (but not -foo.o or @foo.o)
re(`\./.*\.(a|o|obj|dll|dylib|so|tbd)`),
Expand Down
4 changes: 4 additions & 0 deletions src/cmd/go/internal/work/security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ var goodLinkerFlags = [][]string{
{"-Wl,-framework", "-Wl,Chocolate"},
{"-Wl,-framework,Chocolate"},
{"-Wl,-unresolved-symbols=ignore-all"},
{"-Wl,-z,relro"},
{"-Wl,-z,relro,-z,now"},
{"-Wl,-z,now"},
{"-Wl,-z,noexecstack"},
{"libcgotbdtest.tbd"},
{"./libcgotbdtest.tbd"},
}
Expand Down
2 changes: 2 additions & 0 deletions src/cmd/link/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ Flags:
Link with C/C++ address sanitizer support.
-aslr
Enable ASLR for buildmode=c-shared on windows (default true).
-bindnow
Mark a dynamically linked ELF object for immediate function binding (default false).
-buildid id
Record id as Go toolchain build id.
-buildmode mode
Expand Down
32 changes: 25 additions & 7 deletions src/cmd/link/internal/ld/elf.go
Original file line number Diff line number Diff line change
Expand Up @@ -1056,11 +1056,17 @@ func elfdynhash(ctxt *Link) {
}

s = ldr.CreateSymForUpdate(".dynamic", 0)

var dtFlags1 elf.DynFlag1
if *flagBindNow {
dtFlags1 |= elf.DF_1_NOW
Elfwritedynent(ctxt.Arch, s, elf.DT_FLAGS, uint64(elf.DF_BIND_NOW))
}
if ctxt.BuildMode == BuildModePIE {
// https://github.com/bminor/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/elf/elf.h#L986
const DTFLAGS_1_PIE = 0x08000000
Elfwritedynent(ctxt.Arch, s, elf.DT_FLAGS_1, uint64(DTFLAGS_1_PIE))
dtFlags1 |= elf.DF_1_PIE
}
Elfwritedynent(ctxt.Arch, s, elf.DT_FLAGS_1, uint64(dtFlags1))

elfverneed = nfile
if elfverneed != 0 {
elfWriteDynEntSym(ctxt, s, elf.DT_VERNEED, gnuVersionR.Sym())
Expand Down Expand Up @@ -1107,6 +1113,7 @@ func elfphload(seg *sym.Segment) *ElfPhdr {
func elfphrelro(seg *sym.Segment) {
ph := newElfPhdr()
ph.Type = elf.PT_GNU_RELRO
ph.Flags = elf.PF_R
ph.Vaddr = seg.Vaddr
ph.Paddr = seg.Vaddr
ph.Memsz = seg.Length
Expand Down Expand Up @@ -1556,7 +1563,11 @@ func (ctxt *Link) doelf() {

/* global offset table */
got := ldr.CreateSymForUpdate(".got", 0)
got.SetType(sym.SELFGOT) // writable
if ctxt.UseRelro() {
got.SetType(sym.SRODATARELRO)
} else {
got.SetType(sym.SELFGOT) // writable
}

/* ppc64 glink resolver */
if ctxt.IsPPC64() {
Expand All @@ -1569,7 +1580,11 @@ func (ctxt *Link) doelf() {
hash.SetType(sym.SELFROSECT)

gotplt := ldr.CreateSymForUpdate(".got.plt", 0)
gotplt.SetType(sym.SELFSECT) // writable
if ctxt.UseRelro() && *flagBindNow {
gotplt.SetType(sym.SRODATARELRO)
} else {
gotplt.SetType(sym.SELFSECT) // writable
}

plt := ldr.CreateSymForUpdate(".plt", 0)
if ctxt.IsPPC64() {
Expand All @@ -1591,9 +1606,12 @@ func (ctxt *Link) doelf() {

/* define dynamic elf table */
dynamic := ldr.CreateSymForUpdate(".dynamic", 0)
if thearch.ELF.DynamicReadOnly {
switch {
case thearch.ELF.DynamicReadOnly:
dynamic.SetType(sym.SELFROSECT)
} else {
case ctxt.UseRelro():
dynamic.SetType(sym.SRODATARELRO)
default:
dynamic.SetType(sym.SELFSECT)
}

Expand Down
150 changes: 150 additions & 0 deletions src/cmd/link/internal/ld/elf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package ld

import (
"debug/elf"
"fmt"
"internal/testenv"
"os"
"path/filepath"
Expand Down Expand Up @@ -182,3 +183,152 @@ func main() {
}
}
}

func TestElfBindNow(t *testing.T) {
t.Parallel()
testenv.MustHaveGoBuild(t)

const (
prog = `package main; func main() {}`
// with default buildmode code compiles in a statically linked binary, hence CGO
progC = `package main; import "C"; func main() {}`
)

tests := []struct {
name string
args []string
prog string
mustHaveBuildModePIE bool
mustHaveCGO bool
mustInternalLink bool
wantDfBindNow bool
wantDf1Now bool
wantDf1Pie bool
}{
{name: "default", prog: prog},
{
name: "pie-linkmode-internal",
args: []string{"-buildmode=pie", "-ldflags", "-linkmode=internal"},
prog: prog,
mustHaveBuildModePIE: true,
mustInternalLink: true,
wantDf1Pie: true,
},
{
name: "bindnow-linkmode-internal",
args: []string{"-ldflags", "-bindnow -linkmode=internal"},
prog: progC,
mustHaveCGO: true,
mustInternalLink: true,
wantDfBindNow: true,
wantDf1Now: true,
},
{
name: "bindnow-pie-linkmode-internal",
args: []string{"-buildmode=pie", "-ldflags", "-bindnow -linkmode=internal"},
prog: prog,
mustHaveBuildModePIE: true,
mustInternalLink: true,
wantDfBindNow: true,
wantDf1Now: true,
wantDf1Pie: true,
},
{
name: "bindnow-pie-linkmode-external",
args: []string{"-buildmode=pie", "-ldflags", "-bindnow -linkmode=external"},
prog: prog,
mustHaveBuildModePIE: true,
mustHaveCGO: true,
wantDfBindNow: true,
wantDf1Now: true,
wantDf1Pie: true,
},
}

gotDynFlag := func(flags []uint64, dynFlag uint64) bool {
for _, flag := range flags {
if gotFlag := dynFlag&flag != 0; gotFlag {
return true
}
}

return false
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.mustInternalLink {
testenv.MustInternalLink(t, test.mustHaveCGO)
}
if test.mustHaveCGO {
testenv.MustHaveCGO(t)
}
if test.mustHaveBuildModePIE {
testenv.MustHaveBuildMode(t, "pie")
}
if test.mustHaveBuildModePIE && test.mustInternalLink {
testenv.MustInternalLinkPIE(t)
}

var (
dir = t.TempDir()
src = filepath.Join(dir, fmt.Sprintf("elf_%s.go", test.name))
binFile = filepath.Join(dir, test.name)
)

if err := os.WriteFile(src, []byte(test.prog), 0666); err != nil {
t.Fatal(err)
}

cmdArgs := append([]string{"build", "-o", binFile}, append(test.args, src)...)
cmd := testenv.Command(t, testenv.GoToolPath(t), cmdArgs...)

if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("failed to build %v: %v:\n%s", cmd.Args, err, out)
}

fi, err := os.Open(binFile)
if err != nil {
t.Fatalf("failed to open built file: %v", err)
}
defer fi.Close()

elfFile, err := elf.NewFile(fi)
if err != nil {
t.Skip("The system may not support ELF, skipped.")
}
defer elfFile.Close()

flags, err := elfFile.DynValue(elf.DT_FLAGS)
if err != nil {
t.Fatalf("failed to get DT_FLAGS: %v", err)
}

flags1, err := elfFile.DynValue(elf.DT_FLAGS_1)
if err != nil {
t.Fatalf("failed to get DT_FLAGS_1: %v", err)
}

gotDfBindNow := gotDynFlag(flags, uint64(elf.DF_BIND_NOW))
gotDf1Now := gotDynFlag(flags1, uint64(elf.DF_1_NOW))

bindNowFlagsMatch := gotDfBindNow == test.wantDfBindNow && gotDf1Now == test.wantDf1Now

// some external linkers may set one of the two flags but not both.
if !test.mustInternalLink {
bindNowFlagsMatch = gotDfBindNow == test.wantDfBindNow || gotDf1Now == test.wantDf1Now
}

if !bindNowFlagsMatch {
t.Fatalf("Dynamic flags mismatch:\n"+
"DT_FLAGS BIND_NOW got: %v, want: %v\n"+
"DT_FLAGS_1 DF_1_NOW got: %v, want: %v",
gotDfBindNow, test.wantDfBindNow, gotDf1Now, test.wantDf1Now)
}

if gotDf1Pie := gotDynFlag(flags1, uint64(elf.DF_1_PIE)); gotDf1Pie != test.wantDf1Pie {
t.Fatalf("DT_FLAGS_1 DF_1_PIE got: %v, want: %v", gotDf1Pie, test.wantDf1Pie)
}
})
}
}
8 changes: 6 additions & 2 deletions src/cmd/link/internal/ld/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -1599,12 +1599,16 @@ func (ctxt *Link) hostlink() {
}

var altLinker string
if ctxt.IsELF && ctxt.DynlinkingGo() {
// We force all symbol resolution to be done at program startup
if ctxt.IsELF && (ctxt.DynlinkingGo() || *flagBindNow) {
// For ELF targets, when producing dynamically linked Go code
// or when immediate binding is explicitly requested,
// we force all symbol resolution to be done at program startup
// because lazy PLT resolution can use large amounts of stack at
// times we cannot allow it to do so.
argv = append(argv, "-Wl,-z,now")
}

if ctxt.IsELF && ctxt.DynlinkingGo() {
// Do not let the host linker generate COPY relocations. These
// can move symbols out of sections that rely on stable offsets
// from the beginning of the section (like sym.STYPE).
Expand Down
1 change: 1 addition & 0 deletions src/cmd/link/internal/ld/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func init() {
// Flags used by the linker. The exported flags are used by the architecture-specific packages.
var (
flagBuildid = flag.String("buildid", "", "record `id` as Go toolchain build id")
flagBindNow = flag.Bool("bindnow", false, "mark a dynamically linked ELF object for immediate function binding")

flagOutfile = flag.String("o", "", "write output to `file`")
flagPluginPath = flag.String("pluginpath", "", "full path name for plugin")
Expand Down
9 changes: 9 additions & 0 deletions src/internal/testenv/testenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,15 @@ func MustInternalLink(t testing.TB, withCgo bool) {
}
}

// MustInternalLinkPIE checks whether the current system can link PIE binary using
// internal linking.
// If not, MustInternalLinkPIE calls t.Skip with an explanation.
func MustInternalLinkPIE(t testing.TB) {
if !platform.InternalLinkPIESupported(runtime.GOOS, runtime.GOARCH) {
t.Skipf("skipping test: internal linking for buildmode=pie on %s/%s is not supported", runtime.GOOS, runtime.GOARCH)
}
}

// MustHaveBuildMode reports whether the current system can build programs in
// the given build mode.
// If not, MustHaveBuildMode calls t.Skip with an explanation.
Expand Down