Skip to content

Commit 0c38e91

Browse files
maltesanderclaude
andcommitted
fix: write statement attributes at full SQLULEN width
Every non-pointer attribute on the SQLSetStmtAttr page is declared "An SQLULEN value" — not one is SQLUINTEGER — and BufferLength is ignored for them, so the application's buffer is SQLULEN-wide and it is the driver's job to fill it. SQLGetStmtAttr's Comments describe the alternative as a defect to be worked around: "some drivers may only write the lower 32-bit or 16-bit of a buffer and leave the higher-order bit unchanged. Therefore, applications should use a buffer of SQLULEN and initialize the value to 0 before calling this function." An application that did not zero its buffer read SQL_ATTR_MAX_ROWS as 0xFFFFFFFF00000000 where the driver meant "no limit". Reported from the Trino driver, reproduced here with a poisoned buffer before the fix. On the connection side only SQL_ATTR_ASYNC_ENABLE and SQL_ATTR_ODBC_CURSORS are SQLULEN; the rest really are SQLUINTEGER, so that stays a two-attribute exception rather than a blanket change. The test buffers on the statement side were u32 slots receiving the wider write, and are now SQLULEN too. Also adds a pre-commit hook rejecting duplicate registered test names. A function carrying two #[test] attributes is registered twice while its neighbour, whose attribute it absorbed, stops running — both leave the suite green, so the registered names are the only place either shows up. Verified to fail on an injected duplicate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 290e12f commit 0c38e91

5 files changed

Lines changed: 269 additions & 67 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ repos:
3030
stages: [pre-commit, pre-merge-commit]
3131
pass_filenames: false
3232
files: \.rs$|Cargo\.(toml|lock)
33+
# A test function carrying two `#[test]` attributes is registered twice
34+
# and its neighbour, whose attribute was absorbed, silently stops
35+
# running. Both outcomes leave the suite green, so only the registered
36+
# names expose them.
37+
- id: no-duplicate-test-names
38+
name: no-duplicate-test-names
39+
language: system
40+
entry: ./scripts/no-duplicate-test-names.sh
41+
stages: [pre-commit, pre-merge-commit]
42+
pass_filenames: false
43+
files: \.rs$
3344

3445
- id: cargo-rustfmt
3546
name: cargo-rustfmt

‎CHANGELOG.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,22 @@ Everything a driver has to change for the catalog rework, in one place.
878878

879879
### Fixed
880880

881+
- **`SQLGetStmtAttr` wrote four bytes where the spec declares `SQLULEN`.** Every
882+
non-pointer attribute on the `SQLSetStmtAttr` page is declared "An SQLULEN
883+
value" — not one is `SQLUINTEGER` — and `BufferLength` is ignored for them, so
884+
the application's buffer is `SQLULEN`-wide and the driver must fill it.
885+
`SQLGetStmtAttr`'s own Comments describe the alternative as a defect to work
886+
around: "if the value is a SQLULEN value, some drivers may only write the
887+
lower 32-bit or 16-bit of a buffer and leave the higher-order bit unchanged.
888+
Therefore, applications should use a buffer of SQLULEN and initialize the
889+
value to 0 before calling this function." An application that did not zero its
890+
buffer read `SQL_ATTR_MAX_ROWS` as `0xFFFFFFFF00000000` — an enormous row
891+
limit — where the driver meant "no limit". All twenty integer-valued reads now
892+
write a full `SQLULEN`; the pointer-valued ones already did. On the connection
893+
side only `SQL_ATTR_ASYNC_ENABLE` and `SQL_ATTR_ODBC_CURSORS` are `SQLULEN`
894+
and were widened; every other connection attribute really is `SQLUINTEGER` and
895+
is unchanged.
896+
881897
- **`SQLSetStmtAttr` now answers for every value it cannot honour.** Core drives
882898
one forward-only, read-only cursor over one parameter set, and the spec gives
883899
two ways to say so. The `01S02` row names the eight attributes a driver may

‎scripts/no-duplicate-test-names.sh‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env bash
2+
# Fails when the test binary registers the same test name twice.
3+
#
4+
# A function with two `#[test]` attributes is registered twice, and the
5+
# function whose attribute was absorbed stops running altogether. Both leave
6+
# `cargo test` green — the count moves, but nothing fails — so the registered
7+
# names are the only place either shows up.
8+
set -euo pipefail
9+
10+
duplicates=$(cargo test --locked --lib -- --list 2>/dev/null | grep ': test$' | sort | uniq -d)
11+
12+
if [[ -n "$duplicates" ]]; then
13+
echo "These test names are registered more than once:" >&2
14+
echo "$duplicates" >&2
15+
echo >&2
16+
echo "A duplicate almost always means one function carries two #[test]" >&2
17+
echo "attributes, having absorbed the attribute of the function above or" >&2
18+
echo "below it — which is no longer running." >&2
19+
exit 1
20+
fi

‎src/ffi/connect_attr.rs‎

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,32 @@ pub unsafe fn sql_get_connect_attr_w<B: Backend>(
519519
}
520520
};
521521

