Skip to content

CBLAS: fix infinite loop in c/z gemv and gbmv (RowMajor, ConjTrans, M=0) - #1374

Closed
EylonKrause wants to merge 1 commit into
Reference-LAPACK:masterfrom
EylonKrause:fix/cblas-gemv-gbmv-conjtrans-m0-hang
Closed

CBLAS: fix infinite loop in c/z gemv and gbmv (RowMajor, ConjTrans, M=0)#1374
EylonKrause wants to merge 1 commit into
Reference-LAPACK:masterfrom
EylonKrause:fix/cblas-gemv-gbmv-conjtrans-m0-hang

Conversation

@EylonKrause

Copy link
Copy Markdown

Problem

The hand-written row-major shims in the C interface loop forever on a valid (if degenerate) call.

In cblas_cgemv, cblas_zgemv, cblas_cgbmv and cblas_zgbmv, the CblasRowMajor + CblasConjTrans path conjugates Y (and sets the loop stride i and the sentinel st) only inside:

if (M > 0) {
   ...
   if (N > 0) { i = ...; st = y + n; do { *y = -(*y); y += i; } while (y != st); ... }
}

but the post-call loop that un-conjugates Y is gated on if (N > 0) alone:

if (x != X) free(x);
if (N > 0) {           /* <-- runs even when M == 0 */
   do { *y = -(*y); y += i; } while (y != st);
}

When M == 0 and N > 0, the forward block is skipped, so i is still 0 and st is still NULL. The restore loop then advances y by 0 and compares against NULL, so it never terminates — an infinite loop that also keeps negating Y[0]. M = 0 is a documented, in-range argument for these routines (xerbla only rejects M < 0), so this is reachable from a conforming caller.

Fix

Gate the un-conjugation on the same condition under which Y was conjugated, if (M > 0 && N > 0), in all four files. One line each; a normal M > 0 ConjTrans call is byte-for-byte unchanged.

Reproducer / validation

BLIS carries a copy of these same netlib CBLAS wrappers, so I reproduced it there against a real build:

float A[256]={0}, X[256]={0}, Y[256]={0};
float alpha[2]={1,0}, beta[2]={1,0};
cblas_cgbmv(CblasRowMajor, CblasConjTrans, /*M*/0, /*N*/2, /*KL*/0, /*KU*/0,
            alpha, A, 1, X, 1, beta, Y, 1);   /* never returns */

Linked against the unpatched wrapper this is killed by a 5 s timeout; with the one-line gate it returns immediately and leaves Y unchanged. The gemv path behaves the same way.

Disclosure: this contribution was authored with an AI coding assistant (Claude) and reviewed and validated before submission.

In cblas_cgemv, cblas_zgemv, cblas_cgbmv and cblas_zgbmv, the
CblasRowMajor + CblasConjTrans path conjugates Y only inside
`if (M > 0) { ... if (N > 0) { ... } }`, where the loop stride `i` and the
sentinel `st` are assigned. The post-call restore loop that un-conjugates Y is
gated on `if (N > 0)` alone.

When M == 0 and N > 0 the forward conjugation is skipped, so `i` stays 0 and
`st` stays NULL. The restore loop then runs
`do { *y = -(*y); y += i; } while (y != st);` with i == 0 and st == NULL, so y
never advances and never reaches st -- an infinite loop that also repeatedly
negates Y[0]. A normal M > 0 ConjTrans call is unaffected.

Gate the un-conjugation on the same condition under which Y was conjugated
(M > 0 && N > 0).

Verified against a build of the same wrappers (BLIS's copy of the netlib CBLAS
layer): cblas_cgbmv(CblasRowMajor, CblasConjTrans, M=0, N=2, ...) hangs (killed
by a 5s timeout) before the change and returns immediately with Y unchanged
after it; likewise for the gemv path.

Signed-off-by: Eylon Krause <eylon1909@gmail.com>
@langou

langou commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Thanks @EylonKrause ! Well spotted, great fix.

@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
236 4 232 0
View the full list of 4 ❄️ flaky test(s)
LAPACK.eig::ded.out (DOUBLE PRECISION Nonsymmetric Eigenvalue)

Flake rate in main: 40.97% (Passed 657 times, Failed 456 times)

Stack Traces | 0.31s run time
4 numerical error(s), 4 other error(s) (illegal: 0, info: 4), 14678 test(s) run
 DDRVES: DGEES1 returned INFO=     6.
 DDRVES: DGEES1 returned INFO=     6.
 DES:    2 out of  3810 tests failed to pass the threshold
 DGET24: DGEESX1 returned INFO=     6.
 DGET24: DGEESX1 returned INFO=     6.
 DSX:    2 out of  3494 tests failed to pass the threshold
LAPACK.eig::snep.out (REAL Nonsymmetric Eigenvalue Problem)

Flake rate in main: 28.48% (Passed 796 times, Failed 317 times)

Stack Traces | 0.03s run time
1 numerical error(s), 10080 test(s) run
 SHS:    1 out of  2016 tests failed to pass the threshold
LAPACK_64.eig::ded_64.out (DOUBLE PRECISION Nonsymmetric Eigenvalue)

Flake rate in main: 38.45% (Passed 730 times, Failed 456 times)

Stack Traces | 0.3s run time
4 numerical error(s), 4 other error(s) (illegal: 0, info: 4), 14678 test(s) run
 DDRVES_64: DGEES1 returned INFO=     6.
 DDRVES_64: DGEES1 returned INFO=     6.
 DES:    2 out of  3810 tests failed to pass the threshold
 DGET24_64: DGEESX1 returned INFO=     6.
 DGET24_64: DGEESX1 returned INFO=     6.
 DSX:    2 out of  3494 tests failed to pass the threshold
LAPACK_64.eig::snep_64.out (REAL Nonsymmetric Eigenvalue Problem)

Flake rate in main: 26.73% (Passed 869 times, Failed 317 times)

Stack Traces | 0.03s run time
1 numerical error(s), 10080 test(s) run
 SHS:    1 out of  2016 tests failed to pass the threshold

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@EylonKrause

Copy link
Copy Markdown
Author

Closing this — I'll route this fix through a different channel rather than here. Sorry for the noise.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants