Summary
Passing a bare named function to a ptr-typed parameter (or assigning it into a ptr struct field) does not box it into an _AeClosure. A later unbox_closure(...) + call(...) on that pointer then reads a .env field off a raw code address and segfaults at runtime. The compiler emits no diagnostic — it builds clean.
This is a soundness gap: unbox_closure is only valid on a value that was actually boxed (which happens automatically only when the value crosses an fn-typed boundary), but nothing prevents feeding it an unboxed bare-fn pointer.
Repro (segfaults)
struct Holder { cb: ptr }
my_handler(x: int) -> int { return x + 1 }
// ptr-typed param: bare fn is passed as a raw code pointer, NOT boxed
store_ptr(h: *Holder, f: ptr) { h.cb = f }
main() {
h = heap.new(Holder)
store_ptr(h, my_handler) // no box happens here
g = unbox_closure(h.cb) // reads .env off a code address
r = call(g, 41) // -> signal 11
print("result=${r}\n")
}
$ ae run repro.ae
Program crashed (signal 11: segmentation fault)
ae build produces no warning or error.
Working counterpart (correct)
Changing only the parameter type from ptr to fn makes the compiler autobox the bare function at the call site, and the same unbox_closure/call works:
store_fn(h: *Holder, f: fn) { h.cb = f } // fn param -> autobox
...
store_fn(h, my_handler) // boxed here
g = unbox_closure(h.cb)
r = call(g, 41) // => 42
$ ae run safe.ae
result=42
So the boxing is keyed entirely on the static type of the parameter/slot the value flows through (fn boxes, ptr does not), and unbox_closure trusts its input unconditionally.
Why it matters
This is a natural pattern when storing heterogeneous handlers in ptr fields of lists/maps (routers, dispatch tables, callback registries — it surfaced while building a trailing-block HTTP router DSL). The ptr field is often necessary for storage in generic collections, so the developer stores a bare fn into ptr, later unbox_closurees it, and gets a silent segfault with no hint that the missing box is the cause.
Suggested fix (any one, roughly increasing effort)
- Diagnose the sink: have
unbox_closure(p) require that p's provenance is a boxed closure; error at compile time when a value known to be an unboxed bare-fn pointer reaches it.
- Diagnose the source: warn when a bare named function is coerced into a
ptr slot without an intervening box (the point where the .env is silently dropped), suggesting an fn-typed param or an explicit box_closure(...).
- Make it safe by construction: autobox a bare fn whenever it is stored into a
ptr slot that is later unboxed — i.e. box at the fn→ptr coercion the same way fn→fn does. (Costs an allocation even when not needed; (1)/(2) are probably preferable.)
At minimum, the silent-segfault-with-no-diagnostic combination should become a compile-time error or warning.
Environment
- Branch:
main (repro on current tip)
- Discovered while reviewing a closure-heavy router DSL;
_aether_box_closure/_aether_unbox_closure are emitted in compiler/codegen/codegen.c; box_closure/unbox_closure symbols registered in compiler/analysis/typechecker.c.
Summary
Passing a bare named function to a
ptr-typed parameter (or assigning it into aptrstruct field) does not box it into an_AeClosure. A laterunbox_closure(...)+call(...)on that pointer then reads a.envfield off a raw code address and segfaults at runtime. The compiler emits no diagnostic — it builds clean.This is a soundness gap:
unbox_closureis only valid on a value that was actually boxed (which happens automatically only when the value crosses anfn-typed boundary), but nothing prevents feeding it an unboxed bare-fn pointer.Repro (segfaults)
ae buildproduces no warning or error.Working counterpart (correct)
Changing only the parameter type from
ptrtofnmakes the compiler autobox the bare function at the call site, and the sameunbox_closure/callworks:So the boxing is keyed entirely on the static type of the parameter/slot the value flows through (
fnboxes,ptrdoes not), andunbox_closuretrusts its input unconditionally.Why it matters
This is a natural pattern when storing heterogeneous handlers in
ptrfields of lists/maps (routers, dispatch tables, callback registries — it surfaced while building a trailing-block HTTP router DSL). Theptrfield is often necessary for storage in generic collections, so the developer stores a bare fn intoptr, laterunbox_closurees it, and gets a silent segfault with no hint that the missing box is the cause.Suggested fix (any one, roughly increasing effort)
unbox_closure(p)require thatp's provenance is a boxed closure; error at compile time when a value known to be an unboxed bare-fn pointer reaches it.ptrslot without an intervening box (the point where the.envis silently dropped), suggesting anfn-typed param or an explicitbox_closure(...).ptrslot that is later unboxed — i.e. box at the fn→ptr coercion the same way fn→fn does. (Costs an allocation even when not needed; (1)/(2) are probably preferable.)At minimum, the silent-segfault-with-no-diagnostic combination should become a compile-time error or warning.
Environment
main(repro on current tip)_aether_box_closure/_aether_unbox_closureare emitted incompiler/codegen/codegen.c;box_closure/unbox_closuresymbols registered incompiler/analysis/typechecker.c.