diff --git a/CHANGELOG.md b/CHANGELOG.md index b629707e6..667d579f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ - gc: correct the old size calculation in block realloc - gc: correct the leaking allocator bounds and overflow checks - 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 - rp2040: fix -gc=leaking and -gc=none - rp2: handle the RP2350 shared FIFO IRQ for GC (#5482) diff --git a/builder/bdwgc.go b/builder/bdwgc.go index b03b15420..508e9470b 100644 --- a/builder/bdwgc.go +++ b/builder/bdwgc.go @@ -30,10 +30,11 @@ var BoehmGC = Library{ // Use a minimal environment. "-DNO_MSGBOX_ON_ERROR", // don't call MessageBoxA on Windows "-DDONT_USE_ATEXIT", - "-DNO_GETENV", // smaller binary, more predictable configuration - "-DNO_CLOCK", // don't use system clock - "-DNO_DEBUGGING", // reduce code size - "-DGC_NO_FINALIZATION", // finalization is not used at the moment + "-DNO_GETENV", // smaller binary, more predictable configuration + "-DNO_CLOCK", // don't use system clock + "-DNO_DEBUGGING", // reduce code size + "-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. // TODO: try to fix this in LLVM/lld directly so we don't have to diff --git a/builder/testdata/binary-size.txt b/builder/testdata/binary-size.txt index 3a3cd1cc0..8dada1b98 100644 --- a/builder/testdata/binary-size.txt +++ b/builder/testdata/binary-size.txt @@ -1,4 +1,4 @@ target package code rodata data bss -hifive1b examples/echo 4405 323 0 2268 -microbit examples/serial 2922 382 8 2264 -wioterminal examples/pininterrupt 8251 1717 148 7496 +hifive1b examples/echo 4533 323 0 2268 +microbit examples/serial 3002 382 8 2264 +wioterminal examples/pininterrupt 8331 1717 148 7496 diff --git a/compileopts/config.go b/compileopts/config.go index 777cb782e..dc59f9686 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -24,7 +24,7 @@ import ( // library path in advance in several places). var libVersions = map[string]int{ "musl": 3, - "bdwgc": 2, + "bdwgc": 3, "picolibc": 2, "wasmbuiltins": 1, } diff --git a/main_test.go b/main_test.go index 02c7da519..c8208827f 100644 --- a/main_test.go +++ b/main_test.go @@ -114,7 +114,13 @@ func TestBuild(t *testing.T) { // This makes it possible to run one specific test (instead of all), // which is especially useful to quickly check whether some changes // 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 } @@ -223,7 +229,11 @@ func TestBuild(t *testing.T) { }) t.Run("WASIp1", func(t *testing.T) { 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. t.Run("gc.go-boehm", func(t *testing.T) { diff --git a/src/runtime/arch_tinygowasm_malloc.go b/src/runtime/arch_tinygowasm_malloc.go index 694840af9..1877aa575 100644 --- a/src/runtime/arch_tinygowasm_malloc.go +++ b/src/runtime/arch_tinygowasm_malloc.go @@ -3,7 +3,7 @@ package runtime import ( - "internal/gclayout" + "internal/task" "unsafe" ) @@ -11,21 +11,21 @@ import ( // code linked from other languages can allocate memory without colliding with // our GC allocations. -// Map of allocations, where the key is the allocated pointer and the value is -// the size of the allocation. -// TODO: make this a map[unsafe.Pointer]uintptr, since that results in slightly -// smaller binaries. But for that to work, unsafe.Pointer needs to be seen as a -// binary key (which it is not at the moment). -// See https://github.com/tinygo-org/tinygo/pull/4898 for details. -var allocs = make(map[*byte]uintptr) +// Map of allocations, where the key is the allocation address and the value is +// its size. Integer keys intentionally do not act as GC roots: manual +// allocations are retained by the allocator until free. +var allocs = make(map[uintptr]uintptr) +var allocsLock task.PMutex //export malloc func libc_malloc(size uintptr) unsafe.Pointer { if size == 0 { return nil } - ptr := alloc(size, gclayout.NoPtrs.AsPtr()) - allocs[(*byte)(ptr)] = size + ptr := allocManual(size) + allocsLock.Lock() + allocs[uintptr(ptr)] = size + allocsLock.Unlock() return ptr } @@ -34,16 +34,22 @@ func libc_free(ptr unsafe.Pointer) { if ptr == nil { return } - if _, ok := allocs[(*byte)(ptr)]; ok { - delete(allocs, (*byte)(ptr)) + allocsLock.Lock() + if _, ok := allocs[uintptr(ptr)]; ok { + delete(allocs, uintptr(ptr)) + allocsLock.Unlock() + freeManual(ptr) } else { + allocsLock.Unlock() runtimeFatal("free: invalid pointer") } } //export calloc 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) } @@ -54,22 +60,37 @@ func libc_realloc(oldPtr unsafe.Pointer, size uintptr) unsafe.Pointer { return nil } - // 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 := alloc(size, gclayout.NoPtrs.AsPtr()) - + var oldSize uintptr if oldPtr != nil { - if oldSize, ok := allocs[(*byte)(oldPtr)]; ok { - oldBuf := unsafe.Slice((*byte)(oldPtr), oldSize) - newBuf := unsafe.Slice((*byte)(ptr), size) - copy(newBuf, oldBuf) - delete(allocs, (*byte)(oldPtr)) - } else { + allocsLock.Lock() + var ok bool + oldSize, ok = allocs[uintptr(oldPtr)] + allocsLock.Unlock() + if !ok { 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 } diff --git a/src/runtime/baremetal.go b/src/runtime/baremetal.go index 4893297fe..d2963a496 100644 --- a/src/runtime/baremetal.go +++ b/src/runtime/baremetal.go @@ -3,7 +3,6 @@ package runtime import ( - "internal/gclayout" "sync/atomic" "unsafe" ) @@ -12,18 +11,20 @@ import ( func libc_malloc(size uintptr) unsafe.Pointer { // Note: this zeroes the returned buffer which is not necessary. // The same goes for bytealg.MakeNoZero. - return alloc(size, gclayout.NoPtrs.AsPtr()) + return allocManual(size) } //export calloc 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) } //export free func libc_free(ptr unsafe.Pointer) { - free(ptr) + freeManual(ptr) } //export runtime_putchar diff --git a/src/runtime/gc_blocks.go b/src/runtime/gc_blocks.go index c27401d62..72f1dd041 100644 --- a/src/runtime/gc_blocks.go +++ b/src/runtime/gc_blocks.go @@ -205,8 +205,9 @@ func (b gcBlock) free() { // objHeader is a structure appended to every heap object to hold metadata. type objHeader struct { - // next is the next object to scan after this. - next *objHeader + // next links the GC scan list. Manual allocations remain permanently marked + // 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 gcLayout @@ -482,6 +483,7 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer { // Create the object header. size -= unsafe.Sizeof(objHeader{}) header := (*objHeader)(unsafe.Add(pointer, size)) + header.next = 0 header.layout = parseGCLayout(layout) // 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 } -func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { - if ptr == nil { - return alloc(size, gclayout.NoPtrs.AsPtr()) +// allocManual allocates pointer-free memory that remains live until freeManual. +func allocManual(size uintptr) unsafe.Pointer { + if size == 0 { + return alloc_zero(size, gclayout.NoPtrs.AsPtr()) } + ptr := alloc(size, gclayout.NoPtrs.AsPtr()) - // Find the first block of the original allocation. - firstBlock := blockFromAddr(uintptr(ptr)) - - // Find the last block of the original allocation. - lastBlock := firstBlock.findHead() - - // Calculate the size of the original allocation body. - 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 + gcLock.Lock() + head := blockFromAddr(uintptr(ptr)).findHead() + head.setState(blockStateMark) + header := (*objHeader)(unsafe.Add(head.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{}))) + header.next = 1 + gcLock.Unlock() + return ptr } 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. @@ -666,7 +687,7 @@ func finishMark() { if obj == nil { return } - scanList = obj.next + scanList = (*objHeader)(unsafe.Pointer(obj.next)) // Check if the object may contain pointers. if obj.layout.pointerFree() { @@ -724,7 +745,7 @@ func markRoot(addr, root uintptr) { // Add the object to the scan list. header := (*objHeader)(unsafe.Add(head.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{}))) - header.next = scanList + header.next = uintptr(unsafe.Pointer(scanList)) scanList = header } @@ -758,7 +779,10 @@ func sweep() uintptr { // Unmark the next head. block-- - block.unmark() + header := (*objHeader)(unsafe.Add(block.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{}))) + if header.next != 1 { + block.unmark() + } // Skip the tail. for block > 0 && (block-1).state() == blockStateTail { @@ -903,5 +927,19 @@ func SetFinalizer(obj interface{}, finalizer interface{}) { // A nil pointer has nothing to finalize. 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) } diff --git a/src/runtime/gc_boehm.go b/src/runtime/gc_boehm.go index 02ee0aaa9..c4f4fc728 100644 --- a/src/runtime/gc_boehm.go +++ b/src/runtime/gc_boehm.go @@ -98,8 +98,28 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer { 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) { + gcLock.Lock() libgc_free(ptr) + gcResumeWorld() + gcLock.Unlock() } func GC() { @@ -153,6 +173,9 @@ func libgc_malloc(uintptr) unsafe.Pointer //export GC_malloc_atomic func libgc_malloc_atomic(uintptr) unsafe.Pointer +//export GC_malloc_atomic_uncollectable +func libgc_malloc_atomic_uncollectable(uintptr) unsafe.Pointer + //export GC_free func libgc_free(unsafe.Pointer) diff --git a/src/runtime/gc_custom.go b/src/runtime/gc_custom.go index 12f1b2e12..86224bbbd 100644 --- a/src/runtime/gc_custom.go +++ b/src/runtime/gc_custom.go @@ -23,14 +23,7 @@ package runtime // - func SetFinalizer(obj interface{}, finalizer interface{}) // - func ReadMemStats(ms *runtime.MemStats) // -// -// 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 +// The compiler provides the global root ranges used by markRoots. import ( "unsafe" diff --git a/src/runtime/gc_leaking.go b/src/runtime/gc_leaking.go index 3ebee0989..ce19a54b5 100644 --- a/src/runtime/gc_leaking.go +++ b/src/runtime/gc_leaking.go @@ -7,7 +7,6 @@ package runtime // may be the only memory allocator possible. import ( - "internal/gclayout" "internal/task" "sync/atomic" "unsafe" @@ -69,18 +68,6 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.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) { // Memory is never freed. } diff --git a/src/runtime/gc_manual.go b/src/runtime/gc_manual.go new file mode 100644 index 000000000..25f7adc54 --- /dev/null +++ b/src/runtime/gc_manual.go @@ -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) + } +} diff --git a/src/runtime/gc_manual_custom.go b/src/runtime/gc_manual_custom.go new file mode 100644 index 000000000..4f6adc648 --- /dev/null +++ b/src/runtime/gc_manual_custom.go @@ -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) +} diff --git a/src/runtime/gc_manual_leaking.go b/src/runtime/gc_manual_leaking.go new file mode 100644 index 000000000..581358738 --- /dev/null +++ b/src/runtime/gc_manual_leaking.go @@ -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()) +} diff --git a/src/runtime/gc_none.go b/src/runtime/gc_none.go index ce9649c71..8634308d9 100644 --- a/src/runtime/gc_none.go +++ b/src/runtime/gc_none.go @@ -22,8 +22,6 @@ func scanCurrentStack() {} func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer -func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer - func free(ptr unsafe.Pointer) { // Nothing to free when nothing gets allocated. } diff --git a/src/runtime/runtime_wasip2.go b/src/runtime/runtime_wasip2.go index 46ce3d853..5dfe4de62 100644 --- a/src/runtime/runtime_wasip2.go +++ b/src/runtime/runtime_wasip2.go @@ -31,7 +31,22 @@ func os_runtime_args() []string { //export cabi_realloc 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 { diff --git a/testdata/cgo-realloc/main.c b/testdata/cgo-realloc/main.c new file mode 100644 index 000000000..cfa3d6510 --- /dev/null +++ b/testdata/cgo-realloc/main.c @@ -0,0 +1,10 @@ +#include + +int reallocPreservesContents(void) { + int *ptr = malloc(sizeof(int)); + *ptr = 42; + ptr = realloc(ptr, 2 * sizeof(int)); + int value = ptr[0]; + free(ptr); + return value; +} diff --git a/testdata/cgo-realloc/main.go b/testdata/cgo-realloc/main.go new file mode 100644 index 000000000..a4896a61e --- /dev/null +++ b/testdata/cgo-realloc/main.go @@ -0,0 +1,10 @@ +package main + +/* +int reallocPreservesContents(void); +*/ +import "C" + +func main() { + println("realloc preserves contents:", C.reallocPreservesContents()) +} diff --git a/testdata/cgo-realloc/out.txt b/testdata/cgo-realloc/out.txt new file mode 100644 index 000000000..2f8914ef9 --- /dev/null +++ b/testdata/cgo-realloc/out.txt @@ -0,0 +1 @@ +realloc preserves contents: 42 diff --git a/testdata/cgo/main.c b/testdata/cgo/main.c index 94b338dda..82718eb6f 100644 --- a/testdata/cgo/main.c +++ b/testdata/cgo/main.c @@ -1,6 +1,7 @@ #include #include "main.h" #include +#include int global = 3; bool globalBool = 1; @@ -82,3 +83,98 @@ int set_errno(int err) { errno = err; 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 +} diff --git a/testdata/cgo/main.go b/testdata/cgo/main.go index 38d11386a..55992d81f 100644 --- a/testdata/cgo/main.go +++ b/testdata/cgo/main.go @@ -19,6 +19,7 @@ import "C" import "C" import ( + "runtime" "syscall" "unsafe" ) @@ -171,6 +172,35 @@ func main() { 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"))))) + // 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 // matches the errno value that was just set. _, errno := C.set_errno(C.EINVAL) diff --git a/testdata/cgo/main.h b/testdata/cgo/main.h index 3942497f2..0ae9e5fbc 100644 --- a/testdata/cgo/main.h +++ b/testdata/cgo/main.h @@ -1,4 +1,5 @@ #include +#include #include #include @@ -157,3 +158,15 @@ double doSqrt(double); void printf_single_int(char *format, int arg); 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); diff --git a/testdata/cgo/out.txt b/testdata/cgo/out.txt index 1d63f5e82..bd70fae2c 100644 --- a/testdata/cgo/out.txt +++ b/testdata/cgo/out.txt @@ -75,6 +75,11 @@ len(C.GoStringN(nil, 0)): 0 len(C.GoBytes(nil, 0)): 0 len(C.GoBytes(C.CBytes(nil),0)): 0 rountrip CBytes: hello +malloc chain: 42 +hidden malloc: 84 +malloc/free stress: ok +malloc zero: ok +calloc overflow: true EINVAL: true EAGAIN: true copied string: foobar