You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Native C retains every buffered channel closed without a later receiver #1268
I repeatedly created a room-one channel, buffered one value, closed it, and dropped every handle. I compared it with the same loop closing an empty channel.
import Base
def loop(+fuel: Nat) -> IO(Unit):
match fuel:
case 0n:
IO.print("done")
case 1n+more:
do IO<Unit>:
+c : Chan(U32) <- Chan.new(U32, 1)
ok : Bool <- Chan.send(U32, c, 7)
Unit <- Chan.close(U32, c)
loop(more)
def main() -> IO(Unit):
loop(2000000n)
I built native C and emitted JS, then measured each with /usr/bin/time -l.
What happened
Both programs print done and exit 0, but native C retains about 65 bytes for every closed buffered channel. At two million iterations current main uses 131,792,896 bytes peak RSS; the empty-channel control uses 1,982,464 bytes. Emitted JS uses 45,252,608 bytes for the reproducer and 43,974,656 bytes for the control. At 500,000 iterations native C uses 35,684,352 bytes, confirming linear growth.
The same result reproduces on v2.0.34: 131,776,512 bytes for buffered close and 1,982,464 bytes for the empty control.
In bend2/effs/chan.c, chan_shut calls chan_free only when row->size == 0. A closed buffered row is freed if another copied handle later drains it, but if the program drops every handle the row, its ring, and its buffered term become unreachable without ever entering chan_idle. Every leak also consumes one entry from the 16,777,216-row limit, so a sequential program can eventually fail with more than 16777216 channels at once despite having only one source-level channel alive at a time.
The JS runtime does not keep a global row table; the closed object becomes collectible when its handles are unreachable.
I found no matching issue or WONTFIX entry. The leak discussed during #1162/#1172 concerned a dropped parked operation and its timer, not a Chan.close that has already completed. Open PR #1259 changes timed channel operations but preserves the same chan_shut rule, so it does not free this closed buffered row either.
An immediate chan_free in Chan.close would change the existing behavior where a copied handle can drain buffered values after close, so the native fix appears to need handle reachability/reference tracking or an explicit lifetime rule.
bend --version
Reproduced on v2.0.34 (7d8a3eb036042c6549461054d25a10f26d361c5c) and current main (57b6c150f63704f53d533776deb3859578d4b09e). Both --check-only and --verdict print ALL PROOFS CHECK.
Activity