Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

`MemoryError` silently swallowed in 20 sites: 18 `PyObject_HasAttrString` + 2 unguarded `PyErr_Clear` after `PyObject_GetBuffer` (silent data loss in `flush()` / `close()`)

未关闭
#299 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 2 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
62/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
冷清
技术栈
c, python
领域
backend

调研方向

首先在 c-ext/*.c 中 grep PyObject_HasAttrString,并检查 compressor、decompressor、reader、writer 和 copy_stream 的 18 个位置。然后阅读 c-ext/compressor.c:1454 和 c-ext/decompressor.c:1660,以及报告的复现程序,以验证当前的异常行为。完成的标准是:真正的 lookup 和 buffer protocol 异常能够传播,而缺失属性和预期的 buffer 错误保持现有行为。

由索引模型根据 Issue 内容生成。

描述

Summary

PyObject_HasAttrString returns 0 for both "attribute absent" and "an exception was raised during lookup" — making it unsafe in the presence of MemoryError, KeyboardInterrupt, or a user-defined __getattr__ that raises. zstandard uses it at 18 sites. Two observable consequences:

  • flush() / close() on the underlying writer's methods are silently skipped on MemoryError → data silently lost.
  • copy_stream argument validation replaces MemoryError with ValueError("first argument must have a read() method").

Two additional sites in the buffer-protocol path call PyErr_Clear() indiscriminately after PyObject_GetBuffer, replacing MemoryError from __buffer__ with a generic TypeError("item N not a bytes like object").

pythoncapi_compat.h (already included in the codebase!) provides PyObject_HasAttrStringWithError, which returns -1 on error — exactly the missing piece.

Impact

  • Severity: Silent data loss (skipped flush() / close()); clobbered exception class/message elsewhere.
  • Reachability: Any program that can raise MemoryError during attribute access or buffer-protocol operations — OOM, memory-constrained environments, code that explicitly raises via __getattr__ / __buffer__.
  • Version: 0.25.0 (commit 7a77a75).

Pattern 1: 18 PyObject_HasAttrString sites

Reproducer (silent data loss):

import zstandard, io

class EvilWriter:
    def __init__(self):
        self._data = io.BytesIO()
    def write(self, data):
        return self._data.write(data)
    def __getattr__(self, name):
        if name in ('flush', 'close'):
            raise MemoryError("OOM in __getattr__")
        raise AttributeError(name)

comp = zstandard.ZstdCompressor()
writer = comp.stream_writer(EvilWriter())
writer.write(b'hello world' * 100)
writer.flush()
# Silently skips EvilWriter.flush (which would raise).
# CPython 3.14 prints "Exception ignored in PyObject_HasAttrString()" to stderr.

Fix:

int r = PyObject_HasAttrStringWithError(writer, "flush");
if (r < 0) {
    return NULL;              /* propagate the exception */
}
if (r) {
    /* has flush — call it */
} else {
    /* no flush — skip */
}

18 sites across compressor, decompressor, reader, writer, and copy_stream validation. I'll enumerate them explicitly in the PR; a grep PyObject_HasAttrString c-ext/*.c gives the full list.

Pattern 2: Unguarded PyErr_Clear after PyObject_GetBuffer — 2 sites

Clears any error from the buffer-protocol call indiscriminately, including MemoryError from __buffer__.

Sites: c-ext/compressor.c:1454, c-ext/decompressor.c:1660.

Reproducer:

import zstandard

class OOMBuffer:
    def __buffer__(self, flags):
        raise MemoryError("OOM in __buffer__")

zstandard.ZstdCompressor().multi_compress_to_buffer([OOMBuffer()])
# TypeError: item 0 not a bytes like object
# Expected: MemoryError: OOM in __buffer__

Fix — only clear if the exception is a genuine buffer-protocol error:

if (PyObject_GetBuffer(obj, &buf, PyBUF_SIMPLE) != 0) {
    if (PyErr_ExceptionMatches(PyExc_TypeError) ||
        PyErr_ExceptionMatches(PyExc_BufferError)) {
        PyErr_Clear();
    } else {
        goto except;         /* MemoryError etc. — propagate */
    }
    /* fall through to set a specific TypeError with the item index */
}

Suggested PR shape

One PR for Pattern 1 (18 mechanical HasAttrString → HasAttrStringWithError migrations) + one PR for Pattern 2 (2 guarded PyErr_Clear calls). They can also land together — the common thread is "don't let the error-handling infrastructure swallow genuine exceptions".

Methodology

Found via cext-review-toolkit (Tree-sitter-based static analysis with structured naive/informed review passes). Pattern 1 reproducer verified live on CPython 3.14.3 debug — writer.flush() silently skips the EvilWriter.flush call; CPython's own "Exception ignored" warning is the only user-visible hint that something went wrong. Pattern 2 reproducer also verified live — MemoryError is replaced by TypeError. 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
星标
641
派生
117
平均合并
1 天 14 小时
30 天内合并 PR
5

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

indygreg/python-zstandard 的其他 Issue

查看 indygreg/python-zstandard 的全部 Issue

相似的 Issue

更多 C Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。