Skip to content

Commit 03ff3b6

Browse files
Harden cost arithmetic against int64 overflow
Co-authored-by: Andrey G <networmix@gmail.com>
1 parent a971bc7 commit 03ff3b6

4 files changed

Lines changed: 62 additions & 7 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* Overflow-safe helpers for Cost arithmetic. */
2+
#pragma once
3+
4+
#include <cmath>
5+
#include <limits>
6+
7+
#include "netgraph/core/types.hpp"
8+
9+
namespace netgraph::core {
10+
11+
[[nodiscard]] inline constexpr Cost cost_max() noexcept {
12+
return std::numeric_limits<Cost>::max();
13+
}
14+
15+
// Largest finite cost value (keeps cost_max() reserved as "infinity"/unreachable).
16+
[[nodiscard]] inline constexpr Cost cost_finite_max() noexcept {
17+
return static_cast<Cost>(std::numeric_limits<Cost>::max() - 1);
18+
}
19+
20+
// Saturating add for path costs. If either argument is "infinity" (max), or
21+
// the sum would overflow, returns cost_finite_max().
22+
[[nodiscard]] inline constexpr Cost saturating_cost_add(Cost a, Cost b) noexcept {
23+
if (a == cost_max() || b == cost_max()) return cost_max();
24+
if (a >= cost_finite_max() || b >= cost_finite_max()) return cost_finite_max();
25+
if (b > 0 && a > cost_finite_max() - b) return cost_finite_max();
26+
if (b < 0 && a < std::numeric_limits<Cost>::min() - b) return std::numeric_limits<Cost>::min();
27+
return static_cast<Cost>(a + b);
28+
}
29+
30+
// Saturating conversion of base*factor into Cost to avoid UB from out-of-range
31+
// float->int conversions.
32+
[[nodiscard]] inline Cost saturating_cost_mul_factor(Cost base, double factor) noexcept {
33+
if (base <= 0 || factor <= 0.0) return static_cast<Cost>(0);
34+
const long double scaled = static_cast<long double>(base) * static_cast<long double>(factor);
35+
const long double hi = static_cast<long double>(cost_finite_max());
36+
if (!std::isfinite(static_cast<double>(scaled)) || scaled >= hi) return cost_finite_max();
37+
if (scaled <= 0.0L) return static_cast<Cost>(0);
38+
return static_cast<Cost>(scaled);
39+
}
40+
41+
} // namespace netgraph::core

