Skip to content

Commit b3a92ce

Browse files
maltesanderclaude
andcommitted
fix: SQLPutData's HY009 guard matches the clause it stands in for
The row is (DM)-marked and the guard exists only to avoid dereferencing a null pointer, but it refused every null -- stricter than the clause, which exempts a StrLen_or_Ind of 0: "(DM) The argument DataPtr was a null pointer, and the argument StrLen_or_Ind was not 0, SQL_DEFAULT_PARAM, or SQL_NULL_DATA." A zero-length put is how an application sends an empty value. Accepting it also means guarding the slice construction: from_raw_parts on a null pointer is undefined behaviour even at length zero, which Miri confirms -- with the guard removed it reports "constructing invalid value of type &[u8]: encountered a null reference". The put_state assignment stays outside that guard, because a zero-length put is still a put and is what distinguishes an empty value from a parameter nobody supplied. Five doc comments corrected with it. SQLBindParameter called HY021 driver-manager-handled on a row with no (DM) marker, for a state it returns; SQLBindCol listed no HY021 at all and justified deferring 07009 with a Driver Manager check that does not exist -- binding before a result set exists is the real reason, and that row carries no (DM) marker either. SQLPutData was missing 07006, 08S01 and HY008, SQLParamData 22026, and SQLDescribeParam 21S01. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 97c2a19 commit b3a92ce

3 files changed

Lines changed: 137 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2617,4 +2617,20 @@ Everything a driver has to change for the catalog rework, in one place.
26172617
parameter's default, and core refuses `{call ...}` and `{?= call ...}` with
26182618
`HYC00`, so no statement it executes has a parameter carrying one.
26192619

2620+
- **`SQLPutData` accepts a null `DataPtr` with a length of zero.** Its `HY009`
2621+
guard refused every null pointer, which is stricter than the clause it stands
2622+
in for: "(DM) The argument *DataPtr* was a null pointer, and the argument
2623+
*StrLen_or_Ind* was **not** 0, SQL_DEFAULT_PARAM, or SQL_NULL_DATA." A
2624+
zero-length put is how an application sends an empty value, and it now works.
2625+
2626+
- **Five doc comments corrected against the spec's diagnostics tables.**
2627+
`SQLBindParameter` claimed `HY021` was driver-manager-handled, though its row
2628+
carries no `(DM)` marker and the function returns it; `SQLBindCol` did not
2629+
list `HY021` at all, though it returns it under `SQLSetDescRec`'s
2630+
consistency-check mandate, and justified deferring `07009` by asserting a
2631+
Driver Manager check that does not exist — binding before a result set exists
2632+
is the real reason. `SQLPutData` was missing `07006`, `08S01` and `HY008`,
2633+
`SQLParamData` was missing `22026`, and `SQLDescribeParam` was missing
2634+
`21S01`.
2635+
26202636
[Unreleased]: https://github.com/stackabletech/stackable-odbc-core/commits/HEAD

src/ffi/bind.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,20 @@ use crate::types::SqlReturn;
4848
/// not returned here because bookmark columns are not supported)
4949
/// - `07009` Invalid descriptor index — the spec requires returning 07009 when `column_number`
5050
/// exceeds the maximum number of columns in the result set. The binding is stored without
51-
/// checking: column count is not available at bind time (before `SQLExecute`), and
52-
/// the DM validates descriptor indices against the result set. Deferred.
51+
/// checking, because binding is legal before a result set exists: `SQLBindCol` is routinely
52+
/// called before `SQLExecute`, when there is no column count to compare against, and the row
53+
/// carries no `(DM)` marker, so nothing else checks it either. Deferred.
5354
/// - `HY000` General error — returned for unexpected failures
5455
/// - `HY001` Memory allocation error — (driver-manager-handled; not returned here)
5556
/// - `HY003` Invalid application buffer type — returned when `target_type` is not a valid
5657
/// C data type identifier (`c_data_type_from_raw` returns `None`)
5758
/// - `HY010` Function sequence error — (driver-manager-handled; not returned here)
59+
/// - `HY021` Inconsistent descriptor information — **returned by this driver**, although
60+
/// `SQLBindCol`'s own diagnostics table has no such row. `SQLSetDescRec`'s "Consistency
61+
/// Checks" section states the mandate: "This check is always performed when
62+
/// **SQLBindParameter** or **SQLBindCol** is called". The check runs before the record is
63+
/// inserted, so a rejected bind leaves the previous binding — or no binding — in place
64+
/// (`crate::descriptor::consistency_check`).
5865
/// - `HY013` Memory management error — (driver-manager-handled; not returned here)
5966
/// - `HY090` Invalid string or buffer length — (driver-manager-handled; not returned here;
6067
/// the DM checks for `buffer_length < 0`)

src/ffi/params.rs

Lines changed: 112 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ use crate::{
7272
/// - `HY009` Invalid argument value — (driver-manager-handled; not returned here)
7373
/// - `HY010` Function sequence error — (driver-manager-handled; not returned here)
7474
/// - `HY013` Memory management error — (driver-manager-handled; not returned here)
75-
/// - `HY021` Inconsistent descriptor information — (driver-manager-handled; not returned here)
75+
/// - `HY021` Inconsistent descriptor information — **returned by this driver**. The row
76+
/// carries no `(DM)` marker, and `SQLSetDescRec`'s "Consistency Checks" section says when
77+
/// the check runs: "This check is always performed when **SQLBindParameter** or
78+
/// **SQLBindCol** is called". Both halves of the binding are checked before either
79+
/// descriptor is written, so a rejected bind leaves neither changed
80+
/// (`crate::descriptor::consistency_check`).
7681
/// - `HY090` Invalid string or buffer length — (driver-manager-handled; not returned here)
7782
/// - `HY104` Invalid precision or scale value — a driver-returned code (the spec does not mark it
7883
/// (DM)): `column_size` and `decimal_digits` are stored verbatim without range validation.
@@ -356,6 +361,11 @@ pub unsafe fn sql_num_params<B: Backend>(
356361
/// - 01000: General warning — (driver-manager-handled; not returned here).
357362
/// - 07009: Invalid descriptor index — returned when `parameter_number` is 0 or exceeds
358363
/// the number of parameter markers in the prepared statement.
364+
/// - 21S01: Insert value list does not match column list — not returned here. The row is
365+
/// about an `INSERT` whose parameter count differs from the target table's column count,
366+
/// which needs the data source's catalog: core parses the statement only far enough to
367+
/// count `?` markers (`count_params`) and never resolves a table. A backend that describes
368+
/// parameters itself is where this would originate.
359369
/// - 08S01: Communication link failure — not applicable (no backend query).
360370
/// - HY000: General error — returned for unexpected failures.
361371
/// - HY001: Memory allocation error — not applicable; Rust allocation panics are caught by `panic_safe`.
@@ -1303,6 +1313,13 @@ unsafe fn dae_nts_byte_count(c_type: Option<odbc_sys::CDataType>, data_ptr: *con
13031313
/// - 01000: General warning — (driver-manager-handled; not returned here).
13041314
/// - 01004: String data, right truncated — not applicable; data is accumulated without
13051315
/// truncation.
1316+
/// - 07006: Restricted data type attribute violation — not returned here. The pairing is
1317+
/// fixed at `SQLBindParameter`, which refuses the C-to-SQL combinations core cannot convert
1318+
/// before the query runs (`crate::binary_convert`, `crate::numeric_convert`), so a chunk
1319+
/// arriving here is already of a pairing that was accepted.
1320+
/// - 08S01: Communication link failure — not returned here. `SQLPutData` accumulates into a
1321+
/// buffer on the statement handle and makes no backend call at all; the link is next touched
1322+
/// by the `SQLParamData` that completes the execution, which is where this arrives.
13061323
/// - 22001: String data, right truncation — not applicable; no target column size check
13071324
/// at this stage.
13081325
/// - 22003: Numeric value out of range — not applicable; type conversion happens at execute
@@ -1318,9 +1335,17 @@ unsafe fn dae_nts_byte_count(c_type: Option<odbc_sys::CDataType>, data_ptr: *con
13181335
/// - HY000: General error — returned for unexpected failures.
13191336
/// - HY001: Memory allocation error — not applicable; Rust allocation panics are caught by
13201337
/// `panic_safe`.
1321-
/// - HY009: Invalid use of null pointer — returned when `data_ptr` is null but
1322-
/// `str_len_or_ind` is neither `SQL_NULL_DATA` nor `SQL_DEFAULT_PARAM`, the two values
1323-
/// that carry the whole parameter and so need no buffer.
1338+
/// - HY008: Operation canceled — not returned here. This call makes no fallible backend call,
1339+
/// so there is no error for a cancellation to be reported through, and the asynchronous
1340+
/// clause is inapplicable: core never returns `SQL_STILL_EXECUTING`. A `SQLCancel` during a
1341+
/// data-at-execution sequence discards the sequence; the following `SQLParamData` is what
1342+
/// reports it.
1343+
/// - HY009: Invalid use of null pointer — (DM) the row is driver-manager-marked. Core keeps a
1344+
/// guard anyway, because unixODBC does not always run it and the alternative is
1345+
/// dereferencing a null pointer, and the guard matches the clause exactly: "(DM) The
1346+
/// argument DataPtr was a null pointer, and the argument StrLen_or_Ind was not 0,
1347+
/// SQL_DEFAULT_PARAM, or SQL_NULL_DATA." A null pointer with a length of 0 is a legal
1348+
/// zero-length put and is accepted.
13241349
/// - HY010: Function sequence error — returned when no data-at-execution is in progress
13251350
/// (no prior `SQL_NEED_DATA` from `SQLExecute`/`SQLExecDirectW`), or when
13261351
/// `SQLParamData` has not yet been called to identify the current parameter.
@@ -1463,9 +1488,16 @@ pub unsafe fn sql_put_data<B: Backend>(
14631488
return Ok(SqlReturn::SUCCESS);
14641489
}
14651490

1466-
// Spec HY009: data_ptr must not be null (unless SQL_NULL_DATA or
1467-
// SQL_DEFAULT_PARAM, both handled above).
1468-
if data_ptr.is_null() {
1491+
// A refusal to dereference a null pointer, not a spec check: the
1492+
// row is `(DM)`-marked and belongs to the Driver Manager. It is
1493+
// kept because unixODBC does not always run it, and it is written
1494+
// to match the clause exactly — "(DM) The argument DataPtr was a
1495+
// null pointer, and the argument StrLen_or_Ind was not 0,
1496+
// SQL_DEFAULT_PARAM, or SQL_NULL_DATA". The other two values are
1497+
// handled above, so only the zero remains, and a null pointer with
1498+
// a length of zero is a legal zero-length put that this guard used
1499+
// to refuse.
1500+
if data_ptr.is_null() && str_len_or_ind != 0 {
14691501
return Err(OdbcError::general(
14701502
"DataPtr is null",
14711503
SqlState::invalid_use_of_null_pointer(),
@@ -1490,9 +1522,17 @@ pub unsafe fn sql_put_data<B: Backend>(
14901522
str_len_or_ind as usize
14911523
};
14921524

1493-
// SAFETY: caller guarantees data_ptr is valid for byte_count bytes.
1494-
let data = std::slice::from_raw_parts(data_ptr as *const u8, byte_count);
1495-
dae.buffer.extend_from_slice(data);
1525+
if byte_count > 0 {
1526+
// SAFETY: caller guarantees data_ptr is valid for byte_count
1527+
// bytes, and it is non-null: the guard above admits a null only
1528+
// with a length of zero, which this branch excludes.
1529+
// `from_raw_parts` on a null pointer is undefined behaviour
1530+
// even at length zero.
1531+
let data = std::slice::from_raw_parts(data_ptr as *const u8, byte_count);
1532+
dae.buffer.extend_from_slice(data);
1533+
}
1534+
// A zero-length put is still a put: it is what distinguishes an
1535+
// empty value from a parameter nobody supplied.
14961536
dae.put_state = PutDataState::Data;
14971537

14981538
Ok(SqlReturn::SUCCESS)
@@ -1548,6 +1588,10 @@ pub unsafe fn sql_put_data<B: Backend>(
15481588
/// backend.
15491589
/// - 23000: Integrity constraint violation — propagated from backend.
15501590
/// - 24000: Invalid cursor state — propagated from backend.
1591+
/// - 22026: String data, length mismatch — not returned here. The row's condition opens with
1592+
/// "The SQL_NEED_LONG_DATA_LEN information type in `SQLGetInfo` was 'Y'", and core answers
1593+
/// `"N"` for it (`default_get_info`), so the driver never asked the application to declare a
1594+
/// long parameter's length in advance and has nothing to compare against.
15511595
/// - 40001: Serialization failure — propagated from backend.
15521596
/// - 40003: Statement completion unknown — propagated from backend.
15531597
/// - 42000: Syntax error or access violation — propagated from backend.
@@ -2492,6 +2536,64 @@ mod tests {
24922536
}
24932537
}
24942538

2539+
/// The spec's clause, read as written: "(DM) The argument DataPtr was a
2540+
/// null pointer, and the argument StrLen_or_Ind was **not** 0,
2541+
/// SQL_DEFAULT_PARAM, or SQL_NULL_DATA." A null pointer with a length of
2542+
/// zero is therefore a legal zero-length put, and core's own guard — which
2543+
/// exists to avoid dereferencing a null pointer, not to enforce a `(DM)`
2544+
/// row — must not be stricter than the clause it stands in for.
2545+
#[test]
2546+
fn put_data_accepts_a_null_pointer_with_a_zero_length() {
2547+
unsafe {
2548+
let (env, conn, stmt) = connected_stmt();
2549+
let mut indicator: isize = SQL_DATA_AT_EXEC;
2550+
start_dae_loop(
2551+
stmt,
2552+
CDataType::Char,
2553+
SqlDataType::VARCHAR,
2554+
&raw mut indicator,
2555+
);
2556+
2557+
assert_eq!(
2558+
sql_put_data::<MockBackend>(stmt, std::ptr::null_mut(), 0),
2559+
SqlReturn::SUCCESS,
2560+
);
2561+
assert!(
2562+
dae_buffer(stmt).is_empty(),
2563+
"a zero-length put appends nothing",
2564+
);
2565+
2566+
cleanup(env, conn, stmt);
2567+
}
2568+
}
2569+
2570+
/// The half of the clause that stays: a null pointer with a real length is
2571+
/// still HY009, because there is nothing to read those bytes from.
2572+
#[test]
2573+
fn put_data_rejects_a_null_pointer_with_a_real_length() {
2574+
unsafe {
2575+
let (env, conn, stmt) = connected_stmt();
2576+
let mut indicator: isize = SQL_DATA_AT_EXEC;
2577+
start_dae_loop(
2578+
stmt,
2579+
CDataType::Char,
2580+
SqlDataType::VARCHAR,
2581+
&raw mut indicator,
2582+
);
2583+
2584+
assert_eq!(
2585+
sql_put_data::<MockBackend>(stmt, std::ptr::null_mut(), 3),
2586+
SqlReturn::ERROR,
2587+
);
2588+
with_handle::<MockBackend, StatementHandle<MockBackend>, _>(stmt, |h| {
2589+
let rec = h.diagnostics.get(0).expect("record 1 exists");
2590+
assert_eq!(rec.sqlstate.as_str(), "HY009");
2591+
});
2592+
2593+
cleanup(env, conn, stmt);
2594+
}
2595+
}
2596+
24952597
/// `CStr::from_ptr` scans without a bound, so a buffer whose terminator is
24962598
/// missing is read past its own allocation — the only such scan in the
24972599
/// crate, since `utf16_to_string` caps its own at `MAX_NTS_SCAN`.

0 commit comments

Comments
 (0)