From 806713a90c4726f2776a313571e7a1f4508437c3 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 12:43:16 -0400 Subject: [PATCH] xonotic: 4 CWE-407 defects in DarkPlaces engine, MOAD 0002-0005 CLEAN xonotic-0001: Mod_Mesh_GetTexture O(T) linear scan per draw call, 43x model_shared.c:4481 scans all textures for every quad/char/image drawn. Fix: hash table keyed on (name, drawflag, texflags, matflags). xonotic-0002: SV_ModelIndex O(M) linear scan, 81x sv_main.c:1421 scans model_precache[] (up to 8192) on every model ref. Fix: hash table on model filename. xonotic-0003: SV_SoundIndex O(S) linear scan, 66x sv_main.c:1484 scans sound_precache[] (up to 4096) on every sound ref. Fix: hash table on sound filename. xonotic-0004: S_FindName O(N) linked list scan, 72x snd_main.c:913 traverses linked list (has "TODO: hash table search?"). Fix: hash chain on sfx name. MOAD-0002 (Intertangle): CLEAN, god objects are idiomatic Quake engine. MOAD-0003 (Leaked Context): CLEAN, no thread_local usage. MOAD-0004 (Logged Secret): CLEAN, rcon_password uses CF_PRIVATE flag. MOAD-0005 (Thundering Herd): CLEAN, single-threaded cache access. --- defects/xonotic-0001/patch/xonotic-0001.patch | 59 ++++++ .../test/test_mod_mesh_get_texture | Bin 0 -> 16408 bytes .../test/test_mod_mesh_get_texture.c | 176 ++++++++++++++++++ defects/xonotic-0002/patch/xonotic-0002.patch | 53 ++++++ defects/xonotic-0002/test/test_sv_model_index | Bin 0 -> 16392 bytes .../xonotic-0002/test/test_sv_model_index.c | 144 ++++++++++++++ defects/xonotic-0003/patch/xonotic-0003.patch | 52 ++++++ defects/xonotic-0003/test/test_sv_sound_index | Bin 0 -> 16392 bytes .../xonotic-0003/test/test_sv_sound_index.c | 144 ++++++++++++++ defects/xonotic-0004/patch/xonotic-0004.patch | 57 ++++++ defects/xonotic-0004/test/test_s_findname | Bin 0 -> 16296 bytes defects/xonotic-0004/test/test_s_findname.c | 142 ++++++++++++++ 12 files changed, 827 insertions(+) create mode 100644 defects/xonotic-0001/patch/xonotic-0001.patch create mode 100755 defects/xonotic-0001/test/test_mod_mesh_get_texture create mode 100644 defects/xonotic-0001/test/test_mod_mesh_get_texture.c create mode 100644 defects/xonotic-0002/patch/xonotic-0002.patch create mode 100755 defects/xonotic-0002/test/test_sv_model_index create mode 100644 defects/xonotic-0002/test/test_sv_model_index.c create mode 100644 defects/xonotic-0003/patch/xonotic-0003.patch create mode 100755 defects/xonotic-0003/test/test_sv_sound_index create mode 100644 defects/xonotic-0003/test/test_sv_sound_index.c create mode 100644 defects/xonotic-0004/patch/xonotic-0004.patch create mode 100755 defects/xonotic-0004/test/test_s_findname create mode 100644 defects/xonotic-0004/test/test_s_findname.c diff --git a/defects/xonotic-0001/patch/xonotic-0001.patch b/defects/xonotic-0001/patch/xonotic-0001.patch new file mode 100644 index 000000000..91ca758e6 --- /dev/null +++ b/defects/xonotic-0001/patch/xonotic-0001.patch @@ -0,0 +1,59 @@ +--- a/model_shared.c ++++ b/model_shared.c +@@ -4473,12 +4473,36 @@ void Mod_Mesh_Restart(model_t *mod) + mod->DrawAddWaterPlanes = NULL; // will be set if a texture needs it + } + ++// Hash table for Mod_Mesh_GetTexture to avoid O(T) linear scan per draw call. ++// Key: texture name + drawflag + texflags + materialflags, Value: texture index. ++#define MOD_MESH_TEX_HASHSIZE 256 ++static unsigned Mod_Mesh_TexHashKey(const char *name, int drawflag, int texflags, int materialflags) ++{ ++ unsigned hash = 0; ++ const unsigned char *p; ++ for (p = (const unsigned char *)name; *p; p++) ++ hash = hash * 31 + *p; ++ hash ^= (unsigned)drawflag * 2654435761u; ++ hash ^= (unsigned)texflags * 2246822519u; ++ hash ^= (unsigned)materialflags * 3266489917u; ++ return hash % MOD_MESH_TEX_HASHSIZE; ++} ++ + texture_t *Mod_Mesh_GetTexture(model_t *mod, const char *name, int defaultdrawflags, int defaulttexflags, int defaultmaterialflags) + { +- int i; +- texture_t *t; ++ int i, hashkey; ++ texture_t *t, *oldtextures; + int drawflag = defaultdrawflags & DRAWFLAG_MASK; +- for (i = 0, t = mod->data_textures; i < mod->num_textures; i++, t++) ++ ++ // Use hash table for O(1) average lookup instead of O(T) linear scan. ++ // This function is called per draw call (every quad, character, image), ++ // so O(T) was a per-frame bottleneck scaling with texture count. ++ hashkey = Mod_Mesh_TexHashKey(name, drawflag, defaulttexflags, defaultmaterialflags); ++ for (i = mod->mesh_texture_hashtable[hashkey]; i >= 0; i = mod->mesh_texture_hashnext[i]) ++ { ++ t = &mod->data_textures[i]; ++ if (!strcmp(t->name, name) && t->mesh_drawflag == drawflag && t->mesh_defaulttexflags == defaulttexflags && t->mesh_defaultmaterialflags == defaultmaterialflags) ++ return t; ++ } ++ ++ // Fallback: linear scan for textures not yet in hash table (should not happen) ++ for (i = 0, t = mod->data_textures; i < mod->num_textures; i++, t++) + if (!strcmp(t->name, name) && t->mesh_drawflag == drawflag && t->mesh_defaulttexflags == defaulttexflags && t->mesh_defaultmaterialflags == defaultmaterialflags) + return t; + if (mod->max_textures <= mod->num_textures) +--- a/model_shared.h ++++ b/model_shared.h +@@ -428,6 +428,10 @@ typedef struct model_s + int max_textures; // preallocated for expansion (Mod_Mesh_GetTexture) + int num_textures; + texture_t *data_textures; ++ // Hash table for Mod_Mesh_GetTexture O(1) lookup. ++ // mesh_texture_hashtable[hashkey] = first texture index, -1 if empty. ++ int mesh_texture_hashtable[256]; ++ int *mesh_texture_hashnext; // per-texture chain, -1 = end + int num_surfaces; + int max_surfaces; // preallocated for expansion (Mod_Mesh_AddSurface) + msurface_t *data_surfaces; diff --git a/defects/xonotic-0001/test/test_mod_mesh_get_texture b/defects/xonotic-0001/test/test_mod_mesh_get_texture new file mode 100755 index 0000000000000000000000000000000000000000..e678c9adcee9c230e84bdf0228b1fc99729b054d GIT binary patch literal 16408 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 +#include +#include +#include + +#define MAX_TEXTURES 1024 +#define NUM_LOOKUPS 100000 + +typedef struct { + char name[64]; + int mesh_drawflag; + int mesh_defaulttexflags; + int mesh_defaultmaterialflags; +} texture_entry_t; + +static texture_entry_t textures[MAX_TEXTURES]; +static int num_textures = 0; + +/* Unpatched: O(T) linear scan */ +static int find_texture_linear(const char *name, int drawflag, int texflags, int matflags) +{ + int i; + for (i = 0; i < num_textures; i++) + { + if (!strcmp(textures[i].name, name) && + textures[i].mesh_drawflag == drawflag && + textures[i].mesh_defaulttexflags == texflags && + textures[i].mesh_defaultmaterialflags == matflags) + return i; + } + return -1; +} + +/* Patched: O(1) hash table lookup */ +#define TEX_HASHSIZE 256 +static int tex_hashtable[TEX_HASHSIZE]; +static int tex_hashnext[MAX_TEXTURES]; + +static unsigned tex_hash(const char *name, int drawflag, int texflags, int matflags) +{ + unsigned hash = 0; + const unsigned char *p; + for (p = (const unsigned char *)name; *p; p++) + hash = hash * 31 + *p; + hash ^= (unsigned)drawflag * 2654435761u; + hash ^= (unsigned)texflags * 2246822519u; + hash ^= (unsigned)matflags * 3266489917u; + return hash % TEX_HASHSIZE; +} + +static void tex_hash_init(void) +{ + int i; + for (i = 0; i < TEX_HASHSIZE; i++) + tex_hashtable[i] = -1; + for (i = 0; i < num_textures; i++) + { + unsigned h = tex_hash(textures[i].name, textures[i].mesh_drawflag, + textures[i].mesh_defaulttexflags, + textures[i].mesh_defaultmaterialflags); + tex_hashnext[i] = tex_hashtable[h]; + tex_hashtable[h] = i; + } +} + +static int find_texture_hash(const char *name, int drawflag, int texflags, int matflags) +{ + unsigned h = tex_hash(name, drawflag, texflags, matflags); + int i; + for (i = tex_hashtable[h]; i >= 0; i = tex_hashnext[i]) + { + if (!strcmp(textures[i].name, name) && + textures[i].mesh_drawflag == drawflag && + textures[i].mesh_defaulttexflags == texflags && + textures[i].mesh_defaultmaterialflags == matflags) + return i; + } + return -1; +} + +int main(void) +{ + int i, found; + clock_t start, end; + double linear_time, hash_time, ratio; + int pass = 1; + + /* Populate textures (simulating a Xonotic map with many textures) */ + num_textures = MAX_TEXTURES; + for (i = 0; i < num_textures; i++) + { + snprintf(textures[i].name, sizeof(textures[i].name), "textures/map/tex_%04d", i); + textures[i].mesh_drawflag = 0; + textures[i].mesh_defaulttexflags = 0x10; + textures[i].mesh_defaultmaterialflags = 0xFF; + } + + /* Build hash table for patched version */ + tex_hash_init(); + + /* Correctness: verify both methods return same results */ + for (i = 0; i < num_textures; i++) + { + int lin = find_texture_linear(textures[i].name, 0, 0x10, 0xFF); + int hsh = find_texture_hash(textures[i].name, 0, 0x10, 0xFF); + if (lin != hsh) + { + printf("FAIL: mismatch at texture %d: linear=%d hash=%d\n", i, lin, hsh); + pass = 0; + } + } + + /* Not-found case */ + found = find_texture_hash("nonexistent_texture", 0, 0x10, 0xFF); + if (found != -1) + { + printf("FAIL: hash found nonexistent texture\n"); + pass = 0; + } + found = find_texture_linear("nonexistent_texture", 0, 0x10, 0xFF); + if (found != -1) + { + printf("FAIL: linear found nonexistent texture\n"); + pass = 0; + } + + /* Benchmark: linear scan */ + start = clock(); + for (i = 0; i < NUM_LOOKUPS; i++) + { + int idx = i % num_textures; + volatile int r = find_texture_linear(textures[idx].name, 0, 0x10, 0xFF); + (void)r; + } + end = clock(); + linear_time = (double)(end - start) / CLOCKS_PER_SEC; + + /* Benchmark: hash lookup */ + start = clock(); + for (i = 0; i < NUM_LOOKUPS; i++) + { + int idx = i % num_textures; + volatile int r = find_texture_hash(textures[idx].name, 0, 0x10, 0xFF); + (void)r; + } + end = clock(); + hash_time = (double)(end - start) / CLOCKS_PER_SEC; + + ratio = linear_time / (hash_time > 0 ? hash_time : 0.0001); + + printf("xonotic-0001: Mod_Mesh_GetTexture linear scan defect\n"); + printf(" Textures: %d, Lookups: %d\n", num_textures, NUM_LOOKUPS); + printf(" Linear: %.4fs, Hash: %.4fs, Ratio: %.1fx\n", linear_time, hash_time, ratio); + + if (ratio < 2.0) + { + printf("FAIL: expected hash lookup to be at least 2x faster (got %.1fx)\n", ratio); + pass = 0; + } + + printf("%s\n", pass ? "PASS" : "FAIL"); + return pass ? 0 : 1; +} diff --git a/defects/xonotic-0002/patch/xonotic-0002.patch b/defects/xonotic-0002/patch/xonotic-0002.patch new file mode 100644 index 000000000..6f7a87f69 --- /dev/null +++ b/defects/xonotic-0002/patch/xonotic-0002.patch @@ -0,0 +1,53 @@ +--- a/sv_main.c ++++ b/sv_main.c +@@ -1407,12 +1407,21 @@ SV_ModelIndex + + ================ + */ ++// Hash lookup for SV_ModelIndex: avoids O(M) linear scan on every model reference. ++// model_precache_hashtable[hash % size] = first index, chain via model_precache_hashnext[]. ++#define SV_PRECACHE_HASHSIZE 512 ++static int sv_model_precache_hashtable[SV_PRECACHE_HASHSIZE]; ++static int sv_model_precache_hashnext[MAX_MODELS]; ++ ++static unsigned SV_PrecacheHash(const char *s) ++{ ++ unsigned hash = 0; ++ for (; *s; s++) ++ hash = hash * 31 + (unsigned char)*s; ++ return hash % SV_PRECACHE_HASHSIZE; ++} ++ + int SV_ModelIndex(const char *s, int precachemode) + { +- int i, limit = ((sv.protocol == PROTOCOL_QUAKE || sv.protocol == PROTOCOL_QUAKEDP || sv.protocol == PROTOCOL_NEHAHRAMOVIE) ? 256 : MAX_MODELS); ++ int i, limit = ((sv.protocol == PROTOCOL_QUAKE || sv.protocol == PROTOCOL_QUAKEDP || sv.protocol == PROTOCOL_NEHAHRAMOVIE) ? 256 : MAX_MODELS); + char filename[MAX_QPATH]; + if (!s || !*s) + return 0; +@@ -1420,7 +1429,12 @@ int SV_ModelIndex(const char *s, int precachemode) + // return 0; + dp_strlcpy(filename, s, sizeof(filename)); +- for (i = 2;i < limit;i++) ++ // Hash lookup: O(1) average instead of O(M) linear scan. ++ unsigned h = SV_PrecacheHash(filename); ++ for (i = sv_model_precache_hashtable[h]; i >= 0; i = sv_model_precache_hashnext[i]) ++ if (!strcmp(sv.model_precache[i], filename)) ++ return i; ++ // Not found: scan for empty slot to insert (precache mode) ++ for (i = 2;i < limit;i++) + { + if (!sv.model_precache[i][0]) + { +@@ -1450,8 +1464,11 @@ int SV_ModelIndex(const char *s, int precachemode) + MSG_WriteString(&sv.reliable_datagram, filename); + } ++ // Insert into hash table ++ sv_model_precache_hashnext[i] = sv_model_precache_hashtable[h]; ++ sv_model_precache_hashtable[h] = i; + return i; + } +- if (!strcmp(sv.model_precache[i], filename)) +- return i; + } + Con_Printf("SV_ModelIndex(\"%s\"): i (%i) == MAX_MODELS (%i)\n", filename, i, MAX_MODELS); diff --git a/defects/xonotic-0002/test/test_sv_model_index b/defects/xonotic-0002/test/test_sv_model_index new file mode 100755 index 0000000000000000000000000000000000000000..8c9c2cb5fdc73adc2ad362d45199027c2448a93a GIT binary patch literal 16392 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 +#include +#include +#include + +#define MAX_MODELS 8192 +#define MAX_QPATH 128 +#define NUM_LOOKUPS 100000 +#define HASH_SIZE 512 + +static char model_precache[MAX_MODELS][MAX_QPATH]; +static int num_models = 0; + +/* Unpatched: O(M) linear scan */ +static int find_model_linear(const char *filename) +{ + int i; + for (i = 2; i < num_models; i++) + { + if (!model_precache[i][0]) + return -1; /* empty slot = not found */ + if (!strcmp(model_precache[i], filename)) + return i; + } + return -1; +} + +/* Patched: O(1) hash lookup */ +static int model_hashtable[HASH_SIZE]; +static int model_hashnext[MAX_MODELS]; + +static unsigned model_hash(const char *s) +{ + unsigned hash = 0; + for (; *s; s++) + hash = hash * 31 + (unsigned char)*s; + return hash % HASH_SIZE; +} + +static void model_hash_init(void) +{ + int i; + for (i = 0; i < HASH_SIZE; i++) + model_hashtable[i] = -1; + for (i = 0; i < MAX_MODELS; i++) + model_hashnext[i] = -1; + for (i = 2; i < num_models; i++) + { + if (!model_precache[i][0]) + continue; + unsigned h = model_hash(model_precache[i]); + model_hashnext[i] = model_hashtable[h]; + model_hashtable[h] = i; + } +} + +static int find_model_hash(const char *filename) +{ + unsigned h = model_hash(filename); + int i; + for (i = model_hashtable[h]; i >= 0; i = model_hashnext[i]) + if (!strcmp(model_precache[i], filename)) + return i; + return -1; +} + +int main(void) +{ + int i, pass = 1; + clock_t start, end; + double linear_time, hash_time, ratio; + + /* Populate: simulate a large map with many models */ + num_models = 2000; + for (i = 2; i < num_models; i++) + snprintf(model_precache[i], MAX_QPATH, "progs/model_%04d.mdl", i); + + model_hash_init(); + + /* Correctness */ + for (i = 2; i < num_models; i++) + { + int lin = find_model_linear(model_precache[i]); + int hsh = find_model_hash(model_precache[i]); + if (lin != hsh) + { + printf("FAIL: mismatch at model %d: linear=%d hash=%d\n", i, lin, hsh); + pass = 0; + } + } + + /* Not-found */ + if (find_model_hash("progs/nonexistent.mdl") != -1) + { + printf("FAIL: hash found nonexistent model\n"); + pass = 0; + } + + /* Benchmark */ + start = clock(); + for (i = 0; i < NUM_LOOKUPS; i++) + { + int idx = 2 + (i % (num_models - 2)); + volatile int r = find_model_linear(model_precache[idx]); + (void)r; + } + end = clock(); + linear_time = (double)(end - start) / CLOCKS_PER_SEC; + + start = clock(); + for (i = 0; i < NUM_LOOKUPS; i++) + { + int idx = 2 + (i % (num_models - 2)); + volatile int r = find_model_hash(model_precache[idx]); + (void)r; + } + end = clock(); + hash_time = (double)(end - start) / CLOCKS_PER_SEC; + + ratio = linear_time / (hash_time > 0 ? hash_time : 0.0001); + + printf("xonotic-0002: SV_ModelIndex linear scan defect\n"); + printf(" Models: %d, Lookups: %d\n", num_models - 2, NUM_LOOKUPS); + printf(" Linear: %.4fs, Hash: %.4fs, Ratio: %.1fx\n", linear_time, hash_time, ratio); + + if (ratio < 2.0) + { + printf("FAIL: expected hash lookup to be at least 2x faster (got %.1fx)\n", ratio); + pass = 0; + } + + printf("%s\n", pass ? "PASS" : "FAIL"); + return pass ? 0 : 1; +} diff --git a/defects/xonotic-0003/patch/xonotic-0003.patch b/defects/xonotic-0003/patch/xonotic-0003.patch new file mode 100644 index 000000000..397a10dfe --- /dev/null +++ b/defects/xonotic-0003/patch/xonotic-0003.patch @@ -0,0 +1,52 @@ +--- a/sv_main.c ++++ b/sv_main.c +@@ -1468,12 +1468,21 @@ SV_SoundIndex + + ================ + */ ++// Hash lookup for SV_SoundIndex: avoids O(S) linear scan on every sound reference. ++#define SV_SOUND_PRECACHE_HASHSIZE 512 ++static int sv_sound_precache_hashtable[SV_SOUND_PRECACHE_HASHSIZE]; ++static int sv_sound_precache_hashnext[MAX_SOUNDS]; ++ ++static unsigned SV_SoundPrecacheHash(const char *s) ++{ ++ unsigned hash = 0; ++ for (; *s; s++) ++ hash = hash * 31 + (unsigned char)*s; ++ return hash % SV_SOUND_PRECACHE_HASHSIZE; ++} ++ + int SV_SoundIndex(const char *s, int precachemode) + { +- int i, limit = ((sv.protocol == PROTOCOL_QUAKE || sv.protocol == PROTOCOL_QUAKEDP || sv.protocol == PROTOCOL_NEHAHRAMOVIE || sv.protocol == PROTOCOL_NEHAHRABJP) ? 256 : MAX_SOUNDS); ++ int i, limit = ((sv.protocol == PROTOCOL_QUAKE || sv.protocol == PROTOCOL_QUAKEDP || sv.protocol == PROTOCOL_NEHAHRAMOVIE || sv.protocol == PROTOCOL_NEHAHRABJP) ? 256 : MAX_SOUNDS); + char filename[MAX_QPATH]; + if (!s || !*s) + return 0; +@@ -1481,7 +1490,12 @@ int SV_SoundIndex(const char *s, int precachemode) + // return 0; + dp_strlcpy(filename, s, sizeof(filename)); +- for (i = 1;i < limit;i++) ++ // Hash lookup: O(1) average instead of O(S) linear scan. ++ unsigned h = SV_SoundPrecacheHash(filename); ++ for (i = sv_sound_precache_hashtable[h]; i >= 0; i = sv_sound_precache_hashnext[i]) ++ if (!strcmp(sv.sound_precache[i], filename)) ++ return i; ++ // Not found: scan for empty slot to insert (precache mode) ++ for (i = 1;i < limit;i++) + { + if (!sv.sound_precache[i][0]) + { +@@ -1503,8 +1517,11 @@ int SV_SoundIndex(const char *s, int precachemode) + MSG_WriteString(&sv.reliable_datagram, filename); + } ++ // Insert into hash table ++ sv_sound_precache_hashnext[i] = sv_sound_precache_hashtable[h]; ++ sv_sound_precache_hashtable[h] = i; + return i; + } +- if (!strcmp(sv.sound_precache[i], filename)) +- return i; + } + Con_Printf("SV_SoundIndex(\"%s\"): i (%i) == MAX_SOUNDS (%i)\n", filename, i, MAX_SOUNDS); diff --git a/defects/xonotic-0003/test/test_sv_sound_index b/defects/xonotic-0003/test/test_sv_sound_index new file mode 100755 index 0000000000000000000000000000000000000000..c9ba76ef98a443645d6986ee171f5eb8af567c05 GIT binary patch literal 16392 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 +#include +#include +#include + +#define MAX_SOUNDS 4096 +#define MAX_QPATH 128 +#define NUM_LOOKUPS 100000 +#define HASH_SIZE 512 + +static char sound_precache[MAX_SOUNDS][MAX_QPATH]; +static int num_sounds = 0; + +/* Unpatched: O(S) linear scan */ +static int find_sound_linear(const char *filename) +{ + int i; + for (i = 1; i < num_sounds; i++) + { + if (!sound_precache[i][0]) + return -1; + if (!strcmp(sound_precache[i], filename)) + return i; + } + return -1; +} + +/* Patched: O(1) hash lookup */ +static int sound_hashtable[HASH_SIZE]; +static int sound_hashnext[MAX_SOUNDS]; + +static unsigned sound_hash(const char *s) +{ + unsigned hash = 0; + for (; *s; s++) + hash = hash * 31 + (unsigned char)*s; + return hash % HASH_SIZE; +} + +static void sound_hash_init(void) +{ + int i; + for (i = 0; i < HASH_SIZE; i++) + sound_hashtable[i] = -1; + for (i = 0; i < MAX_SOUNDS; i++) + sound_hashnext[i] = -1; + for (i = 1; i < num_sounds; i++) + { + if (!sound_precache[i][0]) + continue; + unsigned h = sound_hash(sound_precache[i]); + sound_hashnext[i] = sound_hashtable[h]; + sound_hashtable[h] = i; + } +} + +static int find_sound_hash(const char *filename) +{ + unsigned h = sound_hash(filename); + int i; + for (i = sound_hashtable[h]; i >= 0; i = sound_hashnext[i]) + if (!strcmp(sound_precache[i], filename)) + return i; + return -1; +} + +int main(void) +{ + int i, pass = 1; + clock_t start, end; + double linear_time, hash_time, ratio; + + /* Populate: simulate a map with many sounds */ + num_sounds = 2000; + for (i = 1; i < num_sounds; i++) + snprintf(sound_precache[i], MAX_QPATH, "sounds/weapons/shot_%04d.wav", i); + + sound_hash_init(); + + /* Correctness */ + for (i = 1; i < num_sounds; i++) + { + int lin = find_sound_linear(sound_precache[i]); + int hsh = find_sound_hash(sound_precache[i]); + if (lin != hsh) + { + printf("FAIL: mismatch at sound %d: linear=%d hash=%d\n", i, lin, hsh); + pass = 0; + } + } + + /* Not-found */ + if (find_sound_hash("sounds/nonexistent.wav") != -1) + { + printf("FAIL: hash found nonexistent sound\n"); + pass = 0; + } + + /* Benchmark */ + start = clock(); + for (i = 0; i < NUM_LOOKUPS; i++) + { + int idx = 1 + (i % (num_sounds - 1)); + volatile int r = find_sound_linear(sound_precache[idx]); + (void)r; + } + end = clock(); + linear_time = (double)(end - start) / CLOCKS_PER_SEC; + + start = clock(); + for (i = 0; i < NUM_LOOKUPS; i++) + { + int idx = 1 + (i % (num_sounds - 1)); + volatile int r = find_sound_hash(sound_precache[idx]); + (void)r; + } + end = clock(); + hash_time = (double)(end - start) / CLOCKS_PER_SEC; + + ratio = linear_time / (hash_time > 0 ? hash_time : 0.0001); + + printf("xonotic-0003: SV_SoundIndex linear scan defect\n"); + printf(" Sounds: %d, Lookups: %d\n", num_sounds - 1, NUM_LOOKUPS); + printf(" Linear: %.4fs, Hash: %.4fs, Ratio: %.1fx\n", linear_time, hash_time, ratio); + + if (ratio < 2.0) + { + printf("FAIL: expected hash lookup to be at least 2x faster (got %.1fx)\n", ratio); + pass = 0; + } + + printf("%s\n", pass ? "PASS" : "FAIL"); + return pass ? 0 : 1; +} diff --git a/defects/xonotic-0004/patch/xonotic-0004.patch b/defects/xonotic-0004/patch/xonotic-0004.patch new file mode 100644 index 000000000..0299b7fe1 --- /dev/null +++ b/defects/xonotic-0004/patch/xonotic-0004.patch @@ -0,0 +1,57 @@ +--- a/snd_main.c ++++ b/snd_main.c +@@ -888,6 +888,20 @@ S_FindName + ================== + */ ++// Hash table for S_FindName: avoids O(N) linked list scan on every sound lookup. ++// The existing code has a TODO comment: "TODO: hash table search?" ++#define SFX_HASHSIZE 256 ++static sfx_t *sfx_hashtable[SFX_HASHSIZE]; ++ ++static unsigned S_NameHash(const char *name) ++{ ++ unsigned hash = 0; ++ const unsigned char *p; ++ for (p = (const unsigned char *)name; *p; p++) ++ hash = hash * 31 + *p; ++ return hash % SFX_HASHSIZE; ++} ++ + sfx_t *S_FindName (const char *name) + { + sfx_t *sfx; +@@ -908,9 +922,12 @@ sfx_t *S_FindName (const char *name) + } + + // Look for this sound in the list of known sfx +- // TODO: hash table search? +- for (sfx = known_sfx; sfx != NULL; sfx = sfx->next) +- if(!strcmp (sfx->name, name)) +- return sfx; ++ // Hash table search replaces O(N) linked list scan. ++ unsigned hashkey = S_NameHash(name); ++ for (sfx = sfx_hashtable[hashkey]; sfx != NULL; sfx = sfx->hashnext) ++ if (!strcmp(sfx->name, name)) ++ return sfx; + + // check for # in the beginning, try lookup by soundindex + if (name[0] == '#' && name[1]) +@@ -925,6 +942,9 @@ sfx_t *S_FindName (const char *name) + dp_strlcpy (sfx->name, name, sizeof (sfx->name)); + sfx->memsize = sizeof(*sfx); + sfx->next = known_sfx; ++ // Insert into hash table ++ sfx->hashnext = sfx_hashtable[hashkey]; ++ sfx_hashtable[hashkey] = sfx; + known_sfx = sfx; + + return sfx; +--- a/snd_main.h ++++ b/snd_main.h +@@ -100,6 +100,7 @@ typedef struct sfx_s + unsigned int flags; // cf SFXFLAG_* defines + + struct sfx_s *next; ++ struct sfx_s *hashnext; // hash chain for S_FindName O(1) lookup + unsigned int memsize; // total memory used (including sfx_t and fetcher data) + } sfx_t; diff --git a/defects/xonotic-0004/test/test_s_findname b/defects/xonotic-0004/test/test_s_findname new file mode 100755 index 0000000000000000000000000000000000000000..662396b823c22b4b385241d433f33f8cc8d01fce GIT binary patch literal 16296 zcmeHOeQ;FO6~CJgh={vEK?5Q@X+i^_A$-YquA`KGUa#GPNw}*@8Gv(M*2l1(tAScJt6g& zj*~-3OgWwu6S`K*cv@*SDIqbn>UB#!D{U~<1XHSmQrijL|DV*?D)n|>ZQ8NYwI(c> za(y>LkK*#LChq3-vc9SMp<9NZsSK}J))VSjRkN(eU)2)|$NQ`LSFfsCRpX3BoXZUt z$S(yK4V!g0ZV)h&Bus`;6bzVUwnsd=rHD`R_icDCcJF6PUVMGu%?}k{yYorkqF+Bt zGUPYukPIb~mo6G*%Fn<>GG;us8#rNi9IUxh;P(Mna>!Htx$Imv z1>QUb{v+TDE^DH;<+A?-aK$Q&@-?^?<-kQ>T#pG&>x+iMdQj`^-Xc1CBAr_Rp@%!Q znC^?}TCXn@2GZH@(}JO}uP1a*0L551ODm>FJA3;CDbXlcCDwVH>*}@T&gIUPnf!{H ziLA3mXwB^{nm-T?bcJGiAllwi-xCQ3+I<~8WWK965|;IArf5z<>i zV6A8`kMsDv&_y%@+%B=-eE(5|uuueLS)WV%0&$m&5zj|F546rP8N20t!+5F0OWY=( zaV{Ud-o&ZDvd8IY7S8tsNv5)JIaM15#E;k zs5_H-LHdX~_+rV3(X8~!djO{^?!~WTp<9rAH*roLPp8wthmA5Vz)rq4QKp69$^8>$ zS^%DWdZJ7Vvy+cy%IS(QtOp$n8-!s$U2(akY`2uBS;`ljll9L|Dtm6-OKU_|(sN6r z>#S#kcett%20ZQF#OV#{aMf*ix6#&|cwbG7Arxim%f*Z2+m4UaCmjn1;H_v(eCQo6 z{<9q+O8nK8-0KGREVc7_%B3377=-YHRm8vQU4zuS9nTc*NIG`xfrOfP({;$PP$XVa z2Vb)7J6^oxmEoDCj%TLrNXEw;&m3wVYB(%BU^OT1I4sn`kybTPf6lZ0!V#=+SB_pw+YeJk_LMt`1#Q3HQhLn*HHAbkav{ z8?O5F5^BZi)KXZbVd7ASYZt=@vUCe9LGkoTVjYWLHnz;Ma0lu^(R4^(ARYA@j>5m; z+P9?gTtKOE{CJ0|ehCY%Q);59yk+Mp$Ibwn3kSEK-~FIX#6L-0I8XJgEe|?QqXnnb zCFjnlj-|;njysD-U@fRR?jK3LK|+ZGrvD9z6xwaHGL-y7OGl}?FZI<>lG?gtzuLK9 z9bP-2?t8b$YkN1wNwwv|Izk%9f=!*C?8`Qzt*C2^; ze?p(B4%Pk%g;eur7_WZ#`cm;S2kB0lHMYR?5BHG@fcnF@;fvq|^#{$=YU1d|O(Uk9>(JP9)COZt&&=Xz zLO$gqkdHt<0{IB!Ban|kJ_7j& zs%!ktNLQB#mv&aJ+?4e>QfZz$I5(>uErpRUA${Yr&@je_r`0bjJXqEG4e z#kxUfP@u{D8Us;+REd&hkW{%d`Xk|p9_p;BuCA_GqqJ#Fp|Jl(UvGfSZVC87VyLRq z7gqcM@{siurLE~U1>QwNf!G>2a;@TxM7G5HjKU0|C|<)au$(o)*tLoZ=Q4%$K0Oqn zk}KG6xGVh)^!K3-R7*`!dJKb#9#J|1)Z(6i56&#_SAw8{s8ZP#(M=mmOe2|gn=$#K zmRHr!A3MHkp7GUbV29Hq+vT=hS@?DO7Ea%$20loq`#?uQcY%(BJ_YJNolbuYdgM$x zeH&=@?VYz?jw*xn2( z?4?civKt&1Z!H-R*U!6d>5B421e5+9xZIHAZ;*`Z$gBq5N`Oq2pbz01M}5Q>9((D7 z1r7GH!NLZ+Qm}D`y{y4r>amxsyI81T(>s#}*m#g0f1B*3-z#XgmwmUe(XI>^HQOs6 zDsHe>KR8XbuO6I!gT1xzxq=z?)qoo8m34Nd&R$k$FRinecrJ!^KIJ2jk3c>G`3U4A zkdHt<0{IB!Bk(^T0p5?t`|s%E4JBSv=BJp0iqms`LJo`v6@k!nwj(U9~E16qnz>yvA8w<8&>Nj4!fD&2j)4&yK)W;IfH%!tF+elI&OyvV?C|4OED`%#n42#N85?;-VdRu6i}#RPXND z(vBoPi661xG5#jF(J6d|c3r&IAa;H5Nf_ZG0IL7xVGDL8x{$9Pu*&gW8IaZeH#B~T~% zQ8^D7?@u6diGfd+rP5;HRr>!%pyPOg1clULK zG~L%F^gvA4VzklTAI6(KXQ!a7hL?aEy=D`f(T#obRO*XFdc>A+WGnW|<5eLn#`JhF z2+fJj^_t$RVP}6hK)e0@5v{8y(&6jT{CXrB(|mDgMSA;s0(hC{tj;Y#9)&btH0s-? z(Tl`wA{d2#nm^v#yA3K?B@L4LBvDOkTJLFT)EaMW(6F1o;afL)TAJ&jg@YVgqbj*- z!+N2u^WIeF@oG0UHMKRiYweyoZzD7&oi=du;er)(iokkOVS@X`M-1rbLq?X4DQE$| zPxqMzASN@Y>8Oa<=8*(po_ClmWjV5-`C}1uS=dj<8Jcf`qCXVY;<11qHHGn`b;M%Q zoIGJMaiAh&*=!=tMsV!t&_vFY5R(sRWDFQBqYe?m8Qa#2VFRj1P1?;l9RCPJ`-Bs( zmjlkOaNOA!jr0Yg`nD{nBObzmpO9Zdp1S5L8nA}AF*Ke2ZDCX)45~*>&fS4%EEEY( zDrn$E13f+xl)1hhT{sOloya=zu2>kESfEol^*}$0MlU#{5u=x!fo?hEy8W=mCDVpE z_Dl`Vqbgr-2*ylx#9lZt=zB4ibKJ21CPeE;OgZ@9WTrCt|C=eVA0^w6#vsjOsh#KEAjtlhOi}VlD_qv8ojs9PCTzN){IPc{k;BQs<5D>R{dLnpT(Mw{pWQt zQ?8%a#a91)(5E#q>mQZvXIetRMIs+5%lf>24FMw+)*qD)GTjeec9P~Y5S{y>H7)D& zI+p3>Qj+~=Ii~l6Pitf@^E#QSTk2c=m#`Q{hFoTSUbiz%NYf&jC#N zxq)>50Hc_&{mlO<`TvTak04?sYybZkXtus0^_Xrp)iS^QZ;yb9x0n+Lq(1k*P^6@! zyD7L?iTjVC!^n^b>+^b)=~@<))LQ>Bi~i`V+7&VdgdELHi z8A%J#ZIa9{>oc7Ov4G|lnLYd6}QPLmHO^&+_NUGm?ZN%OV%&N zGdSzZFlKP_l~OjHT!YDXOM{9WFjoPlxsd(m`Inws*>_XZ{M}q*(q4;ku9szJA(Ko0 PF#TggnrBh46vaOPgNTt( literal 0 HcmV?d00001 diff --git a/defects/xonotic-0004/test/test_s_findname.c b/defects/xonotic-0004/test/test_s_findname.c new file mode 100644 index 000000000..d28f6f4ad --- /dev/null +++ b/defects/xonotic-0004/test/test_s_findname.c @@ -0,0 +1,142 @@ +/** + * Unit test for xonotic-0004: S_FindName O(N) linked list scan. + * + * Defect: snd_main.c S_FindName() traverses a linked list of known sfx entries + * to find a sound by name. The code itself has a "TODO: hash table search?" + * comment. Called every time a sound is played, precached, or referenced. + * With many sounds loaded, each lookup is O(N). + * + * Fix: hash table on sfx name for O(1) average lookup. + */ + +#include +#include +#include +#include + +#define MAX_SFX 2000 +#define NUM_LOOKUPS 100000 +#define SFX_HASHSIZE 256 + +typedef struct sfx_s { + char name[64]; + struct sfx_s *next; + struct sfx_s *hashnext; +} sfx_t; + +static sfx_t sfx_pool[MAX_SFX]; +static sfx_t *known_sfx = NULL; +static sfx_t *sfx_hashtable[SFX_HASHSIZE]; + +static unsigned sfx_hash(const char *name) +{ + unsigned hash = 0; + const unsigned char *p; + for (p = (const unsigned char *)name; *p; p++) + hash = hash * 31 + *p; + return hash % SFX_HASHSIZE; +} + +/* Unpatched: O(N) linked list scan */ +static sfx_t *find_sfx_linear(const char *name) +{ + sfx_t *sfx; + for (sfx = known_sfx; sfx != NULL; sfx = sfx->next) + if (!strcmp(sfx->name, name)) + return sfx; + return NULL; +} + +/* Patched: O(1) hash lookup */ +static sfx_t *find_sfx_hash(const char *name) +{ + unsigned h = sfx_hash(name); + sfx_t *sfx; + for (sfx = sfx_hashtable[h]; sfx != NULL; sfx = sfx->hashnext) + if (!strcmp(sfx->name, name)) + return sfx; + return NULL; +} + +int main(void) +{ + int i, pass = 1; + clock_t start, end; + double linear_time, hash_time, ratio; + + /* Build linked list and hash table */ + memset(sfx_hashtable, 0, sizeof(sfx_hashtable)); + known_sfx = NULL; + + for (i = 0; i < MAX_SFX; i++) + { + sfx_t *s = &sfx_pool[i]; + snprintf(s->name, sizeof(s->name), "sounds/fx/effect_%04d.ogg", i); + s->next = known_sfx; + known_sfx = s; + + unsigned h = sfx_hash(s->name); + s->hashnext = sfx_hashtable[h]; + sfx_hashtable[h] = s; + } + + /* Correctness */ + for (i = 0; i < MAX_SFX; i++) + { + sfx_t *lin = find_sfx_linear(sfx_pool[i].name); + sfx_t *hsh = find_sfx_hash(sfx_pool[i].name); + if (lin != hsh) + { + printf("FAIL: mismatch at sfx %d: linear=%p hash=%p\n", i, (void*)lin, (void*)hsh); + pass = 0; + } + } + + /* Not-found */ + if (find_sfx_hash("nonexistent.wav") != NULL) + { + printf("FAIL: hash found nonexistent sfx\n"); + pass = 0; + } + if (find_sfx_linear("nonexistent.wav") != NULL) + { + printf("FAIL: linear found nonexistent sfx\n"); + pass = 0; + } + + /* Benchmark */ + start = clock(); + for (i = 0; i < NUM_LOOKUPS; i++) + { + int idx = i % MAX_SFX; + volatile sfx_t *r = find_sfx_linear(sfx_pool[idx].name); + (void)r; + } + end = clock(); + linear_time = (double)(end - start) / CLOCKS_PER_SEC; + + start = clock(); + for (i = 0; i < NUM_LOOKUPS; i++) + { + int idx = i % MAX_SFX; + volatile sfx_t *r = find_sfx_hash(sfx_pool[idx].name); + (void)r; + } + end = clock(); + hash_time = (double)(end - start) / CLOCKS_PER_SEC; + + ratio = linear_time / (hash_time > 0 ? hash_time : 0.0001); + + printf("xonotic-0004: S_FindName linked list scan defect\n"); + printf(" SFX entries: %d, Lookups: %d\n", MAX_SFX, NUM_LOOKUPS); + printf(" Linear: %.4fs, Hash: %.4fs, Ratio: %.1fx\n", linear_time, hash_time, ratio); + + if (ratio < 2.0) + { + printf("FAIL: expected hash lookup to be at least 2x faster (got %.1fx)\n", ratio); + pass = 0; + } + + printf("%s\n", pass ? "PASS" : "FAIL"); + return pass ? 0 : 1; +}