src/flow_policy.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515
#include "netgraph/core/flow_policy.hpp"
1616
#include "netgraph/core/constants.hpp"
17+
#include "netgraph/core/cost_utils.hpp"
1718
#include "netgraph/core/algorithms.hpp"
1819
#include "netgraph/core/options.hpp"
1920
#include "netgraph/core/profiling.hpp"
@@ -129,7 +130,8 @@ std::optional<std::pair<PredDAG, Cost>> FlowPolicy::get_path_bundle(const FlowGr
129130
if (max_path_cost_.has_value() || max_path_cost_factor_.has_value()) {
130131
double maxf = max_path_cost_factor_.value_or(1.0);
131132
Cost absmax = max_path_cost_.value_or(std::numeric_limits<Cost>::max());
132-
if (dst_cost > std::min<Cost>(absmax, static_cast<Cost>(static_cast<double>(best_path_cost_) * maxf))) return std::nullopt;
133+
Cost relmax = saturating_cost_mul_factor(best_path_cost_, maxf);
134+
if (dst_cost > std::min<Cost>(absmax, relmax)) return std::nullopt;
133135
}
134136
// Ensure there is at least one predecessor for dst
135137
if (static_cast<std::size_t>(dst) >= dag.parent_offsets.size()-1) return std::nullopt;

src/k_shortest_paths.cpp

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
// Use multipath SPF DAG for spur enumeration
2121
#include "netgraph/core/shortest_paths.hpp"
22+
#include "netgraph/core/cost_utils.hpp"
2223

2324
namespace netgraph::core {
2425

@@ -80,7 +81,7 @@ static std::optional<Path> dijkstra_single(const StrictMultiDiGraph& g, NodeId s
8081
}
8182
}
8283
if (best_eid >= 0) {
83-
Cost nd = static_cast<Cost>(d_u + min_edge_cost);
84+
Cost nd = saturating_cost_add(d_u, min_edge_cost);
8485
auto vi = static_cast<std::size_t>(v);
8586
if (nd < dist[vi]) { dist[vi] = nd; parent[vi] = u; via[vi] = best_eid; pq.emplace(nd, v); }
8687
}
@@ -197,7 +198,9 @@ std::vector<std::pair<std::vector<Cost>, PredDAG>> k_shortest_paths(
197198
dist[static_cast<std::size_t>(P.nodes.front())] = 0;
198199
for (std::size_t i = 1; i < P.nodes.size(); ++i) {
199200
auto u = P.nodes[i-1]; auto v = P.nodes[i]; auto e = P.edges[i-1];
200-
dist[static_cast<std::size_t>(v)] = dist[static_cast<std::size_t>(u)] + static_cast<Cost>(cost_view[static_cast<std::size_t>(e)]);
201+
dist[static_cast<std::size_t>(v)] = saturating_cost_add(
202+
dist[static_cast<std::size_t>(u)],
203+
static_cast<Cost>(cost_view[static_cast<std::size_t>(e)]));
201204
dag.parent_offsets[static_cast<std::size_t>(v+1)] = 1;
202205
}
203206
for (std::size_t v = 1; v < dag.parent_offsets.size(); ++v) dag.parent_offsets[v] += dag.parent_offsets[v-1];
@@ -234,7 +237,9 @@ std::vector<std::pair<std::vector<Cost>, PredDAG>> k_shortest_paths(
234237
for (std::size_t idx = 1; idx < last.nodes.size(); ++idx) {
235238
// edge at idx-1
236239
auto e = last.edges[idx - 1];
237-
prefix_cost[idx] = prefix_cost[idx - 1] + static_cast<Cost>(cost_view[static_cast<std::size_t>(e)]);
240+
prefix_cost[idx] = saturating_cost_add(
241+
prefix_cost[idx - 1],
242+
static_cast<Cost>(cost_view[static_cast<std::size_t>(e)]));
238243
}
239244
// Spur node positions 0..len-2
240245
for (std::size_t j = 0; j + 1 < last.nodes.size(); ++j) {
@@ -300,7 +305,11 @@ std::vector<std::pair<std::vector<Cost>, PredDAG>> k_shortest_paths(
300305
for (auto e : spur_edges) cand_edges.push_back(e);
301306
// Compute candidate cost as prefix_cost[j] + sum(spur_edges)
302307
Cost cand_cost = prefix_cost[j];
303-
for (auto e : spur_edges) cand_cost += static_cast<Cost>(cost_view[static_cast<std::size_t>(e)]);
308+
for (auto e : spur_edges) {
309+
cand_cost = saturating_cost_add(
310+
cand_cost,
311+
static_cast<Cost>(cost_view[static_cast<std::size_t>(e)]));
312+
}
304313
if (cand_cost > max_cost) continue;
305314
auto sig = path_signature(cand_edges);
306315
if (unique && visited.find(sig) != visited.end()) continue;
@@ -332,7 +341,9 @@ std::vector<std::pair<std::vector<Cost>, PredDAG>> k_shortest_paths(
332341
dist[static_cast<std::size_t>(P.nodes.front())] = 0;
333342
for (std::size_t i = 1; i < P.nodes.size(); ++i) {
334343
auto u = P.nodes[i-1]; auto v = P.nodes[i]; auto e = P.edges[i-1];
335-
dist[static_cast<std::size_t>(v)] = dist[static_cast<std::size_t>(u)] + static_cast<Cost>(cost_view[static_cast<std::size_t>(e)]);
344+
dist[static_cast<std::size_t>(v)] = saturating_cost_add(
345+
dist[static_cast<std::size_t>(u)],
346+
static_cast<Cost>(cost_view[static_cast<std::size_t>(e)]));
336347
dag.parent_offsets[static_cast<std::size_t>(v+1)] = 1;
337348
}
338349
for (std::size_t v = 1; v < dag.parent_offsets.size(); ++v) dag.parent_offsets[v] += dag.parent_offsets[v-1];

src/shortest_paths.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ resolve_to_paths(const PredDAG& dag, NodeId src, NodeId dst,
170170
#include <utility>
171171
#include <vector>
172172
#include "netgraph/core/constants.hpp"
173+
#include "netgraph/core/cost_utils.hpp"
173174

174175
namespace netgraph::core {
175176

@@ -317,7 +318,7 @@ shortest_paths_core(const StrictMultiDiGraph& g, NodeId src,
317318
}
318319
// Update distance and predecessors if we found a better path (or equal-cost with better capacity).
319320
if (!selected_edges.empty()) {
320-
Cost new_cost = static_cast<Cost>(d_u + min_edge_cost);
321+
Cost new_cost = saturating_cost_add(d_u, min_edge_cost);
321322
auto v_idx = static_cast<std::size_t>(v);
322323

323324
// Compute bottleneck capacity along path to v through u for node-level tie-breaking.

0 commit comments

Comments
 (0)