#include "Console.h" #include "PluginManager.h" #include "MemAccess.h" #include "DataDefs.h" #include "DataIdentity.h" #include #include using namespace DFHack; DFHACK_PLUGIN("check-structures-sanity"); static command_result command(color_ostream &, std::vector &); #ifdef WIN32 #define UNEXPECTED __debugbreak() #else #define UNEXPECTED __asm__ volatile ("int $0x03") #endif DFhackCExport command_result plugin_init(color_ostream & out, std::vector & commands) { commands.push_back(PluginCommand( "check-structures-sanity", "performs a sanity check on df-structures", command, false, "checks structures to make sure vectors aren't misidentified" )); return CR_OK; } class Checker { color_ostream & out; std::vector mapped; std::set seen_addr; std::vector path; bool ok; bool check_access(void *, type_identity *); void *safe_dereference(void *, ptrdiff_t = 0); void check_global(global_identity *); void check_fields(void *, const struct_field_info *); void check_field(void *, const struct_field_info *); void check_item(void *, type_identity *); void check_pointer(void *, type_identity *); void check_static_array(void *, type_identity *, size_t); void check_container(void *, container_identity *); void check_vector(void *, container_identity *, type_identity *); void check_deque(void *, container_identity *, type_identity *); void check_bit_container(void *, container_identity *); void check_virtual(void *, virtual_identity *); class Scope { Checker *parent; public: Scope(Checker *, const std::string &); Scope(Checker *, size_t); Scope(const Scope &) = delete; ~Scope(); }; public: Checker(color_ostream &); bool check(); }; static command_result command(color_ostream & out, std::vector & parameters) { if (!parameters.empty()) { return CR_WRONG_USAGE; } CoreSuspender suspend; Checker checker(out); return checker.check() ? CR_OK : CR_FAILURE; } Checker::Checker(color_ostream & out) : out(out) { Core::getInstance().p->getMemRanges(mapped); } bool Checker::check() { seen_addr.clear(); ok = true; check_global(&df::global::_identity); return ok; } Checker::Scope::Scope(Checker *parent, const std::string & name) : parent(parent) { parent->path.push_back(name); } Checker::Scope::Scope(Checker *parent, size_t index) : Scope(parent, stl_sprintf("[%lu]", index)) { } Checker::Scope::~Scope() { parent->path.pop_back(); } #define FAIL(message) \ do \ { \ ok = false; \ out << COLOR_LIGHTRED << "sanity check failed (line " << __LINE__ << "): "; \ out << COLOR_RESET << (identity ? identity->getFullName() : "?") << " (accessed as "; \ for (auto & p : path) { out << p; } \ out << "): "; \ out << COLOR_YELLOW << message; \ out << COLOR_RESET << std::endl; \ } while (false) #define PTR_ADD(base, offset) (reinterpret_cast(reinterpret_cast((base)) + static_cast((offset)))) bool Checker::check_access(void *base, type_identity *identity) { if (!base) { // null pointer: can't access, but not an error return false; } for (auto & range : mapped) { if (!range.isInRange(base)) { continue; } if (!range.valid || !range.read) { FAIL("pointer to invalid memory range"); return false; } if (identity && !range.isInRange(PTR_ADD(base, identity->byte_size()))) { FAIL("pointer exceeds mapped memory bounds"); return false; } return true; } FAIL("pointer not in any mapped range"); return false; } void *Checker::safe_dereference(void *base, ptrdiff_t offset) { if (!base) { return nullptr; } type_identity *identity = nullptr; // void for (auto & range : mapped) { if (!range.isInRange(base)) { continue; } if (!range.valid || !range.read) { return nullptr; } if (!range.isInRange(PTR_ADD(base, offset))) { return nullptr; } return *reinterpret_cast(PTR_ADD(base, offset)); } return nullptr; } void Checker::check_global(global_identity *identity) { Scope scope(this, "df::global::"); for (auto field = identity->getFields(); field->mode != struct_field_info::END; field++) { if (!field->offset) { Scope scope_2(this, field->name); FAIL("global address is unknown"); continue; } auto base = reinterpret_cast(field->offset); if (!seen_addr.insert(base).second) { continue; } check_field(*base, field); } } void Checker::check_fields(void *base, const struct_field_info *fields) { for (auto field = fields; field->mode != struct_field_info::END; field++) { check_field(PTR_ADD(base, field->offset), field); } } void Checker::check_field(void *base, const struct_field_info *field) { Scope scope(this, field->name); switch (field->mode) { case struct_field_info::END: // can't happen - already checked break; case struct_field_info::PRIMITIVE: // don't need to check primitives break; case struct_field_info::STATIC_STRING: // TODO: check static strings? break; case struct_field_info::POINTER: check_pointer(base, field->type); break; case struct_field_info::STATIC_ARRAY: check_static_array(base, field->type, field->count); break; case struct_field_info::SUBSTRUCT: check_item(base, field->type); break; case struct_field_info::CONTAINER: check_container(base, static_cast(field->type)); break; case struct_field_info::STL_VECTOR_PTR: { df::stl_ptr_vector_identity temp_identity(field->type, field->eid); check_container(base, &temp_identity); } break; case struct_field_info::OBJ_METHOD: // ignore break; case struct_field_info::CLASS_METHOD: // ignore break; } } void Checker::check_item(void *base, type_identity *identity) { if (!identity) { // void return; } switch (identity->type()) { case IDTYPE_GLOBAL: // impossible break; case IDTYPE_FUNCTION: // ignore break; case IDTYPE_PRIMITIVE: // don't need to check primitives break; case IDTYPE_POINTER: check_pointer(base, static_cast(identity)->getTarget()); break; case IDTYPE_CONTAINER: case IDTYPE_BIT_CONTAINER: case IDTYPE_PTR_CONTAINER: case IDTYPE_STL_PTR_VECTOR: check_container(base, static_cast(identity)); break; case IDTYPE_BITFIELD: // TODO: check bitfields? break; case IDTYPE_ENUM: // TODO: check enums? break; case IDTYPE_STRUCT: { Scope scope(this, "."); check_fields(base, static_cast(identity)->getFields()); } break; case IDTYPE_CLASS: check_virtual(base, static_cast(identity)); break; case IDTYPE_BUFFER: { auto item_identity = static_cast(identity)->getItemType(); check_static_array(base, item_identity, identity->byte_size() / item_identity->byte_size()); } break; case IDTYPE_OPAQUE: // can't check opaque types break; } } void Checker::check_pointer(void *base, type_identity *identity) { if (!seen_addr.insert(base).second) { return; } if (!identity) { // void pointer return; } auto ptr = *reinterpret_cast(base); if (!check_access(ptr, identity)) { return; } check_item(ptr, identity); } void Checker::check_static_array(void *base, type_identity *identity, size_t count) { if (identity->isPrimitive()) { return; } size_t item_size = identity->byte_size(); for (size_t i = 0; i < count; i++) { Scope scope(this, i); check_item(PTR_ADD(base, item_size * i), identity); } } void Checker::check_container(void *base, container_identity *identity) { if (identity->type() == IDTYPE_STRUCT) { // DfLinkedList check_item(base, identity); return; } if (!seen_addr.insert(base).second) { return; } if (identity->type() == IDTYPE_BIT_CONTAINER) { check_bit_container(base, identity); return; } auto item_identity = identity->getItemType(); pointer_identity item_ptr_identity(item_identity); if (identity->type() == IDTYPE_PTR_CONTAINER || identity->type() == IDTYPE_STL_PTR_VECTOR) { item_identity = &item_ptr_identity; } else if (identity->type() != IDTYPE_CONTAINER) { // unexpected return; } auto void_name = identity->getFullName(nullptr); if (void_name == "vector" || void_name == "vector") { check_vector(base, identity, item_identity); } else if (void_name == "deque") { check_deque(base, identity, item_identity); } else { FAIL("TODO: " << void_name); UNEXPECTED; } } void Checker::check_vector(void *base, container_identity *identity, type_identity *item_identity) { struct vector_data { uintptr_t start; uintptr_t finish; uintptr_t end_of_storage; }; if (identity->byte_size() != sizeof(vector_data) || identity->getFullName().find("vector<") != 0) { UNEXPECTED; return; } vector_data vector = *reinterpret_cast(base); size_t item_size = item_identity->byte_size(); ptrdiff_t length = vector.finish - vector.start; ptrdiff_t capacity = vector.end_of_storage - vector.start; bool local_ok = true; if (vector.start > vector.finish) { local_ok = false; FAIL("vector length is negative (" << (length / ssize_t(item_size)) << ")"); } if (vector.start > vector.end_of_storage) { local_ok = false; FAIL("vector capacity is negative (" << (capacity / ssize_t(item_size)) << ")"); } else if (vector.finish > vector.end_of_storage) { local_ok = false; FAIL("vector capacity (" << (capacity / ssize_t(item_size)) << ") is less than its length (" << (length / ssize_t(item_size)) << ")"); } size_t ulength = size_t(length); size_t ucapacity = size_t(capacity); if (ulength % item_size != 0) { local_ok = false; FAIL("vector length is non-integer (" << (ulength / item_size) << " items plus " << (ulength % item_size) << " bytes)"); } if (ucapacity % item_size != 0) { local_ok = false; FAIL("vector capacity is non-integer (" << (ucapacity / item_size) << " items plus " << (ucapacity % item_size) << " bytes)"); } if (local_ok) { check_static_array(reinterpret_cast(vector.start), item_identity, ulength / item_size); } } void Checker::check_deque(void *base, container_identity *identity, type_identity *item_identity) { // TODO: check deque? } void Checker::check_bit_container(void *base, container_identity *identity) { if (identity->getFullName() == "BitArray<>") { // TODO: check DFHack::BitArray? return; } struct biterator_data { uintptr_t ptr; unsigned int offset; }; struct bvector_data { biterator_data start; biterator_data finish; uintptr_t end_of_storage; }; if (identity->byte_size() != sizeof(bvector_data) || identity->getFullName() != "vector") { UNEXPECTED; return; } // TODO: check vector? } void Checker::check_virtual(void *base, virtual_identity *identity) { if (!seen_addr.insert(base).second) { return; } if (!base) { // null pointer return; } auto ptr = reinterpret_cast(base); if (!identity->is_instance(ptr)) { FAIL("vtable is not a known subclass"); // write separately to avoid losing the previous line if this segfaults void *vtable = *reinterpret_cast(base); if (!vtable) { FAIL("(vtable is null)"); UNEXPECTED; return; } auto class_name = Core::getInstance().p->readClassName(vtable); FAIL("(subclass is " << class_name << ")"); return; } Scope scope(this, "."); check_fields(base, identity->getFields()); }