Skip to content

fix: prevent unbounded memory growth in AnyTLS client session writer - #153

Open
KiwwyQ wants to merge 3 commits into
cfal:masterfrom
KiwwyQ:master
Open

KiwwyQ wants to merge 3 commits into
cfal:masterfrom
KiwwyQ:master

Conversation

@KiwwyQ

@KiwwyQ KiwwyQ commented Aug 6, 2026

Copy link
Copy Markdown

The AnyTLS client session’s unified writer channel was created with mpsc::unbounded_channel(). When the writer loop exited (close_notify on session drop, or is_closed on EOF/error) while the session was still referenced by live streams, the unbounded sender side stayed alive. Any messages sent in that window queued into the channel forever - no receiver, no backpressure. This caused RAM to slowly climb (80MB → 100MB+) and eventually OOM.

Switched to a bounded channel so a dead writer applies backpressure instead of silently accumulating messages. Also made the per-stream forwarding task break on close_notify so it never blocks on a gone writer.

kilo-code-bot Bot and others added 2 commits August 6, 2026 10:22
…owth (#1)

The AnyTLS client session's unified writer channel was created with
mpsc::unbounded_channel(). When the writer loop exits (close_notify on
session drop, or is_closed on EOF/error) while the session is still
referenced by live streams, the unbounded sender side stays alive and any
message sent in that window queues into the channel forever — no receiver,
no backpressure. This caused RAM to slowly climb (80MB -> 100MB+) and
eventually OOM.

Switch to a bounded channel so a dead writer applies backpressure instead
of silently accumulating. Also break the per-stream forwarding task on
close_notify so it never blocks on a gone writer.

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
… memory leak on dropped/errored connections (#3)

Reality/VLESS fallback paths spawned copy_bidirectional tasks with no
timeout, no idle cap, and no lifetime link to the parent connection. A
half-open (dropped/errored) client therefore kept the task, both sockets,
and copy buffers alive until TCP keepalive fired (~7 min), so memory grew
without bound under connection churn. Wrap the fallback copy in a 300s
timeout so every fallback task is guaranteed to terminate.

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
AYastrebov added a commit to AYastrebov/shoes that referenced this pull request Aug 9, 2026
…dget

The AnyTLS client session's unified writer channel was created with
mpsc::unbounded_channel(). When the writer loop exits -- close_notify on
session drop, or is_closed on EOF/error -- while the session is still
referenced by live streams, the unbounded sender side stays alive and any
message sent in that window queues into the channel forever: no receiver, no
backpressure. RAM climbed slowly, 80 MB to 100 MB and beyond, and eventually
OOMed.

Switch to a bounded channel so a dead writer applies backpressure instead of
silently accumulating, and break the per-stream forwarding task on
close_notify so it never blocks on a writer that is gone.

The upstream fix (cfal#153) is right in shape but its capacity was
STREAM_CHANNEL_BUFFER * 1024, i.e. 16,384 messages. Each queued message owns
a Bytes that can hold a full frame, so that bound is a few hundred MB. An iOS
packet-tunnel extension has about 50 MB in total, so it would be killed long
before the backpressure ever engaged. 256 keeps the ceiling at a few MB while
staying deep enough that the writer loop is not a bottleneck. Named and
documented rather than derived from the per-stream constant, since the two
bound different things.

Only the AnyTLS client commit from that PR is taken. Its second commit bounds
the Reality/VLESS *server* fallback paths, which this fork does not run.

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AYastrebov added a commit to AYastrebov/shoes that referenced this pull request Aug 24, 2026
…dget

The AnyTLS client session's unified writer channel was created with
mpsc::unbounded_channel(). When the writer loop exits -- close_notify on
session drop, or is_closed on EOF/error -- while the session is still
referenced by live streams, the unbounded sender side stays alive and any
message sent in that window queues into the channel forever: no receiver, no
backpressure. RAM climbed slowly, 80 MB to 100 MB and beyond, and eventually
OOMed.

Switch to a bounded channel so a dead writer applies backpressure instead of
silently accumulating, and break the per-stream forwarding task on
close_notify so it never blocks on a writer that is gone.

The upstream fix (cfal#153) is right in shape but its capacity was
STREAM_CHANNEL_BUFFER * 1024, i.e. 16,384 messages. Each queued message owns
a Bytes that can hold a full frame, so that bound is a few hundred MB. An iOS
packet-tunnel extension has about 50 MB in total, so it would be killed long
before the backpressure ever engaged. 256 keeps the ceiling at a few MB while
staying deep enough that the writer loop is not a bottleneck. Named and
documented rather than derived from the per-stream constant, since the two
bound different things.

Only the AnyTLS client commit from that PR is taken. Its second commit bounds
the Reality/VLESS *server* fallback paths, which this fork does not run.

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AYastrebov added a commit to AYastrebov/shoes that referenced this pull request Aug 24, 2026
…dget

The AnyTLS client session's unified writer channel was created with
mpsc::unbounded_channel(). When the writer loop exits -- close_notify on
session drop, or is_closed on EOF/error -- while the session is still
referenced by live streams, the unbounded sender side stays alive and any
message sent in that window queues into the channel forever: no receiver, no
backpressure. RAM climbed slowly, 80 MB to 100 MB and beyond, and eventually
OOMed.

Switch to a bounded channel so a dead writer applies backpressure instead of
silently accumulating, and break the per-stream forwarding task on
close_notify so it never blocks on a writer that is gone.

The upstream fix (cfal#153) is right in shape but its capacity was
STREAM_CHANNEL_BUFFER * 1024, i.e. 16,384 messages. Each queued message owns
a Bytes that can hold a full frame, so that bound is a few hundred MB. An iOS
packet-tunnel extension has about 50 MB in total, so it would be killed long
before the backpressure ever engaged. 256 keeps the ceiling at a few MB while
staying deep enough that the writer loop is not a bottleneck. Named and
documented rather than derived from the per-stream constant, since the two
bound different things.

Only the AnyTLS client commit from that PR is taken. Its second commit bounds
the Reality/VLESS *server* fallback paths, which this fork does not run.

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant