NaN/Inf lon/lat reaches std::sort with a NaN-producing comparator in snyder_forward, causing undefined behavior
Defect
src/projection_forward.cpp:198-222: snyder_forward computes face_dists[i] = {face_distance(...), i} for all 20 faces and calls std::sort(face_dists.begin(), face_dists.end()). If lon_deg/lat_deg is NaN, face_distance (line 190) produces NaN for every face (via clampd, which passes NaN through unchanged since both its comparisons are false for NaN — icosahedron.cpp:112), so all 20 pairs compare as NaN.
std::pair's operator< on NaN violates strict-weak-ordering, which is undefined behavior for std::sort — this can corrupt memory during introsort's partitioning in libstdc++/MSVC implementations, not merely "produce the wrong order."
Reachability
Directly via cpp_snyder_forward (src/rcpp_projection.cpp:50), and via cpp_lonlat_to_index_ap3/4/7 (src/rcpp_index.cpp:121-163), called from R/hexify_index.R:91 with zero NA/Inf filtering upstream.
Related
icosahedron.cpp's which_face() (lines 181-197) has the analogous issue: with NaN inputs, every candidate distance is NaN, and since d < bestd is always false for NaN, best never updates past its initial value — the function silently returns face 0 for any NaN input instead of erroring, exposed directly via cpp_which_face (rcpp_projection.cpp:30-32).
Impact
tests/testthat/test-projection-forward.R has no NaN/Inf/NA test case, so this UB path is completely untested.
Fix
Add an explicit std::isfinite() check on lon_deg/lat_deg at the top of snyder_forward (and which_face) that throws a clear R-facing error via Rcpp::stop, matching the pattern already used elsewhere in this codebase (e.g. rcpp_aperture.cpp's checked_ij()).
NaN/Inf lon/lat reaches
std::sortwith a NaN-producing comparator insnyder_forward, causing undefined behaviorDefect
src/projection_forward.cpp:198-222:snyder_forwardcomputesface_dists[i] = {face_distance(...), i}for all 20 faces and callsstd::sort(face_dists.begin(), face_dists.end()). Iflon_deg/lat_degis NaN,face_distance(line 190) produces NaN for every face (viaclampd, which passes NaN through unchanged since both its comparisons are false for NaN —icosahedron.cpp:112), so all 20 pairs compare as NaN.std::pair'soperator<on NaN violates strict-weak-ordering, which is undefined behavior forstd::sort— this can corrupt memory during introsort's partitioning in libstdc++/MSVC implementations, not merely "produce the wrong order."Reachability
Directly via
cpp_snyder_forward(src/rcpp_projection.cpp:50), and viacpp_lonlat_to_index_ap3/4/7(src/rcpp_index.cpp:121-163), called fromR/hexify_index.R:91with zero NA/Inf filtering upstream.Related
icosahedron.cpp'swhich_face()(lines 181-197) has the analogous issue: with NaN inputs, every candidate distance is NaN, and sinced < bestdis always false for NaN,bestnever updates past its initial value — the function silently returns face0for any NaN input instead of erroring, exposed directly viacpp_which_face(rcpp_projection.cpp:30-32).Impact
tests/testthat/test-projection-forward.Rhas no NaN/Inf/NA test case, so this UB path is completely untested.Fix
Add an explicit
std::isfinite()check onlon_deg/lat_degat the top ofsnyder_forward(andwhich_face) that throws a clear R-facing error viaRcpp::stop, matching the pattern already used elsewhere in this codebase (e.g.rcpp_aperture.cpp'schecked_ij()).