Skip to content

Commit 4f5c8ec

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 isn't 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 4f5c8ec

File tree

27 files changed

+555
-1211
lines changed

27 files changed

+555
-1211
lines changed

.github/workflows/ponyc-tier2.yml

Lines changed: 0 additions & 336 deletions
This file was deleted.

.github/workflows/pr-ponyc.yml

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,138 @@ jobs:
6464
- name: Test with Release Runtime
6565
run: make test-ci-core config=release usedebugger='${{ matrix.debugger }}'
6666

67+
riscv64-linux:
68+
if: github.event.pull_request.draft == false
69+
runs-on: ubuntu-latest
70+
71+
strategy:
72+
fail-fast: false
73+
matrix:
74+
include:
75+
- image: ghcr.io/ponylang/ponyc-ci-cross-riscv64:20240427
76+
name: riscv64 Linux glibc
77+
78+
name: ${{ matrix.name }}
79+
container:
80+
image: ${{ matrix.image }}
81+
options: --user pony --cap-add=SYS_PTRACE --security-opt seccomp=unconfined
82+
steps:
83+
- name: Checkout
84+
uses: actions/checkout@v4.1.1
85+
- name: Restore Libs Cache
86+
id: restore-libs
87+
uses: actions/cache/restore@v4
88+
with:
89+
path: build/libs
90+
key: libs-${{ matrix.image }}-${{ hashFiles('Makefile', 'CMakeLists.txt', 'lib/CMakeLists.txt', 'lib/llvm/patches/*') }}
91+
- name: Build Libs
92+
if: steps.restore-libs.outputs.cache-hit != 'true'
93+
run: make libs build_flags=-j8
94+
- name: Build Debug Runtime
95+
run: |
96+
make configure config=debug
97+
make build config=debug
98+
- name: Build Debug Cross-Compiled Runtime
99+
run: make cross-libponyrt config=debug CC=riscv64-linux-gnu-gcc-10 CXX=riscv64-linux-gnu-g++-10 arch=rv64gc cross_cflags="-march=rv64gc -mtune=rocket" cross_lflags="-march=riscv64"
100+
- name: Test with Debug Cross-Compiled Runtime
101+
run: make test-cross-ci config=debug PONYPATH=../rv64gc/debug cross_triple=riscv64-unknown-linux-gnu cross_arch=rv64gc cross_cpu=generic-rv64 cross_linker=riscv64-linux-gnu-gcc-10 cross_ponyc_args='--abi=lp64d --features=+m,+a,+f,+d,+c --link-ldcmd=bfd' cross_runner="qemu-riscv64 -L /usr/riscv64-linux-gnu/lib/"
102+
- name: Build Release Runtime
103+
run: |
104+
make configure config=release
105+
make build config=release
106+
- name: Build Release Cross-Compiled Runtime
107+
run: make cross-libponyrt config=release CC=riscv64-linux-gnu-gcc-10 CXX=riscv64-linux-gnu-g++-10 arch=rv64gc cross_cflags="-march=rv64gc -mtune=rocket" cross_lflags="-march=riscv64"
108+
- name: Test with Release Cross-Compiled Runtime
109+
run: make test-cross-ci config=release PONYPATH=../rv64gc/release cross_triple=riscv64-unknown-linux-gnu cross_arch=rv64gc cross_cpu=generic-rv64 cross_linker=riscv64-linux-gnu-gcc-10 cross_ponyc_args='--abi=lp64d --features=+m,+a,+f,+d,+c --link-ldcmd=bfd' cross_runner="qemu-riscv64 -L /usr/riscv64-linux-gnu/lib/"
110+
111+
arm-linux:
112+
if: github.event.pull_request.draft == false
113+
runs-on: ubuntu-latest
114+
115+
strategy:
116+
fail-fast: false
117+
matrix:
118+
include:
119+
- image: ghcr.io/ponylang/ponyc-ci-cross-arm:20250223
120+
name: arm Linux glibc
121+
122+
name: ${{ matrix.name }}
123+
container:
124+
image: ${{ matrix.image }}
125+
options: --user pony --cap-add=SYS_PTRACE --security-opt seccomp=unconfined
126+
steps:
127+
- name: Checkout
128+
uses: actions/checkout@v4.1.1
129+
- name: Restore Libs Cache
130+
id: restore-libs
131+
uses: actions/cache/restore@v4
132+
with:
133+
path: build/libs
134+
key: libs-${{ matrix.image }}-${{ hashFiles('Makefile', 'CMakeLists.txt', 'lib/CMakeLists.txt', 'lib/llvm/patches/*') }}
135+
- name: Build Libs
136+
if: steps.restore-libs.outputs.cache-hit != 'true'
137+
run: make libs build_flags=-j8
138+
- name: Build Debug Runtime
139+
run: |
140+
make configure config=debug
141+
make build config=debug
142+
- name: Build Debug Cross-Compiled Runtime
143+
run: make cross-libponyrt config=debug CC=arm-linux-gnueabi-gcc CXX=arm-linux-gnueabi-g++ arch=armv7-a cross_cflags="-march=armv7-a -mtune=cortex-a9" cross_lflags="-O3;-march=arm"
144+
- name: Test with Debug Cross-Compiled Runtime
145+
run: make test-cross-ci config=debug PONYPATH=../armv7-a/debug cross_triple=arm-unknown-linux-gnueabi cross_arch=armv7-a cross_cpu=cortex-a9 cross_linker=arm-linux-gnueabi-gcc cross_ponyc_args='--link-ldcmd=gold' cross_runner="qemu-arm-static -cpu cortex-a9 -L /usr/local/arm-linux-gnueabi/libc"
146+
- name: Build Release Runtime
147+
run: |
148+
make configure config=release
149+
make build config=release
150+
- name: Build Release Cross-Compiled Runtime
151+
run: make cross-libponyrt config=release CC=arm-linux-gnueabi-gcc CXX=arm-linux-gnueabi-g++ arch=armv7-a cross_cflags="-march=armv7-a -mtune=cortex-a9" cross_lflags="-O3;-march=arm"
152+
- name: Test with Release Cross-Compiled Runtime
153+
run: make test-cross-ci config=release PONYPATH=../armv7-a/release cross_triple=arm-unknown-linux-gnueabi cross_arch=armv7-a cross_cpu=cortex-a9 cross_linker=arm-linux-gnueabi-gcc cross_ponyc_args='--link-ldcmd=gold' cross_runner="qemu-arm-static -cpu cortex-a9 -L /usr/local/arm-linux-gnueabi/libc"
154+
155+
armhf-linux:
156+
if: github.event.pull_request.draft == false
157+
runs-on: ubuntu-latest
158+
159+
strategy:
160+
fail-fast: false
161+
matrix:
162+
include:
163+
- image: ghcr.io/ponylang/ponyc-ci-cross-armhf:20250223
164+
name: armhf Linux glibc
165+
166+
name: ${{ matrix.name }}
167+
container:
168+
image: ${{ matrix.image }}
169+
options: --user pony --cap-add=SYS_PTRACE --security-opt seccomp=unconfined
170+
steps:
171+
- name: Checkout
172+
uses: actions/checkout@v4.1.1
173+
- name: Restore Libs Cache
174+
id: restore-libs
175+
uses: actions/cache/restore@v4
176+
with:
177+
path: build/libs
178+
key: libs-${{ matrix.image }}-${{ hashFiles('Makefile', 'CMakeLists.txt', 'lib/CMakeLists.txt', 'lib/llvm/patches/*') }}
179+
- name: Build Libs
180+
if: steps.restore-libs.outputs.cache-hit != 'true'
181+
run: make libs build_flags=-j8
182+
- name: Build Debug Runtime
183+
run: |
184+
make configure config=debug
185+
make build config=debug
186+
- name: Build Debug Cross-Compiled Runtime
187+
run: make cross-libponyrt config=debug CC=arm-linux-gnueabihf-gcc CXX=arm-linux-gnueabihf-g++ arch=armv7-a cross_cflags="-march=armv7-a -mtune=cortex-a9" cross_lflags="-O3;-march=arm"
188+
- name: Test with Debug Cross-Compiled Runtime
189+
run: make test-cross-ci config=debug PONYPATH=../armv7-a/debug cross_triple=arm-unknown-linux-gnueabihf cross_arch=armv7-a cross_cpu=cortex-a9 cross_linker=arm-linux-gnueabihf-gcc cross_ponyc_args='--link-ldcmd=gold' cross_runner="qemu-arm-static -cpu cortex-a9 -L /usr/local/arm-linux-gnueabihf/libc"
190+
- name: Build Release Runtime
191+
run: |
192+
make configure config=release
193+
make build config=release
194+
- name: Build Release Cross-Compiled Runtime
195+
run: make cross-libponyrt config=release CC=arm-linux-gnueabihf-gcc CXX=arm-linux-gnueabihf-g++ arch=armv7-a cross_cflags="-march=armv7-a -mtune=cortex-a9" cross_lflags="-O3;-march=arm"
196+
- name: Test with Release Cross-Compiled Runtime
197+
run: make test-cross-ci config=release PONYPATH=../armv7-a/release cross_triple=arm-unknown-linux-gnueabihf cross_arch=armv7-a cross_cpu=cortex-a9 cross_linker=arm-linux-gnueabihf-gcc cross_ponyc_args='--link-ldcmd=gold' cross_runner="qemu-arm-static -cpu cortex-a9 -L /usr/local/arm-linux-gnueabihf/libc"
198+
67199
# Currently, Github actions supplied by GH like checkout and cache do not work
68200
# in musl libc environments on arm64. We can work around this by running
69201
# those actions on the host and then "manually" doing our work that would
@@ -283,3 +415,98 @@ jobs:
283415
- name: Build examples
284416
run: .\make.ps1 -Command build-examples
285417

