Skip to content

Statement handle crashes on reuse after a server error inside a parameter-array execute #309

Open

Description

@fdcastel

Summary

When one parameter set of an array execute fails on the server (here a NOT NULL violation in set 3 of 5), SQLExecute returns SQL_ERROR with the server's message, as it should, but the statement handle is left in a state that crashes the process on its next use: SQLFreeStmt(SQL_RESET_PARAMS), SQL_ATTR_PARAMSET_SIZE = 1, two SQLBindParameter calls and an SQLExecDirect end in an access violation inside the driver.

Visible in the same run: SQL_ATTR_PARAMS_PROCESSED_PTR reports 0 (the specification counts the failed set, so 3), and the status array has the two successful sets marked and nothing else (the failed set should be SQL_PARAM_ERROR, the sets after it SQL_PARAM_UNUSED).

Environment

Windows x64, master 35fae3e with #308 applied (the code involved is not touched by #308), Firebird 5.0.3, Microsoft driver manager. The code involved is platform-independent.

Reproduction

CREATE TABLE T_PS4 (ID INTEGER NOT NULL, V BIGINT); INSERT INTO T_PS4 (ID, V) VALUES (?, ?) with SQL_ATTR_PARAMSET_SIZE = 5, column-wise, ID bound SQL_C_SLONG with the indicator of set 3 = SQL_NULL_DATA, status array and processed pointer set. Then reuse the same handle for a single-row insert. Case 5 of a ctypes probe (the status array was pre-filled with 9 to show which elements the driver writes):

5a error in set 3                rc=SQL_ERROR processed=0 status=[OK OK 9 9 9] stored: (1,10) (2,20)
                                 [23000] validation error for column "T_PS4"."ID", value "*** null ***"
5b handle reused, single row     OSError: exception: access violation reading 0x000004B12A2014D8

Cause

OdbcStatement::executeStatementParamArray points applicationParamDescriptor->headBindOffsetPtr at a local variable (bindOffsetPtrTmp) for the duration of the loop, and hands the same address to convert->setBindOffsetPtrFrom for row-wise binding. Both are restored on the normal exit and on the inputParam failure exit. statement->executeStatement() throws SQLException on a server error, and there is no third exit path: sqlExecute catches the exception with the descriptor still pointing into the dead stack frame. The next execute dereferences it in rebindParam (*applicationParamDescriptor->headBindOffsetPtr).

The processed count is written only after the loop (*rowCountPt = rowNumberParamArray), so it keeps the 0 written before it; the status element of the failed set is written only on the inputParam failure path.

Expected

The handle survives the error: the offset pointers are restored on every exit path. Processed = 3; status = SQL_PARAM_SUCCESS, SQL_PARAM_SUCCESS, SQL_PARAM_ERROR, SQL_PARAM_UNUSED, SQL_PARAM_UNUSED.

Related: #299 / #308 (stride and execute-time dispatch on the same path). The status pre-fill and the per-set processed count are part of the operation-pointer follow-up; the exception path is not.

Activity

  1. fdcastel commented on Sep 7, 2026

    @fdcastel
    MemberAuthor

    Same run on the operation-pointer branch (status array pre-filled, processed count kept per set): no crash this time, the reused handle inserts a garbage row instead of the bound (7, 70):

    5a error in set 3                rc=SQL_ERROR processed=3 status=[OK OK UNUSED UNUSED UNUSED] stored: (1,10) (2,20)
    5b handle reused, single row     rc=SQL_SUCCESS stored: (0,519575431585792) (1,10) (2,20)
    

    Same dangling pointer, read as a garbage offset instead of an unmapped one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions