Thin mode: segfault when a cached query is executed again after its first execute failed with ORA-01438

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

@jhchong0405 已经在做这个了。

开始于 2026年11月10日。

  • #632 来自 @jhchong0405 —— 未关闭

评估

难度
4/5
预计耗时
1-2 天
新手友好度
38/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
python

调研方向

Start with MessageWithData._process_error_info() and ExecuteMessage.process() in src/oracledb/impl/thin/messages/base.pyx, then trace how cached statements are re-executed and row data is processed. Reproduce with the provided script in Thin mode using statement caching enabled; stmtcachesize=0 avoids the crash. Done means the second execution returns its row without a segmentation fault, with synchronous and asynchronous regression coverage.

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

描述

  1. What versions are you using?
connection.version: 18.0.0.0.0  (Oracle Database 18c XE 18.4, Docker image gvenzl/oracle-xe:18.4.0-slim)
platform.platform: macOS-27.0.1-arm64-arm-64bit
sys.maxsize > 2**32: True
platform.python_version: 3.12.14
oracledb.__version__: 26.1.0

I also reproduced it with 26.0.1, with 4.0.2 and with main at 4c669ddb6ed18274d394f3d0116321859652f24b (built from source, reports 26.2.0b1).

  1. Is it an error or a hang or a crash?

A crash. The Python process ends with a segmentation fault.

  1. What error(s) or behavior you are seeing?

In Thin mode, a query fails with ORA-01438 the first time it is executed on a connection. That is correct. The next execute of the same SQL text on the same connection crashes the process:

$ python -u repro.py
oracledb 26.1.0 database 18.0.0.0.0
first execute: ORA-01438: value larger than specified precision allowed for this column
Help: https://docs.oracle.com/error-help/db/ora-01438/
Fatal Python error: Segmentation fault

Current thread 0x00000001ed09bf80 (most recent call first):
  File ".venv/lib/python3.12/site-packages/oracledb/cursor.py", line 898 in wrapped_f
  File "repro.py", line 24 in <module>

Extension modules: oracledb.arrow_impl, oracledb.base_impl, oracledb.thick_impl, _cffi_backend, oracledb.thin_impl (total: 5)

The macOS crash report shows EXC_BAD_ACCESS (SIGSEGV), KERN_INVALID_ADDRESS, with the top frame in oracledb.thin_impl.MessageWithData.

What I found:

  • It occurs with oracledb.connect() and with oracledb.connect_async().
  • It needs the statement cache. With stmtcachesize=0 there is no crash.
  • There is no crash if the first execute of the statement succeeds and a later execute fails with ORA-01438.
  • There is no crash if the first execute fails with ORA-01722 or ORA-01476 instead.
  • I did not test Thick mode. I did not test other database versions.

The cause, as far as I can tell from the source (main at 4c669ddb):

  1. The first execute is a full execute (parse, execute, fetch). The database raises ORA-01438 while it produces the first row, so the response contains only the error information (with a cursor id). No describe information arrives, and Statement._fetch_var_impls stays None.
  2. MessageWithData._process_error_info() in src/oracledb/impl/thin/messages/base.pyx stores the cursor id on the statement. Error 1438 is in ERR_INTEGRITY_ERROR_CODES, so clear_cursor() is not called. ExecuteMessage.process() then sets _executed = True. The statement returns to the statement cache with a cursor id, with _executed = True and without fetch variables.
  3. The next execute gets this statement from the cache. ExecuteMessage._write_message() selects TNS_FUNC_REEXECUTE_AND_FETCH. MessageWithData._preprocess_query() returns early because _fetch_var_impls is None, so out_var_impls stays None.
  4. The database returns the row header and the row data, without describe information. MessageWithData._process_row_data() iterates over self.out_var_impls, which is None. The generated C code has no None check at that point, so it reads None as a list.

With PYO_DEBUG_PACKETS=1, the second request has function code 0x4E, and its response starts with message types 6 and 7 (there is no message type 16).

This explains the conditions above: an error that is not in ERR_INTEGRITY_ERROR_CODES clears the cursor, and a statement that succeeded before already has its fetch variables.

A possible fix is to clear the cursor in this case too, so that the next execute is a full execute and gets the describe information:

--- a/src/oracledb/impl/thin/messages/base.pyx
+++ b/src/oracledb/impl/thin/messages/base.pyx
@@ -1357,7 +1357,9 @@ cdef class MessageWithData(Message):
             self.retry = True
             conn_impl._statement_cache.clear_cursor(cursor_impl._statement)
         elif self.error_info.num != 0 and self.error_info.cursor_id != 0:
-            if self.error_info.num not in errors.ERR_INTEGRITY_ERROR_CODES:
+            if self.error_info.num not in errors.ERR_INTEGRITY_ERROR_CODES \
+                    or (cursor_impl._statement._is_query
+                        and cursor_impl._statement._fetch_var_impls is None):
                 conn_impl._statement_cache.clear_cursor(cursor_impl._statement)

With this change on main, the script passes. Two new tests with the same sequence (one with a sync connection, one with an asyncio connection) pass too; both tests segfault without the change. tests/cursor on this XE 18c database has the same result with and without the change: test_cursor_1614 and test_cursor_1714 fail with ORA-00600 [kposcws-0] from the database (the released 26.1.0 fails in the same way), and all other tests pass.

Not tested: the elif self.retry: branch above it also keeps the cursor id for any error, so a failed retry may leave the statement in the same state.

The change, with the two tests and a release note line, is in pull request #632.

Workaround for now: stmtcachesize=0.

  1. Does your application call init_oracle_client()?

No. This is Thin mode.

  1. Include a runnable Python script that shows the problem.

No schema objects are necessary.

import faulthandler
import os

import oracledb

faulthandler.enable()

conn = oracledb.connect(
    user=os.environ["PYO_USER"],
    password=os.environ["PYO_PASSWORD"],
    dsn=os.environ["PYO_DSN"],
)
print("oracledb", oracledb.__version__, "database", conn.version)

sql = "select cast(:1 as number(1)) from dual"

with conn.cursor() as cursor:
    try:
        cursor.execute(sql, [10])
    except oracledb.DatabaseError as e:
        print("first execute:", e)

with conn.cursor() as cursor:
    cursor.execute(sql, [5])
    print("second execute:", cursor.fetchall())
主要语言
Python
星标
458
派生
123
PR 合并指标
30 天内没有已合并 PR

环境准备

从这里开始

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

oracle/python-oracledb 的其他 Issue

查看 oracle/python-oracledb 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 发到你的邮箱

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