418+
use_directives:
419+
if: github.event.pull_request.draft == false
420+
runs-on: ubuntu-latest
421+
needs: x86_64-linux
422+
423+
strategy:
424+
fail-fast: false
425+
matrix:
426+
include:
427+
- image: ghcr.io/ponylang/ponyc-ci-x86-64-unknown-linux-ubuntu24.04-builder:20250115
428+
debugger: lldb
429+
directives: dtrace
430+
- image: ghcr.io/ponylang/ponyc-ci-x86-64-unknown-linux-ubuntu24.04-builder:20250115
431+
debugger: lldb
432+
directives: pool_memalign
433+
- image: ghcr.io/ponylang/ponyc-ci-x86-64-unknown-linux-ubuntu24.04-builder:20250115
434+
debugger: lldb
435+
directives: pool_retain
436+
- image: ghcr.io/ponylang/ponyc-ci-x86-64-unknown-linux-ubuntu24.04-builder:20250115
437+
debugger: lldb
438+
directives: runtimestats
439+
- image: ghcr.io/ponylang/ponyc-ci-x86-64-unknown-linux-ubuntu24.04-builder:20250115
440+
debugger: lldb
441+
directives: runtime_tracing
442+
443+
name: use ${{ matrix.directives }}
444+
container:
445+
image: ${{ matrix.image }}
446+
options: --user pony --cap-add=SYS_PTRACE --security-opt seccomp=unconfined
447+
steps:
448+
- name: Checkout
449+
uses: actions/checkout@v4.1.1
450+
- name: Restore Libs Cache
451+
id: restore-libs
452+
uses: actions/cache/restore@v4
453+
with:
454+
path: build/libs
455+
key: libs-${{ matrix.image }}-${{ hashFiles('Makefile', 'CMakeLists.txt', 'lib/CMakeLists.txt', 'lib/llvm/patches/*') }}
456+
- name: Build Libs
457+
if: steps.restore-libs.outputs.cache-hit != 'true'
458+
run: make libs build_flags=-j8
459+
- name: Build Debug Runtime
460+
run: |
461+
make configure arch=x86-64 config=debug use=${{ matrix.directives }}
462+
make build config=debug
463+
- name: Test with Debug Runtime
464+
run: make test-ci-core config=debug usedebugger='${{ matrix.debugger }}'
465+
- name: Build Release Runtime
466+
run: |
467+
make configure arch=x86-64 config=release use=${{ matrix.directives }}
468+
make build config=release
469+
- name: Test with Release Runtime
470+
run: make test-ci-core config=release usedebugger='${{ matrix.debugger }}'
471+
472+
with_sanitizers:
473+
if: github.event.pull_request.draft == false
474+
runs-on: ubuntu-latest
475+
needs: x86_64-linux
476+
477+
strategy:
478+
fail-fast: false
479+
matrix:
480+
include:
481+
- image: ghcr.io/ponylang/ponyc-ci-x86-64-unknown-linux-ubuntu24.04-builder:20250115
482+
debugger: lldb
483+
directives: pool_memalign,address_sanitizer,undefined_behavior_sanitizer
484+
485+
name: with sanitizers ${{ matrix.directives }}
486+
container:
487+
image: ${{ matrix.image }}
488+
options: --user pony --cap-add=SYS_PTRACE --security-opt seccomp=unconfined
489+
steps:
490+
- name: Checkout
491+
uses: actions/checkout@v4.1.1
492+
- name: Restore Libs Cache
493+
id: restore-libs
494+
uses: actions/cache/restore@v4
495+
with:
496+
path: build/libs
497+
key: libs-${{ matrix.image }}-${{ hashFiles('Makefile', 'CMakeLists.txt', 'lib/CMakeLists.txt', 'lib/llvm/patches/*') }}
498+
- name: Build Libs
499+
if: steps.restore-libs.outputs.cache-hit != 'true'
500+
run: make libs build_flags=-j8
501+
- name: Build Debug Runtime
502+
run: |
503+
ASAN_OPTIONS=detect_leaks=0:external_symbolizer_path=$PWD/build/libs/bin/llvm-symbolizer UBSAN_OPTIONS=external_symbolizer_path=$PWD/build/libs/bin/llvm-symbolizer make configure arch=x86-64 config=debug use=${{ matrix.directives }}
504+
ASAN_OPTIONS=detect_leaks=0:external_symbolizer_path=$PWD/build/libs/bin/llvm-symbolizer UBSAN_OPTIONS=external_symbolizer_path=$PWD/build/libs/bin/llvm-symbolizer make build config=debug
505+
- name: Test with Debug Runtime
506+
run: ASAN_OPTIONS=detect_leaks=0:external_symbolizer_path=$PWD/build/libs/bin/llvm-symbolizer UBSAN_OPTIONS=external_symbolizer_path=$PWD/build/libs/bin/llvm-symbolizer make test-ci-core config=debug test_full_program_timeout=300
507+
- name: Build Release Runtime
508+
run: |
509+
ASAN_OPTIONS=detect_leaks=0:external_symbolizer_path=$PWD/build/libs/bin/llvm-symbolizer UBSAN_OPTIONS=external_symbolizer_path=$PWD/build/libs/bin/llvm-symbolizer make configure arch=x86-64 config=release use=${{ matrix.directives }}
510+
ASAN_OPTIONS=detect_leaks=0:external_symbolizer_path=$PWD/build/libs/bin/llvm-symbolizer UBSAN_OPTIONS=external_symbolizer_path=$PWD/build/libs/bin/llvm-symbolizer make build config=release
511+
- name: Test with Release Runtime
512+
run: ASAN_OPTIONS=detect_leaks=0:external_symbolizer_path=$PWD/build/libs/bin/llvm-symbolizer UBSAN_OPTIONS=external_symbolizer_path=$PWD/build/libs/bin/llvm-symbolizer make test-ci-core config=release test_full_program_timeout=300

.github/workflows/pr-tools.yml

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,20 @@ permissions:
2020

2121
jobs:
2222
tools:
23-
if: github.event.pull_request.draft == false
2423
runs-on: ubuntu-latest
25-
name: Tools
24+
strategy:
25+
fail-fast: false
26+
matrix:
27+
include:
28+
- name: Lint pony-lint
29+
target: lint-pony-lint
30+
- name: Test pony-lsp
31+
target: test-pony-lsp
32+
- name: Test pony-doc
33+
target: test-pony-doc
34+
- name: Test pony-lint
35+
target: test-pony-lint
36+
name: ${{ matrix.name }}
2637
container:
2738
image: ghcr.io/ponylang/ponyc-ci-alpine3.23-builder:20260201
2839
options: --user pony
@@ -42,11 +53,5 @@ jobs:
4253
run: |
4354
make configure config=debug
4455
make build config=debug
45-
- name: Test pony-doc
46-
run: make test-pony-doc config=debug
47-
- name: Test pony-lint
48-
run: make test-pony-lint config=debug
49-
- name: Test pony-lsp
50-
run: make test-pony-lsp config=debug
51-
- name: Lint pony-lint
52-
run: make lint-pony-lint config=debug
56+
- name: ${{ matrix.name }}
57+
run: make ${{ matrix.target }} config=debug
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.

