Repository: LLNL/serac
Pull request #1355: Resolve some undefined behavior reported by -fsanitize=undefined
Author: login: tepperly
Labels: name: bug
description: Something isn't working
Created: 2025-04-04T16:13:31Z | Merged: 2025-04-04T21:56:00Z

Compiling serac with `-fsanitize=undefined` and running tests produced numerous instances of error messages about calling methods on a `nullptr`.  For example,
```sh
$ ./functional_shape_derivatives 
[INFO] Opening mesh file: '/home/generic/serac/data/meshes/patch2D_tris_and_quads.mesh' 
[INFO] Opening mesh file: '/home/generic/serac/data/meshes/patch3D_tets_and_hexes.mesh' 
[==========] Running 4 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 4 tests from ShapeDerivative
[ RUN      ] ShapeDerivative.2DLinear
/usr/bin/../lib/gcc/x86_64-redhat-linux/13/../../../../include/c++/13/bits/shared_ptr_base.h:1350:9: runtime error: reference binding to null pointer of type 'element_type' (aka 'serac::QuadratureData<Nothing>')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /usr/bin/../lib/gcc/x86_64-redhat-linux/13/../../../../include/c++/13/bits/shared_ptr_base.h:1350:9 in 
/home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88: runtime error: member call on null pointer of type 'serac::QuadratureData<Nothing>'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88 in 
/home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/quadrature_data.hpp:153:31: runtime error: member call on null pointer of type 'serac::QuadratureData<Nothing> *'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/quadrature_data.hpp:153:31 in 
/home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88: runtime error: member call on null pointer of type 'serac::QuadratureData<Nothing>'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88 in 
[       OK ] ShapeDerivative.2DLinear (19 ms)
[ RUN      ] ShapeDerivative.2DQuadratic
/home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88: runtime error: member call on null pointer of type 'serac::QuadratureData<Nothing>'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88 in 
/home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88: runtime error: member call on null pointer of type 'serac::QuadratureData<Nothing>'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88 in 
[       OK ] ShapeDerivative.2DQuadratic (29 ms)
[ RUN      ] ShapeDerivative.3DLinear
/home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88: runtime error: member call on null pointer of type 'serac::QuadratureData<Nothing>'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88 in 
/home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88: runtime error: member call on null pointer of type 'serac::QuadratureData<Nothing>'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88 in 
[       OK ] ShapeDerivative.3DLinear (392 ms)
[ RUN      ] ShapeDerivative.3DQuadratic
/home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88: runtime error: member call on null pointer of type 'serac::QuadratureData<Nothing>'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88 in 
/home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88: runtime error: member call on null pointer of type 'serac::QuadratureData<Nothing>'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /home/generic/serac/src/serac/infrastructure/../../serac/numerics/functional/domain_integral_kernels.hpp:374:88 in 
[       OK ] ShapeDerivative.3DQuadratic (933 ms)
[----------] 4 tests from ShapeDerivative (1379 ms total)

[----------] Global test environment tear-down
[==========] 4 tests from 1 test suite ran. (1383 ms total)
[  PASSED  ] 4 tests.
$
```

@tupek2, I think this resolves the issue I reported to you.


Diff:
diff --git a/src/serac/numerics/functional/quadrature_data.cpp b/src/serac/numerics/functional/quadrature_data.cpp
index 37ff6ba973..5ea77bad44 100644
--- a/src/serac/numerics/functional/quadrature_data.cpp
+++ b/src/serac/numerics/functional/quadrature_data.cpp
@@ -15,9 +15,9 @@
 namespace serac {
 
 /// a single instance of a QuadratureData container of `Nothing`s, since they are all interchangeable
-std::shared_ptr<QuadratureData<Nothing> > NoQData;
+std::shared_ptr<QuadratureData<Nothing>> NoQData = ::std::make_shared<QuadratureData<Nothing>>();
 
 /// a single instance of a QuadratureData container of `Empty`s, since they are all interchangeable
-std::shared_ptr<QuadratureData<Empty> > EmptyQData;
+std::shared_ptr<QuadratureData<Empty>> EmptyQData = ::std::make_shared<QuadratureData<Empty>>();
 
 }  // namespace serac
diff --git a/src/serac/numerics/functional/tests/functional_basic_dg.cpp b/src/serac/numerics/functional/tests/functional_basic_dg.cpp
index cfc43c4442..8f2bbbcff3 100644
--- a/src/serac/numerics/functional/tests/functional_basic_dg.cpp
+++ b/src/serac/numerics/functional/tests/functional_basic_dg.cpp
@@ -95,7 +95,9 @@ void L2_qoi_test(std::string meshfile)
   auto fec = mfem::L2_FECollection(p, dim, mfem::BasisType::GaussLobatto);
   mfem::ParFiniteElementSpace fespace(mesh.get(), &fec, dim, serac::ordering);
 
-  mfem::HypreParVector U = *fespace.NewTrueDofVector();
+  ::std::unique_ptr<mfem::HypreParVector> trueDof(fespace.NewTrueDofVector());
+  mfem::HypreParVector U = *trueDof;
+
   int seed = 2;
   U.Randomize(seed);
 
diff --git a/src/serac/numerics/functional/tests/functional_qoi.cpp b/src/serac/numerics/functional/tests/functional_qoi.cpp
index 990bf38b75..05873e876c 100644
--- a/src/serac/numerics/functional/tests/functional_qoi.cpp
+++ b/src/serac/numerics/functional/tests/functional_qoi.cpp
@@ -199,7 +199,11 @@ void qoi_test(mfem::ParMesh& mesh, H1<p> trial, Dimension<dim>, WhichTest which)
   mfem::FunctionCoefficient x_squared([](mfem::Vector x) { return x[0] * x[0]; });
   U_gf.ProjectCoefficient(x_squared);
 
-  mfem::HypreParVector* tmp = fespace->NewTrueDofVector();
+  // NewTrueDofVector returns a raw pointer allocated using new and gives
+  // ownership of the pointer to the caller. Here we wrap the return value
+  // in a std::unique_ptr to discharge our ownership responsibility to
+  // delete it when we're done with it.
+  ::std::unique_ptr<mfem::HypreParVector> tmp(fespace->NewTrueDofVector());
   mfem::HypreParVector U = *tmp;
   U_gf.GetTrueDofs(U);
 
@@ -207,7 +211,11 @@ void qoi_test(mfem::ParMesh& mesh, H1<p> trial, Dimension<dim>, WhichTest which)
   mfem::FunctionCoefficient x_coord([](mfem::Vector x) { return x[0]; });
   V_gf.ProjectCoefficient(x_coord);
 
-  mfem::HypreParVector* tmp2 = fespace->NewTrueDofVector();
+  // NewTrueDofVector returns a raw pointer allocated using new and gives
+  // ownership of the pointer to the caller. Here we wrap the return value
+  // in a std::unique_ptr to discharge our ownership responsibility to
+  // delete it when we're done with it.
+  ::std::unique_ptr<mfem::HypreParVector> tmp2(fespace->NewTrueDofVector());
   mfem::HypreParVector V = *tmp2;
   V_gf.GetTrueDofs(V);
 
@@ -285,8 +293,6 @@ void qoi_test(mfem::ParMesh& mesh, H1<p> trial, Dimension<dim>, WhichTest which)
 
     } break;
   }
-
-  delete tmp;
 }
 
 template <int p1, int p2, int dim>
@@ -308,9 +314,11 @@ void qoi_test(mfem::ParMesh& mesh, H1<p1> trial1, H1<p2> trial2, Dimension<dim>)
   mfem::ParGridFunction U2_gf(fespace2.get());
   U2_gf.ProjectCoefficient(y);
 
-  std::unique_ptr<mfem::HypreParVector> tmp;
-
-  tmp.reset(fespace1->NewTrueDofVector());
+  // NewTrueDofVector returns a raw pointer allocated using new and gives
+  // ownership of the pointer to the caller. Here we wrap the return value
+  // in a std::unique_ptr to discharge our ownership responsibility to
+  // delete it when we're done with it.
+  std::unique_ptr<mfem::HypreParVector> tmp(fespace1->NewTrueDofVector());
   mfem::HypreParVector U1 = *tmp;
   U1_gf.GetTrueDofs(U1);
 
@@ -359,7 +367,11 @@ TEST(QoI, DependsOnVectorValuedInput)
   });
   U_gf.ProjectCoefficient(x_squared);
 
-  mfem::HypreParVector* tmp = fespace->NewTrueDofVector();
+  // NewTrueDofVector returns a raw pointer allocated using new and gives
+  // ownership of the pointer to the caller. Here we wrap the return value
+  // in a std::unique_ptr to discharge our ownership responsibility to
+  // delete it when we're done with it.
+  ::std::unique_ptr<mfem::HypreParVector> tmp(fespace->NewTrueDofVector());
   mfem::HypreParVector U = *tmp;
   U_gf.GetTrueDofs(U);
 
@@ -371,8 +383,6 @@ TEST(QoI, DependsOnVectorValuedInput)
   double exact_answer = 141.3333333333333;
   double relative_error = (f(t, U) - exact_answer) / exact_answer;
   EXPECT_NEAR(0.0, relative_error, 1.0e-10);
-
-  delete tmp;
 }
 
 TEST(QoI, AddAreaIntegral)
@@ -390,7 +400,11 @@ TEST(QoI, AddAreaIntegral)
   mfem::FunctionCoefficient x_squared([](mfem::Vector x) { return x[0] * x[0]; });
   U_gf.ProjectCoefficient(x_squared);
 
-  mfem::HypreParVector* tmp = fespace->NewTrueDofVector();
+  // NewTrueDofVector returns a raw pointer allocated using new and gives
+  // ownership of the pointer to the caller. Here we wrap the return value
+  // in a std::unique_ptr to discharge our ownership responsibility to
+  // delete it when we're done with it.
+  ::std::unique_ptr<mfem::HypreParVector> tmp(fespace->NewTrueDofVector());
   mfem::HypreParVector U = *tmp;
   U_gf.GetTrueDofs(U);
 
@@ -400,8 +414,6 @@ TEST(QoI, AddAreaIntegral)
   measure.AddAreaIntegral(DependsOn<>{}, TrivialIntegrator{}, whole_mesh);
   double relative_error = (measure(t, U) - measure_mfem(mesh)) / measure(t, U);
   EXPECT_NEAR(0.0, relative_error, 1.0e-10);
-
-  delete tmp;
 }
 
 TEST(QoI, AddVolumeIntegral)
@@ -420,7 +432,11 @@ TEST(QoI, AddVolumeIntegral)
   mfem::FunctionCoefficient x_squared([](mfem::Vector x) { return x[0] * x[0]; });
   U_gf.ProjectCoefficient(x_squared);
 
-  mfem::HypreParVector* tmp = fespace->NewTrueDofVector();
+  // NewTrueDofVector returns a raw pointer allocated using new and gives
+  // ownership of the pointer to the caller. Here we wrap the return value
+  // in a std::unique_ptr to discharge our ownership responsibility to
+  // delete it when we're done with it.
+  ::std::unique_ptr<mfem::HypreParVector> tmp(fespace->NewTrueDofVector());
   mfem::HypreParVector U = *tmp;
   U_gf.GetTrueDofs(U);
 
@@ -430,8 +446,6 @@ TEST(QoI, AddVolumeIntegral)
   measure.AddVolumeIntegral(DependsOn<>{}, TrivialIntegrator{}, whole_mesh);
   double relative_error = (measure(t, U) - measure_mfem(mesh)) / measure(t, U);
   EXPECT_NEAR(0.0, relative_error, 1.0e-10);
-
-  delete tmp;
 }
 
 TEST(QoI, UsingL2)
@@ -448,9 +462,17 @@ TEST(QoI, UsingL2)
 
   auto [fespace_1, fec1] = serac::generateParFiniteElementSpace<trial_space_1>(&mesh);
 
