Silent data-correctness bug: `readinto()` / `readinto1()` on `stream_reader` return `tell() == 0` after successful reads
Maintainer thường phản hồi trong vòng 4 ngày
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 76/100
Hướng nghiên cứu
Bắt đầu trong c-ext/compressionreader.c, tại phần thân của các phương thức readinto và readinto1, so sánh ZSTD_outBuffer cục bộ của chúng với việc theo dõi output được duy trì liên tục của đường dẫn read(). Chạy trình tái hiện Python được cung cấp và xác minh rằng tell() sau readinto() và readinto1() tăng nhất quán với read(); kiểm tra mẫu ở phía giải nén nếu phù hợp.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
ZstdCompressionReader.readinto(buf) and .readinto1(buf) correctly copy compressed bytes into the caller's buffer, but the reader's internal bytesCompressed counter is not updated. As a result, stream_reader.tell() returns 0 after any number of successful readinto() calls, even though bytes were in fact written. The equivalent .read() path correctly advances the counter.
Impact
- Severity: Silent data-correctness bug — no crash, no exception, just a wrong value from
tell(). Any caller that relies ontell()to measure progress, compute offsets, or compare positions will silently malfunction. - Reachability: Standard
io.RawIOBaseidioms — any use ofreadinto()/readinto1()on astream_reader. Common in performance-sensitive decode pipelines that reuse a pre-allocated buffer. - Version: 0.25.0 (commit
7a77a75). - Platform: Platform-independent.
Reproducer
import zstandard, io
data = b'hello world ' * 10000
# read() — tell() works correctly
comp1 = zstandard.ZstdCompressor()
r1 = comp1.stream_reader(io.BytesIO(data))
r1.__enter__()
while r1.read(1024):
pass
print("read tell:", r1.tell()) # 29 (correct: total compressed bytes)
r1.__exit__(None, None, None)
# readinto() — tell() stuck at 0
comp2 = zstandard.ZstdCompressor()
r2 = comp2.stream_reader(io.BytesIO(data))
r2.__enter__()
buf = bytearray(1024)
while r2.readinto(buf):
pass
print("readinto tell:", r2.tell()) # 0 — BUG (should match read() path)
r2.__exit__(None, None, None)
Root cause
readinto / readinto1 build a ZSTD_outBuffer on the stack that wraps the caller's buffer:
ZSTD_outBuffer output = {dest, dest_size, 0};
zresult = ZSTD_compressStream2(cctx, &output, &input, ZSTD_e_continue);
After the call, output.pos holds the number of bytes written to dest. The reader then updates its position by reading from self->output.pos — the persistent struct, which the local-struct call never touched. So self->output.pos stays at zero, and bytesCompressed never advances.
The read() path uses self->output directly (not a local copy), so the persistent field is updated by ZSTD_compressStream2. That's why tell() works after read() but not after readinto.
Affected sites
c-ext/compressionreader.c—readintoandreadinto1method bodies.
(Same pattern may warrant a look on the decompression side as well, although the main analysis only flagged compression.)
Suggested fix
Two options; either is minimal.
Option A — advance from the local struct
Read bytesCompressed from the local output.pos before it goes out of scope:
ZSTD_outBuffer output = {dest, dest_size, 0};
zresult = ZSTD_compressStream2(cctx, &output, &input, ZSTD_e_continue);
/* ... */
self->bytesCompressed += output.pos; /* was: effectively + self->output.pos, i.e. 0 */
Option B — share the persistent struct
If you'd rather the two methods share the read() bookkeeping path, use self->output directly instead of a stack-local struct, and update self->output.dst / self->output.size to point at the caller's buffer before the call. Slightly more invasive but avoids duplicating the position-update logic.
Methodology
Found via cext-review-toolkit (Tree-sitter-based static analysis with structured naive/informed review passes). Reproducer verified live on CPython 3.14.3 debug build — read() path returns tell() == 29 (matches the compressed-output length); readinto() path returns tell() == 0 after the exact same compressed sequence is consumed. 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
- 642
- Fork
- 118
- Merge trung bình
- 8 ngày 2 giờ
- Pull request đã merge (30 ngày)
- 7
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 14 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 4 ngày
-
Python 3.15 wheelsCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 55/100
indygreg/python-zstandard#354 · 1 reaction ·
Maintainer thường phản hồi trong vòng 4 ngày
-
`estimated_compression_context_size()` raises SIGFPE with `enable_ldm=TrueCó thể đã có người làm @yousefissa đã nhận 1 ngày trước. Đang mở
Độ 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 4 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 4 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 4 ngày
Tất cả issue của indygreg/python-zstandard
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 66/100
FujiNetWIFI/fujinet-lib#186 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
VIEW_TYPE set for computer playersCó thể đã có người làm @Loobinex đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/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 76/100
-
bug
Độ 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 2 ngày
-
bug
Độ khó 2/5 Dưới một giờ Mức phù hợp với người mới 78/100
brunodev85/winlator-app#90 ·