minetest (Luanti): 5 CWE-407 defects, all 5 MOADs scanned

minetest-0001: mg_ore.cpp c_wherein vector CONTAINS in voxel inner loop O(V*W) HIGH 3.3x
minetest-0002: mg_decoration.cpp c_place_on/c_spawnby vector CONTAINS O(S*P) MEDIUM 1.8x
minetest-0003: l_env.cpp find_node_near/find_nodes_in_area filter CONTAINS O(V*F) MEDIUM 1.6x
minetest-0004: nodedef.cpp nodeboxConnects sorted vector linear scan O(N) MEDIUM 2.1x
minetest-0005: blockmodifier.cpp ABM neighbor check sorted vector O(N) LOW-MEDIUM 1.5x

MOAD-0002: g_settings global singleton (architectural, not patchable)
MOAD-0003: thread_local log streams (properly scoped, not leaked context)
MOAD-0004: CLEAN (no credential logging found)
MOAD-0005: CLEAN (no unsynchronized cache patterns found)
This commit is contained in:
russell@unturf.com 2026-03-31 10:10:03 -04:00
parent a7907f0e12
commit a86a765544
15 changed files with 692 additions and 0 deletions

View file

@ -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 <unordered_set>
+#include <algorithm>
#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<content_t> c_wherein; // the nodes to be placed in
+ std::unordered_set<content_t> 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<content_t> 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;

View file

@ -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<content_t> 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 <vector>
#include <unordered_set>
#include <algorithm>
#include <chrono>
#include <cassert>
#include <cstdio>
#include <cstdint>
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<content_t> &c_wherein,
const std::vector<content_t> &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<std::chrono::nanoseconds>(end - start).count();
// prevent optimization
assert(matches >= 0);
return ns;
}
static long long benchmark_unordered_set(const std::unordered_set<content_t> &c_wherein,
const std::vector<content_t> &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<std::chrono::nanoseconds>(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<content_t> wherein_vec;
std::unordered_set<content_t> 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<content_t> 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;
}

BIN
defects/minetest-0001/test/test Executable file

Binary file not shown.

View file

@ -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<content_t> c_place_on;
+ std::unordered_set<content_t> 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<content_t> c_spawnby;
+ std::unordered_set<content_t> c_spawnby;
s16 nspawnby = -1;
s16 place_offset_y = 0;
std::unordered_set<biome_t> 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<content_t> 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<content_t> 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++;
}

View file

@ -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<content_t> for O(1) lookup.
//
// Severity: MEDIUM
// Location: src/mapgen/mg_decoration.cpp, canPlaceDecoration()
// Pattern: CONTAINS(c_place_on, ...) and CONTAINS(c_spawnby, ...)
#include <vector>
#include <unordered_set>
#include <algorithm>
#include <chrono>
#include <cassert>
#include <cstdio>
#include <cstdint>
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<content_t> &place_on,
const std::vector<content_t> &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<std::chrono::nanoseconds>(end - start).count();
}
static long long bench_set(const std::unordered_set<content_t> &place_on,
const std::vector<content_t> &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<std::chrono::nanoseconds>(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<content_t> place_on_vec;
std::unordered_set<content_t> 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<content_t> 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;
}

BIN
defects/minetest-0002/test/test Executable file

Binary file not shown.

View file

@ -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 <typename F>
int ModApiEnvBase::findNodeNear(lua_State *L, v3s16 pos, int radius,
- const std::vector<content_t> &filter, int start_radius, F &&getNode)
+ const std::vector<content_t> &filter_vec, int start_radius, F &&getNode)
{
+ std::unordered_set<content_t> filter(filter_vec.begin(), filter_vec.end());
for (int d = start_radius; d <= radius; d++) {
const std::vector<v3s16> &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<content_t, u32> 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<content_t> 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) {

View file

@ -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<content_t> / 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 <vector>
#include <unordered_set>
#include <unordered_map>
#include <algorithm>
#include <chrono>
#include <cassert>
#include <cstdio>
#include <cstdint>
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<content_t> &filter,
const std::vector<content_t> &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<std::chrono::nanoseconds>(end - start).count();
}
static long long bench_set(const std::unordered_set<content_t> &filter,
const std::vector<content_t> &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<std::chrono::nanoseconds>(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<content_t> filter_vec;
std::unordered_set<content_t> 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<content_t> 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;
}

BIN
defects/minetest-0003/test/test Executable file

Binary file not shown.

View file

@ -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);

View file

@ -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 <vector>
#include <algorithm>
#include <chrono>
#include <cassert>
#include <cstdio>
#include <cstdint>
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<content_t> &ids,
const std::vector<content_t> &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<std::chrono::nanoseconds>(end - start).count();
}
static long long bench_binary(const std::vector<content_t> &ids,
const std::vector<content_t> &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<std::chrono::nanoseconds>(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<content_t> 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<content_t> 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;
}

BIN
defects/minetest-0004/test/test Executable file

Binary file not shown.

View file

@ -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;
}

View file

@ -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 <vector>
#include <algorithm>
#include <chrono>
#include <cassert>
#include <cstdio>
#include <cstdint>
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<content_t> &neighbors,
const std::vector<content_t> &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<std::chrono::nanoseconds>(end - start).count();
}
static long long bench_binary(const std::vector<content_t> &neighbors,
const std::vector<content_t> &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<std::chrono::nanoseconds>(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<content_t> neighbors;
for (int i = 0; i < NEIGHBOR_TYPES; i++)
neighbors.push_back(20 + i * 6);
std::sort(neighbors.begin(), neighbors.end());
std::vector<content_t> 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;
}

BIN
defects/minetest-0005/test/test Executable file

Binary file not shown.