+  // NewTrueDofVector returns a raw pointer allocated using new and gives
+  // ownership of the pointer to the caller. Here we wrap the return value
+  // in a std::unique_ptr to discharge our ownership responsibility to
+  // delete it when we're done with it.
   std::unique_ptr<mfem::HypreParVector> U0(fespace_0->NewTrueDofVector());
   U0->Randomize(0);
 
+  // NewTrueDofVector returns a raw pointer allocated using new and gives
+  // ownership of the pointer to the caller. Here we wrap the return value
+  // in a std::unique_ptr to discharge our ownership responsibility to
+  // delete it when we're done with it.
   std::unique_ptr<mfem::HypreParVector> U1(fespace_1->NewTrueDofVector());
   U1->Randomize(1);
 
@@ -505,9 +527,17 @@ TEST(QoI, ShapeAndParameter)
 
   serac_volume.AddDomainIntegral(serac::Dimension<dim>{}, serac::DependsOn<>{}, TrivialIntegrator{}, whole_mesh);
 
+  // NewTrueDofVector returns a raw pointer allocated using new and gives
+  // ownership of the pointer to the caller. Here we wrap the return value
+  // in a std::unique_ptr to discharge our ownership responsibility to
+  // delete it when we're done with it.
   std::unique_ptr<mfem::HypreParVector> shape_displacement(shape_fe_space->NewTrueDofVector());
   *shape_displacement = 1.0;
 
+  // NewTrueDofVector returns a raw pointer allocated using new and gives
+  // ownership of the pointer to the caller. Here we wrap the return value
+  // in a std::unique_ptr to discharge our ownership responsibility to
+  // delete it when we're done with it.
   std::unique_ptr<mfem::HypreParVector> parameter(parameter_fe_space->NewTrueDofVector());
   *parameter = 0.1;
 
@@ -569,9 +599,17 @@ TEST(QoI, ShapeAndParameterBoundary)
   serac_area.AddBoundaryIntegral(serac::Dimension<dim - 1>{}, serac::DependsOn<>{}, TrivialIntegrator{},
                                  whole_boundary);
 
+  // NewTrueDofVector returns a raw pointer allocated using new and gives
+  // ownership of the pointer to the caller. Here we wrap the return value
+  // in a std::unique_ptr to discharge our ownership responsibility to
+  // delete it when we're done with it.
   std::unique_ptr<mfem::HypreParVector> shape_displacement(shape_fe_space->NewTrueDofVector());
   *shape_displacement = 1.0;
 
+  // NewTrueDofVector returns a raw pointer allocated using new and gives
+  // ownership of the pointer to the caller. Here we wrap the return value
+  // in a std::unique_ptr to discharge our ownership responsibility to
+  // delete it when we're done with it.
   std::unique_ptr<mfem::HypreParVector> parameter(parameter_fe_space->NewTrueDofVector());
   *parameter = 5.0;
 


Reviews:
user: login: tupek2
body: Incredible!  I think this makes sense.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-04T16:15:37Z
user: login: white238
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T18:52:15Z
user: login: tupek2
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T19:31:50Z
user: login: tepperly
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T20:55:13Z
user: login: tepperly
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T20:59:08Z
user: login: white238
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T21:07:00Z
user: login: white238
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-04T21:15:18Z

Review comments:
diff_hunk: @@ -359,7 +357,7 @@ TEST(QoI, DependsOnVectorValuedInput)
   });
   U_gf.ProjectCoefficient(x_squared);
 
-  mfem::HypreParVector* tmp = fespace->NewTrueDofVector();
+  ::std::unique_ptr<mfem::HypreParVector> tmp(fespace->NewTrueDofVector());
path: src/serac/numerics/functional/tests/functional_qoi.cpp
user: login: white238
body: I thought the general consensus was to always use `std::make_unique` is there a reason not to in this case?
created_at: 2025-04-04T18:52:15Z
author_association: MEMBER
diff_hunk: @@ -359,7 +357,7 @@ TEST(QoI, DependsOnVectorValuedInput)
   });
   U_gf.ProjectCoefficient(x_squared);
 
