Memory leaks in stream lifecycle: 4 `__exit__` + 2 `close()` discard CallMethod returns; 5 types leak on `__init__` re-call (~5.5 KB); 2 `Py_buffer` leaks on closed streams
Maintainer thường phản hồi trong vòng 2 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 48/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Ít trao đổi
- Lĩnh vực
- backend, performance
Hướng nghiên cứu
Bắt đầu với bốn vị trí exit/close trong c-ext/compressionreader.c, c-ext/compressionwriter.c, c-ext/decompressionwriter.c và c-ext/decompressionreader.c, sau đó kiểm tra năm triển khai init bị ảnh hưởng và các phương thức writer được nêu trong issue. Chạy các trình tái hiện tracemalloc, RSS và Py_buffer được cung cấp. Hoàn tất khi từng rò rỉ đã được xử lý và hành vi re-init đã được lựa chọn và xác minh một cách rõ ràng.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
Four distinct memory-leak patterns in stream/resource lifecycle. Three are mechanical (discarded CallMethod return values, missing PyBuffer_Release on a closed-stream branch); one is semantic (re-callable __init__ leaks the prior ZSTD contexts). Filing together because all four surface as "memory grows during normal usage of stream writers/readers", but each has a distinct fix and can be addressed independently.
Impact
- Severity: Memory leak — no crash. Magnitude per occurrence ranges from ~30 bytes (discarded
CallMethodreturns) to ~5.5 KB (re-init contexts). - Reachability: Standard idioms —
with comp.stream_writer(...):, explicit.close(),.write()after close. - Version: 0.25.0 (commit
7a77a75). - Platform: Confirmed Linux x86_64 / CPython 3.14 debug; bug is platform-independent.
Leak 1: 4 __exit__ methods discard close() return — ~31 B per with exit
PyObject_CallMethod(self, "close", NULL) returns a new reference; all 4 __exit__ implementations discard it without Py_DECREF.
Reproducer:
import zstandard, tracemalloc, gc
tracemalloc.start(); gc.collect()
s1 = tracemalloc.take_snapshot()
for _ in range(5000):
comp = zstandard.ZstdCompressor()
with comp.stream_writer(open('/dev/null', 'wb')) as w:
w.write(b'hello' * 100)
gc.collect()
s2 = tracemalloc.take_snapshot()
diff = sum(s.size_diff for s in s2.compare_to(s1, 'lineno') if s.size_diff > 0)
print(f"{diff/5000:.1f} bytes per __exit__") # ~31.1
Sites:
c-ext/compressionreader.c:57(compressionreader_exit)c-ext/compressionwriter.c:53(ZstdCompressionWriter_exit)c-ext/decompressionwriter.c:41(ZstdDecompressionWriter_exit)c-ext/decompressionreader.c:57(decompressionreader_exit)
Fix:
PyObject *result = PyObject_CallMethod(self, "close", NULL);
Py_XDECREF(result);
Leak 2: 2 close() methods discard flush() return — ~32 B per close
Same pattern as Leak 1, different method. close() calls self.flush() via PyObject_CallMethod and discards the return.
Reproducer:
import zstandard, tracemalloc, gc
tracemalloc.start(); gc.collect()
s1 = tracemalloc.take_snapshot()
for _ in range(5000):
comp = zstandard.ZstdCompressor()
w = comp.stream_writer(open('/dev/null', 'wb'))
w.write(b'hello' * 100)
w.close()
gc.collect()
s2 = tracemalloc.take_snapshot()
diff = sum(s.size_diff for s in s2.compare_to(s1, 'lineno') if s.size_diff > 0)
print(f"{diff/5000:.1f} bytes per close") # ~31.7
Sites:
c-ext/compressionwriter.c:219(ZstdCompressionWriter_close)c-ext/decompressionwriter.c:155(ZstdDecompressionWriter_close)
Fix: Py_XDECREF(result); after each PyObject_CallMethod(..., "flush", ...) call.
Leak 3: Re-callable __init__ leaks ZSTD contexts — ~5.5 KB per re-init
Calling comp.__init__(...) on an already-initialized instance allocates a new cctx and params (via ZSTD_createCCtx / ZSTD_createCCtxParams) without freeing the prior ones. Uses system malloc, not CPython's allocator — tracemalloc doesn't observe it; RSS grows.
Affected types: ZstdCompressor, ZstdDecompressor, ZstdCompressionDict, BufferWithSegments, BufferWithSegmentsCollection.
Reproducer:
import zstandard, resource, gc
gc.collect()
r1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
comp = zstandard.ZstdCompressor()
for _ in range(50000):
comp.__init__()
gc.collect()
r2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print(f"{(r2-r1)*1024/50000:.0f} bytes per re-init") # ~5541
Fix options:
Option A — free prior state at top of tp_init
if (self->cctx) { ZSTD_freeCCtx(self->cctx); self->cctx = NULL; }
if (self->params) { ZSTD_freeCCtxParams(self->params); self->params = NULL; }
/* ... then allocate ... */
Option B — reject re-init
if (self->cctx) {
PyErr_SetString(PyExc_RuntimeError, "already initialized");
return -1;
}
Option B composes cleanly with a separately-reported __new__() fix (if __new__ allocates the context in tp_new, tp_init simplifies to argument parsing and runtime-configuration only, and re-init naturally becomes "error").
Leak 4: 2 Py_buffer leaks in writer methods on closed streams
The y* arg format acquires a Py_buffer on input data. The "stream is closed" check returns NULL before PyBuffer_Release → buffer stays locked → a later bytearray.extend() on the same data raises BufferError, and the underlying memory is held until the buffer-owning object itself is collected.
Reproducer:
import zstandard
comp = zstandard.ZstdCompressor()
writer = comp.stream_writer(open('/dev/null', 'wb'))
writer.write(b'hello')
writer.close()
data = bytearray(1000)
try:
writer.write(data) # Py_buffer acquired, error, never released
except ValueError:
pass
data.extend(b'x') # BufferError: Existing exports of data
Sites:
c-ext/compressionwriter.c:85(ZstdCompressionWriter_write— closed-check path)c-ext/decompressionwriter.c:73(ZstdDecompressionWriter_write— closed-check path)c-ext/decompressionwriter.c:103(same function —output.dstleak onwriter.write()raising)
Fix:
if (self->closed) {
PyBuffer_Release(&source);
PyErr_SetString(PyExc_ValueError, "stream is closed");
return NULL;
}
Suggested PR shape
Four independent patches; happy to bundle in one PR or split by leak. The Py_XDECREF-on-CallMethod fixes (Leaks 1 + 2) are trivial; Leak 3 is a semantic choice (free-and-reinit vs. reject-reinit); Leak 4 is mechanical.
Methodology
Found via cext-review-toolkit (Tree-sitter-based static analysis with structured naive/informed review passes). All four leaks verified live on CPython 3.14.3 debug build. Leaks 1 + 2 measured via tracemalloc (per-call deltas match the reference-count-size overhead exactly). Leak 3 measured via resource.getrusage(ru_maxrss) because the allocator is libc malloc, outside CPython's tracking. Leak 4 verified via the BufferError observable. Happy to open a PR.
Discovery, root-cause analysis, and issue drafting were performed by Claude Code and reviewed by a human before filing.
Full report
Complete multi-agent analysis (48 FIX findings across 13 categories, plus a reproducer appendix): https://gist.github.com/devdanzin/b86039ac097141579590c1a0f3a43605
- Ngôn ngữ chính
- C
- Star
- 641
- Fork
- 117
- Merge trung bình
- 1 ngày 14 giờ
- Pull request đã merge (30 ngày)
- 5
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của indygreg/python-zstandard
-
`multi_decompress_to_buffer([])` terminates the process with SIGFPECó thể đã có người làm @mikamikasuki đã nhận 5 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
indygreg/python-zstandard#335 ·
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
indygreg/python-zstandard#295 ·
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 64/100
indygreg/python-zstandard#345 ·
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 55/100
indygreg/python-zstandard#334 ·
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 54/100
indygreg/python-zstandard#333 ·
Maintainer thường phản hồi trong vòng 2 ngày
Tất cả issue của indygreg/python-zstandard
Issue tương tự
-
chore(gateway): emit INFO budget reserved/settled logs for proactivity v2 (chip task_2855f4ec)Đang mởbackend
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
BasedHardware/omi#20940 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Linux notifications: the default action's ' ' label shows as a blank button in xfce4-notifydĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
kovidgoyal/kitty#10625 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Feature Status: Needs Triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 73/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 67/100
Maintainer thường phản hồi trong vòng 1 ngày
-
docs
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
Maintainer thường phản hồi trong vòng 1 ngày