SystemError / wrong behavior: 15+ sites conflate error-return sentinels with data (`PyObject_IsTrue`, `PyObject_IsInstance`, `PyLong_AsSsize_t`)
Maintainer thường phản hồi trong vòng 4 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
- 52/100
Hướng nghiên cứu
Bắt đầu với các điểm vào bị ảnh hưởng trong c-ext/compressor.c, c-ext/decompressor.c và c-ext/compressionparams.c, sau đó chạy các trình tái hiện BadBool và dict_size=-1 từ issue. Rà soát tất cả các vị trí PyObject_IsTrue, PyObject_IsInstance và PyLong_AsSsize_t được liệt kê. Hoàn tất khi các lệnh gọi bị ảnh hưởng phân biệt được lỗi với các giá trị -1 hợp lệ và các trình tái hiện không còn tạo ra SystemError hoặc các ngoại lệ bị ghi đè.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
Three Python/C-API functions — PyObject_IsTrue, PyObject_IsInstance, and PyLong_AsSsize_t — return -1 to signal an error and -1 can be a legitimate data value. zstandard does not distinguish the two at ~15 sites. Observable consequences:
PyObject_IsTrue:-1is stored as anintfield value (closefd = -1,read_across_frames = -1, ...) or used in!r/r == 0comparisons, returning success with a pending exception →SystemError: <method> returned a result with an exception set(fatal abort on debug builds).PyObject_IsInstance:!rmaps-1to0(false), silently replacing the caller's exception withTypeError("dict_data must be zstd.ZstdCompressionDict").PyLong_AsSsize_t: legitimate-1is conflated with the error sentinel without checkingPyErr_Occurred();from_level(dict_size=-1)takes the error branch without setting an exception →SystemError: returned NULL without setting an exception(fatal abort on debug builds).
Impact
- Severity: Fatal
SystemErroron debug builds; silent wrong behavior / clobbered exceptions on release. - Reachability: Standard Python — pass a
__bool__that raises, a negativedict_size, or a__instancecheck__that raises. No unusual setup. - Version: 0.25.0 (commit
7a77a75). - Platform: Confirmed Linux x86_64 / CPython 3.14 debug; bug is platform-independent.
Reproducers
PyObject_IsTrue — 11 sites, one trigger pattern
import zstandard, io
class BadBool:
def __bool__(self):
raise ValueError("bad bool")
comp = zstandard.ZstdCompressor()
comp.stream_writer(io.BytesIO(), closefd=BadBool())
# Fatal Python error: <method 'stream_writer'> returned a result with an exception set
# SystemError: <method 'stream_writer'> returned a result with an exception set
All 11 confirmed sites:
comp.stream_reader(source, closefd=BadBool())
comp.stream_writer(dest, closefd=BadBool())
comp.stream_writer(dest, write_return_read=BadBool())
decomp.stream_reader(source, closefd=BadBool())
decomp.stream_reader(source, read_across_frames=BadBool())
decomp.stream_writer(dest, closefd=BadBool())
decomp.stream_writer(dest, write_return_read=BadBool())
decomp.decompress(data, allow_extra_data=BadBool())
zstandard.ZstdCompressor(write_checksum=BadBool())
zstandard.ZstdCompressor(write_content_size=BadBool())
zstandard.ZstdCompressor(write_dict_id=BadBool())
PyLong_AsSsize_t — from_level(dict_size=-1) aborts
import zstandard
zstandard.ZstdCompressionParameters.from_level(3, dict_size=-1)
# Fatal Python error: returned NULL without setting an exception
# SystemError: <built-in method from_level> returned NULL without setting an exception
PyObject_IsInstance — 3 sites
Pattern: !PyObject_IsInstance(obj, ...) in validation for the dict_data kwarg on compressor / decompressor init. Confirmed by static review — reproducing requires a non-cooperative __instancecheck__, which is less idiomatic to construct than BadBool. Impact is exception clobbering rather than a crash: caller's exception from __instancecheck__ is replaced with TypeError("dict_data must be zstd.ZstdCompressionDict").
Root cause
All three APIs return -1 on error and -1 can be a valid value. The correct patterns:
PyObject_IsTrue/PyObject_IsInstance: check< 0before using the return as a boolean.PyLong_AsSsize_t: when the result is-1, checkPyErr_Occurred()to distinguish a legitimate-1from an error.
zstandard stores the PyObject_IsTrue return directly into an int field (result->closefd = PyObject_IsTrue(obj);), uses !PyObject_IsInstance(...) without a prior < 0 check, and takes a goto cleanup on PyLong_AsSsize_t == -1 without consulting PyErr_Occurred.
Affected sites
PyObject_IsTrue — 11
| File | Line(s) | Kwarg / field |
|---|---|---|
c-ext/compressor.c |
487 | write_checksum, write_content_size, write_dict_id (init) |
c-ext/compressor.c |
752-754 | closefd, write_return_read on stream_reader / stream_writer |
c-ext/decompressor.c |
381 | closefd on stream_reader |
c-ext/decompressor.c |
431 | read_across_frames |
c-ext/decompressor.c |
562-563 | closefd, write_return_read on stream_writer |
c-ext/decompressor.c |
614-615 | allow_extra_data on decompress() |
PyObject_IsInstance — 3
| File | Line | Context |
|---|---|---|
c-ext/compressor.c |
128 | dict_data type check in ZstdCompressor_init |
c-ext/compressor.c |
140 | dict_data type check (2nd occurrence) |
c-ext/decompressor.c |
78 | dict_data type check in Decompressor_init |
PyLong_AsSsize_t — reported site
| File | Line | Context |
|---|---|---|
c-ext/compressionparams.c |
271 | dict_size parameter to from_level() |
The PyLong_AsSsize_t calls in copy_stream (c-ext/compressor.c around 411; c-ext/decompressor.c around 247) interact with an OOM cleanup path to produce a separate Py_DECREF(NULL) — noted here for completeness; details in the full report linked below.
Suggested fix
Three mechanical patterns; one per API.
PyObject_IsTrue:
int r = PyObject_IsTrue(obj);
if (r < 0) goto except;
result->closefd = r ? 1 : 0;
PyObject_IsInstance:
int r = PyObject_IsInstance(obj, (PyObject *)&ZstdCompressionDict_Type);
if (r < 0) goto except;
if (!r) {
PyErr_SetString(PyExc_TypeError, "dict_data must be zstd.ZstdCompressionDict");
goto except;
}
PyLong_AsSsize_t:
Py_ssize_t dict_size_val = PyLong_AsSsize_t(dict_size);
if (dict_size_val == -1 && PyErr_Occurred()) goto cleanup;
if (dict_size_val < 0) {
PyErr_SetString(PyExc_ValueError, "dict_size must be non-negative");
goto cleanup;
}
All three can land in a single PR. If you prefer finer-grained review, happy to split into 3 PRs (one per API).
Methodology
Found via cext-review-toolkit (Tree-sitter-based static analysis with structured naive/informed review passes). 11 of 11 BadBool() sites produce a SystemError fatal abort on CPython 3.14.3 debug. from_level(dict_size=-1) also verified on the same build. PyObject_IsInstance sites confirmed via static review. 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
- 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 9 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
-
Silent data-correctness bug: `readinto()` / `readinto1()` on `stream_reader` return `tell() == 0` after successful readsCó thể đã có người làm @CAOShurong đã nhận 3 ngày trước. Đang mở
Độ 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 4 ngày
-
Python 3.15 wheelsĐ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 ·
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 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
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 68/100
KalicoCrew/kalico#1012 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 79/100
joncampbell123/dosbox-x#6664 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
fails to compile with Qt6Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
osam-cologne/archlinux-proaudio#909 ·
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
tpm2-software/tpm2-abrmd#879 ·
Maintainer thường phản hồi trong vòng 1 ngày