SystemError / wrong behavior: 15+ sites conflate error-return sentinels with data (`PyObject_IsTrue`, `PyObject_IsInstance`, `PyLong_AsSsize_t`)
维护者通常 4 天内回复
还没有人认领这个 Issue。
评估
调研方向
先从 c-ext/compressor.c、c-ext/decompressor.c 和 c-ext/compressionparams.c 中受影响的入口点开始,然后运行 issue 中的 BadBool 和 dict_size=-1 复现程序。检查所有列出的 PyObject_IsTrue、PyObject_IsInstance 和 PyLong_AsSsize_t 调用位置。完成的标准是,受影响的调用能够区分错误和有效的 -1 值,并且复现程序不再产生 SystemError 或覆盖异常。
由索引模型根据 Issue 内容生成。
描述
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
- 主要语言
- C
- 星标
- 642
- 派生
- 118
- 平均合并
- 8 天 2 小时
- 30 天内合并 PR
- 7
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 没有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
indygreg/python-zstandard 的其他 Issue
-
`multi_decompress_to_buffer([])` terminates the process with SIGFPE可能已有人在做 @mikamikasuki 于 13 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 78/100
indygreg/python-zstandard#335 ·
维护者通常 4 天内回复
-
Silent data-correctness bug: `readinto()` / `readinto1()` on `stream_reader` return `tell() == 0` after successful reads可能已有人在做 @CAOShurong 于 6 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 76/100
indygreg/python-zstandard#295 ·
维护者通常 4 天内回复
-
Python 3.15 wheels可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭
难度 2/5 1-3 小时 新手友好度 55/100
indygreg/python-zstandard#354 · 1 个 reaction ·
维护者通常 4 天内回复
-
`estimated_compression_context_size()` raises SIGFPE with `enable_ldm=True可能已有人在做 @yousefissa 于 1 天前认领。 未关闭
难度 3/5 1-2 天 新手友好度 64/100
indygreg/python-zstandard#345 ·
维护者通常 4 天内回复
-
难度 2/5 1-3 小时 新手友好度 55/100
indygreg/python-zstandard#334 ·
维护者通常 4 天内回复
查看 indygreg/python-zstandard 的全部 Issue
相似的 Issue
-
[BUG] Small issues on iMXRT found by Claude可能已有人在做 @Sarthak-Pandey 今天认领。 未关闭Arch: arm Area: Networking OS: Linux Type: Bug
难度 2/5 1-3 小时 新手友好度 66/100
维护者通常 1 天内回复
-
area: docs eco: cpp good first issue help wanted lane: free-agent roadmap size: XS type: docs
难度 2/5 1-3 小时 新手友好度 70/100
arminazimi/evergreen#475 ·
维护者通常 1 天内回复
-
[Issue]: hipsparselt.h C API as shipped now requires C23 -- ONLY because of missing include or guard未关闭
难度 2/5 1-3 小时 新手友好度 62/100
ROCm/rocm-libraries#13736 ·
维护者通常 2 天内回复
-
libc
难度 2/5 1-3 小时 新手友好度 78/100
llvm/llvm-project#232380 · 1 条评论 ·
维护者通常 1 天内回复
-
level/task module/auth/enrollment platform/linux reporter/qa type/bug
难度 2/5 1-3 小时 新手友好度 66/100
维护者通常 1 天内回复