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
full GC: cwist_gc_scope_flush() mid-request retires live objects; reclaim never runs (leak today, UAF if it does) #347
cwist_gc_scope_flush() retires every block the calling thread still tracks. That is safe only when no tracked block is still in use. Nothing in the API or docs says so, and fly.board called it from a request middleware (full_gc_flush_middleware) right after next() returned. The middleware has since been removed in gg582/fly.board@db27855.
Everything below comes from reading the code. The memory growth was not measured.
What the mid-request flush retired
Response headers added with cwist_http_header_add(&res->headers, ...). These nodes have no arena, so they are cwist_alloc'd and tracked. After the flush, the compress middleware still reads and edits them, and the sender serializes them.
h2 session state on the serving thread: out_buf, the HPACK table, and anything allocated after a resume.
Any other per-thread object that lives past the call.
Why it did not crash, and why it would
Retired blocks are freed only by ttak_epoch_reclaim():
The full-GC instance runs in manual rotation, so libttak's rotate thread is never started (epoch_gc.c).
Inside CWIST, the only caller is the io_queue (io_queue.c:108), which is used only by schedulers (cwist_app_use_scheduler, cwist_async_set_timeout).
An app that uses neither, like fly.board, therefore never frees what it flushes:
Today: leak. The flush reclaims nothing. Every retire also allocates a node that is never returned (epoch_node_pool_acquire), and real leaks stay leaked.
As soon as anything reclaims: use-after-free and double free. Request-serving threads never enter an epoch, so reclaim treats every retired block as free to drop. That includes blocks still in use, and blocks their owners already cwist_free'd after the flush removed them from tracking, so cwist_free could no longer untrack them.
Asks
Document the contract on cwist_gc_scope_flush() (gc.h, docs/GC.md): call it only at a point where the thread holds no live tracked block, such as the end of a task or job, and never from a middleware or while a connection or session it serves is alive. Spell out the reclaim dependency.
Make cwist CLI tooling warn when an app calls cwist_gc_scope_flush() (and similar ownership-sensitive APIs) so the author has to confirm they meet the contract.
Decide whether the full-GC flush should keep using EBR retire without anyone driving reclaim. Either drive reclaim, or free directly with a documented "nothing live" precondition.
Reclaim is now driven by the flush itself (dev 0dd803e): cwist_gc_scope_flush() and the thread-exit sweep call ttak_epoch_reclaim() whenever they retired something, so flushed blocks are freed instead of piling up. test_full_gc_tracking checks with mallinfo2() that 32 MiB of forgotten blocks are returned after a flush and after a thread exits. The gc.h contract, docs/GC.md (en/ko) and the CLI hazard text now say a misplaced flush is a use-after-free, not a leak.
Activity
gg582 commentedon Oct 10, 2026
Progress:
gc.h,docs/GC.md, anddocs/ko/GC.md(docs(gc)+cli: cwist_gc_scope_flush contract and hazard warnings (#347) #348).cwist audit/cwist watchernow warn on ownership-sensitive full-GC calls until each call site is acknowledged (docs(gc)+cli: cwist_gc_scope_flush contract and hazard warnings (#347) #348).Merge pull request #348 from c4punks/docs/347-gc-flush-contract
gg582 commentedon Oct 10, 2026
Well it SHOULD reclaim. No other opinions are needed. AI gone weird
fix(gc): full-GC flush and thread-exit sweep reclaim what they retire
gg582 commentedon Oct 11, 2026
Reclaim is now driven by the flush itself (dev 0dd803e):
cwist_gc_scope_flush()and the thread-exit sweep callttak_epoch_reclaim()whenever they retired something, so flushed blocks are freed instead of piling up.test_full_gc_trackingchecks withmallinfo2()that 32 MiB of forgotten blocks are returned after a flush and after a thread exits. The gc.h contract, docs/GC.md (en/ko) and the CLI hazard text now say a misplaced flush is a use-after-free, not a leak.docs(gc): state the cwist_gc_scope_flush() contract and the reclaim d…
feat(cli): warn on ownership-sensitive full-GC calls until acknowledg…
fix(gc): bump libttak for the EBR grace period; flush reclaims twice
fix(gc): bump libttak for the EBR grace period; flush reclaims twice