Hash-collision CPU amplification loading interior-cell names from content files
Interior cell names read from content files (.esm/.esp/.omwaddon) are inserted into an std::unordered_map keyed by the cell name with a fixed, unseeded case-insensitive hash (Misc::StringUtils::CiHash, FNV-1a). Because the hash has no per-process secret, a content file whose interior-cell names are chosen to collide can make cell-store construction during loading approach quadratic CPU work. Loading an untrusted content file/mod is a normal operation, so this is reachable from a single file without any prior interaction.
Affected code
The cell store keys interior cells by name using CiHash:
// apps/openmw/mwworld/store.hpp
typedef std::unordered_map<std::string, ESM::Cell*,
Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> DynamicInt; // :360
DynamicInt mInt; // static (content-file) interior cells :367
DynamicInt mDynamicInt; // savegame-created interior cells :373
// (ESM4 has an analogous unordered_map<std::string, ESM4::Cell*, CiHash, CiEqual> at :287)The name comes directly from the loaded cell record and is inserted without a per-file cap:
// apps/openmw/mwworld/store.cpp
mInt[cell.mName] = &cell; // :668 content-file load
DynamicInt::iterator result = mDynamicInt.emplace(cell.mName, insertedCell).first; // :778CiHash is an unseeded FNV-1a over lowercased bytes (components/misc/strings/algorithm.hpp:85):
std::uint64_t hash{ 0xcbf29ce484222325ull };
constexpr std::uint64_t prime{ 0x00000100000001B3ull };
for (char c : str) { hash ^= toLower(c); hash *= prime; } // fixed basis, no per-process seedThe same CiHash-keyed name maps appear for other record kinds as well; interior cells are the confirmed instance here.
Trigger and impact
- Reachable by loading a single untrusted content file (base data files, or a distributed mod). No authentication and no cross-session accumulation is required.
- The cited insertion sites have no per-file cap on the number of interior-cell names; a content file may declare a large number.
- Impact is CPU amplification while building the cell store during load: inserting many colliding names is O(n²) (each
emplace/operator[]probes the growing bucket). This report demonstrates map-level degradation, not a specific end-to-end stall measurement.
Suggested fix
- Give
CiHash(as used by these content-derived name maps) a per-process random seed so collisions cannot be precomputed, preserving case-insensitive equality; and/or bound the number of interior-cell names accepted per content file.