mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-09-10 22:49:30 +00:00
runtime: preserve malloc allocations until free
C malloc storage has explicit lifetime: it must remain allocated until free even when no GC-visible pointer references it. Treating it as an ordinary NoPtrs allocation breaks bare-metal C object graphs, while conservatively scanning arbitrary C bytes creates false Go roots. Add allocManual/freeManual so collectors can represent pointer-free, explicitly managed storage. Block GC keeps these objects permanently marked and releases their blocks on free; Boehm uses atomic uncollectable allocations; leaking and custom collectors provide equivalent behavior. Wasm keeps its allocation map only for validation and sizes, and WASIp2 realloc now copies min(oldSize, newSize). Bump the Boehm library cache version because enabling atomic uncollectable allocations changes its compiled flags and exported API. Also handle zero-size and overflowing allocations, serialize allocation registries, reject Go finalizers on manual storage, and add CGo regressions for C pointer graphs, hidden until-free allocations, repeated free/reuse, and allocation edge cases.
This commit is contained in:
@@ -84,6 +84,7 @@
|
|||||||
- gc: correct the old size calculation in block realloc
|
- gc: correct the old size calculation in block realloc
|
||||||
- gc: correct the leaking allocator bounds and overflow checks
|
- gc: correct the leaking allocator bounds and overflow checks
|
||||||
- gc: move objHeader to the end of the block header
|
- gc: move objHeader to the end of the block header
|
||||||
|
- cgo: keep malloc allocations alive until free and stop the program for invalid or repeated free calls
|
||||||
- fix the leaking GC build with the cores scheduler
|
- fix the leaking GC build with the cores scheduler
|
||||||
- rp2040: fix -gc=leaking and -gc=none
|
- rp2040: fix -gc=leaking and -gc=none
|
||||||
- rp2: handle the RP2350 shared FIFO IRQ for GC (#5482)
|
- rp2: handle the RP2350 shared FIFO IRQ for GC (#5482)
|
||||||
|
|||||||
+5
-4
@@ -30,10 +30,11 @@ var BoehmGC = Library{
|
|||||||
// Use a minimal environment.
|
// Use a minimal environment.
|
||||||
"-DNO_MSGBOX_ON_ERROR", // don't call MessageBoxA on Windows
|
"-DNO_MSGBOX_ON_ERROR", // don't call MessageBoxA on Windows
|
||||||
"-DDONT_USE_ATEXIT",
|
"-DDONT_USE_ATEXIT",
|
||||||
"-DNO_GETENV", // smaller binary, more predictable configuration
|
"-DNO_GETENV", // smaller binary, more predictable configuration
|
||||||
"-DNO_CLOCK", // don't use system clock
|
"-DNO_CLOCK", // don't use system clock
|
||||||
"-DNO_DEBUGGING", // reduce code size
|
"-DNO_DEBUGGING", // reduce code size
|
||||||
"-DGC_NO_FINALIZATION", // finalization is not used at the moment
|
"-DGC_NO_FINALIZATION", // finalization is not used at the moment
|
||||||
|
"-DGC_ATOMIC_UNCOLLECTABLE", // pointer-free storage retained until GC_free
|
||||||
|
|
||||||
// Special flag to work around the lack of __data_start in ld.lld.
|
// Special flag to work around the lack of __data_start in ld.lld.
|
||||||
// TODO: try to fix this in LLVM/lld directly so we don't have to
|
// TODO: try to fix this in LLVM/lld directly so we don't have to
|
||||||
|
|||||||
Vendored
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
target package code rodata data bss
|
target package code rodata data bss
|
||||||
hifive1b examples/echo 4405 323 0 2268
|
hifive1b examples/echo 4533 323 0 2268
|
||||||
microbit examples/serial 2922 382 8 2264
|
microbit examples/serial 3002 382 8 2264
|
||||||
wioterminal examples/pininterrupt 8251 1717 148 7496
|
wioterminal examples/pininterrupt 8331 1717 148 7496
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
// library path in advance in several places).
|
// library path in advance in several places).
|
||||||
var libVersions = map[string]int{
|
var libVersions = map[string]int{
|
||||||
"musl": 3,
|
"musl": 3,
|
||||||
"bdwgc": 2,
|
"bdwgc": 3,
|
||||||
"picolibc": 2,
|
"picolibc": 2,
|
||||||
"wasmbuiltins": 1,
|
"wasmbuiltins": 1,
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -114,7 +114,13 @@ func TestBuild(t *testing.T) {
|
|||||||
// This makes it possible to run one specific test (instead of all),
|
// This makes it possible to run one specific test (instead of all),
|
||||||
// which is especially useful to quickly check whether some changes
|
// which is especially useful to quickly check whether some changes
|
||||||
// affect a particular target architecture.
|
// affect a particular target architecture.
|
||||||
runPlatTests(optionsFromTarget(*testTarget, sema), tests, t)
|
options := optionsFromTarget(*testTarget, sema)
|
||||||
|
runPlatTests(options, tests, t)
|
||||||
|
if *testTarget == "wasip1" {
|
||||||
|
t.Run("cgo-realloc", func(t *testing.T) {
|
||||||
|
runTest("cgo-realloc/", options, t, nil, nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +229,11 @@ func TestBuild(t *testing.T) {
|
|||||||
})
|
})
|
||||||
t.Run("WASIp1", func(t *testing.T) {
|
t.Run("WASIp1", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
runPlatTests(optionsFromTarget("wasip1", sema), tests, t)
|
options := optionsFromTarget("wasip1", sema)
|
||||||
|
runPlatTests(options, tests, t)
|
||||||
|
t.Run("cgo-realloc", func(t *testing.T) {
|
||||||
|
runTest("cgo-realloc/", options, t, nil, nil)
|
||||||
|
})
|
||||||
|
|
||||||
// Test with -gc=boehm.
|
// Test with -gc=boehm.
|
||||||
t.Run("gc.go-boehm", func(t *testing.T) {
|
t.Run("gc.go-boehm", func(t *testing.T) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"internal/gclayout"
|
"internal/task"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,21 +11,21 @@ import (
|
|||||||
// code linked from other languages can allocate memory without colliding with
|
// code linked from other languages can allocate memory without colliding with
|
||||||
// our GC allocations.
|
// our GC allocations.
|
||||||
|
|
||||||
// Map of allocations, where the key is the allocated pointer and the value is
|
// Map of allocations, where the key is the allocation address and the value is
|
||||||
// the size of the allocation.
|
// its size. Integer keys intentionally do not act as GC roots: manual
|
||||||
// TODO: make this a map[unsafe.Pointer]uintptr, since that results in slightly
|
// allocations are retained by the allocator until free.
|
||||||
// smaller binaries. But for that to work, unsafe.Pointer needs to be seen as a
|
var allocs = make(map[uintptr]uintptr)
|
||||||
// binary key (which it is not at the moment).
|
var allocsLock task.PMutex
|
||||||
// See https://github.com/tinygo-org/tinygo/pull/4898 for details.
|
|
||||||
var allocs = make(map[*byte]uintptr)
|
|
||||||
|
|
||||||
//export malloc
|
//export malloc
|
||||||
func libc_malloc(size uintptr) unsafe.Pointer {
|
func libc_malloc(size uintptr) unsafe.Pointer {
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ptr := alloc(size, gclayout.NoPtrs.AsPtr())
|
ptr := allocManual(size)
|
||||||
allocs[(*byte)(ptr)] = size
|
allocsLock.Lock()
|
||||||
|
allocs[uintptr(ptr)] = size
|
||||||
|
allocsLock.Unlock()
|
||||||
return ptr
|
return ptr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,16 +34,22 @@ func libc_free(ptr unsafe.Pointer) {
|
|||||||
if ptr == nil {
|
if ptr == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := allocs[(*byte)(ptr)]; ok {
|
allocsLock.Lock()
|
||||||
delete(allocs, (*byte)(ptr))
|
if _, ok := allocs[uintptr(ptr)]; ok {
|
||||||
|
delete(allocs, uintptr(ptr))
|
||||||
|
allocsLock.Unlock()
|
||||||
|
freeManual(ptr)
|
||||||
} else {
|
} else {
|
||||||
|
allocsLock.Unlock()
|
||||||
runtimeFatal("free: invalid pointer")
|
runtimeFatal("free: invalid pointer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//export calloc
|
//export calloc
|
||||||
func libc_calloc(nmemb, size uintptr) unsafe.Pointer {
|
func libc_calloc(nmemb, size uintptr) unsafe.Pointer {
|
||||||
// No difference between calloc and malloc.
|
if size != 0 && nmemb > ^uintptr(0)/size {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return libc_malloc(nmemb * size)
|
return libc_malloc(nmemb * size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,22 +60,37 @@ func libc_realloc(oldPtr unsafe.Pointer, size uintptr) unsafe.Pointer {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's hard to optimize this to expand the current buffer with our GC, but
|
var oldSize uintptr
|
||||||
// it is theoretically possible. For now, just always allocate fresh.
|
|
||||||
// TODO: we could skip this if the new allocation is smaller than the old.
|
|
||||||
ptr := alloc(size, gclayout.NoPtrs.AsPtr())
|
|
||||||
|
|
||||||
if oldPtr != nil {
|
if oldPtr != nil {
|
||||||
if oldSize, ok := allocs[(*byte)(oldPtr)]; ok {
|
allocsLock.Lock()
|
||||||
oldBuf := unsafe.Slice((*byte)(oldPtr), oldSize)
|
var ok bool
|
||||||
newBuf := unsafe.Slice((*byte)(ptr), size)
|
oldSize, ok = allocs[uintptr(oldPtr)]
|
||||||
copy(newBuf, oldBuf)
|
allocsLock.Unlock()
|
||||||
delete(allocs, (*byte)(oldPtr))
|
if !ok {
|
||||||
} else {
|
|
||||||
runtimeFatal("realloc: invalid pointer")
|
runtimeFatal("realloc: invalid pointer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allocs[(*byte)(ptr)] = size
|
// It's hard to optimize this to expand the current buffer with our GC, but
|
||||||
|
// it is theoretically possible. For now, just always allocate fresh.
|
||||||
|
// TODO: we could skip this if the new allocation is smaller than the old.
|
||||||
|
ptr := allocManual(size)
|
||||||
|
|
||||||
|
allocsLock.Lock()
|
||||||
|
if oldPtr != nil {
|
||||||
|
if currentSize, ok := allocs[uintptr(oldPtr)]; !ok || currentSize != oldSize {
|
||||||
|
allocsLock.Unlock()
|
||||||
|
runtimeFatal("realloc: invalid pointer")
|
||||||
|
}
|
||||||
|
oldBuf := unsafe.Slice((*byte)(oldPtr), oldSize)
|
||||||
|
newBuf := unsafe.Slice((*byte)(ptr), size)
|
||||||
|
copy(newBuf, oldBuf)
|
||||||
|
delete(allocs, uintptr(oldPtr))
|
||||||
|
}
|
||||||
|
allocs[uintptr(ptr)] = size
|
||||||
|
allocsLock.Unlock()
|
||||||
|
if oldPtr != nil {
|
||||||
|
freeManual(oldPtr)
|
||||||
|
}
|
||||||
return ptr
|
return ptr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"internal/gclayout"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
@@ -12,18 +11,20 @@ import (
|
|||||||
func libc_malloc(size uintptr) unsafe.Pointer {
|
func libc_malloc(size uintptr) unsafe.Pointer {
|
||||||
// Note: this zeroes the returned buffer which is not necessary.
|
// Note: this zeroes the returned buffer which is not necessary.
|
||||||
// The same goes for bytealg.MakeNoZero.
|
// The same goes for bytealg.MakeNoZero.
|
||||||
return alloc(size, gclayout.NoPtrs.AsPtr())
|
return allocManual(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export calloc
|
//export calloc
|
||||||
func libc_calloc(nmemb, size uintptr) unsafe.Pointer {
|
func libc_calloc(nmemb, size uintptr) unsafe.Pointer {
|
||||||
// No difference between calloc and malloc.
|
if size != 0 && nmemb > ^uintptr(0)/size {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return libc_malloc(nmemb * size)
|
return libc_malloc(nmemb * size)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export free
|
//export free
|
||||||
func libc_free(ptr unsafe.Pointer) {
|
func libc_free(ptr unsafe.Pointer) {
|
||||||
free(ptr)
|
freeManual(ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export runtime_putchar
|
//export runtime_putchar
|
||||||
|
|||||||
+74
-36
@@ -205,8 +205,9 @@ func (b gcBlock) free() {
|
|||||||
|
|
||||||
// objHeader is a structure appended to every heap object to hold metadata.
|
// objHeader is a structure appended to every heap object to hold metadata.
|
||||||
type objHeader struct {
|
type objHeader struct {
|
||||||
// next is the next object to scan after this.
|
// next links the GC scan list. Manual allocations remain permanently marked
|
||||||
next *objHeader
|
// and use the otherwise invalid value 1 as an until-free marker.
|
||||||
|
next uintptr
|
||||||
|
|
||||||
// layout holds the layout bitmap used to find pointers in the object.
|
// layout holds the layout bitmap used to find pointers in the object.
|
||||||
layout gcLayout
|
layout gcLayout
|
||||||
@@ -482,6 +483,7 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
|
|||||||
// Create the object header.
|
// Create the object header.
|
||||||
size -= unsafe.Sizeof(objHeader{})
|
size -= unsafe.Sizeof(objHeader{})
|
||||||
header := (*objHeader)(unsafe.Add(pointer, size))
|
header := (*objHeader)(unsafe.Add(pointer, size))
|
||||||
|
header.next = 0
|
||||||
header.layout = parseGCLayout(layout)
|
header.layout = parseGCLayout(layout)
|
||||||
|
|
||||||
// We've claimed this allocation, now we can unlock the heap.
|
// We've claimed this allocation, now we can unlock the heap.
|
||||||
@@ -500,42 +502,61 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
|
|||||||
return pointer
|
return pointer
|
||||||
}
|
}
|
||||||
|
|
||||||
func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
|
// allocManual allocates pointer-free memory that remains live until freeManual.
|
||||||
if ptr == nil {
|
func allocManual(size uintptr) unsafe.Pointer {
|
||||||
return alloc(size, gclayout.NoPtrs.AsPtr())
|
if size == 0 {
|
||||||
|
return alloc_zero(size, gclayout.NoPtrs.AsPtr())
|
||||||
}
|
}
|
||||||
|
ptr := alloc(size, gclayout.NoPtrs.AsPtr())
|
||||||
|
|
||||||
// Find the first block of the original allocation.
|
gcLock.Lock()
|
||||||
firstBlock := blockFromAddr(uintptr(ptr))
|
head := blockFromAddr(uintptr(ptr)).findHead()
|
||||||
|
head.setState(blockStateMark)
|
||||||
// Find the last block of the original allocation.
|
header := (*objHeader)(unsafe.Add(head.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{})))
|
||||||
lastBlock := firstBlock.findHead()
|
header.next = 1
|
||||||
|
gcLock.Unlock()
|
||||||
// Calculate the size of the original allocation body.
|
return ptr
|
||||||
oldSize := uintptr(lastBlock-firstBlock)*bytesPerBlock + (bytesPerBlock - unsafe.Sizeof(objHeader{}))
|
|
||||||
|
|
||||||
if size <= oldSize {
|
|
||||||
// The requested size is less than the old size.
|
|
||||||
// There are likely scenarios for this:
|
|
||||||
// - The caller intended to grow the allocation, but the original size
|
|
||||||
// was rounded up by alloc to a multiple of the block size.
|
|
||||||
// The rounded size is already sufficient.
|
|
||||||
// - The caller intended to shrink the allocation.
|
|
||||||
// We currently ignore this case.
|
|
||||||
// Either way, the current allocation can be left alone.
|
|
||||||
return ptr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new allocation and copy the old data.
|
|
||||||
newAlloc := alloc(size, gclayout.NoPtrs.AsPtr())
|
|
||||||
memcpy(newAlloc, ptr, oldSize)
|
|
||||||
free(ptr)
|
|
||||||
|
|
||||||
return newAlloc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func free(ptr unsafe.Pointer) {
|
func free(ptr unsafe.Pointer) {
|
||||||
// TODO: free blocks on request, when the compiler knows they're unused.
|
if ptr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gcLock.Lock()
|
||||||
|
addr := uintptr(ptr)
|
||||||
|
if !isOnHeap(addr) || (addr-heapStart)%bytesPerBlock != 0 {
|
||||||
|
gcLock.Unlock()
|
||||||
|
runtimeFatal("free: invalid pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstBlock := blockFromAddr(addr)
|
||||||
|
state := firstBlock.state()
|
||||||
|
if state != blockStateTail && state != blockStateHead && state != blockStateMark {
|
||||||
|
gcLock.Unlock()
|
||||||
|
runtimeFatal("free: invalid pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
allocationStart := firstBlock
|
||||||
|
for allocationStart != 0 && (allocationStart-1).state() == blockStateTail {
|
||||||
|
allocationStart--
|
||||||
|
}
|
||||||
|
if allocationStart != firstBlock {
|
||||||
|
gcLock.Unlock()
|
||||||
|
runtimeFatal("free: invalid pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
lastBlock := firstBlock.findHead()
|
||||||
|
header := (*objHeader)(unsafe.Add(lastBlock.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{})))
|
||||||
|
if header.next != 1 {
|
||||||
|
gcLock.Unlock()
|
||||||
|
runtimeFatal("free: invalid pointer")
|
||||||
|
}
|
||||||
|
for block := firstBlock; block <= lastBlock; block++ {
|
||||||
|
block.free()
|
||||||
|
}
|
||||||
|
insertFreeRange(firstBlock.pointer(), uintptr(lastBlock-firstBlock+1))
|
||||||
|
gcLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GC performs a garbage collection cycle.
|
// GC performs a garbage collection cycle.
|
||||||
@@ -666,7 +687,7 @@ func finishMark() {
|
|||||||
if obj == nil {
|
if obj == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
scanList = obj.next
|
scanList = (*objHeader)(unsafe.Pointer(obj.next))
|
||||||
|
|
||||||
// Check if the object may contain pointers.
|
// Check if the object may contain pointers.
|
||||||
if obj.layout.pointerFree() {
|
if obj.layout.pointerFree() {
|
||||||
@@ -724,7 +745,7 @@ func markRoot(addr, root uintptr) {
|
|||||||
|
|
||||||
// Add the object to the scan list.
|
// Add the object to the scan list.
|
||||||
header := (*objHeader)(unsafe.Add(head.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{})))
|
header := (*objHeader)(unsafe.Add(head.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{})))
|
||||||
header.next = scanList
|
header.next = uintptr(unsafe.Pointer(scanList))
|
||||||
scanList = header
|
scanList = header
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -758,7 +779,10 @@ func sweep() uintptr {
|
|||||||
|
|
||||||
// Unmark the next head.
|
// Unmark the next head.
|
||||||
block--
|
block--
|
||||||
block.unmark()
|
header := (*objHeader)(unsafe.Add(block.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{})))
|
||||||
|
if header.next != 1 {
|
||||||
|
block.unmark()
|
||||||
|
}
|
||||||
|
|
||||||
// Skip the tail.
|
// Skip the tail.
|
||||||
for block > 0 && (block-1).state() == blockStateTail {
|
for block > 0 && (block-1).state() == blockStateTail {
|
||||||
@@ -903,5 +927,19 @@ func SetFinalizer(obj interface{}, finalizer interface{}) {
|
|||||||
// A nil pointer has nothing to finalize.
|
// A nil pointer has nothing to finalize.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gcLock.Lock()
|
||||||
|
addr := uintptr(objPtr)
|
||||||
|
manual := false
|
||||||
|
if isOnHeap(addr) {
|
||||||
|
head := blockFromAddr(addr).findHead()
|
||||||
|
header := (*objHeader)(unsafe.Add(head.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{})))
|
||||||
|
manual = header.next == 1
|
||||||
|
}
|
||||||
|
gcLock.Unlock()
|
||||||
|
if manual && finalizer != nil {
|
||||||
|
runtimeFatal("runtime.SetFinalizer: manual allocation")
|
||||||
|
}
|
||||||
|
|
||||||
registerFinalizer(uintptr(objPtr), finalizer)
|
registerFinalizer(uintptr(objPtr), finalizer)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,8 +98,28 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
|
|||||||
return ptr
|
return ptr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func allocManual(size uintptr) unsafe.Pointer {
|
||||||
|
if size == 0 {
|
||||||
|
return alloc_zero(size, gclayout.NoPtrs.AsPtr())
|
||||||
|
}
|
||||||
|
|
||||||
|
gcLock.Lock()
|
||||||
|
ptr := libgc_malloc_atomic_uncollectable(size)
|
||||||
|
gcResumeWorld()
|
||||||
|
gcLock.Unlock()
|
||||||
|
if ptr == nil {
|
||||||
|
runtimeFatal("gc: out of memory")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
memzero(ptr, size)
|
||||||
|
return ptr
|
||||||
|
}
|
||||||
|
|
||||||
func free(ptr unsafe.Pointer) {
|
func free(ptr unsafe.Pointer) {
|
||||||
|
gcLock.Lock()
|
||||||
libgc_free(ptr)
|
libgc_free(ptr)
|
||||||
|
gcResumeWorld()
|
||||||
|
gcLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GC() {
|
func GC() {
|
||||||
@@ -153,6 +173,9 @@ func libgc_malloc(uintptr) unsafe.Pointer
|
|||||||
//export GC_malloc_atomic
|
//export GC_malloc_atomic
|
||||||
func libgc_malloc_atomic(uintptr) unsafe.Pointer
|
func libgc_malloc_atomic(uintptr) unsafe.Pointer
|
||||||
|
|
||||||
|
//export GC_malloc_atomic_uncollectable
|
||||||
|
func libgc_malloc_atomic_uncollectable(uintptr) unsafe.Pointer
|
||||||
|
|
||||||
//export GC_free
|
//export GC_free
|
||||||
func libgc_free(unsafe.Pointer)
|
func libgc_free(unsafe.Pointer)
|
||||||
|
|
||||||
|
|||||||
@@ -23,14 +23,7 @@ package runtime
|
|||||||
// - func SetFinalizer(obj interface{}, finalizer interface{})
|
// - func SetFinalizer(obj interface{}, finalizer interface{})
|
||||||
// - func ReadMemStats(ms *runtime.MemStats)
|
// - func ReadMemStats(ms *runtime.MemStats)
|
||||||
//
|
//
|
||||||
//
|
// The compiler provides the global root ranges used by markRoots.
|
||||||
// In addition, if targeting wasi, the following functions should be exported for interoperability
|
|
||||||
// with wasi libraries that use them. Note, this requires the export directive, not go:linkname.
|
|
||||||
//
|
|
||||||
// - func malloc(size uintptr) unsafe.Pointer
|
|
||||||
// - func free(ptr unsafe.Pointer)
|
|
||||||
// - func calloc(nmemb, size uintptr) unsafe.Pointer
|
|
||||||
// - func realloc(oldPtr unsafe.Pointer, size uintptr) unsafe.Pointer
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package runtime
|
|||||||
// may be the only memory allocator possible.
|
// may be the only memory allocator possible.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"internal/gclayout"
|
|
||||||
"internal/task"
|
"internal/task"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
@@ -69,18 +68,6 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
|
|||||||
return pointer
|
return pointer
|
||||||
}
|
}
|
||||||
|
|
||||||
func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
|
|
||||||
newAlloc := alloc(size, gclayout.NoPtrs.AsPtr())
|
|
||||||
if ptr == nil {
|
|
||||||
return newAlloc
|
|
||||||
}
|
|
||||||
// according to POSIX everything beyond the previous pointer's
|
|
||||||
// size will have indeterminate values so we can just copy garbage
|
|
||||||
memcpy(newAlloc, ptr, size)
|
|
||||||
|
|
||||||
return newAlloc
|
|
||||||
}
|
|
||||||
|
|
||||||
func free(ptr unsafe.Pointer) {
|
func free(ptr unsafe.Pointer) {
|
||||||
// Memory is never freed.
|
// Memory is never freed.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !gc.custom
|
||||||
|
|
||||||
|
package runtime
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
func freeManual(ptr unsafe.Pointer) {
|
||||||
|
if ptr != nil && ptr != unsafe.Pointer(zeroSizeAllocPtr) {
|
||||||
|
free(ptr)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//go:build gc.custom
|
||||||
|
|
||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"internal/gclayout"
|
||||||
|
"internal/task"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Custom collectors retain manual allocations through ordinary typed roots so
|
||||||
|
// the custom GC interface does not need an additional allocation primitive.
|
||||||
|
var manualAllocs = make(map[*byte]struct{})
|
||||||
|
var manualAllocsLock task.PMutex
|
||||||
|
|
||||||
|
func allocManual(size uintptr) unsafe.Pointer {
|
||||||
|
if size == 0 {
|
||||||
|
return alloc_zero(size, gclayout.NoPtrs.AsPtr())
|
||||||
|
}
|
||||||
|
ptr := alloc(size, gclayout.NoPtrs.AsPtr())
|
||||||
|
manualAllocsLock.Lock()
|
||||||
|
manualAllocs[(*byte)(ptr)] = struct{}{}
|
||||||
|
manualAllocsLock.Unlock()
|
||||||
|
return ptr
|
||||||
|
}
|
||||||
|
|
||||||
|
func freeManual(ptr unsafe.Pointer) {
|
||||||
|
if ptr == nil || ptr == unsafe.Pointer(zeroSizeAllocPtr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manualAllocsLock.Lock()
|
||||||
|
delete(manualAllocs, (*byte)(ptr))
|
||||||
|
manualAllocsLock.Unlock()
|
||||||
|
free(ptr)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build gc.leaking || gc.none
|
||||||
|
|
||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"internal/gclayout"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func allocManual(size uintptr) unsafe.Pointer {
|
||||||
|
if size == 0 {
|
||||||
|
return alloc_zero(size, gclayout.NoPtrs.AsPtr())
|
||||||
|
}
|
||||||
|
return alloc(size, gclayout.NoPtrs.AsPtr())
|
||||||
|
}
|
||||||
@@ -22,8 +22,6 @@ func scanCurrentStack() {}
|
|||||||
|
|
||||||
func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer
|
func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer
|
||||||
|
|
||||||
func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer
|
|
||||||
|
|
||||||
func free(ptr unsafe.Pointer) {
|
func free(ptr unsafe.Pointer) {
|
||||||
// Nothing to free when nothing gets allocated.
|
// Nothing to free when nothing gets allocated.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,22 @@ func os_runtime_args() []string {
|
|||||||
|
|
||||||
//export cabi_realloc
|
//export cabi_realloc
|
||||||
func cabi_realloc(ptr, oldsize, align, newsize unsafe.Pointer) unsafe.Pointer {
|
func cabi_realloc(ptr, oldsize, align, newsize unsafe.Pointer) unsafe.Pointer {
|
||||||
return realloc(ptr, uintptr(newsize))
|
size := uintptr(newsize)
|
||||||
|
if size == 0 {
|
||||||
|
freeManual(ptr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newPtr := allocManual(size)
|
||||||
|
if ptr != nil {
|
||||||
|
copySize := uintptr(oldsize)
|
||||||
|
if copySize > size {
|
||||||
|
copySize = size
|
||||||
|
}
|
||||||
|
memcpy(newPtr, ptr, copySize)
|
||||||
|
freeManual(ptr)
|
||||||
|
}
|
||||||
|
return newPtr
|
||||||
}
|
}
|
||||||
|
|
||||||
func ticksToNanoseconds(ticks timeUnit) int64 {
|
func ticksToNanoseconds(ticks timeUnit) int64 {
|
||||||
|
|||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
int reallocPreservesContents(void) {
|
||||||
|
int *ptr = malloc(sizeof(int));
|
||||||
|
*ptr = 42;
|
||||||
|
ptr = realloc(ptr, 2 * sizeof(int));
|
||||||
|
int value = ptr[0];
|
||||||
|
free(ptr);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
int reallocPreservesContents(void);
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
println("realloc preserves contents:", C.reallocPreservesContents())
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
realloc preserves contents: 42
|
||||||
Vendored
+96
@@ -1,6 +1,7 @@
|
|||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
int global = 3;
|
int global = 3;
|
||||||
bool globalBool = 1;
|
bool globalBool = 1;
|
||||||
@@ -82,3 +83,98 @@ int set_errno(int err) {
|
|||||||
errno = err;
|
errno = err;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct malloc_node {
|
||||||
|
struct malloc_node *next;
|
||||||
|
int value;
|
||||||
|
} malloc_node;
|
||||||
|
|
||||||
|
void *makeMallocChain(void) {
|
||||||
|
malloc_node *tail = malloc(sizeof(malloc_node));
|
||||||
|
tail->next = NULL;
|
||||||
|
tail->value = 42;
|
||||||
|
|
||||||
|
malloc_node *head = malloc(sizeof(malloc_node));
|
||||||
|
head->next = tail;
|
||||||
|
head->value = 1;
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clobberMalloc(void) {
|
||||||
|
#if defined(__AVR__)
|
||||||
|
return;
|
||||||
|
#else
|
||||||
|
malloc_node *nodes[64];
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
nodes[i] = malloc(sizeof(malloc_node));
|
||||||
|
nodes[i]->next = NULL;
|
||||||
|
nodes[i]->value = 0;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
free(nodes[i]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int mallocChainValue(void *ptr) {
|
||||||
|
return ((malloc_node *)ptr)->next->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define MALLOC_HIDE_MASK ((uintptr_t)0x5a5a5a5a)
|
||||||
|
|
||||||
|
__attribute__((noinline)) uintptr_t makeHiddenMalloc(void) {
|
||||||
|
malloc_node *node = malloc(sizeof(malloc_node));
|
||||||
|
node->next = NULL;
|
||||||
|
node->value = 84;
|
||||||
|
return (uintptr_t)node ^ MALLOC_HIDE_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int hiddenMallocValue(uintptr_t hidden) {
|
||||||
|
malloc_node *node = (malloc_node *)(hidden ^ MALLOC_HIDE_MASK);
|
||||||
|
return node->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void freeHiddenMalloc(uintptr_t hidden) {
|
||||||
|
free((void *)(hidden ^ MALLOC_HIDE_MASK));
|
||||||
|
}
|
||||||
|
|
||||||
|
void mallocFreeStress(void) {
|
||||||
|
#if defined(__AVR__)
|
||||||
|
const int count = 32;
|
||||||
|
#else
|
||||||
|
const int count = 1024;
|
||||||
|
#endif
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
char *ptr = malloc(1024);
|
||||||
|
ptr[0] = (char)i;
|
||||||
|
free(ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mallocZero(void) {
|
||||||
|
free(malloc(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((noinline)) void *callCalloc(size_t nmemb, size_t size) {
|
||||||
|
return calloc(nmemb, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
int callocOverflowReturnsNull(void) {
|
||||||
|
#if defined(__linux__) || defined(_WIN32) || defined(__APPLE__)
|
||||||
|
return 1;
|
||||||
|
#else
|
||||||
|
volatile size_t nmemb = (size_t)-1;
|
||||||
|
return callCalloc(nmemb, 2) == NULL;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((noinline)) void clobberStack(void) {
|
||||||
|
#if defined(__AVR__)
|
||||||
|
return;
|
||||||
|
#else
|
||||||
|
volatile uintptr_t values[128];
|
||||||
|
for (int i = 0; i < 128; i++) {
|
||||||
|
values[i] = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+30
@@ -19,6 +19,7 @@ import "C"
|
|||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"runtime"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
@@ -171,6 +172,35 @@ func main() {
|
|||||||
println("len(C.GoBytes(C.CBytes(nil),0)):", len(C.GoBytes(C.CBytes(nil), 0)))
|
println("len(C.GoBytes(C.CBytes(nil),0)):", len(C.GoBytes(C.CBytes(nil), 0)))
|
||||||
println(`rountrip CBytes:`, C.GoString((*C.char)(C.CBytes([]byte("hello\000")))))
|
println(`rountrip CBytes:`, C.GoString((*C.char)(C.CBytes([]byte("hello\000")))))
|
||||||
|
|
||||||
|
// malloc allocations remain live until free, even when C pointers are the
|
||||||
|
// only links between them.
|
||||||
|
mallocChain := C.makeMallocChain()
|
||||||
|
runtime.GC()
|
||||||
|
C.clobberMalloc()
|
||||||
|
println("malloc chain:", C.mallocChainValue(mallocChain))
|
||||||
|
|
||||||
|
// malloc lifetime ends at free, not when the allocation becomes invisible
|
||||||
|
// to the GC. Encode the address so neither Go nor C exposes a pointer root.
|
||||||
|
hiddenMallocChan := make(chan C.uintptr_t, 1)
|
||||||
|
hiddenMallocDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
hiddenMallocChan <- C.makeHiddenMalloc()
|
||||||
|
close(hiddenMallocDone)
|
||||||
|
}()
|
||||||
|
hiddenMalloc := <-hiddenMallocChan
|
||||||
|
<-hiddenMallocDone
|
||||||
|
C.clobberStack()
|
||||||
|
runtime.GC()
|
||||||
|
C.clobberMalloc()
|
||||||
|
println("hidden malloc:", C.hiddenMallocValue(hiddenMalloc))
|
||||||
|
C.freeHiddenMalloc(hiddenMalloc)
|
||||||
|
|
||||||
|
C.mallocFreeStress()
|
||||||
|
println("malloc/free stress: ok")
|
||||||
|
C.mallocZero()
|
||||||
|
println("malloc zero: ok")
|
||||||
|
println("calloc overflow:", C.callocOverflowReturnsNull() != 0)
|
||||||
|
|
||||||
// Check that errno is returned from the second return value, and that it
|
// Check that errno is returned from the second return value, and that it
|
||||||
// matches the errno value that was just set.
|
// matches the errno value that was just set.
|
||||||
_, errno := C.set_errno(C.EINVAL)
|
_, errno := C.set_errno(C.EINVAL)
|
||||||
|
|||||||
Vendored
+13
@@ -1,4 +1,5 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
||||||
@@ -157,3 +158,15 @@ double doSqrt(double);
|
|||||||
void printf_single_int(char *format, int arg);
|
void printf_single_int(char *format, int arg);
|
||||||
|
|
||||||
int set_errno(int err);
|
int set_errno(int err);
|
||||||
|
|
||||||
|
void *makeMallocChain(void);
|
||||||
|
void clobberMalloc(void);
|
||||||
|
int mallocChainValue(void *ptr);
|
||||||
|
uintptr_t makeHiddenMalloc(void);
|
||||||
|
int hiddenMallocValue(uintptr_t hidden);
|
||||||
|
void freeHiddenMalloc(uintptr_t hidden);
|
||||||
|
void mallocFreeStress(void);
|
||||||
|
void mallocZero(void);
|
||||||
|
void *callCalloc(size_t nmemb, size_t size);
|
||||||
|
int callocOverflowReturnsNull(void);
|
||||||
|
void clobberStack(void);
|
||||||
|
|||||||
Vendored
+5
@@ -75,6 +75,11 @@ len(C.GoStringN(nil, 0)): 0
|
|||||||
len(C.GoBytes(nil, 0)): 0
|
len(C.GoBytes(nil, 0)): 0
|
||||||
len(C.GoBytes(C.CBytes(nil),0)): 0
|
len(C.GoBytes(C.CBytes(nil),0)): 0
|
||||||
rountrip CBytes: hello
|
rountrip CBytes: hello
|
||||||
|
malloc chain: 42
|
||||||
|
hidden malloc: 84
|
||||||
|
malloc/free stress: ok
|
||||||
|
malloc zero: ok
|
||||||
|
calloc overflow: true
|
||||||
EINVAL: true
|
EINVAL: true
|
||||||
EAGAIN: true
|
EAGAIN: true
|
||||||
copied string: foobar
|
copied string: foobar
|
||||||
|
|||||||
Reference in New Issue
Block a user