HIGH — Dangling References/UB
Files Affected
stan/lib/stan_math/stan/math/prim/functor/reduce_sum_static.hpp
- Return type call to
reduce_sum_impl
Root Cause Analysis
reduce_sum carefully wraps shared arguments with ref_type_t<Args&&> to materialize temporary Eigen expression templates before passing to TBB parallelization:
// In reduce_sum (correct):
return internal::reduce_sum_impl<ReduceFunction, void, return_type, Vec,
ref_type_t<Args&&>...>()(...);
// ^^^^^^^^^^^^^^ Wrapping!
But reduce_sum_static was missing this wrapper:
// BEFORE (buggy):
return internal::reduce_sum_impl<ReduceFunction, void, return_type, Vec,
Args...>()(...); // ← No ref_type_t!
Why This Matters:
Eigen uses expression templates — operations like matrix.row(i) don't create objects, they create lightweight proxy objects. These proxies hold references to the underlying matrix.
When an Eigen expression is passed as a shared argument:
Eigen::MatrixXd M = ...;
std::vector<int> data = ...;
// This creates temporary expression: M.row(0)
auto result = reduce_sum_static<F>(
data, grainsize, nullptr,
M.row(0) // ← Temporary Eigen::Block<MatrixXd> (expression template)
);
The Eigen::Block object (proxy) holds a reference to M. It's a temporary that will be destroyed immediately after the function call.
The Dangling Reference Problem
// Without ref_type_t:
void reduce_sum_impl(..., const Eigen::Block<MatrixXd>& row) {
// row is a reference to a temporary that was destroyed!
// Dangling reference → undefined behavior
// In TBB worker threads (running asynchronously):
for (size_t i = 0; i < num_chunks; ++i) {
// Accessing row → reading from deallocated memory
double val = row(0); // ← Undefined behavior!
}
}
How ref_type_t Fixes It
// ref_type_t materializes the expression
template<typename T>
using ref_type_t = ...; // Specialization that:
// - For Eigen expressions: creates actual matrix/vector copy or view
// - For scalars: passes by value
// - For vectors: passes by value
// With ref_type_t<Eigen::Block<MatrixXd>&&>:
// → Deduces to actual VectorXd (materialized)
// → Copy is created, not just reference
// → Copy is valid for duration of reduce_sum_impl
The Fix
// AFTER (correct):
return internal::reduce_sum_impl<ReduceFunction, void, return_type, Vec,
ref_type_t<Args&&>...>()(...);
// ^^^^^^^^^^^^^^ Added wrapping!
This ensures arguments are materialized (actual objects created, not just references) before being passed to the parallelized TBB workers.
Before Fix:
- Passing Eigen expression templates caused dangling references
- Error only manifests in TBB workers (asynchronous)
- Unpredictable crashes or memory corruption
- Very difficult to debug (race condition-like behavior)
After Fix:
- Expressions materialized safely
- No dangling references
- Deterministic behavior
HIGH — Dangling References/UB
Files Affected
stan/lib/stan_math/stan/math/prim/functor/reduce_sum_static.hppreduce_sum_implRoot Cause Analysis
reduce_sumcarefully wraps shared arguments withref_type_t<Args&&>to materialize temporary Eigen expression templates before passing to TBB parallelization:But
reduce_sum_staticwas missing this wrapper:Why This Matters:
Eigen uses expression templates — operations like
matrix.row(i)don't create objects, they create lightweight proxy objects. These proxies hold references to the underlying matrix.When an Eigen expression is passed as a shared argument:
The
Eigen::Blockobject (proxy) holds a reference toM. It's a temporary that will be destroyed immediately after the function call.The Dangling Reference Problem
How ref_type_t Fixes It
The Fix
This ensures arguments are materialized (actual objects created, not just references) before being passed to the parallelized TBB workers.
Before Fix:
After Fix: