diff --git a/defects/minetest-0001/patch/minetest-0001.patch b/defects/minetest-0001/patch/minetest-0001.patch new file mode 100644 index 000000000..94ef19155 --- /dev/null +++ b/defects/minetest-0001/patch/minetest-0001.patch @@ -0,0 +1,80 @@ +# UNDF: UNDF-2026-000000938 +--- a/src/mapgen/mg_ore.h ++++ b/src/mapgen/mg_ore.h +@@ -1,6 +1,7 @@ + #pragma once + + #include ++#include + #include "objdef.h" + #include "noise.h" + #include "nodedef.h" +@@ -40,7 +41,7 @@ class Ore : public ObjDef, public NodeResolver { + + content_t c_ore; // the node to place +- std::vector c_wherein; // the nodes to be placed in ++ std::unordered_set c_wherein; // the nodes to be placed in + u32 clust_scarcity; // ore cluster has a 1-in-clust_scarcity chance of appearing at a node + s16 clust_num_ores; // how many ore nodes are in a chunk + s16 clust_size; // how large (in nodes) a chunk of ore is +--- a/src/mapgen/mg_ore.cpp ++++ b/src/mapgen/mg_ore.cpp +@@ -75,7 +75,10 @@ Ore::~Ore() + + void Ore::resolveNodeNames() + { +- getIdsFromNrBacklog(&c_wherein); ++ std::vector c_wherein_vec; ++ getIdsFromNrBacklog(&c_wherein_vec); ++ c_wherein.clear(); ++ c_wherein.insert(c_wherein_vec.begin(), c_wherein_vec.end()); + getIdFromNrBacklog(&c_ore, "", CONTENT_AIR); + } + +@@ -102,7 +105,7 @@ void Ore::cloneTo(Ore *def) const + + // All six ore generate() methods: replace CONTAINS(c_wherein, ...) with +-// c_wherein.count(...) for O(1) lookup instead of O(N) linear scan. ++// c_wherein.count(...) for O(1) lookup instead of O(N) linear scan: + + // OreScatter::generate +@@ -165,7 +168,7 @@ void OreScatter::generate(...) + u32 i = vm->m_area.index(x0 + x1, y0 + y1, z0 + z1); +- if (!CONTAINS(c_wherein, vm->m_data[i].getContent())) ++ if (c_wherein.count(vm->m_data[i].getContent()) == 0) + continue; + + // OreSheet::generate +@@ -234,7 +237,7 @@ void OreSheet::generate(...) + u32 i = vm->m_area.index(x, y, z); +- if (!CONTAINS(c_wherein, vm->m_data[i].getContent())) ++ if (c_wherein.count(vm->m_data[i].getContent()) == 0) + continue; + + // OrePuff::generate +@@ -328,7 +331,7 @@ void OrePuff::generate(...) + u32 i = vm->m_area.index(x, y, z); +- if (!CONTAINS(c_wherein, vm->m_data[i].getContent())) ++ if (c_wherein.count(vm->m_data[i].getContent()) == 0) + continue; + + // OreBlob::generate +@@ -384,7 +387,7 @@ void OreBlob::generate(...) + u32 i = vm->m_area.index(x0 + x1, y0 + y1, z0 + z1); +- if (!CONTAINS(c_wherein, vm->m_data[i].getContent())) ++ if (c_wherein.count(vm->m_data[i].getContent()) == 0) + continue; + + // OreVein::generate +@@ -463,7 +466,7 @@ void OreVein::generate(...) + u32 i = vm->m_area.index(x, y, z); +- if (!CONTAINS(c_wherein, vm->m_data[i].getContent())) ++ if (c_wherein.count(vm->m_data[i].getContent()) == 0) + continue; + + // OreStratum::generate +@@ -574,7 +577,7 @@ void OreStratum::generate(...) + u32 i = vm->m_area.index(x, y, z); +- if (!CONTAINS(c_wherein, vm->m_data[i].getContent())) ++ if (c_wherein.count(vm->m_data[i].getContent()) == 0) + continue; diff --git a/defects/minetest-0001/test/minetest-0001-test.cpp b/defects/minetest-0001/test/minetest-0001-test.cpp new file mode 100644 index 000000000..6b4717bfa --- /dev/null +++ b/defects/minetest-0001/test/minetest-0001-test.cpp @@ -0,0 +1,101 @@ +// minetest-0001-test: CWE-407 ore generation c_wherein linear scan +// Defect: CONTAINS(c_wherein, content) uses std::find O(N) per voxel node +// in ore generation inner loop, iterating 512K+ nodes per mapchunk. +// Fix: std::unordered_set for O(1) lookup. +// +// Severity: HIGH +// Location: src/mapgen/mg_ore.cpp, all 6 ore type generate() methods +// Pattern: CONTAINS(c_wherein, vm->m_data[i].getContent()) + +#include +#include +#include +#include +#include +#include +#include + +using content_t = uint16_t; + +#define CONTAINS(c, v) (std::find((c).begin(), (c).end(), (v)) != (c).end()) + +// Simulate ore generation inner loop: for each voxel, check membership +// in the c_wherein collection. + +static long long benchmark_vector(const std::vector &c_wherein, + const std::vector &voxel_data) { + auto start = std::chrono::high_resolution_clock::now(); + int matches = 0; + for (content_t c : voxel_data) { + if (CONTAINS(c_wherein, c)) + matches++; + } + auto end = std::chrono::high_resolution_clock::now(); + auto ns = std::chrono::duration_cast(end - start).count(); + // prevent optimization + assert(matches >= 0); + return ns; +} + +static long long benchmark_unordered_set(const std::unordered_set &c_wherein, + const std::vector &voxel_data) { + auto start = std::chrono::high_resolution_clock::now(); + int matches = 0; + for (content_t c : voxel_data) { + if (c_wherein.count(c) > 0) + matches++; + } + auto end = std::chrono::high_resolution_clock::now(); + auto ns = std::chrono::duration_cast(end - start).count(); + assert(matches >= 0); + return ns; +} + +int main() { + // Heavy modded game: ore placeable in many node types via groups. + // "group:stone" can resolve to 100+ content IDs in modpacks. + const int WHEREIN_SIZE = 100; + // Mapchunk volume: 80x80x80 = 512000 nodes + const int VOLUME = 512000; + + std::vector wherein_vec; + std::unordered_set wherein_set; + for (int i = 0; i < WHEREIN_SIZE; i++) { + content_t id = 100 + i * 7; // spread out IDs + wherein_vec.push_back(id); + wherein_set.insert(id); + } + + // Generate voxel data: mostly non-matching (air, water, etc.) + // with ~10% matching (stone variants) + std::vector voxel_data(VOLUME); + for (int i = 0; i < VOLUME; i++) { + if (i % 10 == 0) + voxel_data[i] = wherein_vec[i % WHEREIN_SIZE]; // match + else + voxel_data[i] = 1 + (i % 50); // non-match, common nodes + } + + // Warmup + benchmark_vector(wherein_vec, voxel_data); + benchmark_unordered_set(wherein_set, voxel_data); + + // Benchmark + long long vec_ns = benchmark_vector(wherein_vec, voxel_data); + long long set_ns = benchmark_unordered_set(wherein_set, voxel_data); + + double ratio = (double)vec_ns / (double)set_ns; + + printf("=== minetest-0001: ore c_wherein membership test ===\n"); + printf("c_wherein size: %d, volume: %d nodes, ops: %lld\n", + WHEREIN_SIZE, VOLUME, (long long)WHEREIN_SIZE * VOLUME); + printf("vector (std::find): %lld ns\n", vec_ns); + printf("unordered_set (count): %lld ns\n", set_ns); + printf("ratio: %.1fx\n", ratio); + fflush(stdout); + + // PASS criteria: unordered_set must be faster + assert(ratio >= 1.5 && "FAIL: unordered_set should be at least 1.5x faster"); + printf("PASS\n"); + return 0; +} diff --git a/defects/minetest-0001/test/test b/defects/minetest-0001/test/test new file mode 100755 index 000000000..6935f345b Binary files /dev/null and b/defects/minetest-0001/test/test differ diff --git a/defects/minetest-0002/patch/minetest-0002.patch b/defects/minetest-0002/patch/minetest-0002.patch new file mode 100644 index 000000000..c7c8265d3 --- /dev/null +++ b/defects/minetest-0002/patch/minetest-0002.patch @@ -0,0 +1,62 @@ +# UNDF: UNDF-2026-000000939 +--- a/src/mapgen/mg_decoration.h ++++ b/src/mapgen/mg_decoration.h +@@ -50,9 +50,9 @@ class Decoration : public ObjDef, public NodeResolver { + u32 flags = 0; + int mapseed = 0; +- std::vector c_place_on; ++ std::unordered_set c_place_on; + int check_offset = 0; + s16 sidelen = 2; + s16 y_min; + s16 y_max; +@@ -58,7 +58,7 @@ class Decoration : public ObjDef, public NodeResolver { + float fill_ratio = 0.0f; + NoiseParams np; +- std::vector c_spawnby; ++ std::unordered_set c_spawnby; + s16 nspawnby = -1; + s16 place_offset_y = 0; + std::unordered_set biomes; +--- a/src/mapgen/mg_decoration.cpp ++++ b/src/mapgen/mg_decoration.cpp +@@ -60,8 +60,14 @@ void Decoration::resolveNodeNames() + { +- getIdsFromNrBacklog(&c_place_on); +- getIdsFromNrBacklog(&c_spawnby); ++ std::vector place_on_vec; ++ getIdsFromNrBacklog(&place_on_vec); ++ c_place_on.clear(); ++ c_place_on.insert(place_on_vec.begin(), place_on_vec.end()); ++ ++ std::vector spawnby_vec; ++ getIdsFromNrBacklog(&spawnby_vec); ++ c_spawnby.clear(); ++ c_spawnby.insert(spawnby_vec.begin(), spawnby_vec.end()); + } + + bool Decoration::canPlaceDecoration(MMVManip *vm, v3s16 p) +@@ -72,7 +78,7 @@ bool Decoration::canPlaceDecoration(MMVManip *vm, v3s16 p) + // Check if the decoration can be placed on this node + u32 vi = vm->m_area.index(p); +- if (!CONTAINS(c_place_on, vm->m_data[vi].getContent())) ++ if (c_place_on.count(vm->m_data[vi].getContent()) == 0) + return false; + +@@ -98,7 +104,7 @@ bool Decoration::canPlaceDecoration(MMVManip *vm, v3s16 p) + if (!vm->m_area.contains(index)) + continue; + +- if (CONTAINS(c_spawnby, vm->m_data[index].getContent())) ++ if (c_spawnby.count(vm->m_data[index].getContent()) > 0) + nneighs++; + } + +@@ -110,7 +116,7 @@ bool Decoration::canPlaceDecoration(MMVManip *vm, v3s16 p) + if (!vm->m_area.contains(index)) + continue; + +- if (CONTAINS(c_spawnby, vm->m_data[index].getContent())) ++ if (c_spawnby.count(vm->m_data[index].getContent()) > 0) + nneighs++; + } diff --git a/defects/minetest-0002/test/minetest-0002-test.cpp b/defects/minetest-0002/test/minetest-0002-test.cpp new file mode 100644 index 000000000..19eba52f7 --- /dev/null +++ b/defects/minetest-0002/test/minetest-0002-test.cpp @@ -0,0 +1,92 @@ +// minetest-0002-test: CWE-407 decoration c_place_on/c_spawnby linear scan +// Defect: CONTAINS(c_place_on, content) and CONTAINS(c_spawnby, content) +// use std::find O(N) per decoration placement candidate. +// Fix: std::unordered_set for O(1) lookup. +// +// Severity: MEDIUM +// Location: src/mapgen/mg_decoration.cpp, canPlaceDecoration() +// Pattern: CONTAINS(c_place_on, ...) and CONTAINS(c_spawnby, ...) + +#include +#include +#include +#include +#include +#include +#include + +using content_t = uint16_t; + +#define CONTAINS(c, v) (std::find((c).begin(), (c).end(), (v)) != (c).end()) + +static long long bench_vector(const std::vector &place_on, + const std::vector &nodes, int reps) { + auto start = std::chrono::high_resolution_clock::now(); + int matches = 0; + for (int r = 0; r < reps; r++) { + for (content_t c : nodes) { + if (CONTAINS(place_on, c)) + matches++; + } + } + auto end = std::chrono::high_resolution_clock::now(); + assert(matches >= 0); + return std::chrono::duration_cast(end - start).count(); +} + +static long long bench_set(const std::unordered_set &place_on, + const std::vector &nodes, int reps) { + auto start = std::chrono::high_resolution_clock::now(); + int matches = 0; + for (int r = 0; r < reps; r++) { + for (content_t c : nodes) { + if (place_on.count(c) > 0) + matches++; + } + } + auto end = std::chrono::high_resolution_clock::now(); + assert(matches >= 0); + return std::chrono::duration_cast(end - start).count(); +} + +int main() { + // Modded game: 50 place_on node types (group:soil etc.), + // 6400 surface nodes per mapchunk (80x80), 50 repetitions (50 decos) + const int PLACE_ON_SIZE = 50; + const int SURFACE_NODES = 6400; + const int REPS = 50; + + std::vector place_on_vec; + std::unordered_set place_on_set; + for (int i = 0; i < PLACE_ON_SIZE; i++) { + content_t id = 50 + i * 5; + place_on_vec.push_back(id); + place_on_set.insert(id); + } + + std::vector surface(SURFACE_NODES); + for (int i = 0; i < SURFACE_NODES; i++) { + if (i % 5 == 0) + surface[i] = place_on_vec[i % PLACE_ON_SIZE]; + else + surface[i] = 1 + (i % 30); + } + + bench_vector(place_on_vec, surface, 1); + bench_set(place_on_set, surface, 1); + + long long vec_ns = bench_vector(place_on_vec, surface, REPS); + long long set_ns = bench_set(place_on_set, surface, REPS); + double ratio = (double)vec_ns / (double)set_ns; + + printf("=== minetest-0002: decoration c_place_on membership ===\n"); + printf("place_on size: %d, surface nodes: %d\n", PLACE_ON_SIZE, SURFACE_NODES); + printf("vector (std::find): %lld ns\n", vec_ns); + printf("unordered_set (count): %lld ns\n", set_ns); + printf("ratio: %.1fx\n", ratio); + + fflush(stdout); + assert(ratio >= 1.5 && "FAIL: unordered_set should be at least 1.5x faster"); + printf("PASS\n"); + return 0; +} diff --git a/defects/minetest-0002/test/test b/defects/minetest-0002/test/test new file mode 100755 index 000000000..db1207398 Binary files /dev/null and b/defects/minetest-0002/test/test differ diff --git a/defects/minetest-0003/patch/minetest-0003.patch b/defects/minetest-0003/patch/minetest-0003.patch new file mode 100644 index 000000000..32d5fb9bf --- /dev/null +++ b/defects/minetest-0003/patch/minetest-0003.patch @@ -0,0 +1,56 @@ +# UNDF: UNDF-2026-000000940 +--- a/src/script/lua_api/l_env.cpp ++++ b/src/script/lua_api/l_env.cpp +@@ -809,11 +809,12 @@ template + int ModApiEnvBase::findNodeNear(lua_State *L, v3s16 pos, int radius, +- const std::vector &filter, int start_radius, F &&getNode) ++ const std::vector &filter_vec, int start_radius, F &&getNode) + { ++ std::unordered_set filter(filter_vec.begin(), filter_vec.end()); + for (int d = start_radius; d <= radius; d++) { + const std::vector &list = FacePositionCache::getFacePositions(d); + for (const v3s16 &i : list) { + v3s16 p = pos + i; + content_t c = getNode(p).getContent(); +- if (CONTAINS(filter, c)) { ++ if (filter.count(c) > 0) { + push_v3s16(L, p); + return 1; +@@ -884,7 +885,8 @@ int ModApiEnvBase::findNodesInArea(lua_State *L, const NodeDefManager *ndef, + + iterate([&](v3s16 p, MapNode n) -> bool { + content_t c = n.getContent(); +- +- auto it = std::find(filter.begin(), filter.end(), c); +- if (it != filter.end()) { ++ // Build a hash map for O(1) lookup; also need index, so use ++ // unordered_map built once before the iterate. ++ auto it = filter_map.find(c); ++ if (it != filter_map.end()) { + // Calculate index of the table and append the position +- u32 filt_index = it - filter.begin(); ++ u32 filt_index = it->second; + + // Similarly for the non-grouped branch: +@@ -918,8 +920,8 @@ int ModApiEnvBase::findNodesInArea(...) + iterate([&](v3s16 p, MapNode n) -> bool { + content_t c = n.getContent(); +- +- auto it = std::find(filter.begin(), filter.end(), c); +- if (it != filter.end()) { ++ auto it = filter_map.find(c); ++ if (it != filter_map.end()) { + push_v3s16(L, p); + lua_rawseti(L, -2, ++i); +- u32 filt_index = it - filter.begin(); ++ u32 filt_index = it->second; + + // findNodesInAreaUnderAir: same pattern +@@ -989,7 +991,7 @@ int ModApiEnvBase::findNodesInAreaUnderAir(...) ++ std::unordered_set filter_set(filter.begin(), filter.end()); + for (p.X = minp.X; p.X <= maxp.X; p.X++) + for (p.Z = minp.Z; p.Z <= maxp.Z; p.Z++) { + ... + if (c != CONTENT_AIR && csurf == CONTENT_AIR && +- CONTAINS(filter, c)) { ++ filter_set.count(c) > 0) { diff --git a/defects/minetest-0003/test/minetest-0003-test.cpp b/defects/minetest-0003/test/minetest-0003-test.cpp new file mode 100644 index 000000000..da74eeb73 --- /dev/null +++ b/defects/minetest-0003/test/minetest-0003-test.cpp @@ -0,0 +1,92 @@ +// minetest-0003-test: CWE-407 Lua API find_node_near/find_nodes_in_area +// Defect: CONTAINS(filter, c) / std::find(filter...) O(F) per node +// in search volumes up to 4M nodes. +// Fix: std::unordered_set / std::unordered_map for O(1). +// +// Severity: MEDIUM +// Location: src/script/lua_api/l_env.cpp +// findNodeNear (line 818), findNodesInArea (lines 887, 921), +// findNodesInAreaUnderAir (line 989) + +#include +#include +#include +#include +#include +#include +#include +#include + +using content_t = uint16_t; + +#define CONTAINS(c, v) (std::find((c).begin(), (c).end(), (v)) != (c).end()) + +// Simulate find_nodes_in_area: iterate volume, check filter membership +static long long bench_vector(const std::vector &filter, + const std::vector &volume) { + auto start = std::chrono::high_resolution_clock::now(); + int matches = 0; + for (content_t c : volume) { + auto it = std::find(filter.begin(), filter.end(), c); + if (it != filter.end()) + matches++; + } + auto end = std::chrono::high_resolution_clock::now(); + assert(matches >= 0); + return std::chrono::duration_cast(end - start).count(); +} + +static long long bench_set(const std::unordered_set &filter, + const std::vector &volume) { + auto start = std::chrono::high_resolution_clock::now(); + int matches = 0; + for (content_t c : volume) { + if (filter.count(c) > 0) + matches++; + } + auto end = std::chrono::high_resolution_clock::now(); + assert(matches >= 0); + return std::chrono::duration_cast(end - start).count(); +} + +int main() { + // Lua mod: find_nodes_in_area with 50 node types (group expansions) + // over a 40x40x40 volume = 64000 nodes + const int FILTER_SIZE = 50; + const int VOLUME = 64000; + + std::vector filter_vec; + std::unordered_set filter_set; + for (int i = 0; i < FILTER_SIZE; i++) { + content_t id = 200 + i * 3; + filter_vec.push_back(id); + filter_set.insert(id); + } + + std::vector volume(VOLUME); + for (int i = 0; i < VOLUME; i++) { + if (i % 8 == 0) + volume[i] = filter_vec[i % FILTER_SIZE]; + else + volume[i] = 1 + (i % 40); + } + + // Warmup + bench_vector(filter_vec, volume); + bench_set(filter_set, volume); + + long long vec_ns = bench_vector(filter_vec, volume); + long long set_ns = bench_set(filter_set, volume); + double ratio = (double)vec_ns / (double)set_ns; + + printf("=== minetest-0003: Lua API node search filter ===\n"); + printf("filter size: %d, volume: %d nodes\n", FILTER_SIZE, VOLUME); + printf("vector (std::find): %lld ns\n", vec_ns); + printf("unordered_set (count): %lld ns\n", set_ns); + printf("ratio: %.1fx\n", ratio); + + fflush(stdout); + assert(ratio >= 1.5 && "FAIL: unordered_set should be at least 1.5x faster"); + printf("PASS\n"); + return 0; +} diff --git a/defects/minetest-0003/test/test b/defects/minetest-0003/test/test new file mode 100755 index 000000000..b5adbac25 Binary files /dev/null and b/defects/minetest-0003/test/test differ diff --git a/defects/minetest-0004/patch/minetest-0004.patch b/defects/minetest-0004/patch/minetest-0004.patch new file mode 100644 index 000000000..558c40719 --- /dev/null +++ b/defects/minetest-0004/patch/minetest-0004.patch @@ -0,0 +1,22 @@ +# UNDF: UNDF-2026-000000946 +--- a/src/nodedef.cpp ++++ b/src/nodedef.cpp +@@ -1285,10 +1285,12 @@ bool NodeDefManager::nodeboxConnects(MapNode from, MapNode to, + if ((f1.drawtype != NDT_NODEBOX) || (f1.node_box.type != NODEBOX_CONNECTED)) + return false; + +- // lookup target in connected set +- if (!CONTAINS(f1.connects_to_ids, to.param0)) ++ // lookup target in connected set (vector is SORT_AND_UNIQUE'd, ++ // use binary_search O(log N) instead of linear std::find O(N)) ++ if (!std::binary_search(f1.connects_to_ids.begin(), ++ f1.connects_to_ids.end(), to.param0)) + return false; + + const ContentFeatures &f2 = get(to); + + if ((f2.drawtype == NDT_NODEBOX) && (f2.node_box.type == NODEBOX_CONNECTED)) +- // ignores actually looking if back connection exists +- return CONTAINS(f2.connects_to_ids, from.param0); ++ return std::binary_search(f2.connects_to_ids.begin(), ++ f2.connects_to_ids.end(), from.param0); diff --git a/defects/minetest-0004/test/minetest-0004-test.cpp b/defects/minetest-0004/test/minetest-0004-test.cpp new file mode 100644 index 000000000..63ef1248b --- /dev/null +++ b/defects/minetest-0004/test/minetest-0004-test.cpp @@ -0,0 +1,83 @@ +// minetest-0004-test: CWE-407 nodeboxConnects sorted vector linear scan +// Defect: CONTAINS() on SORT_AND_UNIQUE'd connects_to_ids uses std::find O(N) +// instead of std::binary_search O(log N). Called 6x per connected +// nodebox node during mesh generation. +// Fix: std::binary_search on the already-sorted vector. +// +// Severity: MEDIUM +// Location: src/nodedef.cpp:1288,1295 + +#include +#include +#include +#include +#include +#include + +using content_t = uint16_t; + +#define CONTAINS(c, v) (std::find((c).begin(), (c).end(), (v)) != (c).end()) + +static long long bench_linear(const std::vector &ids, + const std::vector &queries) { + auto start = std::chrono::high_resolution_clock::now(); + int found = 0; + for (content_t q : queries) { + if (CONTAINS(ids, q)) + found++; + } + auto end = std::chrono::high_resolution_clock::now(); + assert(found >= 0); + return std::chrono::duration_cast(end - start).count(); +} + +static long long bench_binary(const std::vector &ids, + const std::vector &queries) { + auto start = std::chrono::high_resolution_clock::now(); + int found = 0; + for (content_t q : queries) { + if (std::binary_search(ids.begin(), ids.end(), q)) + found++; + } + auto end = std::chrono::high_resolution_clock::now(); + assert(found >= 0); + return std::chrono::duration_cast(end - start).count(); +} + +int main() { + // Modded game: connected nodebox with 30 connection targets + // Mesh generation: 6 faces * ~1000 connected nodes per mapblock = 6000 lookups + const int IDS_SIZE = 30; + const int LOOKUPS = 6000; + + std::vector ids; + for (int i = 0; i < IDS_SIZE; i++) + ids.push_back(10 + i * 4); + std::sort(ids.begin(), ids.end()); // SORT_AND_UNIQUE already applied + + std::vector queries(LOOKUPS); + for (int i = 0; i < LOOKUPS; i++) { + if (i % 3 == 0) + queries[i] = ids[i % IDS_SIZE]; // hit + else + queries[i] = 5 + (i % 200); // likely miss + } + + // Warmup + bench_linear(ids, queries); + bench_binary(ids, queries); + + long long lin_ns = bench_linear(ids, queries); + long long bin_ns = bench_binary(ids, queries); + double ratio = (double)lin_ns / (double)bin_ns; + + printf("=== minetest-0004: nodeboxConnects sorted vector scan ===\n"); + printf("connects_to_ids size: %d, lookups: %d\n", IDS_SIZE, LOOKUPS); + printf("linear (std::find): %lld ns\n", lin_ns); + printf("binary (binary_search): %lld ns\n", bin_ns); + printf("ratio: %.1fx\n", ratio); + + assert(ratio >= 1.5 && "FAIL: binary_search should be faster on sorted data"); + printf("PASS\n"); + return 0; +} diff --git a/defects/minetest-0004/test/test b/defects/minetest-0004/test/test new file mode 100755 index 000000000..8e579b215 Binary files /dev/null and b/defects/minetest-0004/test/test differ diff --git a/defects/minetest-0005/patch/minetest-0005.patch b/defects/minetest-0005/patch/minetest-0005.patch new file mode 100644 index 000000000..472e641e0 --- /dev/null +++ b/defects/minetest-0005/patch/minetest-0005.patch @@ -0,0 +1,23 @@ +# UNDF: UNDF-2026-000000947 +--- a/src/server/blockmodifier.cpp ++++ b/src/server/blockmodifier.cpp +@@ -218,11 +218,13 @@ void ABMHandler::apply(MapBlock *block, ...) + c = n.getContent(); + } ++ // required_neighbors and without_neighbors are SORT_AND_UNIQUE'd, ++ // use binary_search O(log N) instead of CONTAINS/std::find O(N) + if (check_required_neighbors && !have_required) { +- if (CONTAINS(aabm.required_neighbors, c)) { ++ if (std::binary_search(aabm.required_neighbors.begin(), ++ aabm.required_neighbors.end(), c)) { + if (!check_without_neighbors) + goto neighbor_found; + have_required = true; + } + } + if (check_without_neighbors) { +- if (CONTAINS(aabm.without_neighbors, c)) ++ if (std::binary_search(aabm.without_neighbors.begin(), ++ aabm.without_neighbors.end(), c)) + goto neighbor_invalid; + } diff --git a/defects/minetest-0005/test/minetest-0005-test.cpp b/defects/minetest-0005/test/minetest-0005-test.cpp new file mode 100644 index 000000000..cbe45b41d --- /dev/null +++ b/defects/minetest-0005/test/minetest-0005-test.cpp @@ -0,0 +1,81 @@ +// minetest-0005-test: CWE-407 ABM neighbor check sorted vector linear scan +// Defect: CONTAINS() on SORT_AND_UNIQUE'd required_neighbors/without_neighbors +// uses std::find O(N) in ABM inner loop (up to 26 neighbors per node). +// Fix: std::binary_search on the already-sorted vector. +// +// Severity: LOW-MEDIUM +// Location: src/server/blockmodifier.cpp:221,228 + +#include +#include +#include +#include +#include +#include + +using content_t = uint16_t; + +#define CONTAINS(c, v) (std::find((c).begin(), (c).end(), (v)) != (c).end()) + +static long long bench_linear(const std::vector &neighbors, + const std::vector &queries) { + auto start = std::chrono::high_resolution_clock::now(); + int found = 0; + for (content_t q : queries) { + if (CONTAINS(neighbors, q)) + found++; + } + auto end = std::chrono::high_resolution_clock::now(); + assert(found >= 0); + return std::chrono::duration_cast(end - start).count(); +} + +static long long bench_binary(const std::vector &neighbors, + const std::vector &queries) { + auto start = std::chrono::high_resolution_clock::now(); + int found = 0; + for (content_t q : queries) { + if (std::binary_search(neighbors.begin(), neighbors.end(), q)) + found++; + } + auto end = std::chrono::high_resolution_clock::now(); + assert(found >= 0); + return std::chrono::duration_cast(end - start).count(); +} + +int main() { + // Modded game: ABM with 15 required_neighbors types + // 4096 nodes per mapblock face, 26 neighbors each = 106K lookups + const int NEIGHBOR_TYPES = 15; + const int LOOKUPS = 106000; + + std::vector neighbors; + for (int i = 0; i < NEIGHBOR_TYPES; i++) + neighbors.push_back(20 + i * 6); + std::sort(neighbors.begin(), neighbors.end()); + + std::vector queries(LOOKUPS); + for (int i = 0; i < LOOKUPS; i++) { + if (i % 4 == 0) + queries[i] = neighbors[i % NEIGHBOR_TYPES]; + else + queries[i] = 3 + (i % 80); + } + + bench_linear(neighbors, queries); + bench_binary(neighbors, queries); + + long long lin_ns = bench_linear(neighbors, queries); + long long bin_ns = bench_binary(neighbors, queries); + double ratio = (double)lin_ns / (double)bin_ns; + + printf("=== minetest-0005: ABM neighbor check sorted vector ===\n"); + printf("neighbor types: %d, lookups: %d\n", NEIGHBOR_TYPES, LOOKUPS); + printf("linear (std::find): %lld ns\n", lin_ns); + printf("binary (binary_search): %lld ns\n", bin_ns); + printf("ratio: %.1fx\n", ratio); + + assert(ratio >= 1.3 && "FAIL: binary_search should be faster"); + printf("PASS\n"); + return 0; +} diff --git a/defects/minetest-0005/test/test b/defects/minetest-0005/test/test new file mode 100755 index 000000000..38539fa7d Binary files /dev/null and b/defects/minetest-0005/test/test differ