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:
Jake Bailey
2026-08-10 16:32:45 -07:00
committed by Ron Evans
parent 59fb104004
commit 02021b5853
23 changed files with 413 additions and 99 deletions
+10
View File
@@ -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;
}
+10
View File
@@ -0,0 +1,10 @@
package main
/*
int reallocPreservesContents(void);
*/
import "C"
func main() {
println("realloc preserves contents:", C.reallocPreservesContents())
}
+1
View File
@@ -0,0 +1 @@
realloc preserves contents: 42
+96
View File
@@ -1,6 +1,7 @@
#include <math.h>
#include "main.h"
#include <stdio.h>
#include <stdlib.h>
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
}
+30
View File
@@ -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)
+13
View File
@@ -1,4 +1,5 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <errno.h>
@@ -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);
+5
View File
@@ -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