libresource/Visitor.cpp (456 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "utils/Visitor.h" #include "androidfw/TypeWrappers.h" #include <limits> #include <sstream> #include <string> namespace arsc { #define VERY_VERBOSE false #if VERY_VERBOSE #define LOGVV(...) ALOGV(__VA_ARGS__) #else #define LOGVV(...) \ do { \ } while (0) #endif // Resource table structs have been added to over time. Define a set of // backwards compatible minimum known sizes for the structs that could exist // if generated via old tools. constexpr size_t MIN_PACKAGE_SIZE = sizeof(android::ResTable_package) - sizeof(android::ResTable_package::typeIdOffset); // The minimum size required to read any version of ResTable_type. ResTable_type // has a ResTable_config, and ResTable_config has been augmented several times // (and itself will denote its size) thus the smallest conceviable config is // just a 4 byte int denoting that. constexpr size_t MIN_RES_TABLE_TYPE_SIZE = sizeof(android::ResTable_type) - sizeof(android::ResTable_config) + sizeof(android::ResTable_config::size); inline std::string dump_chunk(android::ResChunk_header* header) { std::stringstream ss; ss << "type=" << std::hex << dtohs(header->type); ss << " header_size=" << dtohs(header->headerSize); ss << " size=" << dtohl(header->size); return ss.str(); } template <typename T, size_t MinSize = sizeof(T)> inline static T* convert_chunk(android::ResChunk_header* chunk) { if (dtohs(chunk->headerSize) < MinSize) { return nullptr; } return reinterpret_cast<T*>(chunk); } inline static uint8_t* get_data(android::ResChunk_header* chunk) { return reinterpret_cast<uint8_t*>(chunk) + dtohs(chunk->headerSize); } inline static uint32_t get_data_len(android::ResChunk_header* chunk) { return dtohl(chunk->size) - dtohs(chunk->headerSize); } // Modeled after aapt2's ResChunkPullParser. Simple iteration over // ResChunk_header structs with validation of sizes in the header. class ResChunkPullParser { public: enum class Event { StartDocument, EndDocument, BadDocument, Chunk, }; // Returns false if the event is EndDocument or BadDocument. static bool IsGoodEvent(Event event) { return event != Event::EndDocument && event != Event::BadDocument; } ResChunkPullParser(void* data, size_t len) : m_event(Event::StartDocument), m_data(reinterpret_cast<android::ResChunk_header*>(data)), m_len(len), m_current_chunk(nullptr) {} Event event() { return m_event; } android::ResChunk_header* chunk() { return m_current_chunk; } // Move to the next android::ResChunk_header. Event Next() { if (!IsGoodEvent(m_event)) { return m_event; } if (m_event == Event::StartDocument) { m_current_chunk = m_data; } else { m_current_chunk = (android::ResChunk_header*)(((const char*)m_current_chunk) + dtohl(m_current_chunk->size)); } const std::ptrdiff_t diff = (const char*)m_current_chunk - (const char*)m_data; LOG_FATAL_IF(diff < 0, "diff is negative"); const size_t offset = static_cast<const size_t>(diff); if (offset == m_len) { m_current_chunk = nullptr; return (m_event = Event::EndDocument); } else if (offset + sizeof(android::ResChunk_header) > m_len) { ALOGE("chunk is past the end of the document"); m_current_chunk = nullptr; return (m_event = Event::BadDocument); } if (dtohs(m_current_chunk->headerSize) < sizeof(android::ResChunk_header)) { ALOGE("chunk has too small header"); m_current_chunk = nullptr; return (m_event = Event::BadDocument); } else if (dtohl(m_current_chunk->size) < dtohs(m_current_chunk->headerSize)) { ALOGE("chunk's total size is smaller than header %s", dump_chunk(m_current_chunk).c_str()); m_current_chunk = nullptr; return (m_event = Event::BadDocument); } else if (offset + dtohl(m_current_chunk->size) > m_len) { ALOGE("chunk's data extends past the end of the document %s", dump_chunk(m_current_chunk).c_str()); m_current_chunk = nullptr; return (m_event = Event::BadDocument); } return (m_event = Event::Chunk); } private: Event m_event; android::ResChunk_header* m_data; size_t m_len; android::ResChunk_header* m_current_chunk; }; void collect_spans(android::ResStringPool_span* ptr, std::vector<android::ResStringPool_span*>* out) { while (dtohl(*reinterpret_cast<const uint32_t*>(ptr)) != android::ResStringPool_span::END) { out->emplace_back(ptr); ptr++; } } bool ResourceTableVisitor::valid(const android::ResTable_package* package) { if (package == nullptr) { return false; } uint32_t package_id = dtohl(package->id); if (package_id > std::numeric_limits<uint8_t>::max()) { ALOGE("Package ID is too big: %x. Offset = %ld", package_id, get_file_offset(package)); return false; } return true; } bool ResourceTableVisitor::valid(const android::ResTable_typeSpec* type_spec) { if (type_spec == nullptr) { return false; } if (type_spec->id == 0) { ALOGE("ResTable_typeSpec has invalid id: %x. Offset = %ld", type_spec->id, get_file_offset(type_spec)); return false; } const size_t entry_count = dtohl(type_spec->entryCount); // Lower two bytes of a resource ID are used to denote entries. if (entry_count > std::numeric_limits<uint16_t>::max()) { ALOGE("ResTable_typeSpec has too many entries: %zu. Offset = %ld", entry_count, get_file_offset(type_spec)); return false; } return true; } bool ResourceTableVisitor::valid(const android::ResTable_type* type) { if (type == nullptr) { return false; } if (type->id == 0) { ALOGE("ResTable_type has invalid id. Offset = %ld", get_file_offset(type)); return false; } return true; } bool ResourceTableVisitor::visit(void* data, size_t len) { m_data = data; m_length = len; android::ResTable_header* table = convert_chunk<android::ResTable_header>((android::ResChunk_header*)data); if (!table) { ALOGE("corrupt ResTable_header chunk"); return false; } return visit_table(table); } bool ResourceTableVisitor::visit_table(android::ResTable_header* table) { LOGVV("visit ResTable_header, offset = %ld", get_file_offset(table)); android::ResStringPool_header* global_string_pool = nullptr; android::ResTable_package* package = nullptr; ResChunkPullParser parser(get_data(&table->header), get_data_len(&table->header)); while (ResChunkPullParser::IsGoodEvent(parser.Next())) { switch (dtohs(parser.chunk()->type)) { case android::RES_STRING_POOL_TYPE: if (global_string_pool == nullptr) { global_string_pool = convert_chunk<android::ResStringPool_header>(parser.chunk()); if (global_string_pool == nullptr) { ALOGE("bad string pool chunk"); return false; } if (!visit_global_strings(global_string_pool)) { return false; } } else { ALOGE("unexpected string pool in ResTable, ignoring"); } break; case android::RES_TABLE_PACKAGE_TYPE: package = convert_chunk<android::ResTable_package, MIN_PACKAGE_SIZE>( parser.chunk()); if (!valid(package)) { ALOGE("bad package chunk"); return false; } if (!visit_package(package)) { return false; } break; default: ALOGE("unexpected chunk type %x, ignoring", dtohs(parser.chunk()->type)); break; } } if (parser.event() == ResChunkPullParser::Event::BadDocument) { ALOGE("corrupt resource table"); return false; } return true; } bool ResourceTableVisitor::visit_global_strings( android::ResStringPool_header* pool) { LOGVV("visit global string pool, offset = %ld", get_file_offset(pool)); // Callers expected to override if inspecting strings/styles are required. return true; } bool ResourceTableVisitor::visit_package(android::ResTable_package* package) { LOGVV("visit ResTable_package, offset = %ld", get_file_offset(package)); uint32_t package_id = dtohl(package->id); android::ResStringPool_header* type_strings = nullptr; android::ResStringPool_header* key_strings = nullptr; android::ResTable_typeSpec* type_spec = nullptr; android::ResTable_type* type = nullptr; ResChunkPullParser parser(get_data(&package->header), get_data_len(&package->header)); while (ResChunkPullParser::IsGoodEvent(parser.Next())) { switch (dtohs(parser.chunk()->type)) { case android::RES_STRING_POOL_TYPE: if (type_strings == nullptr) { type_strings = convert_chunk<android::ResStringPool_header>(parser.chunk()); if (type_strings == nullptr) { ALOGE("bad string pool chunk"); return false; } if (!visit_type_strings(package, type_strings)) { return false; } } else if (key_strings == nullptr) { key_strings = convert_chunk<android::ResStringPool_header>(parser.chunk()); if (key_strings == nullptr) { ALOGE("bad string pool chunk"); return false; } if (!visit_key_strings(package, key_strings)) { return false; } } else { ALOGE("unexpected string pool in package %x, ignoring", package_id); } break; case android::RES_TABLE_TYPE_SPEC_TYPE: type_spec = convert_chunk<android::ResTable_typeSpec>(parser.chunk()); if (!valid(type_spec)) { ALOGE("bad type spec chunk"); return false; } if (!visit_type_spec(package, type_spec)) { return false; } break; case android::RES_TABLE_TYPE_TYPE: type = convert_chunk<android::ResTable_type, MIN_RES_TABLE_TYPE_SIZE>( parser.chunk()); if (!valid(type)) { ALOGE("bad type chunk"); return false; } if (!visit_type(package, type_spec, type)) { return false; } break; default: auto unknown = parser.chunk(); ALOGE("unexpected chunk type %x in package", dtohs(unknown->type)); if (!visit_unknown_chunk(package, unknown)) { return false; } break; } } if (parser.event() == ResChunkPullParser::Event::BadDocument) { ALOGE("corrupt package %x", package_id); return false; } return true; } bool ResourceTableVisitor::visit_unknown_chunk( android::ResTable_package* package, android::ResChunk_header* header) { LOGVV("visit unknown chunk, offset = %ld", get_file_offset(header)); return true; } bool ResourceTableVisitor::visit_type_strings( android::ResTable_package* package, android::ResStringPool_header* pool) { LOGVV("visit type strings, offset = %ld", get_file_offset(pool)); // Callers expected to override if inspecting strings/styles are required. return true; } bool ResourceTableVisitor::visit_key_strings( android::ResTable_package* package, android::ResStringPool_header* pool) { LOGVV("visit key strings, offset = %ld", get_file_offset(pool)); // Callers expected to override if inspecting strings/styles are required. return true; } bool ResourceTableVisitor::visit_type_spec( android::ResTable_package* package, android::ResTable_typeSpec* type_spec) { LOGVV("visit ResTable_typeSpec ID %x, offset = %ld", type_spec->id, get_file_offset(type_spec)); return true; } bool ResourceTableVisitor::visit_type(android::ResTable_package* package, android::ResTable_typeSpec* type_spec, android::ResTable_type* type) { LOGVV("visit ResTable_type (of ResTable_typeSpec ID %x), offset = %ld", type_spec->id, get_file_offset(type)); android::TypeVariant tv(type); for (auto it = tv.beginEntries(); it != tv.endEntries(); ++it) { android::ResTable_entry* entry = const_cast<android::ResTable_entry*>(*it); if (!entry) { continue; } if (dtohs(entry->flags) & android::ResTable_entry::FLAG_COMPLEX) { auto map_entry = static_cast<android::ResTable_map_entry*>(entry); auto entry_count = dtohl(map_entry->count); if (!visit_map_entry(package, type_spec, type, map_entry)) { return false; } for (size_t i = 0; i < entry_count; i++) { android::ResTable_map* value = (android::ResTable_map*)((uint8_t*)entry + dtohl(entry->size) + i * sizeof(android::ResTable_map)); if (!visit_map_value(package, type_spec, type, map_entry, value)) { return false; } } } else { android::Res_value* value = (android::Res_value*)((uint8_t*)entry + dtohl(entry->size)); if (!visit_entry(package, type_spec, type, entry, value)) { return false; } } } return true; } bool ResourceTableVisitor::visit_entry(android::ResTable_package* package, android::ResTable_typeSpec* type_spec, android::ResTable_type* type, android::ResTable_entry* entry, android::Res_value* value) { LOGVV("visit entry offset = %ld, value offset = %ld", get_file_offset(entry), get_file_offset(value)); return true; } bool ResourceTableVisitor::visit_map_entry( android::ResTable_package* package, android::ResTable_typeSpec* type_spec, android::ResTable_type* type, android::ResTable_map_entry* entry) { LOGVV("visit map entry offset = %ld", get_file_offset(entry)); return true; } bool ResourceTableVisitor::visit_map_value( android::ResTable_package* package, android::ResTable_typeSpec* type_spec, android::ResTable_type* type, android::ResTable_map_entry* entry, android::ResTable_map* value) { LOGVV("visit map value offset = %ld", get_file_offset(value)); return true; } // BEGIN StringPoolRefVisitor bool StringPoolRefVisitor::visit_key_strings_ref( android::ResTable_package* package, android::ResStringPool_ref* ref) { LOGVV("visit key ResStringPool_ref, offset = %ld", get_file_offset(ref)); // Subclasses meant to override. return true; } bool StringPoolRefVisitor::visit_global_strings_ref(android::Res_value* value) { LOGVV("visit string Res_value, offset = %ld", get_file_offset(value)); // Subclasses meant to override. return true; } bool StringPoolRefVisitor::visit_global_strings_ref( android::ResStringPool_ref* value) { LOGVV("visit global ResStringPool_ref, offset = %ld", get_file_offset(value)); // Subclasses meant to override. return true; } bool StringPoolRefVisitor::visit_global_strings( android::ResStringPool_header* pool) { LOGVV("visit global string pool, offset = %ld", get_file_offset(pool)); // Iterate ResStringPool_span items and their ResStringPool_refs (which point // to this pool). auto style_count = dtohl(pool->styleCount); auto styles_start = dtohl(pool->stylesStart); if (style_count > 0 && styles_start > 0) { auto style_idx = (uint32_t*)(((uint8_t*)pool) + dtohs(pool->header.headerSize) + dtohl(pool->stringCount) * sizeof(uint32_t)); for (size_t i = 0; i < style_count; i++) { auto span = (android::ResStringPool_span*)(((uint8_t*)pool) + styles_start + *style_idx); while (dtohl(span->name.index) != android::ResStringPool_span::END) { LOGVV("visit ResStringPool_span, offset = %ld", get_file_offset(span)); if (!visit_global_strings_ref(&span->name)) { return false; } span++; } style_idx++; } } return true; } bool StringPoolRefVisitor::visit_entry(android::ResTable_package* package, android::ResTable_typeSpec* type_spec, android::ResTable_type* type, android::ResTable_entry* entry, android::Res_value* value) { LOGVV("visit entry offset = %ld, value offset = %ld", get_file_offset(entry), get_file_offset(value)); if (!visit_key_strings_ref(package, &entry->key)) { return false; } if (value->dataType == android::Res_value::TYPE_STRING) { if (!visit_global_strings_ref(value)) { return false; } } return true; } bool StringPoolRefVisitor::visit_map_entry( android::ResTable_package* package, android::ResTable_typeSpec* type_spec, android::ResTable_type* type, android::ResTable_map_entry* entry) { LOGVV("visit map entry offset = %ld", get_file_offset(entry)); if (!visit_key_strings_ref(package, &entry->key)) { return false; } return true; } bool StringPoolRefVisitor::visit_map_value( android::ResTable_package* package, android::ResTable_typeSpec* type_spec, android::ResTable_type* type, android::ResTable_map_entry* entry, android::ResTable_map* value) { LOGVV("visit map value offset = %ld", get_file_offset(value)); if (value->value.dataType == android::Res_value::TYPE_STRING) { if (!visit_global_strings_ref(&value->value)) { return false; } } return true; } } // namespace arsc