Skip to content
This repository was archived by the owner on Apr 18, 2026. It is now read-only.

experimental: Ring buffer reuse for leaked 160-byte objects#1

Closed
dalsoop wants to merge 1 commit intomasterfrom
experimental/ring-buffer-reuse
Closed

experimental: Ring buffer reuse for leaked 160-byte objects#1
dalsoop wants to merge 1 commit intomasterfrom
experimental/ring-buffer-reuse

Conversation

@dalsoop
Copy link
Copy Markdown
Owner

@dalsoop dalsoop commented Mar 21, 2026

Summary

Replace the pool-based approach with a ring buffer that forcibly recycles old allocations.

Why

The current master branch pools freed 160-byte blocks for reuse. But testing confirmed free() is never called for the leaked objects (free=0 in all tests). This means the pool stays empty and memory keeps growing — Tier 1 is effectively a no-op for this leak.

What this does

After threshold (10GB / ~60M allocations):

  • New malloc(160) calls recycle the oldest allocation from a ring buffer
  • Most recent 1M allocations (~160MB) are preserved, giving the caller time to finish using them
  • Older allocations are zeroed and returned to new callers
[slot 0] [slot 1] ... [slot 999999] [slot 0] [slot 1] ...
  ↑ recycled                          ↑ current
  (oldest, safe to reuse?)            (just allocated)

Risk

If the caller still holds a reference to a recycled allocation and reads it, data corruption is possible.

Evidence that this is safe (but unproven):

  • Heap dump shows all objects are 85% zeros and identical structure
  • Objects appear to be write-once, never-read (allocated, minimally initialized, then abandoned)
  • 1.15 billion objects accumulate with no observable side effects → references are likely lost

What's needed to merge

  • Long-running interactive session test (several hours of active tool use)
  • Verify reuse counter increases and no crashes/corruption occur
  • Compare output quality with and without guard active

How to test

git checkout experimental/ring-buffer-reuse
make
# Use a low threshold for faster testing:
# Edit THRESHOLD to 100 in claude_malloc_guard.c, rebuild
sudo make install
claude-safe
# Use actively for a while, then:
kill -USR1 $(pgrep -n claude)
cat /tmp/claude_malloc_guard.log
# Look for reuse > 0 and no crashes

🤖 Generated with Claude Code

The pool-based approach on master cannot reclaim leaked objects
because free() is never called (confirmed: free=0 in all tests).

This branch uses a ring buffer instead:
- After threshold (10GB), new malloc(160) calls recycle the oldest
  allocation in the ring
- Most recent 1M allocations (~160MB) are preserved
- Older allocations are overwritten and returned to new callers

⚠ EXPERIMENTAL — needs long-running session validation.
Risk: if caller reads recycled memory, data corruption possible.
Heap dump shows objects are 85% zeros and likely write-once,
but this is unverified under sustained load.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Owner Author

@dalsoop dalsoop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main correctness risk I see is the interaction between the new ring and free().

In this branch, ring slots are never cleared or updated when a pointer is later freed by the caller. So if even a small fraction of these 160-byte allocations are eventually freed in real workloads, the ring can keep a stale pointer and later hand that freed allocation back out from malloc(160).

That means the safety argument here is stronger than just “maybe the caller still reads old allocations”. There is also a more direct possibility of reissuing memory that has already been returned to the allocator.

If you want to keep this approach experimental, I would at least make the ring aware of frees for tracked pointers before trusting long-running sessions. Otherwise the branch is assuming both:

  • leaked objects are never read again
  • and they are never freed later either

The first assumption is already risky; the second one is not enforced by the code.

@dalsoop
Copy link
Copy Markdown
Owner Author

dalsoop commented Mar 29, 2026

리뷰 포인트 1개는 치명적이라 바로 남깁니다.

이 branch는 free() 쪽 tracking/pool 로직을 완전히 제거했는데, 그 결과 ring 안에 이미 등록된 160-byte 포인터가 나중에 실제로 free()되더라도 ring에서는 빠지지 않습니다. 그러면 이후 threshold 이후 malloc(160)이 그 dangling pointer를 다시 꺼내서 caller에게 돌려줄 수 있습니다. 즉 지금 구현은 "caller가 정말 never-free인지"를 강하게 가정하고 있고, 그 가정이 한 번만 깨져도 use-after-free / double-allocation 경로가 생깁니다.

PR 설명에도 free=0 관측은 적혀 있지만, 이건 테스트에서 그랬다는 뜻이지 계약 보장이 아닙니다. 최소한 ring에 들어간 포인터가 실제 free()될 때 제거되도록 tracking을 유지하거나, 재사용 대상이 아직 allocator 소유권 바깥에 있다는 걸 증명할 장치가 있어야 merge 가능한 수준이라고 봅니다.

@dalsoop
Copy link
Copy Markdown
Owner Author

dalsoop commented Apr 10, 2026

20일 묵은 experimental PR. long-running session test 미수행 + 위험 (data corruption 가능). 필요 시 재오픈.

@dalsoop dalsoop closed this Apr 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant