From e6d158a95a458d2beef32b5c56d846030bfef4d2 Mon Sep 17 00:00:00 2001 From: Giulia Pagallo Date: Mon, 6 Jul 2026 17:08:25 -0700 Subject: [PATCH 1/3] Add mole::check_spacing helper for input validation Introduces a small helper that rejects zero, negative, NaN and Inf spacing arguments with std::invalid_argument. Used by every operator constructor and AddScalarBC free function that takes a dx/dy/dz parameter, so validation is active in both Debug and Release builds (unlike assert(), which vanishes under NDEBUG). Matches the existing throw std::invalid_argument pattern already used in MixedBC. --- src/cpp/utils.cpp | 12 ++++++++++++ src/cpp/utils.h | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/cpp/utils.cpp b/src/cpp/utils.cpp index b6f02d6d5..a35369f39 100644 --- a/src/cpp/utils.cpp +++ b/src/cpp/utils.cpp @@ -17,6 +17,9 @@ #include "utils.h" #include +#include +#include +#include #ifdef EIGEN #include @@ -244,3 +247,12 @@ double Utils::trapz(const vec &x, const vec &y) { return 0.5 * sum; } +// Spacing validation shared across every operator entry point. +void mole::check_spacing(Real h, const char* name) { + if (!std::isfinite(h) || h <= 0.0) { + throw std::invalid_argument( + std::string("MOLE: ") + name + + " must be a positive finite number, got " + std::to_string(h)); + } +} + diff --git a/src/cpp/utils.h b/src/cpp/utils.h index dc860a85b..1318bed07 100644 --- a/src/cpp/utils.h +++ b/src/cpp/utils.h @@ -116,4 +116,23 @@ class Utils { static double trapz(const vec &x, const vec &y); }; +namespace mole { + +/** + * @brief Validate a cell-spacing argument. + * + * Throws std::invalid_argument if @p h is zero, negative, NaN, or Inf. + * Called at the top of every operator constructor and AddScalarBC free + * function that takes a dx / dy / dz argument, so validation is active + * in both Debug and Release builds (unlike assert(), which vanishes + * under NDEBUG). + * + * @param h Spacing value to check. + * @param name Parameter name for the error message (e.g. "dx"). + * @throws std::invalid_argument if h is not a positive finite number. + */ +void check_spacing(Real h, const char* name); + +} // namespace mole + #endif // UTILS_H From 1bf170af6e04decaca4fd9bac187114d902d0d55 Mon Sep 17 00:00:00 2001 From: Giulia Pagallo Date: Mon, 6 Jul 2026 17:14:46 -0700 Subject: [PATCH 2/3] Validate dx/dy/dz in all operator entry points (H2) Adds mole::check_spacing calls at the top of every C++ operator constructor and AddScalarBC function that takes a spacing argument (29 entry points across 6 source files, including the periodicGrad1D and periodicDiv1D static helpers). Each entry point is validated individually so error messages point at the function the caller actually invoked. Closes finding H2 from the June 2026 codebase security audit. --- src/cpp/addscalarbc.cpp | 12 ++++++++++++ src/cpp/divergence.cpp | 13 +++++++++++++ src/cpp/gradient.cpp | 13 +++++++++++++ src/cpp/laplacian.cpp | 6 ++++++ src/cpp/mixedbc.cpp | 6 ++++++ src/cpp/robinbc.cpp | 6 ++++++ 6 files changed, 56 insertions(+) diff --git a/src/cpp/addscalarbc.cpp b/src/cpp/addscalarbc.cpp index d3f44d8bf..9bb4affd5 100644 --- a/src/cpp/addscalarbc.cpp +++ b/src/cpp/addscalarbc.cpp @@ -145,6 +145,7 @@ void applyBCPairRhs(vec &b, const vec &dc, const vec &nc, void addScalarBClhs(u16 k, u32 m, Real dx, const vec &dc, const vec &nc, sp_mat &Al, sp_mat &Ar) { + mole::check_spacing(dx, "dx"); Al = sp_mat(m+2, m+2); Ar = sp_mat(m+2, m+2); @@ -171,6 +172,8 @@ void addScalarBClhs(u16 k, u32 m, Real dx, void addScalarBClhs(u16 k, u32 m, Real dx, u32 n, Real dy, const vec &dc, const vec &nc, sp_mat &Al, sp_mat &Ar, sp_mat &Ab, sp_mat &At) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); Al.set_size(0, 0); Ar.set_size(0, 0); Ab.set_size(0, 0); At.set_size(0, 0); @@ -202,6 +205,9 @@ void addScalarBClhs(u16 k, u32 m, Real dx, u32 n, Real dy, u32 o, Real dz, const vec &dc, const vec &nc, sp_mat &Al, sp_mat &Ar, sp_mat &Ab, sp_mat &At, sp_mat &Af, sp_mat &Ak) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); + mole::check_spacing(dz, "dz"); Al.set_size(0,0); Ar.set_size(0,0); Ab.set_size(0,0); At.set_size(0,0); Af.set_size(0,0); Ak.set_size(0,0); @@ -284,6 +290,7 @@ void addScalarBCrhs(vec &b, const vec &dc, const vec &nc, // ============================================================================ void addScalarBC(sp_mat &A, vec &b, u16 k, u32 m, Real dx, const BC1D &bc) { + mole::check_spacing(dx, "dx"); assert(bc.dc.n_elem == 2 && "dc must be a 2x1 vector"); assert(bc.nc.n_elem == 2 && "nc must be a 2x1 vector"); assert(A.n_rows == A.n_cols && "A must be square"); @@ -306,6 +313,8 @@ void addScalarBC(sp_mat &A, vec &b, u16 k, u32 m, Real dx, const BC1D &bc) { void addScalarBC(sp_mat &A, vec &b, u16 k, u32 m, Real dx, u32 n, Real dy, const BC2D &bc) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); assert(bc.dc.n_elem == 4 && "dc must be a 4x1 vector"); assert(bc.nc.n_elem == 4 && "nc must be a 4x1 vector"); assert(bc.v.size() == 4 && "v must have 4 vectors"); @@ -328,6 +337,9 @@ void addScalarBC(sp_mat &A, vec &b, u16 k, u32 m, Real dx, void addScalarBC(sp_mat &A, vec &b, u16 k, u32 m, Real dx, u32 n, Real dy, u32 o, Real dz, const BC3D &bc) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); + mole::check_spacing(dz, "dz"); assert(bc.dc.n_elem == 6 && "dc must be a 6x1 vector"); assert(bc.nc.n_elem == 6 && "nc must be a 6x1 vector"); assert(bc.v.size() == 6 && "v must have 6 vectors"); diff --git a/src/cpp/divergence.cpp b/src/cpp/divergence.cpp index 106376956..da5cbdae4 100644 --- a/src/cpp/divergence.cpp +++ b/src/cpp/divergence.cpp @@ -32,6 +32,7 @@ int Divergence::isPeriodic(const ivec &dc, const ivec &nc) { } sp_mat Divergence::periodicDiv1D(u16 k, u32 m, Real dx) { + mole::check_spacing(dx, "dx"); assert(!(k % 2)); assert(k > 1 && k < 9); assert(m >= 2 * k); @@ -99,6 +100,7 @@ sp_mat Divergence::periodicDiv1D(u16 k, u32 m, Real dx) { // ============================================================================ Divergence::Divergence(u16 k, u32 m, Real dx) : sp_mat(m + 2, m + 1) { + mole::check_spacing(dx, "dx"); assert(!(k % 2)); assert(k > 1 && k < 9); assert(k > 1 && k < 9); @@ -290,6 +292,8 @@ void Divergence::build_divergence(sp_mat &D_m, sp_mat &I, u16 k, u32 dim, // ============================================================================ Divergence::Divergence(u16 k, u32 m, u32 n, Real dx, Real dy) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); Divergence Dx(k, m, dx); Divergence Dy(k, n, dy); @@ -313,6 +317,9 @@ Divergence::Divergence(u16 k, u32 m, u32 n, Real dx, Real dy) { // ============================================================================ Divergence::Divergence(u16 k, u32 m, u32 n, u32 o, Real dx, Real dy, Real dz) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); + mole::check_spacing(dz, "dz"); Divergence Dx(k, m, dx); Divergence Dy(k, n, dy); Divergence Dz(k, o, dz); @@ -347,6 +354,7 @@ Divergence::Divergence(u16 k, u32 m, u32 n, u32 o, Real dx, Real dy, Real dz) { Divergence::Divergence(u16 k, u32 m, Real dx, const ivec &dc, const ivec &nc) : sp_mat() { + mole::check_spacing(dx, "dx"); assert(dc.n_elem == 2 && nc.n_elem == 2); if (isPeriodic(dc, nc)) { @@ -367,6 +375,8 @@ Divergence::Divergence(u16 k, u32 m, Real dx, const ivec &dc, const ivec &nc) Divergence::Divergence(u16 k, u32 m, u32 n, Real dx, Real dy, const ivec &dc, const ivec &nc) : sp_mat() { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); assert(dc.n_elem == 4 && nc.n_elem == 4); // dc/nc index convention: entries [0,1] control the x-axis (left, right), @@ -394,6 +404,9 @@ Divergence::Divergence(u16 k, u32 m, u32 n, Real dx, Real dy, const ivec &dc, Divergence::Divergence(u16 k, u32 m, u32 n, u32 o, Real dx, Real dy, Real dz, const ivec &dc, const ivec &nc) : sp_mat() { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); + mole::check_spacing(dz, "dz"); assert(dc.n_elem == 6 && nc.n_elem == 6); // dc/nc index convention: diff --git a/src/cpp/gradient.cpp b/src/cpp/gradient.cpp index 4ab362f38..65058c956 100644 --- a/src/cpp/gradient.cpp +++ b/src/cpp/gradient.cpp @@ -35,6 +35,7 @@ int Gradient::isPeriodic(const ivec &dc, const ivec &nc) { } sp_mat Gradient::periodicGrad1D(u16 k, u32 m, Real dx) { + mole::check_spacing(dx, "dx"); assert(!(k % 2)); assert(k > 1 && k < 9); assert(m >= 2 * k); @@ -97,6 +98,7 @@ sp_mat Gradient::periodicGrad1D(u16 k, u32 m, Real dx) { // ============================================================================ Gradient::Gradient(u16 k, u32 m, Real dx) : sp_mat(m + 1, m + 2) { + mole::check_spacing(dx, "dx"); assert(!(k % 2)); assert(k > 1 && k < 9); assert(m >= 2 * k); @@ -325,6 +327,8 @@ void Gradient::build_gradient(sp_mat &G_m, sp_mat &I, u16 k, u32 dim, // ============================================================================ Gradient::Gradient(u16 k, u32 m, u32 n, Real dx, Real dy) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); Gradient Gx(k, m, dx); Gradient Gy(k, n, dy); @@ -348,6 +352,9 @@ Gradient::Gradient(u16 k, u32 m, u32 n, Real dx, Real dy) { // ============================================================================ Gradient::Gradient(u16 k, u32 m, u32 n, u32 o, Real dx, Real dy, Real dz) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); + mole::check_spacing(dz, "dz"); Gradient Gx(k, m, dx); Gradient Gy(k, n, dy); Gradient Gz(k, o, dz); @@ -376,6 +383,7 @@ Gradient::Gradient(u16 k, u32 m, u32 n, u32 o, Real dx, Real dy, Real dz) { Gradient::Gradient(u16 k, u32 m, Real dx, const ivec &dc, const ivec &nc) : sp_mat() { + mole::check_spacing(dx, "dx"); assert(dc.n_elem == 2 && nc.n_elem == 2); if (isPeriodic(dc, nc)) { @@ -396,6 +404,8 @@ Gradient::Gradient(u16 k, u32 m, Real dx, const ivec &dc, const ivec &nc) Gradient::Gradient(u16 k, u32 m, u32 n, Real dx, Real dy, const ivec &dc, const ivec &nc) : sp_mat() { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); assert(dc.n_elem == 4 && nc.n_elem == 4); // dc/nc index convention: entries [0,1] control the x-axis (left, right), // entries [2,3] control the y-axis (bottom, top). @@ -422,6 +432,9 @@ Gradient::Gradient(u16 k, u32 m, u32 n, Real dx, Real dy, const ivec &dc, Gradient::Gradient(u16 k, u32 m, u32 n, u32 o, Real dx, Real dy, Real dz, const ivec &dc, const ivec &nc) : sp_mat() { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); + mole::check_spacing(dz, "dz"); assert((dc.n_elem == 6) && (nc.n_elem == 6)); // dc/nc index convention: // [0,1] = left, right (x-axis) diff --git a/src/cpp/laplacian.cpp b/src/cpp/laplacian.cpp index 21defda13..f8490e23a 100644 --- a/src/cpp/laplacian.cpp +++ b/src/cpp/laplacian.cpp @@ -19,6 +19,7 @@ // 1-D Constructor Laplacian::Laplacian(u16 k, u32 m, Real dx) { + mole::check_spacing(dx, "dx"); Divergence div(k, m, dx); Gradient grad(k, m, dx); @@ -28,6 +29,8 @@ Laplacian::Laplacian(u16 k, u32 m, Real dx) { // 2-D Constructor Laplacian::Laplacian(u16 k, u32 m, u32 n, Real dx, Real dy) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); Divergence div(k, m, n, dx, dy); Gradient grad(k, m, n, dx, dy); @@ -37,6 +40,9 @@ Laplacian::Laplacian(u16 k, u32 m, u32 n, Real dx, Real dy) { // 3-D Constructor Laplacian::Laplacian(u16 k, u32 m, u32 n, u32 o, Real dx, Real dy, Real dz) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); + mole::check_spacing(dz, "dz"); Divergence div(k, m, n, o, dx, dy, dz); Gradient grad(k, m, n, o, dx, dy, dz); diff --git a/src/cpp/mixedbc.cpp b/src/cpp/mixedbc.cpp index 8bd639a99..8cc7b903c 100644 --- a/src/cpp/mixedbc.cpp +++ b/src/cpp/mixedbc.cpp @@ -18,6 +18,7 @@ MixedBC::MixedBC(u16 k, u32 m, Real dx, const std::string &left, const std::vector &coeffs_left, const std::string &right, const std::vector &coeffs_right) { + mole::check_spacing(dx, "dx"); sp_mat A(m + 2, m + 2); sp_mat BG(m + 2, m + 2); @@ -65,6 +66,8 @@ MixedBC::MixedBC(u16 k, u32 m, Real dx, u32 n, Real dy, const std::string &left, const std::string &bottom, const std::vector &coeffs_bottom, const std::string &top, const std::vector &coeffs_top) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); MixedBC Bm(k, m, dx, left, coeffs_left, right, coeffs_right); MixedBC Bn(k, n, dy, bottom, coeffs_bottom, top, coeffs_top); @@ -90,6 +93,9 @@ MixedBC::MixedBC(u16 k, u32 m, Real dx, u32 n, Real dy, u32 o, Real dz, const std::vector &coeffs_top, const std::string &front, const std::vector &coeffs_front, const std::string &back, const std::vector &coeffs_back) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); + mole::check_spacing(dz, "dz"); MixedBC Bm(k, m, dx, left, coeffs_left, right, coeffs_right); MixedBC Bn(k, n, dy, bottom, coeffs_bottom, top, coeffs_top); MixedBC Bo(k, o, dz, front, coeffs_front, back, coeffs_back); diff --git a/src/cpp/robinbc.cpp b/src/cpp/robinbc.cpp index 3bb0d7f81..9c5ea7cb8 100644 --- a/src/cpp/robinbc.cpp +++ b/src/cpp/robinbc.cpp @@ -14,6 +14,7 @@ #include "robinbc.h" RobinBC::RobinBC(u16 k, u32 m, Real dx, Real a, Real b) { + mole::check_spacing(dx, "dx"); sp_mat A(m + 2, m + 2); sp_mat BG(m + 2, m + 2); @@ -30,6 +31,8 @@ RobinBC::RobinBC(u16 k, u32 m, Real dx, Real a, Real b) { RobinBC::RobinBC(u16 k, u32 m, Real dx, u32 n, Real dy, Real a, Real b) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); RobinBC Bm(k, m, dx, a, b); RobinBC Bn(k, n, dy, a, b); @@ -48,6 +51,9 @@ RobinBC::RobinBC(u16 k, u32 m, Real dx, u32 n, Real dy, Real a, Real b) { RobinBC::RobinBC(u16 k, u32 m, Real dx, u32 n, Real dy, u32 o, Real dz, Real a, Real b) { + mole::check_spacing(dx, "dx"); + mole::check_spacing(dy, "dy"); + mole::check_spacing(dz, "dz"); RobinBC Bm(k, m, dx, a, b); RobinBC Bn(k, n, dy, a, b); RobinBC Bo(k, o, dz, a, b); From aab0ca4991d8c83e11cf7b37d9bc3c3aeba669a3 Mon Sep 17 00:00:00 2001 From: Giulia Pagallo Date: Mon, 6 Jul 2026 17:14:46 -0700 Subject: [PATCH 3/3] Add tests for spacing validation Covers Gradient, Divergence, Laplacian, RobinBC, MixedBC and AddScalarBC with zero, negative, NaN and Inf inputs, plus positive sanity checks and a check that the error message names the offending parameter. --- tests/cpp/CMakeLists.txt | 1 + tests/cpp/test_spacing_validation.cpp | 145 ++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 tests/cpp/test_spacing_validation.cpp diff --git a/tests/cpp/CMakeLists.txt b/tests/cpp/CMakeLists.txt index 81fdf19f7..7c88f2861 100644 --- a/tests/cpp/CMakeLists.txt +++ b/tests/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ set(TEST_SOURCES test4.cpp test5.cpp test_addscalarbc.cpp + test_spacing_validation.cpp ) set(TEST_EXECUTABLES "") diff --git a/tests/cpp/test_spacing_validation.cpp b/tests/cpp/test_spacing_validation.cpp new file mode 100644 index 000000000..c3eb76e30 --- /dev/null +++ b/tests/cpp/test_spacing_validation.cpp @@ -0,0 +1,145 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * © 2008-2024 San Diego State University Research Foundation (SDSURF). + * See LICENSE file or https://www.gnu.org/licenses/gpl-3.0.html for details. + */ + +/* + * @file test_spacing_validation.cpp + * + * @brief Verifies that every operator entry point rejects malformed cell + * spacings (zero, negative, NaN, Inf) with std::invalid_argument. + * + * Covers finding H2 in the June 2026 codebase security audit. + */ + +#include "mole.h" +#include + +#include +#include +#include +#include +#include + +namespace { + +constexpr Real INF_D = std::numeric_limits::infinity(); +const Real NAN_D = std::numeric_limits::quiet_NaN(); + +} // namespace + +// --------------------------------------------------------------------------- +// Gradient +// --------------------------------------------------------------------------- + +TEST(SpacingValidation, GradientRejectsZero) { + EXPECT_THROW(Gradient(2, 10, 0.0), std::invalid_argument); +} + +TEST(SpacingValidation, GradientRejectsNegative) { + EXPECT_THROW(Gradient(2, 10, -1.0), std::invalid_argument); +} + +TEST(SpacingValidation, GradientRejectsNaN) { + EXPECT_THROW(Gradient(2, 10, NAN_D), std::invalid_argument); +} + +TEST(SpacingValidation, GradientRejectsInf) { + EXPECT_THROW(Gradient(2, 10, INF_D), std::invalid_argument); +} + +TEST(SpacingValidation, Gradient2DRejectsZeroDy) { + EXPECT_THROW(Gradient(2, 10, 10, 0.1, 0.0), std::invalid_argument); +} + +TEST(SpacingValidation, Gradient3DRejectsNegativeDz) { + EXPECT_THROW(Gradient(2, 10, 10, 10, 0.1, 0.1, -0.1), + std::invalid_argument); +} + +// --------------------------------------------------------------------------- +// Divergence +// --------------------------------------------------------------------------- + +TEST(SpacingValidation, DivergenceRejectsZero) { + EXPECT_THROW(Divergence(2, 10, 0.0), std::invalid_argument); +} + +TEST(SpacingValidation, Divergence3DRejectsInfDz) { + EXPECT_THROW(Divergence(2, 10, 10, 10, 0.1, 0.1, INF_D), + std::invalid_argument); +} + +// --------------------------------------------------------------------------- +// Laplacian +// --------------------------------------------------------------------------- + +TEST(SpacingValidation, LaplacianRejectsZeroDz) { + EXPECT_THROW(Laplacian(2, 10, 10, 10, 0.1, 0.1, 0.0), + std::invalid_argument); +} + +// --------------------------------------------------------------------------- +// RobinBC +// --------------------------------------------------------------------------- + +TEST(SpacingValidation, RobinBCRejectsZero) { + EXPECT_THROW(RobinBC(2, 10, 0.0, 1.0, 1.0), std::invalid_argument); +} + +// --------------------------------------------------------------------------- +// MixedBC +// --------------------------------------------------------------------------- + +TEST(SpacingValidation, MixedBCRejectsNegative) { + std::vector cl = {1.0}; + std::vector cr = {1.0}; + EXPECT_THROW(MixedBC(2, 10, -0.1, "Dirichlet", cl, "Dirichlet", cr), + std::invalid_argument); +} + +// --------------------------------------------------------------------------- +// AddScalarBC (free function) +// --------------------------------------------------------------------------- + +TEST(SpacingValidation, AddScalarBCRejectsZero) { + sp_mat A(12, 12); // (m+2) x (m+2) for m = 10 + vec b(12, fill::zeros); + AddScalarBC::BC1D bc; // defaults are safe; dx check fires first + EXPECT_THROW(AddScalarBC::addScalarBC(A, b, 2, 10, 0.0, bc), + std::invalid_argument); +} + +// --------------------------------------------------------------------------- +// Positive sanity check +// --------------------------------------------------------------------------- + +TEST(SpacingValidation, GradientAcceptsValidSpacing) { + EXPECT_NO_THROW(Gradient(2, 10, 0.1)); +} + +TEST(SpacingValidation, Laplacian3DAcceptsValidSpacing) { + EXPECT_NO_THROW(Laplacian(2, 10, 10, 10, 0.1, 0.1, 0.1)); +} + +// --------------------------------------------------------------------------- +// Error-message content +// --------------------------------------------------------------------------- + +TEST(SpacingValidation, ErrorMessageNamesTheParameter) { + try { + Gradient G(2, 10, 0.0); + FAIL() << "expected std::invalid_argument"; + } catch (const std::invalid_argument& e) { + const std::string msg(e.what()); + EXPECT_NE(msg.find("dx"), std::string::npos) + << "expected the message to mention the parameter name, got: " + << msg; + } +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}