Skip to content

Commit 30418c9

Browse files
lsotomayorb4claude
andcommitted
fix(libgraphmatch): cap node ID at 16M to prevent OOM DoS in add_node
add_node(UINT32_MAX, ...) would resize internal vectors to 4GB+, crashing the process. Added MAX_NODE_ID = 16M (≈64MB for type+score arrays). IDs at or above the limit are silently rejected, matching the existing drop semantics for out-of-range edges in finalize(). Two new tests pin the boundary: MAX_NODE_ID rejected, MAX_NODE_ID-1 accepted. All 33 test cases pass (79 assertions). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 300b1a6 commit 30418c9

2 files changed

Lines changed: 33 additions & 2 deletions

File tree

libgraphmatch/include/graphmatch/csr_graph.hpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,16 @@ class CSRTemporalGraph {
8888

8989
// -- Builder API (before finalize) ------------------------------------
9090

91-
/// Add a node with the given type. Duplicate IDs are ignored.
91+
/// Maximum node ID accepted by add_node(). Prevents OOM from a single
92+
/// malicious or buggy call (e.g. add_node(UINT32_MAX, ...) would try to
93+
/// allocate >4 GB). 16M nodes ≈ 64 MB for the type+score arrays — a
94+
/// reasonable ceiling for a single CSR graph. Raise if needed.
95+
static constexpr NodeId MAX_NODE_ID = 16'000'000;
96+
97+
/// Add a node with the given type. IDs above MAX_NODE_ID are silently
98+
/// rejected (same drop semantics as out-of-range edges in finalize).
9299
void add_node(NodeId id, EntityTypeTag type) {
100+
if (id >= MAX_NODE_ID) return;
93101
if (id >= node_types_.size()) {
94102
node_types_.resize(id + 1, GM_ENTITY_ANY);
95103
node_scores_.resize(id + 1, 0.0f);
@@ -101,7 +109,8 @@ class CSRTemporalGraph {
101109
if (id >= node_count_) node_count_ = id + 1;
102110
}
103111

104-
/// Add a directed temporal edge.
112+
/// Add a directed temporal edge. Edges referencing nodes above
113+
/// MAX_NODE_ID will be dropped during finalize().
105114
void add_edge(NodeId src, NodeId dst, RelTypeTag rel_type, Timestamp ts) {
106115
pending_edges_.push_back({src, dst, rel_type, ts});
107116
}

libgraphmatch/tests/test_csr_graph.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,4 +470,26 @@ TEST_CASE("out-of-range edges do not crash empty graph") {
470470
CHECK(g.is_finalized());
471471
}
472472

473+
TEST_CASE("add_node above MAX_NODE_ID is silently rejected") {
474+
CSRTemporalGraph g;
475+
g.add_node(0, GM_ENTITY_IP);
476+
// Attempt to add a node with an absurd ID — must NOT allocate 4 GB+
477+
g.add_node(CSRTemporalGraph::MAX_NODE_ID, GM_ENTITY_HOST); // at limit — rejected
478+
g.add_node(CSRTemporalGraph::MAX_NODE_ID + 1, GM_ENTITY_USER); // above limit — rejected
479+
g.add_node(UINT32_MAX, GM_ENTITY_HOST); // worst case — rejected
480+
g.finalize();
481+
482+
CHECK(g.node_count() == 1); // only node 0 was accepted
483+
CHECK(g.node_type(0) == GM_ENTITY_IP);
484+
}
485+
486+
TEST_CASE("add_node just below MAX_NODE_ID is accepted") {
487+
CSRTemporalGraph g;
488+
g.add_node(CSRTemporalGraph::MAX_NODE_ID - 1, GM_ENTITY_FILE);
489+
g.finalize();
490+
491+
CHECK(g.node_count() == CSRTemporalGraph::MAX_NODE_ID);
492+
CHECK(g.node_type(CSRTemporalGraph::MAX_NODE_ID - 1) == GM_ENTITY_FILE);
493+
}
494+
473495
} // TEST_SUITE add_edge_boundary

0 commit comments

Comments
 (0)