-  mfem::HypreParVector* tmp = fespace->NewTrueDofVector();
+  ::std::unique_ptr<mfem::HypreParVector> tmp(fespace->NewTrueDofVector());
path: src/serac/numerics/functional/tests/functional_qoi.cpp
user: login: tupek2
body: I think this is because NewTrueDofVector is returning an already allocated *, so that unique_ptr constructor is just converting it to a unique_ptr so its memory will auto be deleted when it goes out of scope.  We cannot call make_shared, because we are not calling the HypreParVector constructor directly?
created_at: 2025-04-04T19:31:50Z
author_association: COLLABORATOR
diff_hunk: @@ -359,7 +357,7 @@ TEST(QoI, DependsOnVectorValuedInput)
   });
   U_gf.ProjectCoefficient(x_squared);
 
-  mfem::HypreParVector* tmp = fespace->NewTrueDofVector();
+  ::std::unique_ptr<mfem::HypreParVector> tmp(fespace->NewTrueDofVector());
path: src/serac/numerics/functional/tests/functional_qoi.cpp
user: login: tepperly
body: The `NewTrueDofVector` method returns a raw pointer allocated by `new`, and the caller assumes ownership of the pointer (i.e., the responsibility to delete it when done). As @tupek2 points out, we cannot call `std::make_unique` here because the raw pointer has already been created.  Given the mfem method return value is fixed from our perspective, wrapping the return value as a `std::unique_ptr` seems a reasonable way to discharge our responsibilities.
created_at: 2025-04-04T20:55:13Z
author_association: MEMBER
diff_hunk: @@ -359,7 +357,7 @@ TEST(QoI, DependsOnVectorValuedInput)
   });
   U_gf.ProjectCoefficient(x_squared);
 
-  mfem::HypreParVector* tmp = fespace->NewTrueDofVector();
+  ::std::unique_ptr<mfem::HypreParVector> tmp(fespace->NewTrueDofVector());
path: src/serac/numerics/functional/tests/functional_qoi.cpp
user: login: tepperly
body: If you look around line 311-313, you'll see other uses of `std::unique_ptr` to manage  the result value of `NewTrueDofVector`.
created_at: 2025-04-04T20:59:08Z
author_association: MEMBER
diff_hunk: @@ -359,7 +357,7 @@ TEST(QoI, DependsOnVectorValuedInput)
   });
   U_gf.ProjectCoefficient(x_squared);
 
-  mfem::HypreParVector* tmp = fespace->NewTrueDofVector();
+  ::std::unique_ptr<mfem::HypreParVector> tmp(fespace->NewTrueDofVector());
path: src/serac/numerics/functional/tests/functional_qoi.cpp
user: login: white238
body: Ah thank you for explaining. I think we should put a comment to explain that.
created_at: 2025-04-04T21:06:59Z
author_association: MEMBER

PR comments:
user: login: tepperly
created_at: 2025-04-04T21:13:49Z
body: For all of them or just the first?
author_association: MEMBER
user: login: white238
created_at: 2025-04-04T21:14:47Z
body: > For all of them or just the first?

At least the new one, but if its not too much trouble the others. I can also do it if you don't want to. I do realize you are doing us a huge favor fixing this and don't want to abuse that help.

It could also create an issue and merge this.
author_association: MEMBER
user: login: tepperly
created_at: 2025-04-04T21:19:16Z
body: > > For all of them or just the first?
> 
> At least the new one, but if its not too much trouble the others. I can also do it if you don't want to. I do realize you are doing us a huge favor fixing this and don't want to abuse that help.
> 
> It could also create an issue and merge this.

I've added the comment for all instances. You can delete some of them if you think it's overkill.
author_association: MEMBER
user: login: btalamini
created_at: 2025-04-04T21:19:55Z
body: Thanks, @tepperly ! It takes a village!
author_association: MEMBER