Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

Silent data-correctness bug: `readinto()` / `readinto1()` on `stream_reader` return `tell() == 0` after successful reads

Đang mở Phù hợp với người mới
#295 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 4 ngày

@CAOShurong đang làm issue này rồi.

Từ ngày 9/10/2026.

  • #351 của @CAOShurong — đang mở

Đá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
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Ít trao đổi
Công nghệ
c, python
Lĩnh vực
backend

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 on tell() to measure progress, compute offsets, or compare positions will silently malfunction.
  • Reachability: Standard io.RawIOBase idioms — any use of readinto() / readinto1() on a stream_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 — readinto and readinto1 method 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

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. 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.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của indygreg/python-zstandard

Tất cả issue của indygreg/python-zstandard

Issue tương tự

Thêm issue về C

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.