From c57cb6bec592a8b9d0967668ec2769560f1d95b1 Mon Sep 17 00:00:00 2001 From: KRGulaj Date: Tue, 6 Oct 2026 22:48:02 +0200 Subject: [PATCH 1/3] Fix off-by-one bounds checks of the Python wrapper matrix views The element access of CPyWrapperMatrixView, CPyWrapperMarkerMatrixView and CPyWrapper3DMatrixView checks zero-based indices with "index > extent", so the first index past the end passes the check. For example, driver.Coordinates()(nPoint, 0) or Solution(iSolver)(0, nVar) returns data instead of raising the "out of bounds" error: - a column index equal to the number of columns reads or writes the first element of the next row (Set with a list one value too long does the same); - a row index equal to the number of rows reads or writes past the end of the buffer; - for the marker views, a vertex index equal to the number of vertices dereferences a pointer past the vertex array and crashes. Compare with ">=" in the three const Access functions. The non-const Access functions and every Get/Set/operator() of the views go through them, so this covers all entry points. Results do not change: valid indices pass the check as before, and no solver path uses these views. --- Common/include/containers/CPyWrapperMatrixView.hpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Common/include/containers/CPyWrapperMatrixView.hpp b/Common/include/containers/CPyWrapperMatrixView.hpp index 739773de4923..effcae95b05c 100644 --- a/Common/include/containers/CPyWrapperMatrixView.hpp +++ b/Common/include/containers/CPyWrapperMatrixView.hpp @@ -92,7 +92,7 @@ class CPyWrapperMatrixView { /*--- Define the functions required by the interface macro. ---*/ inline const su2double& Access(unsigned long row, unsigned long col) const { - if (row > rows_ || col > cols_) SU2_MPI::Error(name_ + " out of bounds", "CPyWrapperMatrixView"); + if (row >= rows_ || col >= cols_) SU2_MPI::Error(name_ + " out of bounds", "CPyWrapperMatrixView"); return data_[row * cols_ + col]; } inline su2double& Access(unsigned long row, unsigned long col) { @@ -133,7 +133,7 @@ class CPyWrapperMarkerMatrixView { /*--- Define the functions required by the interface macro. ---*/ inline const su2double& Access(unsigned long row, unsigned long col) const { - if (row > rows_ || col > cols_) SU2_MPI::Error(name_ + " out of bounds", "CPyWrapperMarkerMatrixView"); + if (row >= rows_ || col >= cols_) SU2_MPI::Error(name_ + " out of bounds", "CPyWrapperMarkerMatrixView"); return data_[vertices_[row]->GetNode() * cols_ + col]; } inline su2double& Access(unsigned long row, unsigned long col) { @@ -183,7 +183,8 @@ class CPyWrapper3DMatrixView { /*--- Define the functions required by the interface macro. ---*/ inline const su2double& Access(unsigned long row, unsigned long col, unsigned long dim) const { - if (row > rows_ || col > cols_ || dim > dims_) SU2_MPI::Error(name_ + " out of bounds", "CPyWrapper3DMatrixView"); + if (row >= rows_ || col >= cols_ || dim >= dims_) + SU2_MPI::Error(name_ + " out of bounds", "CPyWrapper3DMatrixView"); return data_[row * (cols_ * dims_) + col * dims_ + dim]; } inline su2double& Access(unsigned long row, unsigned long col, unsigned long dim) { From fdffa4b9eb1bf23aa0de9cb60024f92ed7cdedb7 Mon Sep 17 00:00:00 2001 From: KRGulaj Date: Tue, 6 Oct 2026 22:48:02 +0200 Subject: [PATCH 2/3] Add Kajetan R. Gulaj to AUTHORS.md --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index d82ccb599fe9..08b2823f0ad4 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -103,6 +103,7 @@ JonathanSmith1936 Joseph Signorelli Joshua A. Kelly Josy P. Pullockara +Kajetan R. Gulaj Kedar Naik Kürşat Yurt LaSerpe From 5e0447e8df9c0f2821766294a6ce0cd64e68058f Mon Sep 17 00:00:00 2001 From: KRGulaj Date: Wed, 7 Oct 2026 21:52:25 +0200 Subject: [PATCH 3/3] Add a regression test for the bounds checks of the Python wrapper matrix views The test runs a small steady heat conduction case on a generated rectangle mesh through the Python wrapper and uses one view of each class: Solution, MarkerSolution and Gradient of the heat solver. For every view and every dimension, a child process accesses the first index past the end and must stop with the "out of bounds" error. The error ends the process (SU2_MPI::Error), so it cannot be caught in the script itself, and each access runs in its own process. After the iterations, the script sets and reads back the last valid element of each view, so the checks must also accept every valid index. If any check fails, the script names it and exits with an error. Without the previous fix, all seven accesses pass the checks: six return a value of another element or from past the end of the buffer, and the marker row past the end crashes. Only a new entry is added, the other regression values are unchanged. --- .../matrix_view_bounds/heat_rectangle.cfg | 36 ++++++++ .../py_wrapper/matrix_view_bounds/run.py | 90 +++++++++++++++++++ TestCases/serial_regression.py | 13 +++ 3 files changed, 139 insertions(+) create mode 100644 TestCases/py_wrapper/matrix_view_bounds/heat_rectangle.cfg create mode 100755 TestCases/py_wrapper/matrix_view_bounds/run.py diff --git a/TestCases/py_wrapper/matrix_view_bounds/heat_rectangle.cfg b/TestCases/py_wrapper/matrix_view_bounds/heat_rectangle.cfg new file mode 100644 index 000000000000..531d6c681125 --- /dev/null +++ b/TestCases/py_wrapper/matrix_view_bounds/heat_rectangle.cfg @@ -0,0 +1,36 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% SU2 configuration file % +% Case description: Bounds checks of the Python wrapper matrix views % +% File Version 8.5.0 "Harrier" % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +SOLVER= HEAT_EQUATION +INC_NONDIM= DIMENSIONAL + +FREESTREAM_TEMPERATURE= 300.0 +MATERIAL_DENSITY= 1000.0 +SPECIFIC_HEAT_CP= 500.0 +THERMAL_CONDUCTIVITY_CONSTANT= 10.0 + +MARKER_ISOTHERMAL= ( x_minus, 310.0, x_plus, 300.0 ) +MARKER_HEATFLUX= ( y_minus, 0.0, y_plus, 0.0 ) +MARKER_MONITORING= ( x_minus ) + +NUM_METHOD_GRAD= GREEN_GAUSS +CFL_NUMBER= 10.0 +CFL_ADAPT= NO + +LINEAR_SOLVER= FGMRES +LINEAR_SOLVER_PREC= ILU +LINEAR_SOLVER_ERROR= 1E-6 +LINEAR_SOLVER_ITER= 10 + +MESH_FORMAT= RECTANGLE +MESH_BOX_SIZE= ( 9, 5, 0 ) +MESH_BOX_LENGTH= ( 0.1, 0.05, 0 ) + +OUTPUT_FILES= NONE +SCREEN_OUTPUT= INNER_ITER, RMS_RES, MAX_RES, TOTAL_HEATFLUX + +ITER= 11 +CONV_RESIDUAL_MINVAL= -12 diff --git a/TestCases/py_wrapper/matrix_view_bounds/run.py b/TestCases/py_wrapper/matrix_view_bounds/run.py new file mode 100755 index 000000000000..2f12106ab1f9 --- /dev/null +++ b/TestCases/py_wrapper/matrix_view_bounds/run.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python + +## \file run.py +# \brief Bounds checks of the Python wrapper matrix views. +# \version 8.5.0 "Harrier" +# +# SU2 Project Website: https://su2code.github.io +# +# The SU2 Project is maintained by the SU2 Foundation +# (http://su2foundation.org) +# +# Copyright 2012-2026, SU2 Contributors (cf. AUTHORS.md) +# +# SU2 is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# SU2 is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with SU2. If not, see . + +import subprocess +import sys +import pysu2 + +def Views(driver): + """ + One view of each class: CPyWrapperMatrixView, CPyWrapperMarkerMatrixView, CPyWrapper3DMatrixView. + """ + iSolver = driver.GetSolverIndices()['HEAT'] + iMarker = driver.GetMarkerIndices()['x_plus'] + return {'Solution': driver.Solution(iSolver), + 'MarkerSolution': driver.MarkerSolution(iSolver, iMarker), + 'Gradient': driver.Gradient(iSolver)} + +def main(): + """ + Run the heat solver, then check that the views accept the last valid index and + stop with an error at the first index past the end, in every dimension. + Usage: run.py [ ], the second form is the child process. + """ + cfg = sys.argv[1] + + if len(sys.argv) == 4: + # Child process: access the view with the first index past the end of one dimension. + driver = pysu2.CSinglezoneDriver(cfg, 1, 0) + view = Views(driver)[sys.argv[2]] + iDim = int(sys.argv[3]) + index = [0] * len(view.Shape()) + index[iDim] = view.Shape()[iDim] + print(f'{sys.argv[2]}{tuple(index)} returned {view(*index)}') + return + + # SU2_MPI::Error ends the process, so each access past the end runs in a child process, + # which must stop with the "out of bounds" error. + failed = [] + for name, nDim in (('Solution', 2), ('MarkerSolution', 2), ('Gradient', 3)): + for iDim in range(nDim): + child = subprocess.run([sys.executable, __file__, cfg, name, str(iDim)], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) + if child.returncode == 0 or 'out of bounds' not in child.stdout: + returned = [line for line in child.stdout.splitlines() if ' returned ' in line] + failed.append(f'{name}, index past the end of dimension {iDim}: exit code {child.returncode}, ' + f'no out of bounds error. {" ".join(returned)}') + + driver = pysu2.CSinglezoneDriver(cfg, 1, 0) + driver.StartSolver() + + # The last valid index of each view returns the value set through it. + for name, view in Views(driver).items(): + index = [n - 1 for n in view.Shape()] + value = view(*index) + 1.0 + view.Set(*index, value) + if view(*index) != value: + failed.append(f'{name}{tuple(index)} returned {view(*index)} instead of {value}') + + driver.Finalize() + + for message in failed: + print(f'FAILED: {message}') + if failed: + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/TestCases/serial_regression.py b/TestCases/serial_regression.py index c34fe6f47225..9050208f7139 100755 --- a/TestCases/serial_regression.py +++ b/TestCases/serial_regression.py @@ -1725,6 +1725,19 @@ def main(): test_list.append(pywrapper_custom_inlet) pass_list.append(pywrapper_custom_inlet.run_test(args.tsan, args.asan)) + # Bounds checks of the matrix views + pywrapper_matrix_view_bounds = TestCase('pywrapper_matrix_view_bounds') + pywrapper_matrix_view_bounds.cfg_dir = "py_wrapper/matrix_view_bounds" + pywrapper_matrix_view_bounds.cfg_file = "heat_rectangle.cfg" + pywrapper_matrix_view_bounds.test_iter = 10 + pywrapper_matrix_view_bounds.test_vals = [-5.122599, -4.820574, 127.760000] + pywrapper_matrix_view_bounds.command = TestCase.Command(exec = "python", param = "run.py") + pywrapper_matrix_view_bounds.timeout = 1600 + pywrapper_matrix_view_bounds.tol = 0.00001 + pywrapper_matrix_view_bounds.enabled_with_asan = False + test_list.append(pywrapper_matrix_view_bounds) + pass_list.append(pywrapper_matrix_view_bounds.run_test(args.tsan, args.asan)) + # Tests summary print('==================================================================') print('Summary of the serial tests')