Skip to content

Commit daab566

Browse files
committed
Fix memory leak when a behavior parameter has a different capability than the trait it implements
When a behavior is called through a trait and the concrete actor's parameter has a different trace-significant capability (e.g., trait has iso, concrete has val), the sender traces with one trace kind and the receiver traces with another. This ORCA GC mismatch causes field reference counts to never reach zero, leaking objects reachable from the parameter. The fix includes trace-kind characters in mangled names for behaviors and constructors, so methods whose parameters differ in trace-significant ways get distinct vtable indices. When a forwarding method is created due to a cap mismatch, genfun_forward now adds a dispatch case that traces with the forwarding method's params (the trait's capabilities) but calls the concrete method's handler. A two-pass ordering in genfun_method_bodies ensures concrete handlers are generated before forwarding dispatch cases that reference them. The leak is not currently active because make_might_reference_actor forces full tracing of immutable objects. Without this fix, re-enabling that optimization would expose the leak. Design: #4943 Closes #4102
1 parent 09941a6 commit daab566

File tree

9 files changed

+279
-10
lines changed

9 files changed

+279
-10
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
## Fix memory leak
2+
3+
When a behavior was called through a trait reference and the concrete actor's parameter had a different trace-significant capability (e.g., the trait declared `iso` but the actor declared `val`), the ORCA garbage collector's reference counting was broken. The sender traced the parameter with one trace kind and the receiver traced with another, causing field reference counts to never reach zero. Objects reachable from the parameter were leaked.
4+
5+
```pony
6+
trait tag Receiver
7+
be receive(b: SomeClass iso)
8+
9+
actor MyActor is Receiver
10+
be receive(b: SomeClass val) =>
11+
// b's fields were leaked when called through a Receiver reference
12+
None
13+
```
14+
15+
The leak is not currently active because `make_might_reference_actor` — an optimization that was disabled as a safety net — masks it by forcing full tracing of all immutable objects. Without this fix, the leak would have become active as we started re-enabling that optimization.
16+
17+
The sender and receiver now use consistent tracing for each parameter, regardless of capability differences between the trait and concrete method.

src/libponyc/codegen/genfun.c

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,43 @@ static bool genfun_forward(compile_t* c, reach_type_t* t,
818818
genfun_build_ret(c, ret);
819819
codegen_finishfun(c);
820820
ponyint_pool_free_size(buf_size, args);
821+
822+
// For forwarding behaviors/constructors on actors, add a dispatch case that
823+
// traces with the forwarding method's params (the trait's capabilities) but
824+
// calls the concrete method's handler.
825+
//
826+
// Only do this when all parameter types match between the forwarding and
827+
// concrete methods — i.e. the forwarding was created solely because of a
828+
// trace-kind (cap) difference. When the types themselves differ (e.g. a
829+
// tuple vs Any), the LLVM types are incompatible and the existing forwarding
830+
// sender path handles dispatch correctly without a separate dispatch case.
831+
if(t->underlying == TK_ACTOR)
832+
{
833+
token_id fun_id = ast_id(m->fun->ast);
834+
835+
if((fun_id == TK_BE) || (fun_id == TK_NEW))
836+
{
837+
bool types_match = (m->param_count == m2->param_count);
838+
839+
for(size_t i = 0; types_match && (i < m->param_count); i++)
840+
{
841+
if(m->params[i].type != m2->params[i].type)
842+
types_match = false;
843+
}
844+
845+
if(types_match)
846+
{
847+
pony_assert(c_m2->func_handler != NULL);
848+
add_dispatch_case(c, t, m->params, m->vtable_index,
849+
c_m2->func_handler, c_m2->func_type, c_m->msg_type);
850+
851+
#if defined(USE_RUNTIME_TRACING)
852+
add_get_behavior_name_case(c, t, m->name, m->vtable_index);
853+
#endif
854+
}
855+
}
856+
}
857+
821858
return true;
822859
}
823860

@@ -890,6 +927,7 @@ void genfun_param_attrs(compile_t* c, reach_type_t* t, reach_method_t* m,
890927
LLVMValueRef fun)
891928
{
892929
LLVM_DECLARE_ATTRIBUTEREF(noalias_attr, noalias, 0);
930+
LLVM_DECLARE_ATTRIBUTEREF(readonly_attr, readonly, 0);
893931

894932
LLVMValueRef param = LLVMGetFirstParam(fun);
895933
reach_type_t* type = t;
@@ -928,9 +966,12 @@ void genfun_param_attrs(compile_t* c, reach_type_t* t, reach_method_t* m,
928966

929967
case TK_TRN:
930968
case TK_REF:
931-
case TK_TAG:
969+
break;
970+
932971
case TK_VAL:
972+
case TK_TAG:
933973
case TK_BOX:
974+
LLVMAddAttributeAtIndex(fun, i + offset, readonly_attr);
934975
break;
935976

936977
default:
@@ -1019,19 +1060,45 @@ bool genfun_method_bodies(compile_t* c, reach_type_t* t)
10191060
size_t j = HASHMAP_BEGIN;
10201061
reach_method_t* m;
10211062

1063+
// First pass: non-forwarding methods (generates handlers that forwarding
1064+
// dispatch cases will reference).
10221065
while((m = reach_mangled_next(&n->r_mangled, &j)) != NULL)
10231066
{
1024-
if(!genfun_method(c, t, n, m))
1067+
if(!m->forwarding)
10251068
{
1026-
if(errors_get_count(c->opt->check.errors) == 0)
1069+
if(!genfun_method(c, t, n, m))
10271070
{
1028-
pony_assert(m->fun != NULL);
1029-
ast_error(c->opt->check.errors, m->fun->ast,
1030-
"internal failure: code generation failed for method %s",
1031-
m->full_name);
1071+
if(errors_get_count(c->opt->check.errors) == 0)
1072+
{
1073+
pony_assert(m->fun != NULL);
1074+
ast_error(c->opt->check.errors, m->fun->ast,
1075+
"internal failure: code generation failed for method %s",
1076+
m->full_name);
1077+
}
1078+
1079+
return false;
10321080
}
1081+
}
1082+
}
10331083

1034-
return false;
1084+
// Second pass: forwarding methods (may need handlers from first pass).
1085+
j = HASHMAP_BEGIN;
1086+
while((m = reach_mangled_next(&n->r_mangled, &j)) != NULL)
1087+
{
1088+
if(m->forwarding)
1089+
{
1090+
if(!genfun_method(c, t, n, m))
1091+
{
1092+
if(errors_get_count(c->opt->check.errors) == 0)
1093+
{
1094+
pony_assert(m->fun != NULL);
1095+
ast_error(c->opt->check.errors, m->fun->ast,
1096+
"internal failure: code generation failed for method %s",
1097+
m->full_name);
1098+
}
1099+
1100+
return false;
1101+
}
10351102
}
10361103
}
10371104
}

src/libponyc/reach/reach.c

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,74 @@ static void set_method_types(reach_t* r, reach_method_t* m,
212212
ast_free_unattached(r_result);
213213
}
214214

215+
static char trace_kind_char(ast_t* type)
216+
{
217+
switch(ast_id(type))
218+
{
219+
case TK_NOMINAL:
220+
{
221+
ast_t* def = (ast_t*)ast_data(type);
222+
223+
switch(ast_id(def))
224+
{
225+
case TK_ACTOR:
226+
return 't';
227+
228+
case TK_PRIMITIVE:
229+
return 'p';
230+
231+
default:
232+
switch(ast_id(cap_fetch(type)))
233+
{
234+
case TK_VAL: return 'i';
235+
case TK_TAG: return 't';
236+
default: return 'm';
237+
}
238+
}
239+
}
240+
241+
case TK_UNIONTYPE:
242+
case TK_ISECTTYPE:
243+
case TK_TUPLETYPE:
244+
// Compound types all return 'x'. This means trace-kind mismatches
245+
// within union/intersection/tuple parameters won't be distinguished.
246+
// The simple nominal case covers the reported bug; compound parameter
247+
// mismatches are valid but rare and can be addressed separately.
248+
return 'x';
249+
250+
default:
251+
return 'x';
252+
}
253+
}
254+
215255
static const char* make_mangled_name(reach_method_t* m)
216256
{
217257
// Generate the mangled name.
218-
// cap_name[_Arg1_Arg2]_args_result
258+
// cap_name[_Arg1_Arg2]_[<trace_kind>]<args>_result
259+
//
260+
// Trace kind characters are only included for behaviors and constructors,
261+
// since those are the only methods where a trace-kind mismatch between
262+
// sender and receiver can cause ORCA GC problems. Functions don't have
263+
// dispatch cases, and their symbol names may be hard-coded in the runtime
264+
// (e.g. Main_runtime_override_defaults).
265+
bool include_trace_kind = false;
266+
267+
if(m->fun != NULL)
268+
{
269+
token_id fun_id = ast_id(m->fun->ast);
270+
include_trace_kind = (fun_id == TK_BE) || (fun_id == TK_NEW);
271+
}
272+
219273
printbuf_t* buf = printbuf_new();
220274
printbuf(buf, "%s_", m->name);
221275

222276
for(size_t i = 0; i < m->param_count; i++)
277+
{
278+
if(include_trace_kind)
279+
printbuf(buf, "%c", trace_kind_char(m->params[i].ast));
280+
223281
printbuf(buf, "%s", m->params[i].type->mangle);
282+
}
224283

225284
if(!m->internal)
226285
printbuf(buf, "%s", m->result->mangle);
@@ -232,7 +291,7 @@ static const char* make_mangled_name(reach_method_t* m)
232291
static const char* make_full_name(reach_type_t* t, reach_method_t* m)
233292
{
234293
// Generate the full mangled name.
235-
// pkg_Type[_Arg1_Arg2]_cap_name[_Arg1_Arg2]_args_result
294+
// pkg_Type[_Arg1_Arg2]_cap_name[_Arg1_Arg2]_[<trace_kind>]<args>_result
236295
printbuf_t* buf = printbuf_new();
237296
printbuf(buf, "%s_%s", t->name, m->mangled_name);
238297
const char* name = stringtab(buf->m);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use "lib:codegen-trace-additional"
2+
3+
use @raw_cast[B ref](obj: B tag)
4+
use @gc_local[Pointer[None]](target: Main)
5+
use @gc_local_snapshot[Pointer[None]](target: Main)
6+
use @gc_local_snapshot_destroy[None](obj_map: Pointer[None])
7+
use @objectmap_has_object[Bool](obj_map: Pointer[None], obj: Any tag)
8+
use @objectmap_has_object_rc[Bool](obj_map: Pointer[None], obj: Any tag, rc: USize)
9+
use @pony_exitcode[None](code: I32)
10+
11+
class A
12+
13+
class B
14+
let a: A = A
15+
16+
trait tag Receiver
17+
be receive(b: B iso)
18+
19+
actor Main is Receiver
20+
var map_before: Pointer[None] = Pointer[None]
21+
22+
new create(env: Env) =>
23+
// Call through a trait-typed variable to exercise the forwarding path.
24+
let r: Receiver = this
25+
r.receive(recover B end)
26+
map_before = @gc_local_snapshot(this)
27+
28+
be receive(b: B tag) =>
29+
let b' = @raw_cast(b)
30+
let map_after = @gc_local(this)
31+
// The sender traces b as MUTABLE (iso from the trait), which recurses into
32+
// B's fields, incrementing a.rc. The receiver's dispatch case must also
33+
// recurse to decrement a.rc. Without the fix, the dispatch traces as
34+
// OPAQUE (tag from the concrete method), skipping field recursion entirely,
35+
// leaving a.rc stuck at 1.
36+
let ok = @objectmap_has_object_rc(map_before, b', USize(1)) and
37+
@objectmap_has_object_rc(map_before, b'.a, USize(1)) and
38+
@objectmap_has_object_rc(map_after, b', USize(0)) and
39+
@objectmap_has_object_rc(map_after, b'.a, USize(0))
40+
@gc_local_snapshot_destroy(map_before)
41+
@pony_exitcode(I32(if ok then 1 else 0 end))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use "lib:codegen-trace-additional"
2+
3+
use @raw_cast[B ref](obj: B tag)
4+
use @gc_local[Pointer[None]](target: Main)
5+
use @gc_local_snapshot[Pointer[None]](target: Main)
6+
use @gc_local_snapshot_destroy[None](obj_map: Pointer[None])
7+
use @objectmap_has_object[Bool](obj_map: Pointer[None], obj: Any tag)
8+
use @objectmap_has_object_rc[Bool](obj_map: Pointer[None], obj: Any tag, rc: USize)
9+
use @pony_exitcode[None](code: I32)
10+
11+
class A
12+
13+
class B
14+
let a: A = A
15+
16+
trait tag Receiver
17+
be receive(b: B iso)
18+
19+
actor Main is Receiver
20+
var map_before: Pointer[None] = Pointer[None]
21+
22+
new create(env: Env) =>
23+
// Call through a trait-typed variable to exercise the forwarding path.
24+
let r: Receiver = this
25+
r.receive(recover B end)
26+
map_before = @gc_local_snapshot(this)
27+
28+
be receive(b: B val) =>
29+
let b' = @raw_cast(b)
30+
let map_after = @gc_local(this)
31+
// The sender traces b as MUTABLE (iso from the trait), which recurses into
32+
// B's fields, incrementing a.rc. The receiver's dispatch case must also
33+
// recurse to decrement a.rc. Without the fix, the dispatch traces as
34+
// IMMUTABLE (val from the concrete method), skipping field recursion when
35+
// might_reference_actor is false, leaving a.rc stuck at 1.
36+
let ok = @objectmap_has_object_rc(map_before, b', USize(1)) and
37+
@objectmap_has_object_rc(map_before, b'.a, USize(1)) and
38+
@objectmap_has_object_rc(map_after, b', USize(0)) and
39+
@objectmap_has_object_rc(map_after, b'.a, USize(0))
40+
@gc_local_snapshot_destroy(map_before)
41+
@pony_exitcode(I32(if ok then 1 else 0 end))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use "lib:codegen-trace-additional"
2+
3+
use @gc_local[Pointer[None]](target: Main)
4+
use @gc_local_snapshot[Pointer[None]](target: Main)
5+
use @gc_local_snapshot_destroy[None](obj_map: Pointer[None])
6+
use @objectmap_has_object[Bool](obj_map: Pointer[None], obj: Any tag)
7+
use @objectmap_has_object_rc[Bool](obj_map: Pointer[None], obj: Any tag, rc: USize)
8+
use @pony_exitcode[None](code: I32)
9+
10+
class A
11+
12+
class B
13+
let a: A = A
14+
15+
trait tag Receiver
16+
be receive(b: B val)
17+
18+
actor Main is Receiver
19+
var map_before: Pointer[None] = Pointer[None]
20+
21+
new create(env: Env) =>
22+
// Call through a trait-typed variable, same as the iso-to-val test.
23+
// Both trait and concrete method agree on val, so no forwarding is created.
24+
// This verifies the non-forwarding val path isn't broken by the dispatch
25+
// and mangling changes.
26+
let r: Receiver = this
27+
r.receive(recover B end)
28+
map_before = @gc_local_snapshot(this)
29+
30+
be receive(b: B val) =>
31+
let map_after = @gc_local(this)
32+
// Both sender and receiver trace b as IMMUTABLE (val). Since B has no
33+
// actor fields (might_reference_actor is false), immutable tracing does
34+
// not recurse into fields. So b gets rc bookkeeping but b.a does not
35+
// appear in the object map at all.
36+
let ok = @objectmap_has_object_rc(map_before, b, USize(1)) and
37+
not @objectmap_has_object(map_before, b.a) and
38+
@objectmap_has_object_rc(map_after, b, USize(0)) and
39+
not @objectmap_has_object(map_after, b.a)
40+
@gc_local_snapshot_destroy(map_before)
41+
@pony_exitcode(I32(if ok then 1 else 0 end))

0 commit comments

Comments
 (0)