Search docsSearch this language
Limits and support
Release candidatev0.20260809.0-rc.5

These docs describe the current coordinated release candidate.

Limits and support

Stack escape audit

Understand C2Go's stack-pointer escape limitation and make the standalone LTO audit a release gate.

Release-blocking limitation: C2Go does not perform Go-style escape promotion for C locals. A C stack allocation remains on the goroutine stack even when its address is saved in heap or global state. That address can dangle after the function returns or the goroutine stack moves.

This is the most important current memory-safety constraint. Treat the checks below as mandatory for code that will be released.

The unsafe shape

The store may be direct or hidden behind another function:

#include <c2go.h>

struct managed Box {
    void *slot;
};

static void retain(void **slot, void *value) {
    *slot = value;
}

c2go_extern void save_local(struct Box *box) {
    int local = 7;
    retain(&box->slot, &local); // unsafe: local does not move to the heap
}

&local is not limited to one call frame. The pointer may be passed and used along a synchronous call chain while the automatic object is alive and no callee retains it; that cross-frame use is not itself an escape.

Escape means returning the pointer or handing it to state that may retain it independently of the current synchronous call chain, such as asynchronous work, the native heap, a Go-managed heap object, or a mutable global. Such state may keep the old address after the object dies or the goroutine stack moves.

Use both checks

The frontend’s default-on -Wc2go-managed-stack-escape diagnostic catches direct assignments such as box->slot = &local. Make direct cases fatal in maintained builds:

-Werror=c2go-managed-stack-escape

That syntactic check cannot follow every value through helper calls or across translation units. Build an audit-only bitcode set for all translation units, then run standalone c2go-lto without --c2go-escape-nonfatal:

mkdir -p .c2go-audit

c2go-clang --target="$C2GO_TARGET" \
  -fc2go -fc2go-package="$C2GO_PACKAGE" \
  -std=c2go23 -O0 -gline-tables-only \
  -c src/parser.c -o .c2go-audit/parser.o

c2go-clang --target="$C2GO_TARGET" \
  -fc2go -fc2go-package="$C2GO_PACKAGE" \
  -std=c2go23 -O0 -gline-tables-only \
  -c src/storage.c -o .c2go-audit/storage.o

c2go-lto .c2go-audit/parser.o .c2go-audit/storage.o

These .o files are LLVM bitcode because -fc2go -c stops at the C2Go pre-link boundary. -O0 preserves source shapes for this separate audit build, while -gline-tables-only gives the audit useful file and line locations. Generate the release assembly and manifest with your normal optimized command after the audit passes.

Expected clean output:

c2go-lto: 0 stack-address escape point(s)

An escape prints c2go-lto: stack->heap in <function> at <location> and exits with status 1. Status 2 is a tool/input error and status 3 is a cross-translation-unit attribute conflict; neither is a clean audit.

The points-to analysis is deliberately conservative. A store through a pointer parameter can be reported when the audit cannot prove that every caller supplies stack-local storage. Trace the reported value and destination through all call sites; --c2go-andersen-field-sensitive can reduce some field-insensitive noise. Do not turn on nonfatal mode merely to hide an unresolved report—refactor the lifetime or the boundary until the release gate is clean.

The ordinary driver is not the gate

In this release, the normal c2go-clang assembly/manifest pipeline invokes c2go-lto with --c2go-escape-nonfatal and suppresses the escape report. A successful ordinary compile therefore does not prove that the program is free of stack-to-heap escapes.

Do not use --c2go-escape-nonfatal in a release audit. --c2go-escape-dedup=false can show every store while investigating a report, and --c2go-andersen-field-sensitive can reduce field-insensitive noise at additional analysis cost.

Rewrite retained data onto the heap

Allocate data that must outlive the frame with the correct Go-heap type information:

#include <c2go.h>
#include <stddef.h>

struct managed Value {
    int number;
};

struct managed Box {
    struct Value *slot;
};

c2go_extern void save_value(struct Box *box) {
    struct Value *value = gc_malloc(
        c2go_typeinfo(struct Value),
        sizeof(*value)
    );
    if (value == NULL) {
        return;
    }
    value->number = 7;
    box->slot = value;
}

If native code owns the retained object, use the allocator required by that native API and define who releases it. A stable integer handle is often safer than exposing a managed pointer to a native owner.

What the audit does not prove

c2go-lto uses a whole-program Andersen-style points-to audit and is much stronger than the frontend’s direct-pattern warning, but it is still an engineering safety check rather than a formal lifetime proof. Opaque native calls, incorrect inline assembly constraints, or code omitted from the bitcode set can hide behavior. Keep the input set complete, review every native retention boundary, and add end-to-end Go tests.

Use Troubleshooting when a compiler, LTO, binder, or runtime failure needs to be isolated by stage.