python3.14-scipy/scipy-coverity-fixes.patch
Charalampos Stratakis 4c20d7493c Additional coverity fixes
Related: RHEL-120814
2026-02-17 01:51:33 +01:00

780 lines
27 KiB
Diff

From 831457080986da1b686e8060f5957c507d5af18e Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Sat, 20 Dec 2025 00:39:36 +0100
Subject: [PATCH 01/16] BUG: ndimage: fix potential double-free in
NI_InitFilterOffsets
Null out offset pointers after freeing them in the error path of
NI_InitFilterOffsets. Without this, callers that also free these
pointers in their cleanup code could trigger a double-free if the
function fails after allocation.
Found by Coverity static analysis.
---
scipy/ndimage/src/ni_support.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/scipy/ndimage/src/ni_support.c b/scipy/ndimage/src/ni_support.c
index a6e3ff0..934ac74 100644
--- a/scipy/ndimage/src/ni_support.c
+++ b/scipy/ndimage/src/ni_support.c
@@ -734,8 +734,10 @@ int NI_InitFilterOffsets(PyArrayObject *array, npy_bool *footprint,
exit:
if (PyErr_Occurred()) {
free(*offsets);
+ *offsets = NULL;
if (coordinate_offsets) {
free(*coordinate_offsets);
+ *coordinate_offsets = NULL;
}
return 0;
} else {
--
2.53.0
From ac244882ec070351e8de297245d2fb002a93bfa8 Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Sat, 20 Dec 2025 01:08:47 +0100
Subject: [PATCH 02/16] BUG: signal/_firfilter.cc: fix out-of-bounds read in
pylab_convolve_2d
Move the type_num bounds check before using it as an array index.
Previously, OneMultAdd[type_num] was accessed before validating that
type_num was within bounds, which could cause an out-of-bounds read
when passed an unsupported array dtype.
Also fix off-by-one error via changing > MAXTYPES to >= MAXTYPES since
the arrays have MAXTYPES-1 elements.
Found by Coverity static analysis scan.
---
scipy/signal/_firfilter.cc | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/scipy/signal/_firfilter.cc b/scipy/signal/_firfilter.cc
index 9636961..b1f3f19 100644
--- a/scipy/signal/_firfilter.cc
+++ b/scipy/signal/_firfilter.cc
@@ -180,10 +180,11 @@ int pylab_convolve_2d (char *in, /* Input data Ns[0] x Ns[1] */
const int type_num = (flag & TYPE_MASK) >> TYPE_SHIFT;
/*type_size*/
+ if (type_num < 0 || type_num >= MAXTYPES) return -4; /* Invalid type */
+
OneMultAddFunction *mult_and_add = OneMultAdd[type_num];
if (mult_and_add == NULL) return -5; /* Not available for this type */
- if (type_num < 0 || type_num > MAXTYPES) return -4; /* Invalid type */
const int type_size = elsizes[type_num];
int64_t Os[2];
--
2.53.0
From 901a5326ffd89038b21d89d8b3df5f8458ef305e Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Sat, 20 Dec 2025 01:20:43 +0100
Subject: [PATCH 03/16] BUG: optimize/__lbfgsb.c: fix pointer arithmetic bug in
cauchy function
Change nseg += 1 to *nseg += 1 to increment the segment counter
value rather than performing pointer arithmetic on the pointer itself.
Found by Coverity static analysis.
---
scipy/optimize/__lbfgsb.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scipy/optimize/__lbfgsb.c b/scipy/optimize/__lbfgsb.c
index 306f6d6..4a1dc98 100644
--- a/scipy/optimize/__lbfgsb.c
+++ b/scipy/optimize/__lbfgsb.c
@@ -1694,7 +1694,7 @@ cauchy(int n, double* x, double* l, double* u,
}
// Update the derivative information.
- nseg += 1;
+ *nseg += 1;
dibp2 = pow(dibp, 2.0);
// Update f1 and f2
--
2.53.0
From d10a6599ba758887c7a1c9b2e15d39f0f387da53 Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Sat, 20 Dec 2025 03:43:12 +0100
Subject: [PATCH 04/16] BUG: optimize/tnc/tnc.c: fix uninitialized xoffset when
scale is provided
When calling tnc() with a non-NULL scale array but NULL offset array,
and scale[i] != 0.0, the xoffset[i] element was never initialized.
The uninitialized value was then read in tnc_minimize() -> scalex().
Found via Coverity scan analysis.
---
scipy/optimize/tnc/tnc.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/scipy/optimize/tnc/tnc.c b/scipy/optimize/tnc/tnc.c
index 0b06d0e..d902dbf 100644
--- a/scipy/optimize/tnc/tnc.c
+++ b/scipy/optimize/tnc/tnc.c
@@ -353,6 +353,8 @@ int tnc(int n, double x[], double *f, double g[], tnc_function * function,
xscale[i] = fabs(scale[i]);
if (xscale[i] == 0.0) {
xoffset[i] = low[i] = up[i] = x[i];
+ } else {
+ xoffset[i] = x[i];
}
} else if (low[i] != -HUGE_VAL && up[i] != HUGE_VAL) {
xscale[i] = up[i] - low[i];
--
2.53.0
From 75a7278f79a0ab3d3b7a65b0611b89bb47f88806 Mon Sep 17 00:00:00 2001
From: SoheilStar <75124326+soheil-star01@users.noreply.github.com>
Date: Sat, 20 Dec 2025 23:21:11 +0200
Subject: [PATCH 05/16] BUG: optimize: Remove redundant conditional in
_shgo.sampling_custom
Both if and else branches executed the same code. Simplifies to a
single assignment as confirmed by the original author.
---
scipy/optimize/_shgo.py | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/scipy/optimize/_shgo.py b/scipy/optimize/_shgo.py
index b98a4b5..5fe0899 100644
--- a/scipy/optimize/_shgo.py
+++ b/scipy/optimize/_shgo.py
@@ -1458,10 +1458,7 @@ class SHGO:
"""
# Generate sampling points.
# Generate uniform sample points in [0, 1]^m \subset R^m
- if self.n_sampled == 0:
- self.C = self.sampling_function(n, dim)
- else:
- self.C = self.sampling_function(n, dim)
+ self.C = self.sampling_function(n, dim)
# Distribute over bounds
for i in range(len(self.bounds)):
self.C[:, i] = (self.C[:, i] *
--
2.53.0
From 7a648a78aa56fc79bd7e0ec26320b17d26bc698a Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Mon, 22 Dec 2025 01:40:36 +0100
Subject: [PATCH 06/16] BUG: sparse.linalg: Fix copy-paste error in
get_OPinv_matvec
In the type check condition for matrix M, is_pydata_spmatrix() was
incorrectly called with A instead of M.
---
scipy/sparse/linalg/_eigen/arpack/arpack.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scipy/sparse/linalg/_eigen/arpack/arpack.py b/scipy/sparse/linalg/_eigen/arpack/arpack.py
index f678dea..0c70bf9 100644
--- a/scipy/sparse/linalg/_eigen/arpack/arpack.py
+++ b/scipy/sparse/linalg/_eigen/arpack/arpack.py
@@ -1084,7 +1084,7 @@ def get_OPinv_matvec(A, M, sigma, hermitian=False, tol=0):
M, sigma, tol=tol).matvec
else:
if ((not isdense(A) and not issparse(A) and not is_pydata_spmatrix(A)) or
- (not isdense(M) and not issparse(M) and not is_pydata_spmatrix(A))):
+ (not isdense(M) and not issparse(M) and not is_pydata_spmatrix(M))):
return IterOpInv(aslinearoperator(A),
aslinearoperator(M),
sigma, tol=tol).matvec
--
2.53.0
From 90cba299c613da720e923c65c57230b046c22b87 Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Mon, 22 Dec 2025 02:23:55 +0100
Subject: [PATCH 07/16] BUG: optimize/_direct: Fix memory leaks in
direct_direct_()
Two early return paths bypassed the cleanup section, leaking all
allocated memory (13 malloc'd buffers).
After direct_dirinit_() failure changed return NULL to goto
cleanup. The ret variable is already NULL in this case.
After Python callback failure set ret = NULL and goto cleanup
to ensure proper memory deallocation.
Found by Coverity static analysis.
---
scipy/optimize/_direct/DIRect.c | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/scipy/optimize/_direct/DIRect.c b/scipy/optimize/_direct/DIRect.c
index 05249fa..9ec6a42 100644
--- a/scipy/optimize/_direct/DIRect.c
+++ b/scipy/optimize/_direct/DIRect.c
@@ -455,7 +455,7 @@
fmax, &ifeasiblef, &iinfesiblef, ierror, args, jones,
force_stop);
if (!ret) {
- return NULL;
+ goto cleanup;
}
/* +-----------------------------------------------------------------------+ */
/* | Added error checking. | */
@@ -773,7 +773,8 @@
PyObject* callback_py = PyObject_CallObject(callback, arg_tuple);
Py_DECREF(arg_tuple);
if( !callback_py ) {
- return NULL;
+ ret = NULL;
+ goto cleanup;
}
}
/* L10: */
--
2.53.0
From db2d85df49602f5fe82da03ec5750cad597a3648 Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Tue, 13 Jan 2026 23:07:38 +0100
Subject: [PATCH 08/16] BUG: fix freeing of uninitialized memory in error paths
in ndimage
Initialize pointer arrays immediately after allocation and error check,
before any jumps to cleanup code. Previously, if an allocation failed
after data_offsets, offsets, or splvals were allocated but before
their elements were initialized, the cleanup code would attempt to free
uninitialized pointers.
Found via Coverity scan analysis
---
scipy/ndimage/src/ni_interpolation.c | 22 ++++++++++++++--------
1 file changed, 14 insertions(+), 8 deletions(-)
diff --git a/scipy/ndimage/src/ni_interpolation.c b/scipy/ndimage/src/ni_interpolation.c
index f0f86a7..5a11419 100644
--- a/scipy/ndimage/src/ni_interpolation.c
+++ b/scipy/ndimage/src/ni_interpolation.c
@@ -301,6 +301,8 @@ NI_GeometricTransform(PyArrayObject *input, int (*map)(npy_intp*, double*,
PyErr_NoMemory();
goto exit;
}
+ for(jj = 0; jj < irank; jj++)
+ data_offsets[jj] = NULL;
if (mode == NI_EXTEND_GRID_CONSTANT) {
// boolean indicating if the current point in the filter footprint is
@@ -323,8 +325,6 @@ NI_GeometricTransform(PyArrayObject *input, int (*map)(npy_intp*, double*,
}
}
- for(jj = 0; jj < irank; jj++)
- data_offsets[jj] = NULL;
for(jj = 0; jj < irank; jj++) {
data_offsets[jj] = malloc((order + 1) * sizeof(npy_intp));
if (NPY_UNLIKELY(!data_offsets[jj])) {
@@ -717,19 +717,25 @@ int NI_ZoomShift(PyArrayObject *input, PyArrayObject* zoom_ar,
}
/* store offsets, along each axis: */
offsets = malloc(rank * sizeof(npy_intp*));
+ if (NPY_UNLIKELY(!offsets)) {
+ NPY_END_THREADS;
+ PyErr_NoMemory();
+ goto exit;
+ }
+ for(jj = 0; jj < rank; jj++)
+ offsets[jj] = NULL;
+
/* store spline coefficients, along each axis: */
splvals = malloc(rank * sizeof(double**));
- /* store offsets at all edges: */
-
- if (NPY_UNLIKELY(!offsets || !splvals)) {
+ if (NPY_UNLIKELY(!splvals)) {
NPY_END_THREADS;
PyErr_NoMemory();
goto exit;
}
- for(jj = 0; jj < rank; jj++) {
- offsets[jj] = NULL;
+ for(jj = 0; jj < rank; jj++)
splvals[jj] = NULL;
- }
+
+ /* store offsets at all edges: */
for(jj = 0; jj < rank; jj++) {
offsets[jj] = malloc(odimensions[jj] * sizeof(npy_intp));
splvals[jj] = malloc(odimensions[jj] * sizeof(double*));
--
2.53.0
From 0ee12cdbbd73242b1febdf951803f60711b95fe8 Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Wed, 14 Jan 2026 04:14:29 +0100
Subject: [PATCH 09/16] BUG: fix uninitialized variables in odr
The if-else clauses handling pwe and pwd arrays had no terminal
else clause. If an object passed validation but didn't match any
handling branch, ldwe,ld2we,ldwd and ld2wd would be used uninitialized.
Add else clauses to ensure all code paths either initialize these
variables or exit with an error.
Found via Coverity static analysis
---
scipy/odr/__odrpack.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/scipy/odr/__odrpack.c b/scipy/odr/__odrpack.c
index f86a65f..2027166 100644
--- a/scipy/odr/__odrpack.c
+++ b/scipy/odr/__odrpack.c
@@ -780,6 +780,10 @@ PyObject *odr(PyObject * self, PyObject * args, PyObject * kwds)
PYERR(PyExc_ValueError, "could not convert we to a suitable array");
}
} /* we */
+ else
+ {
+ PYERR(PyExc_ValueError, "could not convert we to a suitable array");
+ }
if (pwd == NULL)
{
@@ -871,6 +875,10 @@ PyObject *odr(PyObject * self, PyObject * args, PyObject * kwds)
}
} /* wd */
+ else
+ {
+ PYERR(PyExc_ValueError, "could not convert wd to a suitable array");
+ }
if (pifixb == NULL)
--
2.53.0
From 9da2c240fa0a277dc2db35b3754840ed7561d6a3 Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Wed, 14 Jan 2026 05:07:07 +0100
Subject: [PATCH 10/16] BUG: fix uninitialized variable in ILU complex copy at
sparse/SuperLU
The SMILU_3 case in the second loop of ilu_ccopy_to_ucol.c and
ilu_zcopy_to_ucol.c used tmp which could be uninitialized. Compute
the absolute value directly instead.
Found via Coverity static analysis
---
scipy/sparse/linalg/_dsolve/SuperLU/SRC/ilu_ccopy_to_ucol.c | 2 +-
scipy/sparse/linalg/_dsolve/SuperLU/SRC/ilu_zcopy_to_ucol.c | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/scipy/sparse/linalg/_dsolve/SuperLU/SRC/ilu_ccopy_to_ucol.c b/scipy/sparse/linalg/_dsolve/SuperLU/SRC/ilu_ccopy_to_ucol.c
index 9817fe3..93a3c1b 100644
--- a/scipy/sparse/linalg/_dsolve/SuperLU/SRC/ilu_ccopy_to_ucol.c
+++ b/scipy/sparse/linalg/_dsolve/SuperLU/SRC/ilu_ccopy_to_ucol.c
@@ -190,7 +190,7 @@ ilu_ccopy_to_ucol(
c_add(sum, sum, &ucol[i]);
break;
case SMILU_3:
- sum->r += tmp;
+ sum->r += c_abs1(&ucol[i]);
break;
case SILU:
default:
diff --git a/scipy/sparse/linalg/_dsolve/SuperLU/SRC/ilu_zcopy_to_ucol.c b/scipy/sparse/linalg/_dsolve/SuperLU/SRC/ilu_zcopy_to_ucol.c
index bb444d7..063d685 100644
--- a/scipy/sparse/linalg/_dsolve/SuperLU/SRC/ilu_zcopy_to_ucol.c
+++ b/scipy/sparse/linalg/_dsolve/SuperLU/SRC/ilu_zcopy_to_ucol.c
@@ -190,7 +190,7 @@ ilu_zcopy_to_ucol(
z_add(sum, sum, &ucol[i]);
break;
case SMILU_3:
- sum->r += tmp;
+ sum->r += z_abs1(&ucol[i]);
break;
case SILU:
default:
--
2.53.0
From 33b64448e9dd754b2fed17c4a05bd62e09a06b82 Mon Sep 17 00:00:00 2001
From: ilayn <ilhanpolat@gmail.com>
Date: Thu, 15 Jan 2026 18:10:05 +0100
Subject: [PATCH 11/16] MAINT:optimize: Fix memleaks in DIRECT solver and
ext.mod.
---
scipy/optimize/_direct/DIRect.c | 6 +++++-
scipy/optimize/_direct/DIRserial.c | 1 +
scipy/optimize/_direct/DIRsubrout.c | 21 ++++++++++++++++++---
scipy/optimize/_directmodule.c | 13 ++++++++++---
4 files changed, 34 insertions(+), 7 deletions(-)
diff --git a/scipy/optimize/_direct/DIRect.c b/scipy/optimize/_direct/DIRect.c
index 9ec6a42..85bdaed 100644
--- a/scipy/optimize/_direct/DIRect.c
+++ b/scipy/optimize/_direct/DIRect.c
@@ -595,11 +595,15 @@
/* +-----------------------------------------------------------------------+ */
/* | JG 01/22/01 Added variable to keep track of the maximum value found. | */
/* +-----------------------------------------------------------------------+ */
- direct_dirsamplef_(c__, arrayi, &delta, &help, &start, length,
+ Py_XDECREF(ret); /* DECREF previous return value before getting new one */
+ ret = direct_dirsamplef_(c__, arrayi, &delta, &help, &start, length,
logfile, f, &ifree, &maxi, point, fcn, &x[
1], x_seq, &l[1], minf, &minpos, &u[1], n, &MAXFUNC, &
MAXDEEP, &oops, &fmax, &ifeasiblef, &iinfesiblef,
args, force_stop);
+ if (!ret) {
+ goto cleanup;
+ }
if (force_stop && *force_stop) {
*ierror = -102;
*numiter = t;
diff --git a/scipy/optimize/_direct/DIRserial.c b/scipy/optimize/_direct/DIRserial.c
index 10a4870..c2c1688 100644
--- a/scipy/optimize/_direct/DIRserial.c
+++ b/scipy/optimize/_direct/DIRserial.c
@@ -87,6 +87,7 @@
if (force_stop && *force_stop) /* skip eval after forced stop */
f[(pos << 1) + 1] = *fmax;
else {
+ Py_XDECREF(ret); /* DECREF previous iteration's return value */
ret = direct_dirinfcn_(fcn, &x[1], x_seq, &l[1], &u[1], n, &f[(pos << 1) + 1],
&kret, args);
if (!ret) {
diff --git a/scipy/optimize/_direct/DIRsubrout.c b/scipy/optimize/_direct/DIRsubrout.c
index 35b10c7..718802f 100644
--- a/scipy/optimize/_direct/DIRsubrout.c
+++ b/scipy/optimize/_direct/DIRsubrout.c
@@ -7,8 +7,19 @@
#include <math.h>
// #include "numpy/ndarrayobject.h"
-/* Table of constant values */
-
+/*
+ * SCIPY NOTE (2026-01-15):
+ * The following are read-only variables after initialization.
+ *
+ * They exist because f2c passes all arguments by reference (Fortran convention),
+ * so literal constants need addresses. The code is still thread-safe since they
+ * are never modified.
+ *
+ * Note: Cannot mark as 'const' due to f2c function signatures expecting non-const
+ * pointers which makes the surgery more involved. Instead an f2c-free rewrite
+ * is better for long-term maintenance.
+ *
+ */
static integer c__1 = 1;
static integer c__32 = 32;
static integer c__0 = 0;
@@ -1295,11 +1306,15 @@ L50:
/* | JG 01/22/01 Added variable to keep track of the maximum value found. | */
/* | Added variable to keep track if feasible point was found. | */
/* +-----------------------------------------------------------------------+ */
- direct_dirsamplef_(&c__[c_offset], &arrayi[1], &delta, &c__1, &new__, &length[
+ Py_DECREF(ret); /* DECREF before overwriting with new return value */
+ ret = direct_dirsamplef_(&c__[c_offset], &arrayi[1], &delta, &c__1, &new__, &length[
length_offset], logfile, &f[3], free, maxi, &point[
1], fcn, &x[1], x_seq, &l[1], minf, minpos, &u[1], n, maxfunc,
maxdeep, &oops, fmax, ifeasiblef, iinfeasible, args,
force_stop);
+ if (!ret) {
+ return NULL;
+ }
if (force_stop && *force_stop) {
*ierror = -102;
return ret;
diff --git a/scipy/optimize/_directmodule.c b/scipy/optimize/_directmodule.c
index d1bfb33..d21734d 100644
--- a/scipy/optimize/_directmodule.c
+++ b/scipy/optimize/_directmodule.c
@@ -36,7 +36,7 @@ direct(PyObject *self, PyObject *args)
dimension = PyArray_DIMS((PyArrayObject*)lb)[0];
x = (double *) malloc(sizeof(double) * (dimension + 1));
- if (!(x)) {
+ if (!x) {
ret_code = DIRECT_OUT_OF_MEMORY;
}
PyObject *x_seq = PyList_New(dimension);
@@ -46,17 +46,24 @@ direct(PyObject *self, PyObject *args)
force_stop = 0;
direct_return_info info;
- if (!direct_optimize(f, x, x_seq, f_args, dimension, lower_bounds,
+ PyObject *direct_ret = direct_optimize(f, x, x_seq, f_args, dimension, lower_bounds,
upper_bounds, &minf, max_feval, max_iter,
magic_eps, magic_eps_abs, volume_reltol,
sigma_reltol, &force_stop, fglobal, fglobal_reltol,
- logfile, algorithm, &info, &ret_code, callback)) {
+ logfile, algorithm, &info, &ret_code, callback);
+ if (!direct_ret) {
+ Py_DECREF(x_seq);
if (x)
free(x);
return NULL;
}
+ /* DECREF the return value from direct_optimize - we only needed it for error checking */
+ Py_DECREF(direct_ret);
PyObject* ret_py = Py_BuildValue("Odiii", x_seq, minf, (int) ret_code,
info.numfunc, info.numiter);
+ /* Py_BuildValue with "O" increments refcount. We need to DECREF our
+ original reference since the tuple now owns it. */
+ Py_DECREF(x_seq);
if (x)
free(x);
return ret_py;
--
2.53.0
From da3df170f45d970ddf3ef0e35e7ea241a46ce306 Mon Sep 17 00:00:00 2001
From: ilayn <ilhanpolat@gmail.com>
Date: Thu, 15 Jan 2026 18:19:06 +0100
Subject: [PATCH 12/16] MAINT:optimize: Enable multi-phase init to DIRECT
---
scipy/optimize/_directmodule.c | 54 +++++++++++++++++++---------------
1 file changed, 31 insertions(+), 23 deletions(-)
diff --git a/scipy/optimize/_directmodule.c b/scipy/optimize/_directmodule.c
index d21734d..aa3a80b 100644
--- a/scipy/optimize/_directmodule.c
+++ b/scipy/optimize/_directmodule.c
@@ -73,38 +73,46 @@ direct(PyObject *self, PyObject *args)
* Standard Python module interface
*/
-static PyMethodDef
-DIRECTMethods[] = {
+static struct PyMethodDef direct_module_methods[] = {
{"direct", direct, METH_VARARGS, "DIRECT Optimization Algorithm"},
{NULL, NULL, 0, NULL}
};
-static struct PyModuleDef moduledef = {
- PyModuleDef_HEAD_INIT,
- "_direct",
- NULL,
- -1,
- DIRECTMethods,
- NULL,
- NULL,
- NULL,
- NULL
-};
-PyMODINIT_FUNC
-PyInit__direct(void)
-{
- PyObject *module;
+static int module_exec(PyObject *module) {
+ (void)module; /* unused */
- import_array();
- module = PyModule_Create(&moduledef);
- if (module == NULL) {
- return module;
- }
+ if (_import_array() < 0) { return -1; }
#if Py_GIL_DISABLED
PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED);
#endif
+ return 0;
+}
+
- return module;
+static struct PyModuleDef_Slot direct_slots[] = {
+ {Py_mod_exec, module_exec},
+ {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
+#if PY_VERSION_HEX >= 0x030d00f0 /* Python 3.13+ */
+ /* signal that this module supports running without an active GIL */
+ {Py_mod_gil, Py_MOD_GIL_NOT_USED},
+#endif
+ {0, NULL},
+};
+
+
+static struct PyModuleDef moduledef = {
+ .m_base = PyModuleDef_HEAD_INIT,
+ .m_name = "_direct",
+ .m_size = 0,
+ .m_methods = direct_module_methods,
+ .m_slots = direct_slots
+};
+
+
+PyMODINIT_FUNC
+PyInit__direct(void)
+{
+ return PyModuleDef_Init(&moduledef);
}
--
2.53.0
From 73c1ffae44562b099b69c72922897a2ff2807ff7 Mon Sep 17 00:00:00 2001
From: ilayn <ilhanpolat@gmail.com>
Date: Thu, 15 Jan 2026 20:51:09 +0100
Subject: [PATCH 13/16] MAINT:optimize: Decref callback in DIRECT solver
---
scipy/optimize/_direct/DIRect.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/scipy/optimize/_direct/DIRect.c b/scipy/optimize/_direct/DIRect.c
index 85bdaed..16c6426 100644
--- a/scipy/optimize/_direct/DIRect.c
+++ b/scipy/optimize/_direct/DIRect.c
@@ -780,6 +780,7 @@
ret = NULL;
goto cleanup;
}
+ Py_DECREF(callback_py); /* DECREF the callback's return value */
}
/* L10: */
}
--
2.53.0
From 2758feb6b3e9f0677d06a6764759886c8a188a09 Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Wed, 28 Jan 2026 01:00:20 +0100
Subject: [PATCH 14/16] BUG: Initialize icoor array in NI_GeometricTransform in
ndimage
The icoor array could be used uninitialized if a user-provided
LowLevelCallable fails to write its output, or if the function is
called without map, matrix, or coordinates set. Initialize to zero
to ensure defined behavior in these edge cases.
Additionally, add explicit error when of map, matrix, or
coordinates is not provided, rather than silently proceeding
with zero-initialized coordinates.
Uncovered by Coverity static analysis
---
scipy/ndimage/src/ni_interpolation.c | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/scipy/ndimage/src/ni_interpolation.c b/scipy/ndimage/src/ni_interpolation.c
index 5a11419..84e9a72 100644
--- a/scipy/ndimage/src/ni_interpolation.c
+++ b/scipy/ndimage/src/ni_interpolation.c
@@ -265,7 +265,7 @@ NI_GeometricTransform(PyArrayObject *input, int (*map)(npy_intp*, double*,
npy_intp ftmp[NPY_MAXDIMS], *fcoordinates = NULL, *foffsets = NULL;
npy_intp cstride = 0, kk, hh, ll, jj;
npy_intp size;
- double **splvals = NULL, icoor[NPY_MAXDIMS], tmp;
+ double **splvals = NULL, icoor[NPY_MAXDIMS] = {0}, tmp;
npy_intp idimensions[NPY_MAXDIMS], istrides[NPY_MAXDIMS];
NI_Iterator io, ic;
npy_double *matrix = matrix_ar ? (npy_double*)PyArray_DATA(matrix_ar) : NULL;
@@ -467,6 +467,11 @@ NI_GeometricTransform(PyArrayObject *input, int (*map)(npy_intp*, double*,
"coordinate array data type not supported");
goto exit;
}
+ } else {
+ NPY_END_THREADS;
+ PyErr_SetString(PyExc_RuntimeError,
+ "One of `map`, `matrix` or `coordinates` must be provided");
+ goto exit;
}
/* iterate over axes: */
--
2.53.0
From 80c6e128c8ad9d4c1208a75d08d135ddf5ad5acf Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Wed, 28 Jan 2026 02:17:37 +0100
Subject: [PATCH 15/16] BUG: optimize: validate itmax in trlib_eigen_inverse
Add early return when itmax <= 0 to prevent use of uninitialized
residuals array. The inverse iteration loop sets residuals[jj] only
after the first iteration completes. With itmax <= 0, the loop breaks
immediately, leaving residuals uninitialized.
Uncovered by Coverity static analysis
---
scipy/optimize/_trlib/trlib_eigen_inverse.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/scipy/optimize/_trlib/trlib_eigen_inverse.c b/scipy/optimize/_trlib/trlib_eigen_inverse.c
index 79c4aaf..4156483 100644
--- a/scipy/optimize/_trlib/trlib_eigen_inverse.c
+++ b/scipy/optimize/_trlib/trlib_eigen_inverse.c
@@ -78,7 +78,8 @@ trlib_int_t trlib_eigen_inverse(
*lam_pert = -minuslam;
if ( *iter_inv == TRLIB_EIR_FAIL_FACTOR ) { TRLIB_PRINTLN_2("Failure on factorizing in inverse correction!") TRLIB_RETURN(TRLIB_EIR_FAIL_FACTOR) }
-
+ if ( itmax <= 0 ) { TRLIB_PRINTLN_2("Failure on solving inverse correction! (itmax <= 0)") TRLIB_RETURN(TRLIB_EIR_FAIL_LINSOLVE) }
+
// try with TRLIB_EIR_N_STARTVEC different start vectors and hope that it converges for one
seeds[0] = time(NULL);
for(jj = 1; jj < TRLIB_EIR_N_STARTVEC; ++jj ) { seeds[jj] = rand(); }
--
2.53.0
From e0d53160e19ac80621446c3793d8f94ea711a126 Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Tue, 17 Feb 2026 01:14:29 +0100
Subject: [PATCH 16/16] BUG: Split edge_offsets/data_offsets error checks in
NI_GeometricTransform
The combined check allowed a path where edge_offsets fails but
data_offsets succeeds, reaching cleanup before data_offsets elements
are NULL-initialized.
Completes the fix in 339ccf65c3.
---
scipy/ndimage/src/ni_interpolation.c | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/scipy/ndimage/src/ni_interpolation.c b/scipy/ndimage/src/ni_interpolation.c
index 84e9a72..1030ca1 100644
--- a/scipy/ndimage/src/ni_interpolation.c
+++ b/scipy/ndimage/src/ni_interpolation.c
@@ -295,8 +295,13 @@ NI_GeometricTransform(PyArrayObject *input, int (*map)(npy_intp*, double*,
/* offsets used at the borders: */
edge_offsets = malloc(irank * sizeof(npy_intp*));
+ if (NPY_UNLIKELY(!edge_offsets)) {
+ NPY_END_THREADS;
+ PyErr_NoMemory();
+ goto exit;
+ }
data_offsets = malloc(irank * sizeof(npy_intp*));
- if (NPY_UNLIKELY(!edge_offsets || !data_offsets)) {
+ if (NPY_UNLIKELY(!data_offsets)) {
NPY_END_THREADS;
PyErr_NoMemory();
goto exit;
--
2.53.0