522+
// The two connection attributes the spec declares `SQLULEN` rather
523+
// than `SQLUINTEGER`: `SQL_ATTR_ASYNC_ENABLE` ("A SQLULEN value
524+
// that specifies whether a function called with a statement on the
525+
// specified connection is executed asynchronously") and
526+
// `SQL_ATTR_ODBC_CURSORS` ("An SQLULEN value specifying how the
527+
// Driver Manager uses the ODBC cursor library"). Every other
528+
// integer-valued connection attribute really is `SQLUINTEGER`, so
529+
// this is a two-attribute exception rather than the blanket rule
530+
// that applies to statement attributes — where *no* non-pointer
531+
// attribute is `SQLUINTEGER`.
532+
//
533+
// SAFETY: as `write_u32` above, but the buffer is SQLULEN-wide.
534+
let write_ulen = |v: usize| {
535+
if !value_ptr.is_null() {
536+
// SAFETY: non-null checked above; caller guarantees writable SQLULEN
537+
std::ptr::write_unaligned(value_ptr as *mut usize, v);
538+
}
539+
if !string_length_ptr.is_null() {
540+
// SAFETY: non-null checked above; caller guarantees writable i32
541+
std::ptr::write_unaligned(
542+
string_length_ptr,
543+
std::mem::size_of::<usize>() as i32,
544+
);
545+
}
546+
};
547+
522548
match attr {
523549
_ if attr == ConnectionAttribute::ACCESS_MODE => {
524550
let v = conn
@@ -579,7 +605,7 @@ pub unsafe fn sql_get_connect_attr_w<B: Backend>(
579605
.get(&attribute)
580606
.copied()
581607
.unwrap_or(SQL_CUR_USE_DRIVER);
582-
write_u32(v as u32);
608+
write_ulen(v);
583609
Ok(SqlReturn::SUCCESS)
584610
}
585611
_ if attr == ConnectionAttribute::PACKET_SIZE => {
@@ -611,7 +637,7 @@ pub unsafe fn sql_get_connect_attr_w<B: Backend>(
611637
.get(&attribute)
612638
.copied()
613639
.unwrap_or(SQL_ASYNC_ENABLE_OFF);
614-
write_u32(v as u32);
640+
write_ulen(v);
615641
Ok(SqlReturn::SUCCESS)
616642
}
617643
_ if attr == ConnectionAttribute::TRANSLATE_OPTION => {
@@ -906,6 +932,48 @@ mod tests {
906932
}
907933
}
908934

935+
/// `SQL_ATTR_ASYNC_ENABLE` and `SQL_ATTR_ODBC_CURSORS` are the two
936+
/// connection attributes the spec declares `SQLULEN` — "A SQLULEN value
937+
/// that specifies whether a function called with a statement on the
938+
/// specified connection is executed asynchronously" and "An SQLULEN value
939+
/// specifying how the Driver Manager uses the ODBC cursor library". Every
940+
/// other integer-valued connection attribute is `SQLUINTEGER`, which is why
941+
/// this is a two-attribute exception here and a blanket rule for statement
942+
/// attributes.
943+
#[test]
944+
fn the_two_sqlulen_connection_attributes_are_written_at_full_width() {
945+
for (attribute, name, expected) in [
946+
(
947+
ConnectionAttribute::ODBC_CURSORS.0,
948+
"SQL_ATTR_ODBC_CURSORS",
949+
SQL_CUR_USE_DRIVER,
950+
),
951+
(
952+
ConnectionAttribute::ASYNC_ENABLE.0,
953+
"SQL_ATTR_ASYNC_ENABLE",
954+
SQL_ASYNC_ENABLE_OFF,
955+
),
956+
] {
957+
unsafe {
958+
let (env, conn) = alloc_env_conn();
959+
let mut value: usize = usize::MAX;
960+
let ret = sql_get_connect_attr_w::<MockBackend>(
961+
conn,
962+
attribute,
963+
std::ptr::from_mut(&mut value).cast(),
964+
0,
965+
std::ptr::null_mut(),
966+
);
967+
assert_eq!(ret, SqlReturn::SUCCESS);
968+
assert_eq!(
969+
value, expected,
970+
"{name}: the high half of the SQLULEN buffer kept its poison"
971+
);
972+
cleanup(env, conn);
973+
}
974+
}
975+
}
976+
909977
/// An attribute `SQLSetConnectAttr` stores is one `SQLGetConnectAttr` can
910978
/// report: the spec makes the getter the way an application reads back what
911979
/// it set, so a stored value that answers HY092 would be unreachable.
@@ -969,7 +1037,12 @@ mod tests {
9691037
SqlReturn::SUCCESS,
9701038
"{name} was not accepted"
9711039
);
972-
let mut out: u32 = u32::MAX;
1040+
// An SQLULEN buffer, zeroed: two of these attributes are
1041+
// SQLULEN and the rest SQLUINTEGER, and the spec tells
1042+
// applications to "use a buffer of SQLULEN and initialize the
1043+
// value to 0" precisely so one buffer serves both. A `u32`
1044+
// here would be overflowed by the SQLULEN pair.
1045+
let mut out: usize = 0;
9731046
assert_eq!(
9741047
sql_get_connect_attr_w::<MockBackend>(
9751048
conn,
@@ -981,7 +1054,7 @@ mod tests {
9811054
SqlReturn::SUCCESS,
9821055
"{name} was stored but cannot be read back"
9831056
);
984-
assert_eq!(out as usize, *value, "{name} read back a different value");
1057+
assert_eq!(out, *value, "{name} read back a different value");
9851058
cleanup(env, conn);
9861059
}
9871060
}

0 commit comments

Comments
 (0)