Skip to content

Commit d3db52f

Browse files
authored
Fix segfault when using Generator.map with PonyCheck shrinking (#5006)
When a lambda is nested inside an object literal inside a generic method, collect_type_params copies type parameters twice, producing an ast_data chain of length > 1 (copy'' → copy' → original). reify_typeparamref followed this chain only one level, so the doubly-copied type params never matched. This caused the lambda's apply method to not be marked as reachable during codegen, leaving a hole in the vtable and segfaulting at runtime. The fix follows the ast_data chain to the root (the self-referential node set during the scope pass) for both sides of the comparison, handling any depth of nesting. The same single-level ast_data pattern existed in reify_reference. While investigation showed that code path is not currently reachable in the nested lambda scenario (TK_REFERENCE nodes are resolved by the refer pass before any reify call encounters them), the pattern is structurally vulnerable. Applied the same chain-following fix defensively, guarded by a TK_TYPEPARAM check on ref_def since scope lookup can return other node types. Closes #4838 Closes #5005
1 parent 46b6750 commit d3db52f

File tree

3 files changed

+79
-8
lines changed

3 files changed

+79
-8
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## Fix segfault when using Generator.map with PonyCheck shrinking
2+
3+
Using `Generator.map` to transform values from one type to another would segfault during shrinking when a property test failed. For example, this program would crash:
4+
5+
```pony
6+
let gen = recover val
7+
Generators.u32().map[String]({(n: U32): String^ => n.string()})
8+
end
9+
PonyCheck.for_all[String](gen, h)(
10+
{(sample: String, ph: PropertyHelper) =>
11+
ph.assert_true(sample.size() > 0)
12+
})?
13+
```
14+
15+
The underlying compiler bug affected any code where a lambda appeared inside an object literal inside a generic method and was then passed to another generic method. The lambda's `apply` method was silently omitted from the vtable, causing a segfault when called at runtime.

src/libponyc/type/reify.c

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,19 @@ static void reify_typeparamref(ast_t** astp, ast_t* typeparam, ast_t* typearg)
1717
ast_t* ref_def = (ast_t*)ast_data(ast);
1818

1919
// We can't compare ref_def and typeparam, as they could be a copy or
20-
// a iftype shadowing. However, their data points back to the original
21-
// typeparam definition, which can be compared.
22-
ref_def = (ast_t*)ast_data(ref_def);
23-
typeparam = (ast_t*)ast_data(typeparam);
24-
20+
// an iftype shadowing. Follow the ast_data chain to the root
21+
// (self-referential node set during the scope pass) for both sides.
22+
// A single-level follow is insufficient when type parameters are copied
23+
// multiple times, e.g. a lambda inside an object literal inside a generic
24+
// method — collect_type_params creates copies of copies, producing a chain
25+
// of length > 1.
2526
pony_assert(ref_def != NULL);
27+
while((ast_t*)ast_data(ref_def) != ref_def)
28+
ref_def = (ast_t*)ast_data(ref_def);
29+
2630
pony_assert(typeparam != NULL);
31+
while((ast_t*)ast_data(typeparam) != typeparam)
32+
typeparam = (ast_t*)ast_data(typeparam);
2733

2834
if(ref_def != typeparam)
2935
return;
@@ -97,10 +103,22 @@ static void reify_reference(ast_t** astp, ast_t* typeparam, ast_t* typearg)
97103
if(ref_def == NULL)
98104
return;
99105

100-
ast_t* param_def = (ast_t*)ast_data(typeparam);
101-
pony_assert(param_def != NULL);
106+
// Follow the ast_data chain to the root (self-referential node set during
107+
// the scope pass) for both sides of the comparison, same as
108+
// reify_typeparamref. A single-level follow is insufficient when type
109+
// parameters are copied multiple times by collect_type_params for nested
110+
// lambdas/object literals.
111+
if(ast_id(ref_def) == TK_TYPEPARAM)
112+
{
113+
while((ast_t*)ast_data(ref_def) != ref_def)
114+
ref_def = (ast_t*)ast_data(ref_def);
115+
}
102116

103-
if(ref_def != param_def)
117+
pony_assert(typeparam != NULL);
118+
while((ast_t*)ast_data(typeparam) != typeparam)
119+
typeparam = (ast_t*)ast_data(typeparam);
120+
121+
if(ref_def != typeparam)
104122
return;
105123

106124
ast_setid(ast, TK_TYPEREF);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use "pony_check"
2+
use @pony_exitcode[None](code: I32)
3+
4+
// Regression test for https://github.com/ponylang/ponyc/issues/4838
5+
//
6+
// When a lambda is nested inside an object literal inside a generic method,
7+
// type parameters are copied twice by collect_type_params. The compiler's
8+
// reify_typeparamref followed the ast_data chain only one level, failing to
9+
// match these doubly-copied type parameters. This caused the lambda's apply
10+
// method to not be marked as reachable, leaving a hole in the vtable and
11+
// producing a segfault at runtime.
12+
//
13+
// The bug manifests when Generator.map's internal _map_shrunken method
14+
// passes a lambda to Iter.map, creating a chain of nested object literals.
15+
// Iterating the resulting shrink iterator triggers the segfault because the
16+
// lambda's apply method has no vtable entry.
17+
18+
actor Main
19+
new create(env: Env) =>
20+
let gen = recover val
21+
Generators.u32().map[U32]({(n: U32): U32^ => n + 1})
22+
end
23+
let rnd = Randomness
24+
try
25+
// generate_and_shrink returns the mapped value and a shrink iterator.
26+
// The shrink iterator is produced by _map_shrunken, which uses the
27+
// nested lambda pattern that triggers the bug.
28+
(let value: U32, let shrunken: Iterator[U32^]) =
29+
gen.generate_and_shrink(rnd)?
30+
// Iterating the shrink iterator calls the nested lambda's apply.
31+
// Without the fix, this segfaults due to the missing vtable entry.
32+
while shrunken.has_next() do
33+
shrunken.next()?
34+
end
35+
@pony_exitcode(0)
36+
else
37+
@pony_exitcode(1)
38+
end

0 commit comments

Comments
 (0)