.release-notes/next-release.md

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -69,34 +69,3 @@ The FFI declaration now correctly uses `I32`, and the accept loop bails out on `
6969

7070
We've updated the LLVM version used to build Pony from 18.1.8 to 21.1.8.
7171

72-
## Compile-time string literal concatenation
73-
74-
The compiler now folds adjacent string literal concatenation at compile time, avoiding runtime allocation and copying. This works across chains that mix literals and variables — adjacent literals are merged while non-literal operands are left as runtime `.add()` calls. For example, `"a" + "b" + x + "c" + "d"` is folded to the equivalent of `"ab".add(x).add("cd")`, reducing four runtime concatenations down to two.
75-
76-
## Exempt unsplittable string literals from line length rule
77-
78-
The `style/line-length` lint rule no longer flags lines where the only reason for exceeding 80 columns is a string literal that contains no spaces. Strings without spaces — URLs, file paths, qualified identifiers — cannot be meaningfully split across lines, so flagging them produced noise with no actionable fix.
79-
80-
Strings that contain spaces are still flagged because they can be split at space boundaries using compile-time string concatenation at zero runtime cost:
81-
82-
```pony
83-
// Before: flagged, and splitting is awkward
84-
let url = "https://github.com/ponylang/ponyc/blob/main/packages/builtin/string.pony"
85-
86-
// After: no longer flagged — the string has no spaces and can't be split
87-
88-
// Strings with spaces can still be split, so they remain flagged:
89-
let msg = "This is a very long error message that should be split across multiple lines"
90-
91-
// Fix by splitting at spaces:
92-
let msg =
93-
"This is a very long error message that should be split "
94-
+ "across multiple lines"
95-
```
96-
97-
Lines inside triple-quoted strings (docstrings) and lines containing `"""` delimiters are not eligible for this exemption — docstring content should be wrapped regardless of whether it contains spaces.
98-
99-
## Fix compiler crash on `return error`
100-
101-
Previously, writing `return error` in a function body would crash the compiler with an assertion failure instead of producing a diagnostic error. The compiler now correctly reports that a return value cannot be a control statement.
102-

CHANGELOG.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@ All notable changes to the Pony compiler and standard library will be documented
1616

1717
### Added
1818

19-
- Compile-time string literal concatenation ([PR #4900](https://github.com/ponylang/ponyc/pull/4900))
2019

2120
### Changed
2221

2322
- Update to LLVM 21.1.8 ([PR #4876](https://github.com/ponylang/ponyc/pull/4876))
24-
- Exempt unsplittable string literals from line length rule ([PR #4923](https://github.com/ponylang/ponyc/pull/4923))
2523

2624
## [0.61.0] - 2026-02-28
2725

src/libponyc/ast/ast.c

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -787,15 +787,6 @@ void ast_set_name(ast_t* ast, const char* name)
787787
token_set_string(ast->t, name, 0);
788788
}
789789

790-
void ast_set_name_len(ast_t* ast, const char* name, size_t len)
791-
{
792-
pony_assert(ast != NULL);
793-
#ifndef PONY_NDEBUG
794-
pony_assert(!ast->frozen);
795-
#endif
796-
token_set_string(ast->t, name, len);
797-
}
798-
799790
double ast_float(ast_t* ast)
800791
{
801792
pony_assert(ast != NULL);

src/libponyc/ast/ast.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ const char* ast_name(ast_t* ast);
9393
const char* ast_nice_name(ast_t* ast);
9494
size_t ast_name_len(ast_t* ast);
9595
void ast_set_name(ast_t* ast, const char* name);
96-
void ast_set_name_len(ast_t* ast, const char* name, size_t len);
9796
double ast_float(ast_t* ast);
9897
lexint_t* ast_int(ast_t* ast);
9998
ast_t* ast_type(ast_t* ast);

0 commit comments

Comments
 (0)