@@ -1189,8 +1189,14 @@ pub unsafe fn sql_set_desc_rec<B: Backend>(
11891189/// - `01000` General warning — not produced here.
11901190/// - `08S01` Communication link failure — not returned; this call performs no
11911191/// I/O.
1192- /// - `HY000` General error — returned for a failure with no more specific code,
1193- /// including an internal panic caught by `panic_safe`.
1192+ /// - `HY000` General error — returned for a failure with no more specific
1193+ /// code, including an internal panic. Phase two's own panic is caught by
1194+ /// `panic_safe`, as everywhere else; phase one's is caught by
1195+ /// `panic::catch_panic_as_error` (`src/panic.rs`, crate-private), since
1196+ /// phase one holds no target handle to post a diagnostic through yet —
1197+ /// both are folded into the same
1198+ /// `OdbcError::Panic` and posted to the target's queue by phase two's
1199+ /// `panic_safe`, exactly where `HY007` is.
11941200/// - `HY001` Memory allocation error — not returned; allocation here is an
11951201/// infallible `Box`/`HashMap` clone.
11961202/// - `HY007` Associated statement is not prepared — returned when the source is
@@ -1244,7 +1250,15 @@ pub unsafe fn sql_copy_desc<B: Backend>(
12441250 // guard — so that the lock is released before phase two is a fact its
12451251 // signature states rather than something this function has to remember.
12461252 let Some ( snapshot) = HandleScope :: with_group ( source_desc_handle, HandleKind :: Desc , |scope| {
1247- scope. snapshot_descriptor :: < B > ( source_desc_handle)
1253+ // `with_group` is a plain lock-then-call with no `catch_unwind` of its
1254+ // own, so a panic from `describe_col` (via `snapshot_ird`) would
1255+ // otherwise unwind straight through it and across the `extern
1256+ // "system"` boundary. Caught here and folded into the same `Err`
1257+ // shape a non-panicking failure already returns, so it flows through
1258+ // the `snapshot?` below and phase two's `panic_safe` posts it to the
1259+ // target's queue exactly where every other diagnostic this call
1260+ // makes belongs.
1261+ crate :: panic:: catch_panic_as_error ( || scope. snapshot_descriptor :: < B > ( source_desc_handle) )
12481262 } ) else {
12491263 tracing:: debug!( "SQLCopyDesc -> INVALID_HANDLE (source)" ) ;
12501264 return SqlReturn :: INVALID_HANDLE ;
@@ -1331,8 +1345,9 @@ mod tests {
13311345 use crate :: ffi:: diag:: sql_get_diag_rec_w;
13321346 use crate :: ffi:: stmt_attr:: sql_get_stmt_attr_w;
13331347 use crate :: test_utils:: {
1334- MockAltBackend , MockBackend , MockLongDataBackend , MockRecordingBackend ,
1335- MockTypeInfoBackend , alloc_env_conn_stmt, cleanup_env_conn_stmt, with_descriptor,
1348+ MockAltBackend , MockBackend , MockLongDataBackend , MockPanickingDescribeBackend ,
1349+ MockRecordingBackend , MockTypeInfoBackend , alloc_env_conn_stmt, cleanup_env_conn_stmt,
1350+ with_descriptor,
13361351 } ;
13371352 use crate :: types:: sql_state;
13381353 use odbc_sys:: { CDataType , HandleType , ParamType , SqlDataType , StatementAttribute } ;
@@ -3180,6 +3195,87 @@ mod tests {
31803195 }
31813196 }
31823197
3198+ /// Audit finding B4: phase one runs `describe_col` (through
3199+ /// `snapshot_ird`) before phase two's `panic_safe` is ever reached, so a
3200+ /// panicking `describe_col` used to unwind straight out of
3201+ /// `sql_copy_desc` and across the `extern "system"` boundary
3202+ /// `forward_ffi!` generates for it — an abort, not a `SqlReturn`. Driven
3203+ /// through `catch_unwind` because that escape is exactly what a bare
3204+ /// `assert_eq!` on the return value cannot see: before the fix, this test
3205+ /// itself never gets to the `assert_eq!`, because the panic unwinds
3206+ /// through it too.
3207+ #[ test]
3208+ fn copy_desc_from_ird_with_panicking_describe_col_returns_error_not_abort ( ) {
3209+ unsafe {
3210+ let ( env, conn, stmt) =
3211+ crate :: test_utils:: alloc_connected_env_conn_stmt :: < MockPanickingDescribeBackend > ( ) ;
3212+
3213+ let sql: Vec < u16 > = "SELECT 1" . encode_utf16 ( ) . collect ( ) ;
3214+ assert_eq ! (
3215+ crate :: ffi:: execute:: sql_exec_direct_w:: <MockPanickingDescribeBackend >(
3216+ stmt,
3217+ sql. as_ptr( ) ,
3218+ i32 :: try_from( sql. len( ) ) . expect( "short" ) ,
3219+ ) ,
3220+ SqlReturn :: SUCCESS ,
3221+ "precondition: the statement ran and the IRD has a column to describe"
3222+ ) ;
3223+
3224+ let ird =
3225+ desc_token_of :: < MockPanickingDescribeBackend > ( stmt, StatementAttribute :: ImpRowDesc ) ;
3226+ let mut target: * mut c_void = std:: ptr:: null_mut ( ) ;
3227+ assert_eq ! (
3228+ crate :: ffi:: handle:: sql_alloc_handle:: <MockPanickingDescribeBackend >(
3229+ HandleType :: Desc as i16 ,
3230+ conn,
3231+ & mut target,
3232+ ) ,
3233+ SqlReturn :: SUCCESS ,
3234+ "precondition: an explicit target descriptor"
3235+ ) ;
3236+
3237+ let result = std:: panic:: catch_unwind ( || {
3238+ sql_copy_desc :: < MockPanickingDescribeBackend > ( ird, target)
3239+ } ) ;
3240+ assert ! (
3241+ result. is_ok( ) ,
3242+ "a panic in phase one must be caught and reported as SQL_ERROR, \
3243+ not unwind across the extern \" system\" boundary"
3244+ ) ;
3245+ assert_eq ! ( result. unwrap( ) , SqlReturn :: ERROR ) ;
3246+ assert_eq ! (
3247+ first_sqlstate_of:: <MockPanickingDescribeBackend >( target) ,
3248+ sql_state:: GENERAL_ERROR ,
3249+ "the panic is routed through phase two's panic_safe onto the \
3250+ target's queue, exactly where SQLCopyDesc's other diagnostics \
3251+ (e.g. HY007) are posted"
3252+ ) ;
3253+
3254+ // The group lock was never held across the panic (it is caught
3255+ // inside phase one's own closure, before `with_group` even sees
3256+ // it), so it is not poisoned and the statement is still usable —
3257+ // prove it with an ordinary follow-up call.
3258+ let mut columns: i16 = -1 ;
3259+ assert_eq ! (
3260+ crate :: ffi:: cursor:: sql_num_result_cols:: <MockPanickingDescribeBackend >(
3261+ stmt,
3262+ & mut columns,
3263+ ) ,
3264+ SqlReturn :: SUCCESS ,
3265+ "the statement's group lock must still be usable after the panic"
3266+ ) ;
3267+ assert_eq ! ( columns, 1 ) ;
3268+
3269+ let _ = crate :: ffi:: handle:: sql_free_handle :: < MockPanickingDescribeBackend > (
3270+ HandleType :: Desc as i16 ,
3271+ target,
3272+ ) ;
3273+ crate :: test_utils:: cleanup_connected_env_conn_stmt :: < MockPanickingDescribeBackend > (
3274+ env, conn, stmt,
3275+ ) ;
3276+ }
3277+ }
3278+
31833279 /// The APD's token, as the application receives it.
31843280 unsafe fn apd_of ( stmt : * mut c_void ) -> * mut c_void {
31853281 let mut token: * mut c_void = std:: ptr:: null_mut ( ) ;
0 commit comments