Skip to content

reduce_sum_static: Missing ref_type_t Wrapper #3304

@andrepfeuffer

Description

@andrepfeuffer

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions