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;  // :778

CiHash 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 seed

The 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.