fix(dbc): reject negative IDs in DBC::Record

DBC::Record checked the ID against the store's count as a signed int.
A DBC column uses -1 for "none", and -1 passed that check, so the
helper returned records[0xFFFFFFFF]. On 32-bit that wraps to
records[-1], the heap word in front of the index array. A caller that
passed an unchecked column read a garbage "record": an empty
SpellVisualKit effect column gave the pointer 0xF and crashed.

The engine rejects these IDs before it compares with the count: the
char-select ghost kit's effect step tests the ID with a signed jl at
0x00472797, then a jg against the count. Record now compares
unsigned, so it rejects 0, every negative ID, an ID above the count,
and an unloaded store, whose count is -1.

PvP::Arena had its own copy of the lookup with the same signed
compare. It now calls DBC::Record.
This commit is contained in:
Brues
2026-11-04 15:32:22 -06:00
parent 8f4f574174
commit f6b98bc3ec
3 changed files with 10 additions and 15 deletions
+5 -3
View File
@@ -25,11 +25,13 @@ const char *NormalizeEmpty(const char *s) {
} // namespace
// The id is compared unsigned: a DBC column's "none" is -1, which a signed
// compare passes as less than the count and then indexes records[-1]. The
// engine rejects it with a signed `jl` before its `jg` against the count
// (0x00472797). An unloaded store's count is -1, so it takes no id.
const uint8_t *Record(uintptr_t recordsVar, uintptr_t countVar, uint32_t id) {
if (id == 0)
return nullptr;
const int count = *reinterpret_cast<const int *>(countVar);
if (static_cast<int>(id) > count)
if (id == 0 || count <= 0 || id > static_cast<uint32_t>(count))
return nullptr;
const uint8_t *const *records =
*reinterpret_cast<const uint8_t *const *const *>(recordsVar);
+2 -1
View File
@@ -39,7 +39,8 @@ namespace DBC {
// Resolves `id` to its record pointer in the table whose records-base
// global is at `recordsVar` and count global at `countVar`. Returns
// `nullptr` for `id == 0`, OOR ids (> count), and unpopulated slots.
// `nullptr` for `id == 0`, OOR ids (> count, which includes -1 and every
// other negative id), and unpopulated slots.
// Caller reads whatever fields it needs off the returned record.
//
// Use this when `LocalizedField` / `StringField` don't fit — e.g. for
+3 -11
View File
@@ -13,6 +13,7 @@
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
@@ -46,17 +47,8 @@ namespace {
constexpr uint32_t MAP_INSTANCE_TYPE_PVP = 3;
const uint8_t *DbcRecord(uintptr_t recordsVar, uintptr_t countVar, uint32_t id) {
const int count = *reinterpret_cast<const int *>(countVar);
if (id == 0 || static_cast<int>(id) > count)
return nullptr;
const uint8_t *const *records =
*reinterpret_cast<const uint8_t *const *const *>(recordsVar);
return records != nullptr ? records[id] : nullptr;
}
bool IsArenaArea(uint32_t areaID) {
const uint8_t *rec = DbcRecord(Offsets::VAR_AREATABLE_RECORDS,
const uint8_t *rec = DBC::Record(Offsets::VAR_AREATABLE_RECORDS,
Offsets::VAR_AREATABLE_COUNT, areaID);
if (rec == nullptr)
return false;
@@ -69,7 +61,7 @@ bool InArena() {
const uint32_t mapID = *reinterpret_cast<const uint32_t *>(
static_cast<uintptr_t>(Offsets::VAR_CURRENT_MAP_ID));
const uint8_t *map =
DbcRecord(Offsets::VAR_MAP_RECORDS, Offsets::VAR_MAP_COUNT, mapID);
DBC::Record(Offsets::VAR_MAP_RECORDS, Offsets::VAR_MAP_COUNT, mapID);
if (map == nullptr)
return false;
const uint32_t type =