opt/app_module_usage/AppModuleUsage.cpp (381 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 "AppModuleUsage.h" #include <algorithm> #include <boost/none.hpp> #include <boost/none_t.hpp> #include <boost/optional/optional.hpp> #include <sstream> #include <string> #include "ConfigFiles.h" #include "CppUtil.h" #include "DexAnnotation.h" #include "DexClass.h" #include "DexStore.h" #include "DexUtil.h" #include "IRInstruction.h" #include "PassManager.h" #include "Show.h" #include "Trace.h" #include "Walkers.h" namespace { constexpr const char* APP_MODULE_USAGE_OUTPUT_FILENAME = "redex-app-module-usage.txt"; constexpr const char* APP_MODULE_COUNT_OUTPUT_FILENAME = "redex-app-module-count.csv"; constexpr const char* USES_AM_ANNO_VIOLATIONS_FILENAME = "redex-app-module-annotation-violations.csv"; void write_violations_to_file(const app_module_usage::Violations& violations, const std::string& path) { std::ofstream ofs(path, std::ofstream::out | std::ofstream::trunc); for (const auto& [entrypoint, modules] : violations) { ofs << entrypoint; for (const auto& module : modules) { ofs << ", " << module; } ofs << std::endl; } ofs.close(); } void write_method_module_usages_to_file( const app_module_usage::MethodStoresReferenced& method_store_refs, const ConcurrentMap<DexType*, DexStore*>& type_store_map, const std::string& path) { TRACE(APP_MOD_USE, 4, "Outputting module usages at %s", path.c_str()); std::ofstream ofs(path, std::ofstream::out | std::ofstream::trunc); for (const auto& [method, store_refs] : method_store_refs) { if (store_refs.empty()) { continue; } auto it = type_store_map.find(method->get_class()); if (it != type_store_map.end()) { ofs << "(" << it->second->get_name() << ") "; } ofs << show(method); for (const auto& [store, refl_only] : store_refs) { ofs << ", "; if (refl_only) { ofs << "(reflection) "; } ofs << store->get_name(); } ofs << std::endl; } ofs.close(); } void write_app_module_use_stats( const app_module_usage::MethodStoresReferenced& method_store_refs, const std::string& path) { struct ModuleUseCount { unsigned int direct_count{0}; unsigned int reflective_count{0}; }; std::unordered_map<DexStore*, ModuleUseCount> counts; for (const auto& [method, store_refs] : method_store_refs) { for (const auto& [store, refl_only] : store_refs) { auto& count = counts[store]; if (refl_only) { count.reflective_count++; } else { count.direct_count++; } } } TRACE(APP_MOD_USE, 4, "Outputting module use count at %s", path.c_str()); std::ofstream ofs(path, std::ofstream::out | std::ofstream::trunc); for (const auto& [module, use_count] : counts) { ofs << module->get_name() << ", " << use_count.direct_count << ", " << use_count.reflective_count << std::endl; } ofs.close(); } } // namespace void AppModuleUsagePass::run_pass(DexStoresVector& stores, ConfigFiles& conf, PassManager& mgr) { if (!m_uses_app_module_annotation) { fprintf( stderr, "WARNING: Annotation class not found. Skipping AppModuleUsagePass."); return; } for (auto& store : stores) { Scope scope = build_class_scope(store.get_dexen()); walk::classes(scope, [&](DexClass* cls) { m_type_store_map.emplace(cls->get_type(), &store); }); } load_preexisting_violations(stores); const auto& full_scope = build_class_scope(stores); // TODO: Remove classes from scope that are exempt from checking. auto method_store_refs = analyze_method_xstore_references(full_scope); auto field_store_refs = analyze_field_xstore_references(full_scope); if (m_output_module_use) { auto module_use_path = conf.metafile(APP_MODULE_USAGE_OUTPUT_FILENAME); write_method_module_usages_to_file(method_store_refs, m_type_store_map, module_use_path); auto module_count_path = conf.metafile(APP_MODULE_COUNT_OUTPUT_FILENAME); write_app_module_use_stats(method_store_refs, module_count_path); } app_module_usage::Violations violations; auto num_violations = gather_violations(method_store_refs, field_store_refs, violations); auto report_path = conf.metafile(USES_AM_ANNO_VIOLATIONS_FILENAME); write_violations_to_file(violations, report_path); mgr.set_metric("num_methods_access_app_module", method_store_refs.size()); mgr.set_metric("num_violations", num_violations); if (m_crash_with_violations) { always_assert_log(num_violations == 0, "There are @UsesAppModule violations. See %s \n", report_path.c_str()); } } void AppModuleUsagePass::load_preexisting_violations(DexStoresVector& stores) { if (m_preexisting_violations_filepath.empty()) { TRACE(APP_MOD_USE, 1, "No preexisting violations provided."); return; } std::ifstream ifs(m_preexisting_violations_filepath); if (!ifs.is_open()) { fprintf(stderr, "WARNING: Could not open preexisting violations at \"%s\"\n", m_preexisting_violations_filepath.c_str()); return; } // To quickly look up wich DexStore ("module") a name represents std::unordered_map<std::string, DexStore*> names_to_stores; for (auto& store : stores) { names_to_stores.emplace(store.get_name(), &store); } std::string line; while (getline(ifs, line)) { boost::optional<std::string> entrypoint; for (std::string_view csv_component : split_string(line, ",")) { csv_component = trim_whitespaces(csv_component); if (!entrypoint) { entrypoint = std::string(csv_component); } else { std::string store_name(csv_component); auto it = names_to_stores.find(store_name); if (it != names_to_stores.end()) { m_preexisting_violations[*entrypoint].emplace(it->second); } else { TRACE(APP_MOD_USE, 2, "Module %s from preexisting violations no longer exists.", store_name.c_str()); } } } } ifs.close(); } ConcurrentMap<DexMethod*, app_module_usage::StoresReferenced> AppModuleUsagePass::analyze_method_xstore_references(const Scope& scope) { auto get_type_ref_for_insn = [](IRInstruction* insn) -> DexType* { if (insn->has_method()) { return insn->get_method()->get_class(); } else if (insn->has_field()) { return insn->get_field()->get_class(); } else if (insn->has_type()) { return insn->get_type(); } return nullptr; }; ConcurrentMap<DexMethod*, app_module_usage::StoresReferenced> method_store_refs; reflection::MetadataCache refl_metadata_cache; walk::parallel::code(scope, [&](DexMethod* method, IRCode& code) { const auto* method_store = m_type_store_map.at(method->get_class()); std::unique_ptr<reflection::ReflectionAnalysis> analysis = std::make_unique<reflection::ReflectionAnalysis>( /* dex_method */ method, /* context (interprocedural only) */ nullptr, /* summary_query_fn (interprocedural only) */ nullptr, /* metadata_cache */ &refl_metadata_cache); auto get_reflective_type_ref_for_insn = [&analysis](IRInstruction* insn) -> DexType* { if (!opcode::is_an_invoke(insn->opcode())) { // If an object type is from reflection it will be in the // RESULT_REGISTER for some instruction. const auto& o = analysis->get_abstract_object(RESULT_REGISTER, insn); if (o && (o.get().obj_kind != reflection::CLASS || (analysis->get_class_source(RESULT_REGISTER, insn).has_value() && analysis->get_class_source(RESULT_REGISTER, insn).get() == reflection::REFLECTION))) { // If the obj is a CLASS then it must have a class source of // REFLECTION return o.get().dex_type; } } return nullptr; }; auto get_store_if_access_is_xstore = [&](DexType* to) -> DexStore* { // The type may be external. auto it = m_type_store_map.find(to); if (it == m_type_store_map.end()) { return nullptr; } auto store = it->second; if (!store->is_root_store() && store != method_store) { return store; } return nullptr; }; app_module_usage::StoresReferenced stores_referenced; for (const auto& mie : InstructionIterable(code)) { IRInstruction* insn = mie.insn; auto maybe_type_ref = get_type_ref_for_insn(insn); if (maybe_type_ref) { auto maybe_store = get_store_if_access_is_xstore(maybe_type_ref); if (maybe_store) { // Creates the value if it doesn't exist. stores_referenced[maybe_store] = false /* used_only_reflectively */; } } auto maybe_refl_type_ref = get_reflective_type_ref_for_insn(insn); if (maybe_refl_type_ref) { auto maybe_store = get_store_if_access_is_xstore(maybe_refl_type_ref); if (maybe_store) { if (!stores_referenced.count(maybe_store)) { stores_referenced.emplace(maybe_store, true); } // If the entry already exists, doesn't matter if we add it because // |= true is always no-op. } } } if (!stores_referenced.empty()) { method_store_refs.emplace(method, std::move(stores_referenced)); } }); return method_store_refs; } ConcurrentMap<DexField*, DexStore*> AppModuleUsagePass::analyze_field_xstore_references(const Scope& scope) { ConcurrentMap<DexField*, DexStore*> ret; walk::parallel::fields(scope, [&](DexField* field) { auto field_store = m_type_store_map.at(field->get_class()); auto field_type = field->get_type(); auto it = m_type_store_map.find(field_type); if (it == m_type_store_map.end()) { // Type may be external. return; } auto store = it->second; if (!store->is_root_store() && store != field_store) { ret.emplace(field, store); } }); return ret; } unsigned AppModuleUsagePass::gather_violations( const app_module_usage::MethodStoresReferenced& method_store_refs, const ConcurrentMap<DexField*, DexStore*>& field_store_refs, app_module_usage::Violations& violations) const { unsigned n_violations{0u}; for (const auto& [method, stores_referenced] : method_store_refs) { auto method_name = show(method); for (const auto& [store, only_reflection] : stores_referenced) { if (access_granted_by_annotation(method, store)) { continue; } if (access_excused_due_to_preexisting(show(method), store)) { continue; } TRACE( APP_MOD_USE, 0, "%s (from module \"%s\") uses app module \"%s\" without annotation\n", method_name.c_str(), m_type_store_map.at(method->get_class())->get_name().c_str(), store->get_name().c_str()); violations[method_name].emplace(store->get_name()); n_violations++; } } for (const auto& [field, store] : field_store_refs) { auto field_name = show(field); if (access_granted_by_annotation(field, store)) { continue; } if (access_excused_due_to_preexisting(show(field), store)) { continue; } TRACE(APP_MOD_USE, 0, "%s (from module \"%s\") uses app module \"%s\" without annotation\n", field_name.c_str(), m_type_store_map.at(field->get_class())->get_name().c_str(), store->get_name().c_str()); violations[field_name].emplace(store->get_name()); n_violations++; } return n_violations; } template <typename T> std::unordered_set<std::string> AppModuleUsagePass::get_modules_used( T* entrypoint, DexType* annotation_type) { std::unordered_set<std::string> modules = {}; auto anno_set = entrypoint->get_anno_set(); if (anno_set) { for (const auto& annotation : anno_set->get_annotations()) { if (annotation->type() == annotation_type) { for (const DexAnnotationElement& anno_elem : annotation->anno_elems()) { always_assert(anno_elem.string->str() == "value"); always_assert(anno_elem.encoded_value->evtype() == DEVT_ARRAY); const auto* array = static_cast<const DexEncodedValueArray*>( anno_elem.encoded_value.get()); for (const auto& value : *(array->evalues())) { always_assert(value->evtype() == DEVT_STRING); modules.emplace( ((DexEncodedValueString*)value.get())->string()->str()); } } break; } } } return modules; } template std::unordered_set<std::string> AppModuleUsagePass::get_modules_used<DexMethod>(DexMethod*, DexType*); template std::unordered_set<std::string> AppModuleUsagePass::get_modules_used<DexField>(DexField*, DexType*); template std::unordered_set<std::string> AppModuleUsagePass::get_modules_used<DexClass>(DexClass*, DexType*); bool AppModuleUsagePass::access_excused_due_to_preexisting( const std::string& entrypoint_name, DexStore* store_used) const { auto it = m_preexisting_violations.find(entrypoint_name); if (it != m_preexisting_violations.end()) { return it->second.count(store_used); } return false; } bool AppModuleUsagePass::access_granted_by_annotation(DexMethod* method, DexStore* target) const { if (get_modules_used(method, m_uses_app_module_annotation) .count(target->get_name())) { return true; } return access_granted_by_annotation(type_class(method->get_class()), target); } bool AppModuleUsagePass::access_granted_by_annotation(DexField* field, DexStore* target) const { if (get_modules_used(field, m_uses_app_module_annotation) .count(target->get_name())) { return true; } return access_granted_by_annotation(type_class(field->get_class()), target); } bool AppModuleUsagePass::access_granted_by_annotation(DexClass* cls, DexStore* target) const { if (!cls) { return false; } if (get_modules_used(cls, m_uses_app_module_annotation) .count(target->get_name())) { return true; } // Check outer class. std::string_view cls_name = cls->str(); auto dollar_sign_idx = cls_name.rfind("$"); while (dollar_sign_idx != std::string_view::npos) { cls_name.remove_suffix(cls_name.size() - dollar_sign_idx); std::string new_class_name = std::string(cls_name) + ";"; auto* ty = DexType::get_type(new_class_name); DexClass* outer_class = ty ? type_class(ty) : nullptr; if (outer_class) { return access_granted_by_annotation(outer_class, target); } dollar_sign_idx = cls_name.rfind("$"); } return false; } static AppModuleUsagePass s_pass;