runtime: precisely scan globals on all platforms

This commit is contained in:
Jake Bailey
2026-08-07 12:08:32 -07:00
committed by Ron Evans
parent ede501ae2b
commit 59fb104004
18 changed files with 136 additions and 324 deletions
-15
View File
@@ -1,15 +0,0 @@
//go:build (baremetal || tinygo.wasm) && !uefi
package runtime
// This file implements findGlobals for all systems where the start and end of
// the globals section can be found through linker-defined symbols.
// findGlobals finds all globals (which are reachable by definition) and calls
// the callback for them.
//
// This implementation marks all globals conservatively and assumes it can use
// linker-defined symbols for the start and end of the .data section.
func findGlobals(found func(start, end uintptr)) {
found(globalsStart, globalsEnd)
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build (gc.conservative || gc.precise) && tinygo.wasm
//go:build gc.conservative || gc.precise
package runtime
@@ -1,4 +1,4 @@
//go:build gc.boehm && tinygo.wasm
//go:build gc.boehm
package runtime
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build gc.custom && tinygo.wasm
//go:build gc.custom
package runtime
+6
View File
@@ -0,0 +1,6 @@
//go:build gc.leaking || gc.none
package runtime
func markGlobals() {
}
+2 -2
View File
@@ -26,7 +26,7 @@ func gcMarkReachable() {
}
// Scan globals.
findGlobals(markRoots)
markGlobals()
// Nothing more to do: the other cores haven't started yet.
return
@@ -57,7 +57,7 @@ func gcMarkReachable() {
}
// Scan globals.
findGlobals(markRoots)
markGlobals()
// Signal each core in turn that they can scan the stack.
for i := uint32(0); i < numCPU; i++ {
+1 -1
View File
@@ -12,7 +12,7 @@ var gcScanState atomic.Uint32
func gcMarkReachable() {
markStack()
findGlobals(markRoots)
markGlobals()
}
// markStack marks all root pointers found on the stack.
+1 -1
View File
@@ -13,7 +13,7 @@ func gcMarkReachable() {
//
//go:linkname gcScanGlobals internal/task.gcScanGlobals
func gcScanGlobals() {
findGlobals(markRoots)
markGlobals()
}
// Function called from assembly with all registers pushed, to actually scan the
-91
View File
@@ -31,97 +31,6 @@ const (
sig_SIGSEGV = 11
)
// https://opensource.apple.com/source/xnu/xnu-7195.141.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html
type machHeader struct {
magic uint32
cputype uint32
cpusubtype uint32
filetype uint32
ncmds uint32
sizeofcmds uint32
flags uint32
reserved uint32
}
// Struct for the LC_SEGMENT_64 load command.
type segmentLoadCommand struct {
cmd uint32 // LC_SEGMENT_64
cmdsize uint32
segname [16]byte
vmaddr uintptr
vmsize uintptr
fileoff uintptr
filesize uintptr
maxprot uint32
initprot uint32
nsects uint32
flags uint32
}
// MachO header of the currently running process.
//
//go:extern _mh_execute_header
var libc_mh_execute_header machHeader
// Find global variables in .data/.bss sections.
// The MachO linker doesn't seem to provide symbols for the start and end of the
// data section. There is get_etext, get_edata, and get_end, but these are
// undocumented and don't work with ASLR (which is enabled by default).
// Therefore, read the MachO header directly.
func findGlobals(found func(start, end uintptr)) {
// Here is a useful blog post to understand the MachO file format:
// https://h3adsh0tzz.com/2020/01/macho-file-format/
const (
MH_MAGIC_64 = 0xfeedfacf
LC_SEGMENT_64 = 0x19
VM_PROT_WRITE = 0x02
)
// Sanity check that we're actually looking at a MachO header.
if gcAsserts && libc_mh_execute_header.magic != MH_MAGIC_64 {
runtimeFatal("gc: unexpected MachO header")
}
// Iterate through the load commands.
// Because we're only interested in LC_SEGMENT_64 load commands, cast the
// pointer to that struct in advance.
var offset uintptr
var hasOffset bool
cmd := (*segmentLoadCommand)(unsafe.Pointer(uintptr(unsafe.Pointer(&libc_mh_execute_header)) + unsafe.Sizeof(machHeader{})))
for i := libc_mh_execute_header.ncmds; i != 0; i-- {
if cmd.cmd == LC_SEGMENT_64 {
if cmd.fileoff == 0 && cmd.nsects != 0 {
// Detect ASLR offset by checking fileoff and nsects. This
// locates the __TEXT segment. This matches getsectiondata:
// https://opensource.apple.com/source/cctools/cctools-973.0.1/libmacho/getsecbyname.c.auto.html
offset = uintptr(unsafe.Pointer(&libc_mh_execute_header)) - cmd.vmaddr
hasOffset = true
}
if cmd.maxprot&VM_PROT_WRITE != 0 {
// Found a writable segment, which may contain Go globals.
if gcAsserts && !hasOffset {
// No ASLR offset detected. Did the __TEXT segment come
// after the __DATA segment?
// Note that when ASLR is disabled (for example, when
// running inside lldb), the offset is zero. That's why we
// need a separate hasOffset for this assert.
runtimeFatal("gc: did not detect ASLR offset")
}
// Scan this segment for GC roots.
// This could be improved by only reading the memory areas
// covered by sections. That would reduce the amount of memory
// scanned a little bit (up to a single VM page).
found(offset+cmd.vmaddr, offset+cmd.vmaddr+cmd.vmsize)
}
}
// Move on to the next load command (which may or may not be a
// LC_SEGMENT_64).
cmd = (*segmentLoadCommand)(unsafe.Add(unsafe.Pointer(cmd), cmd.cmdsize))
}
}
func hardwareRand() (n uint64, ok bool) {
n |= uint64(libc_arc4random())
n |= uint64(libc_arc4random()) << 32
-90
View File
@@ -34,101 +34,11 @@ const (
sig_SIGSEGV = linux_SIGSEGV
)
// For the definition of the various header structs, see:
// https://refspecs.linuxfoundation.org/elf/elf.pdf
// Also useful:
// https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
type elfHeader struct {
ident_magic uint32
ident_class uint8
ident_data uint8
ident_version uint8
ident_osabi uint8
ident_abiversion uint8
_ [7]byte // reserved
filetype uint16
machine uint16
version uint32
entry uintptr
phoff uintptr
shoff uintptr
flags uint32
ehsize uint16
phentsize uint16
phnum uint16
shentsize uint16
shnum uint16
shstrndx uint16
}
type elfProgramHeader64 struct {
_type uint32
flags uint32
offset uintptr
vaddr uintptr
paddr uintptr
filesz uintptr
memsz uintptr
align uintptr
}
type elfProgramHeader32 struct {
_type uint32
offset uintptr
vaddr uintptr
paddr uintptr
filesz uintptr
memsz uintptr
flags uint32
align uintptr
}
// ELF header of the currently running process.
//
//go:extern __ehdr_start
var ehdr_start elfHeader
// int *__errno_location(void);
//
//export __errno_location
func libc_errno_location() *int32
// findGlobals finds globals in the .data/.bss sections.
// It parses the ELF program header to find writable segments.
func findGlobals(found func(start, end uintptr)) {
// Relevant constants from the ELF specification.
// See: https://refspecs.linuxfoundation.org/elf/elf.pdf
const (
PT_LOAD = 1
PF_W = 0x2 // program flag: write access
)
headerPtr := unsafe.Pointer(uintptr(unsafe.Pointer(&ehdr_start)) + ehdr_start.phoff)
for i := 0; i < int(ehdr_start.phnum); i++ {
// Look for a writable segment and scan its contents.
// There is a little bit of duplication here, which is unfortunate. But
// the alternative would be to put elfProgramHeader in separate files
// which is IMHO a lot uglier. If only the ELF spec was consistent
// between 32-bit and 64-bit...
if TargetBits == 64 {
header := (*elfProgramHeader64)(headerPtr)
if header._type == PT_LOAD && header.flags&PF_W != 0 {
start := header.vaddr
end := start + header.memsz
found(start, end)
}
} else {
header := (*elfProgramHeader32)(headerPtr)
if header._type == PT_LOAD && header.flags&PF_W != 0 {
start := header.vaddr
end := start + header.memsz
found(start, end)
}
}
headerPtr = unsafe.Add(headerPtr, ehdr_start.phentsize)
}
}
//export getpagesize
func libc_getpagesize() int
-30
View File
@@ -6,36 +6,6 @@ const GOOS = "windows"
const zeroSizeAllocPtr uintptr = 16 // part of the first protected page
//export GetModuleHandleExA
func _GetModuleHandleExA(dwFlags uint32, lpModuleName unsafe.Pointer, phModule **exeHeader) bool
// Mark global variables.
// Unfortunately, the linker doesn't provide symbols for the start and end of
// the data/bss sections. Therefore these addresses need to be determined at
// runtime. This might seem complex and it kind of is, but it only compiles to
// around 160 bytes of amd64 instructions.
// Most of this function is based on the documentation in
// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format.
func findGlobals(found func(start, end uintptr)) {
// Constants used in this function.
const (
// https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandleexa
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 0x00000002
)
if module == nil {
// Obtain a handle to the currently executing image. What we're getting
// here is really just __ImageBase, but it's probably better to obtain
// it using GetModuleHandle to account for ASLR etc.
result := _GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, nil, &module)
if gcAsserts && (!result || module.signature != 0x5A4D) { // 0x4D5A is "MZ"
runtimeFatal("cannot get module handle")
}
}
findGlobalsForPE(found)
}
type systeminfo struct {
anon0 [4]byte
dwpagesize uint32
-63
View File
@@ -1,63 +0,0 @@
//go:build windows || uefi
package runtime
import "unsafe"
// MS-DOS stub with PE header offset:
// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#ms-dos-stub-image-only
type exeHeader struct {
signature uint16
_ [58]byte // skip DOS header
peHeader uint32 // at offset 0x3C
}
// COFF file header:
// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#file-headers
type peHeader struct {
magic uint32
machine uint16
numberOfSections uint16
timeDateStamp uint32
pointerToSymbolTable uint32
numberOfSymbols uint32
sizeOfOptionalHeader uint16
characteristics uint16
}
// COFF section header:
// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers
type peSection struct {
name [8]byte
virtualSize uint32
virtualAddress uint32
sizeOfRawData uint32
pointerToRawData uint32
pointerToRelocations uint32
pointerToLinenumbers uint32
numberOfRelocations uint16
numberOfLinenumbers uint16
characteristics uint32
}
var module *exeHeader
func findGlobalsForPE(found func(start, end uintptr)) {
// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
const imageSCNMemWrite = 0x80000000
pe := (*peHeader)(unsafe.Add(unsafe.Pointer(module), module.peHeader))
if gcAsserts && pe.magic != 0x00004550 { // 0x4550 is "PE"
runtimeFatal("cannot find PE header")
}
section := (*peSection)(unsafe.Pointer(uintptr(unsafe.Pointer(pe)) + uintptr(pe.sizeOfOptionalHeader) + unsafe.Sizeof(peHeader{})))
for i := 0; i < int(pe.numberOfSections); i++ {
if section.characteristics&imageSCNMemWrite != 0 {
start := uintptr(unsafe.Pointer(module)) + uintptr(section.virtualAddress)
end := uintptr(unsafe.Pointer(module)) + uintptr(section.virtualAddress) + uintptr(section.virtualSize)
found(start, end)
}
section = (*peSection)(unsafe.Add(unsafe.Pointer(section), unsafe.Sizeof(peSection{})))
}
}
-25
View File
@@ -257,31 +257,6 @@ func getHeapEnd() uintptr {
return heapEnd
}
//go:extern __data_start
var dataStartSymbol [0]byte
//go:extern __data_end
var dataEndSymbol [0]byte
//go:extern __bss_start
var bssStartSymbol [0]byte
//go:extern __bss_end
var bssEndSymbol [0]byte
// Find global variables.
// The linker script provides __*_start and __*_end symbols that can be used to
// scan the given sections. They are already aligned so don't need to be
// manually aligned here.
func findGlobals(found func(start, end uintptr)) {
dataStart := uintptr(unsafe.Pointer(&dataStartSymbol))
dataEnd := uintptr(unsafe.Pointer(&dataEndSymbol))
found(dataStart, dataEnd)
bssStart := uintptr(unsafe.Pointer(&bssStartSymbol))
bssEnd := uintptr(unsafe.Pointer(&bssEndSymbol))
found(bssStart, bssEnd)
}
// getContextPtr returns the hblauncher context
// this is externally linked by gonx
func getContextPtr() uintptr {