Fix-propagation gaps: `compressionreader` missing 3 mirrors of fixes previously applied to `decompressionreader`
メンテナーはふだん 10 日以内に返信
まだ誰も着手していません。
評価
調査の方向性
c-ext/compressionreader.c から始め、compressionreader_read と compressionreader_dealloc を decompressionreader 側の対応するものと比較し、次に c-ext/compressor.c の read_to_iter 周辺とその except ラベルを調べます。3 つのエラーパスを確認し、read leak 用に提供されている Python reproducer を実行します。圧縮パスが対応するコードのクリーンアップ動作を踏襲し、通常のストリーミング動作を変更していなければ完了です。
索引モデルが issue の本文から書いたものです。
説明
Summary
Three bugs in compressionreader.c / compressor.c mirror fixes that were previously applied to the decompression side but never propagated to the compression side. Each is a memory leak or a mis-routed error path on the compression reader.
Impact
- Severity: Memory leaks on error paths (Gaps 1, 2); missing iterator cleanup on an error branch (Gap 3). Not crashes.
- Reachability: Error paths —
read()raising, GC during streaming, compression-iterator constructor failure. - Version: 0.25.0 (commit
7a77a75).
Gap 1: Missing Py_XDECREF(result) in compressionreader_read — ~1.1 KB per read error
The sibling function decompressionreader_read correctly does Py_XDECREF(result) at the same error point (compare c-ext/decompressionreader.c:290-291). The compression reader has the same result variable but is missing the cleanup — when read_compressor_input fails inside the read loop, the allocated result PyBytes is not freed.
Site: c-ext/compressionreader.c:294-295.
Reproducer:
import zstandard, tracemalloc, gc
class FailingReader:
def __init__(self, data):
self.data, self.pos, self.calls = data, 0, 0
def read(self, size):
self.calls += 1
if self.calls > 1:
raise IOError("read failed")
chunk = self.data[self.pos:self.pos + size]
self.pos += len(chunk)
return chunk
tracemalloc.start(); gc.collect()
s1 = tracemalloc.take_snapshot()
for _ in range(2000):
comp = zstandard.ZstdCompressor()
r = comp.stream_reader(FailingReader(b'x' * 100_000))
r.__enter__()
try:
r.read(1024); r.read(1024)
except IOError:
pass
try:
r.__exit__(None, None, None)
except:
pass
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/2000:.1f} bytes per error") # ~1121 (vs ~64 for decompressionreader equivalent)
Fix: Add Py_XDECREF(result); on the error branch, mirroring decompressionreader_read.
Gap 2: Missing Py_CLEAR(readResult) in compressionreader_dealloc
Historical analog: an identical bug was fixed in decompressionreader_dealloc in commit 35af547. The compression reader has the same readResult field, but the fix was never propagated. Leak manifests when the reader is destroyed while holding unprocessed data (exception during compression, GC during active streaming).
Site: c-ext/compressionreader.c:13-22.
Fix (mirror the sibling):
static void compressionreader_dealloc(ZstdCompressionReader *self) {
PyTypeObject *tp = Py_TYPE(self);
Py_CLEAR(self->compressor);
Py_CLEAR(self->source);
Py_CLEAR(self->readResult); /* NEW — missing */
tp->tp_free(self);
Py_DECREF(tp); /* to land together with the heap-type dealloc fix */
}
Gap 3: return NULL bypasses cleanup label in read_to_iter
At c-ext/compressor.c:665, a return NULL; short-circuits past the except: label that would clean up the partially-constructed ZstdCompressorIterator and its held buffer/reader refs. Every call site that hits this branch leaks an iterator object.
Site: c-ext/compressor.c:665.
Fix: Change return NULL; to goto except;.
Suggested PR shape
Three small patches; could be a single PR (they're all in the compression-reader/compressor family) or three tiny ones. The Gap 1 fix is the only one with a measurable runtime reproducer; Gaps 2 and 3 are static-confirmed from sibling diff and error-path review.
Methodology
Found via cext-review-toolkit (Tree-sitter-based static analysis with structured naive/informed review passes). The toolkit's git-history analyzer flagged commit 35af547 (the decompressor fix) and searched the codebase for structurally similar unpatched code — Gap 2 came out of that search directly. Gap 1 was verified live on CPython 3.14.3 debug: 1,121 B per error vs. ~64 B on the sibling path. Gap 3 was confirmed via static review of the error-path structure. 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
- 主要言語
- C
- スター
- 642
- フォーク
- 118
- 平均マージ
- 9日 3時間
- マージ済み PR(30日)
- 6
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
indygreg/python-zstandard のほかの issue
-
`multi_decompress_to_buffer([])` terminates the process with SIGFPE対応中かも @mikamikasuki が 16 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
indygreg/python-zstandard#335 ·
メンテナーはふだん 10 日以内に返信
-
Silent data-correctness bug: `readinto()` / `readinto1()` on `stream_reader` return `tell() == 0` after successful reads対応中かも @CAOShurong が 10 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
indygreg/python-zstandard#295 ·
メンテナーはふだん 10 日以内に返信
-
CFFI backend: crash when ZstdDecompressor instances share a ZstdCompressionDict対応中かも @pylaterreur が 2 日前に担当しました。 オープン
難易度 3/5 1〜2日 初心者へのやさしさ 45/100
indygreg/python-zstandard#361 ·
メンテナーはふだん 10 日以内に返信
-
Python 3.15 wheels対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 55/100
indygreg/python-zstandard#354 · リアクション 1 件 ·
メンテナーはふだん 10 日以内に返信
-
`estimated_compression_context_size()` raises SIGFPE with `enable_ldm=True対応中かも @yousefissa が 4 日前に担当しました。 オープン
難易度 3/5 1〜2日 初心者へのやさしさ 64/100
indygreg/python-zstandard#345 ·
メンテナーはふだん 10 日以内に返信
indygreg/python-zstandard の issue をすべて見る
似ている issue
-
mewencode: base256[] and base128[] are indexed out of bounds by a non-ASCII octet対応中かも @kazu-yamamoto が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
kazu-yamamoto/Mew#276 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 91/100
FujiNetWIFI/fujinet-firmware#2045 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
メンテナーはふだん 1 日以内に返信
-
area:gates area:gfx found-by:critic kind:gate-defect
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
InauguralSystems/EigenScript#1825 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 75/100
gbdev/homebrewhub-submit#20 ·