From 70544ceb0a9b3a17338dcc837fb9278221dfe199 Mon Sep 17 00:00:00 2001 From: Nikolai Tillmann Date: Tue, 16 May 2023 12:33:04 -0700 Subject: [PATCH] add "relaxed_keep_class_members " option Summary: Both *RMU and DelInit remove "unreachable" fields and methods. (RMU also removes unreachable classes, but that's not important for this story.) They differ in what they consider "unreachable". RMU somewhat straightforwardly visits everything that reachable from some roots, and conditionally holds on root fields and methods in reachable classes. (There's a little quirk where it keeps all volatile ifields, some of which DelInit will actually happily remove, but that's a minor detail.) DelInit is... less intuitive. On one side, it's more aggressive when it comes to discarding root fields and methods, as it only considers some ways in which their class is referenced --- I think the intuition is that it only consider those kind of references which are "actually" (= historically observed to be) used to access fields and methods via reflection or from native code. On the other side, it's more conservative than RMU, as it also considers the `marked_by_string` flag in addition to the `root` flag, and it may keep extra constructors for undisclosed reasons, and it keeps all ifields and vmethods of instantiable classes. However, RMU might cut some or all those before or after DelInit. This diffs introduces a new option to RMU that relaxes what members need to be kept, tightening what's considered reachable in RMU, in a way that exactly cuts out what DelInit considers unreachable. We effectively get the intersection between both schemes, but combined in a single reachability visitor, which then identifies a bit more unreachable objects in a single run. This is a behavior preserving diff, as the new option is disabled. Reviewed By: agampe Differential Revision: D45628244 fbshipit-source-id: 55a2c24878370cd675a3a4eaed022d4d518e0163 --- libredex/Reachability.cpp | 389 +++++++++++++++--- libredex/Reachability.h | 85 +++- opt/reachable-natives/ReachableNatives.cpp | 16 +- opt/remove-unreachable/RemoveUnreachable.cpp | 6 +- opt/remove-unreachable/RemoveUnreachable.h | 4 + .../TypeAnalysisAwareRemoveUnreachable.cpp | 30 +- .../TypeAnalysisAwareRemoveUnreachable.h | 1 + test/integ/ReachabilityTest.cpp | 4 +- 8 files changed, 441 insertions(+), 94 deletions(-) diff --git a/libredex/Reachability.cpp b/libredex/Reachability.cpp index b5580b92ae..a1cb106cad 100644 --- a/libredex/Reachability.cpp +++ b/libredex/Reachability.cpp @@ -32,6 +32,10 @@ namespace { static ReachableObject SEED_SINGLETON{}; +bool consider_dynamically_referenced(const DexClass* cls) { + return !root(cls) && !is_interface(cls) && !is_annotation(cls); +} + } // namespace namespace reachability { @@ -72,23 +76,36 @@ void RootSetMarker::mark_all_as_seed(const Scope& scope) { for (auto const& f : cls->get_ifields()) { TRACE(REACH, 3, "Visiting seed: %s", SHOW(f)); - push_seed(f); + push_seed(f, Condition::ClassRetained); } for (auto const& f : cls->get_sfields()) { TRACE(REACH, 3, "Visiting seed: %s", SHOW(f)); - push_seed(f); + push_seed(f, Condition::ClassRetained); } for (auto const& m : cls->get_dmethods()) { TRACE(REACH, 3, "Visiting seed: %s", SHOW(m)); - push_seed(m); + push_seed(m, Condition::ClassRetained); } for (auto const& m : cls->get_vmethods()) { TRACE(REACH, 3, "Visiting seed: %s (root)", SHOW(m)); - push_seed(m); + push_seed(m, Condition::ClassRetained); } }); } +bool RootSetMarker::is_rootlike_clinit(const DexMethod* m) { + return method::is_clinit(m) && + (!m->get_code() || !method::is_trivial_clinit(*m->get_code())); +} + +bool RootSetMarker::is_rootlike_init(const DexMethod* m) const { + // We keep the parameterless constructor, in case it's constructed via + // .class or Class.forName() + // if m_remove_no_argument_constructors, make an exception. This is only + // used for testing + return !m_remove_no_argument_constructors && method::is_argless_init(m); +} + /* * Initializes the root set by marking and pushing nodes onto the work queue. * Also conditionally marks class member seeds. @@ -99,28 +116,55 @@ void RootSetMarker::mark(const Scope& scope) { TRACE(REACH, 3, "Visiting seed: %s", SHOW(cls)); push_seed(cls); } + // Applying the same exclusions as DelInitPass + auto relaxed = + m_relaxed_keep_class_members && consider_dynamically_referenced(cls); + // push_seed for an ifield or vmethod + auto push_iv_seed = [&](auto* m) { + if (relaxed) { + push_seed(m, Condition::ClassDynamicallyReferenced); + push_seed(m, Condition::ClassInstantiable); + } else { + push_seed(m, Condition::ClassRetained); + } + }; + // push_seed for a dmethod + auto push_d_seed = [&](auto* m) { + push_seed(m, (m->get_code() && !method::is_clinit(m) && relaxed) + ? Condition::ClassDynamicallyReferenced + : Condition::ClassRetained); + }; for (auto const& f : cls->get_ifields()) { - if (root(f) || is_volatile(f)) { + if (root(f)) { TRACE(REACH, 3, "Visiting seed: %s", SHOW(f)); - push_seed(f); + push_iv_seed(f); + } else if (is_volatile(f) && !m_relaxed_keep_class_members) { + TRACE(REACH, 3, "Visiting seed (volatile): %s", SHOW(f)); + push_iv_seed(f); } } for (auto const& f : cls->get_sfields()) { if (root(f)) { TRACE(REACH, 3, "Visiting seed: %s", SHOW(f)); - push_seed(f); + push_seed(f, Condition::ClassRetained); } } for (auto const& m : cls->get_dmethods()) { - if (root(m)) { + if (is_rootlike_clinit(m)) { + TRACE(REACH, 3, "Visiting seed (root-like clinit): %s", SHOW(m)); + push_d_seed(m); + } else if (is_rootlike_init(m)) { + TRACE(REACH, 3, "Visiting seed (root-like init): %s", SHOW(m)); + push_d_seed(m); + } else if (root(m)) { TRACE(REACH, 3, "Visiting seed: %s", SHOW(m)); - push_seed(m); + push_d_seed(m); } } for (auto const& m : cls->get_vmethods()) { if (root(m)) { TRACE(REACH, 3, "Visiting seed: %s (root)", SHOW(m)); - push_seed(m); + push_iv_seed(m); } } }); @@ -151,25 +195,26 @@ void RootSetMarker::mark_with_exclusions( for (const auto* f : cls->get_ifields()) { if ((root(f) || is_volatile(f)) && !excluded(f)) { TRACE(REACH, 3, "Visiting seed: %s", SHOW(f)); - push_seed(f); + push_seed(f, Condition::ClassRetained); } } for (const auto* f : cls->get_sfields()) { if (root(f) && !excluded(f)) { TRACE(REACH, 3, "Visiting seed: %s", SHOW(f)); - push_seed(f); + push_seed(f, Condition::ClassRetained); } } for (const auto* m : cls->get_dmethods()) { - if (root(m) && !excluded(m)) { + if ((root(m) || is_rootlike_clinit(m) || is_rootlike_init(m)) && + !excluded(m)) { TRACE(REACH, 3, "Visiting seed: %s", SHOW(m)); - push_seed(m); + push_seed(m, Condition::ClassRetained); } } for (const auto* m : cls->get_vmethods()) { if (root(m) && !excluded(m)) { TRACE(REACH, 3, "Visiting seed: %s (root)", SHOW(m)); - push_seed(m); + push_seed(m, Condition::ClassRetained); } } }); @@ -184,19 +229,38 @@ void RootSetMarker::push_seed(const DexClass* cls) { m_root_set->emplace(cls); } -void RootSetMarker::push_seed(const DexField* field) { +void RootSetMarker::push_seed(const DexField* field, Condition condition) { if (!field) return; - m_cond_marked->fields.insert(field); -} - -void RootSetMarker::push_seed(const DexMethod* method) { - if (!method) return; - m_cond_marked->methods.insert(method); + switch (condition) { + case Condition::ClassRetained: + m_cond_marked->if_class_retained.fields.insert(field); + break; + case Condition::ClassDynamicallyReferenced: + m_cond_marked->if_class_dynamically_referenced.fields.insert(field); + break; + case Condition::ClassInstantiable: + m_cond_marked->if_class_instantiable.fields.insert(field); + break; + default: + not_reached(); + } } -void RootSetMarker::push_seed_if_instantiable(const DexMethod* method) { +void RootSetMarker::push_seed(const DexMethod* method, Condition condition) { if (!method) return; - m_cond_marked->methods_if_instantiable.insert(method); + switch (condition) { + case Condition::ClassRetained: + m_cond_marked->if_class_retained.methods.insert(method); + break; + case Condition::ClassDynamicallyReferenced: + m_cond_marked->if_class_dynamically_referenced.methods.insert(method); + break; + case Condition::ClassInstantiable: + m_cond_marked->if_class_instantiable.methods.insert(method); + break; + default: + not_reached(); + } } template @@ -226,7 +290,7 @@ void RootSetMarker::mark_external_method_overriders() { if (!overriding->is_external()) { TRACE(REACH, 3, "Visiting seed: %s (implements %s)", SHOW(overriding), SHOW(method)); - push_seed_if_instantiable(overriding); + push_seed(overriding, Condition::ClassInstantiable); } } } @@ -322,13 +386,14 @@ void TransitiveClosureMarker::push(const DexMethodRef* parent, this->template push(parent, method); } -void TransitiveClosureMarker::push_if_instantiable(const DexMethod* method) { +void TransitiveClosureMarker::push_if_class_instantiable( + const DexMethod* method) { if (!method || m_reachable_objects->marked(method)) return; TRACE(REACH, 4, "Conditionally marking method if declaring class is instantiable: %s", SHOW(method)); auto clazz = type_class(method->get_class()); - m_cond_marked->methods_if_instantiable.insert(method); + m_cond_marked->if_class_instantiable.methods.insert(method); // If :clazz is already known to be instantiable, then we cannot count on // instantiable(DexClass*) to have moved the // conditionally-if-instantiable-marked methods into the actually-marked ones @@ -341,26 +406,195 @@ void TransitiveClosureMarker::push_if_instantiable(const DexMethod* method) { } } +void TransitiveClosureMarker::push_if_class_instantiable( + const DexField* field) { + if (!field || m_reachable_objects->marked(field)) return; + TRACE(REACH, 4, + "Conditionally marking field if declaring class is instantiable: %s", + SHOW(field)); + auto clazz = type_class(field->get_class()); + m_cond_marked->if_class_instantiable.fields.insert(field); + if (m_instantiable_types->count(clazz)) { + push(clazz, field); + } +} + +void TransitiveClosureMarker::push_if_class_retained(const DexMethod* method) { + if (!method || m_reachable_objects->marked(method)) return; + TRACE(REACH, 4, + "Conditionally marking method if declaring class is instantiable: %s", + SHOW(method)); + auto clazz = type_class(method->get_class()); + m_cond_marked->if_class_retained.methods.insert(method); + if (m_reachable_objects->marked(clazz)) { + push(clazz, method); + } +} + +void TransitiveClosureMarker::push_if_class_retained(const DexField* field) { + if (!field || m_reachable_objects->marked(field)) return; + TRACE(REACH, 4, + "Conditionally marking field if declaring class is instantiable: %s", + SHOW(field)); + auto clazz = type_class(field->get_class()); + m_cond_marked->if_class_retained.fields.insert(field); + if (m_reachable_objects->marked(clazz)) { + push(clazz, field); + } +} + +// Adapted from DelInitPass +namespace relaxed_keep_class_members_impl { + +void process_signature_anno(const DexString* dstring, References* references) { + const char* cstr = dstring->c_str(); + size_t len = strlen(cstr); + if (len < 3) return; + if (cstr[0] != 'L') return; + if (cstr[len - 1] == ';') { + auto cls = type_class(DexType::get_type(dstring)); + if (cls) { + references->classes_dynamically_referenced.insert(cls); + } + return; + } + std::string buf(cstr); + buf += ';'; + auto cls = type_class(DexType::get_type(buf)); + if (cls) { + references->classes_dynamically_referenced.insert(cls); + } +} + +void gather_dynamic_references_impl(const DexAnnotation* anno, + References* references) { + static DexType* dalviksig = + DexType::get_type("Ldalvik/annotation/Signature;"); + // Signature annotations contain strings that Jackson uses + // to construct the underlying types. + if (anno->type() == dalviksig) { + auto& elems = anno->anno_elems(); + for (auto const& elem : elems) { + auto& ev = elem.encoded_value; + if (ev->evtype() != DEVT_ARRAY) continue; + auto arrayev = static_cast(ev.get()); + auto const& evs = arrayev->evalues(); + for (auto& strev : *evs) { + if (strev->evtype() != DEVT_STRING) continue; + auto stringev = static_cast(strev.get()); + process_signature_anno(stringev->string(), references); + } + } + return; + } + // Class literals in annotations. + // Example: + // @JsonDeserialize(using=MyJsonDeserializer.class) + if (anno->runtime_visible()) { + auto& elems = anno->anno_elems(); + std::vector ltype; + for (auto const& dae : elems) { + auto& evalue = dae.encoded_value; + evalue->gather_types(ltype); + } + for (auto dextype : ltype) { + auto cls = type_class(dextype); + if (cls) { + references->classes_dynamically_referenced.insert(cls); + } + } + } +} + template -static References generic_gather(T t) { +void gather_dynamic_references(T item, References* references) { + auto* anno_set = item->get_anno_set(); + if (anno_set) { + for (auto& anno : anno_set->get_annotations()) { + gather_dynamic_references_impl(anno.get(), references); + } + } +} + +template <> +void gather_dynamic_references(const DexAnnotation* item, + References* references) { + gather_dynamic_references_impl(item, references); +} + +// Note: this method will return nullptr if the dotname refers to an unknown +// type. +DexType* get_dextype_from_dotname(std::string_view dotname) { + std::string buf; + buf.reserve(dotname.size() + 2); + buf += 'L'; + buf += dotname; + buf += ';'; + std::replace(buf.begin(), buf.end(), '.', '/'); + return DexType::get_type(buf); +} + +template <> +void gather_dynamic_references(const IRCode* item, References* references) { + if (!item) { + return; + } + for (const auto& mie : InstructionIterable(item)) { + auto opcode = mie.insn; + // Matches any stringref that name-aliases a type. + if (opcode->has_string()) { + const DexString* dsclzref = opcode->get_string(); + auto* cls = type_class(get_dextype_from_dotname(dsclzref->str())); + if (cls) { + references->classes_dynamically_referenced.insert(cls); + } + } + if (opcode->has_type()) { + auto* cls = type_class(opcode->get_type()); + if (cls) { + references->classes_dynamically_referenced.insert(cls); + } + } + } +} +} // namespace relaxed_keep_class_members_impl + +void gather_dynamic_references(const DexAnnotation* item, + References* references) { + relaxed_keep_class_members_impl::gather_dynamic_references(item, references); +} + +void gather_dynamic_references(const IRCode* item, References* references) { + relaxed_keep_class_members_impl::gather_dynamic_references(item, references); +} + +template +static References generic_gather(T t, bool include_dynamic_references) { References refs; t->gather_strings(refs.strings); t->gather_types(refs.types); t->gather_fields(refs.fields); t->gather_methods(refs.methods); + if (include_dynamic_references) { + relaxed_keep_class_members_impl::gather_dynamic_references(t, &refs); + } return refs; } References TransitiveClosureMarker::gather(const DexAnnotation* anno) const { - return generic_gather(anno); + return generic_gather(anno, m_relaxed_keep_class_members); } References TransitiveClosureMarker::gather(const DexMethod* method) const { - return generic_gather(method); + auto refs = generic_gather(method, m_relaxed_keep_class_members); + if (m_relaxed_keep_class_members) { + gather_dynamic_references(method->get_code(), &refs); + } + return refs; } References TransitiveClosureMarker::gather(const DexField* field) const { - return generic_gather(field); + return generic_gather(field, m_relaxed_keep_class_members); } bool TransitiveClosureMarker::has_class_forname(DexMethod* meth) { @@ -404,9 +638,10 @@ void TransitiveClosureMarker::gather_and_push(DexMethod* meth) { push(meth, refs.types.begin(), refs.types.end()); push(meth, refs.fields.begin(), refs.fields.end()); push(meth, refs.methods.begin(), refs.methods.end()); - for (auto* cond_meth : refs.cond_methods) { - push_if_instantiable(cond_meth); + for (auto* vmeth : refs.vmethods_if_class_instantiable) { + push_if_class_instantiable(vmeth); } + dynamically_referenced(refs.classes_dynamically_referenced); } template @@ -416,6 +651,7 @@ void TransitiveClosureMarker::gather_and_push(T t) { push(t, refs.types.begin(), refs.types.end()); push(t, refs.fields.begin(), refs.fields.end()); push(t, refs.methods.begin(), refs.methods.end()); + dynamically_referenced(refs.classes_dynamically_referenced); } template @@ -436,20 +672,6 @@ void TransitiveClosureMarker::visit_cls(const DexClass* cls) { if (is_native(cls)) { instantiable(cls->get_type()); } - for (auto& m : cls->get_dmethods()) { - if (method::is_clinit(m)) { - if (!m->get_code() || !method::is_trivial_clinit(*m->get_code())) { - push(cls, m); - } - } else if (!m_remove_no_argument_constructors && - method::is_argless_init(m)) { - // Push the parameterless constructor, in case it's constructed via - // .class or Class.forName() - // if m_remove_no_argument_constructors, make an exception. This is only - // used for testing - push(cls, m); - } - } push(cls, type_class(cls->get_super_class())); for (auto const& t : *cls->get_interfaces()) { push(cls, t); @@ -463,6 +685,11 @@ void TransitiveClosureMarker::visit_cls(const DexClass* cls) { "Stop marking from %s by system anno: %s", SHOW(cls), SHOW(anno->type())); + if (m_relaxed_keep_class_members) { + References refs; + gather_dynamic_references(anno.get(), &refs); + dynamically_referenced(refs.classes_dynamically_referenced); + } continue; } record_reachability(cls, anno.get()); @@ -470,23 +697,28 @@ void TransitiveClosureMarker::visit_cls(const DexClass* cls) { } } + if (m_relaxed_keep_class_members && consider_dynamically_referenced(cls) && + marked_by_string(cls)) { + dynamically_referenced(cls); + } + for (auto const& m : cls->get_ifields()) { - if (m_cond_marked->fields.count(m)) { + if (m_cond_marked->if_class_retained.fields.count(m)) { push(cls, m); } } for (auto const& m : cls->get_sfields()) { - if (m_cond_marked->fields.count(m)) { + if (m_cond_marked->if_class_retained.fields.count(m)) { push(cls, m); } } for (auto const& m : cls->get_dmethods()) { - if (m_cond_marked->methods.count(m)) { + if (m_cond_marked->if_class_retained.methods.count(m)) { push(cls, m); } } for (auto const& m : cls->get_vmethods()) { - if (m_cond_marked->methods.count(m)) { + if (m_cond_marked->if_class_retained.methods.count(m)) { push(cls, m); } } @@ -545,13 +777,51 @@ void TransitiveClosureMarker::instantiable(DexType* type) { for (auto* intf : *cls->get_interfaces()) { instantiable(intf); } + for (auto const& f : cls->get_ifields()) { + if (m_cond_marked->if_class_instantiable.fields.count(f)) { + push(cls, f); + } + } + for (auto const& m : cls->get_dmethods()) { + if (m_cond_marked->if_class_instantiable.methods.count(m)) { + push(cls, m); + } + } for (auto const& m : cls->get_vmethods()) { - if (m_cond_marked->methods_if_instantiable.count(m)) { + if (m_cond_marked->if_class_instantiable.methods.count(m)) { push(cls, m); } } } +void TransitiveClosureMarker::dynamically_referenced(const DexClass* cls) { + always_assert(m_relaxed_keep_class_members); + if (!consider_dynamically_referenced(cls) || + !m_dynamically_referenced_classes->insert(cls)) { + return; + } + for (auto const& f : cls->get_ifields()) { + if (m_cond_marked->if_class_dynamically_referenced.fields.count(f)) { + push_if_class_retained(f); + } + } + for (auto const& f : cls->get_sfields()) { + if (m_cond_marked->if_class_dynamically_referenced.fields.count(f)) { + push_if_class_retained(f); + } + } + for (auto const& m : cls->get_dmethods()) { + if (m_cond_marked->if_class_dynamically_referenced.methods.count(m)) { + push_if_class_retained(m); + } + } + for (auto const& m : cls->get_vmethods()) { + if (m_cond_marked->if_class_dynamically_referenced.methods.count(m)) { + push_if_class_retained(m); + } + } +} + void TransitiveClosureMarker::visit_method_ref(const DexMethodRef* method) { TRACE(REACH, 4, "Visiting method: %s", SHOW(method)); auto cls = type_class(method->get_class()); @@ -579,7 +849,7 @@ void TransitiveClosureMarker::visit_method_ref(const DexMethodRef* method) { const auto& overriding_methods = mog::get_overriding_methods(m_method_override_graph, m); for (auto* overriding : overriding_methods) { - push_if_instantiable(overriding); + push_if_class_instantiable(overriding); } } } @@ -598,6 +868,7 @@ std::unique_ptr compute_reachable_objects( const IgnoreSets& ignore_sets, int* num_ignore_check_strings, bool record_reachability, + bool relaxed_keep_class_members, bool should_mark_all_as_seed, std::unique_ptr* out_method_override_graph, bool remove_no_argument_constructors) { @@ -609,8 +880,9 @@ std::unique_ptr compute_reachable_objects( ConcurrentSet root_set; RootSetMarker root_set_marker(*method_override_graph, record_reachability, - &cond_marked, reachable_objects.get(), - &root_set); + relaxed_keep_class_members, + remove_no_argument_constructors, &cond_marked, + reachable_objects.get(), &root_set); if (should_mark_all_as_seed) { root_set_marker.mark_all_as_seed(scope); @@ -619,15 +891,16 @@ std::unique_ptr compute_reachable_objects( } InstantiableTypes instantiable_types; + reachability::DynamicallyReferencedClasses dynamically_referenced_classes; size_t num_threads = redex_parallel::default_num_threads(); auto stats_arr = std::make_unique(num_threads); workqueue_run( [&](MarkWorkerState* worker_state, const ReachableObject& obj) { TransitiveClosureMarker transitive_closure_marker( ignore_sets, *method_override_graph, record_reachability, - &cond_marked, reachable_objects.get(), &instantiable_types, - worker_state, &stats_arr[worker_state->worker_id()], - remove_no_argument_constructors); + relaxed_keep_class_members, &cond_marked, reachable_objects.get(), + &instantiable_types, &dynamically_referenced_classes, worker_state, + &stats_arr[worker_state->worker_id()]); transitive_closure_marker.visit(obj); return nullptr; }, diff --git a/libredex/Reachability.h b/libredex/Reachability.h index a375849dae..907f0a7f3b 100644 --- a/libredex/Reachability.h +++ b/libredex/Reachability.h @@ -168,24 +168,50 @@ class ReachableObjects { friend class TransitiveClosureMarker; }; +enum class Condition { + ClassRetained, + ClassDynamicallyReferenced, + ClassInstantiable, +}; + struct ConditionallyMarked { - ConcurrentSet fields; - ConcurrentSet methods; - ConcurrentSet methods_if_instantiable; + struct ConcurrentMethodsAndFields { + ConcurrentSet fields; + ConcurrentSet methods; + }; + + // If any reference to the class if retained as part of any reachable + // structure. + ConcurrentMethodsAndFields if_class_retained; + + // If the class is referenced in a certain way that makes it discoverable via + // reflection, using the rules of the DelInitPass. + ConcurrentMethodsAndFields if_class_dynamically_referenced; + + // If the class is not abstract and has a constructor, or has a derived class + // that does. + ConcurrentMethodsAndFields if_class_instantiable; }; -using InstantiableTypes = ConcurrentSet; +using InstantiableTypes = ConcurrentSet; +using DynamicallyReferencedClasses = ConcurrentSet; struct References { std::vector strings; std::vector types; std::vector fields; std::vector methods; - // Conditional method references. They are already resolved DexMethods + // Conditional virtual method references. They are already resolved DexMethods // conditionally reachable at virtual call sites. - std::vector cond_methods; + std::vector vmethods_if_class_instantiable; + std::unordered_set classes_dynamically_referenced; }; +void gather_dynamic_references(const DexAnnotation* item, + References* references); + +void gather_dynamic_references(const IRCode* item, References* references); + // Each thread will have its own instance of Stats, so align it in order to // avoid false sharing. struct alignas(CACHE_LINE_SIZE) Stats { @@ -213,11 +239,15 @@ class RootSetMarker { public: RootSetMarker(const method_override_graph::Graph& method_override_graph, bool record_reachability, + bool relaxed_keep_class_members, + bool remove_no_argument_constructors, ConditionallyMarked* cond_marked, ReachableObjects* reachable_objects, ConcurrentSet* root_set) : m_method_override_graph(method_override_graph), m_record_reachability(record_reachability), + m_relaxed_keep_class_members(relaxed_keep_class_members), + m_remove_no_argument_constructors(remove_no_argument_constructors), m_cond_marked(cond_marked), m_reachable_objects(reachable_objects), m_root_set(root_set) {} @@ -246,11 +276,9 @@ class RootSetMarker { private: void push_seed(const DexClass* cls); - void push_seed(const DexField* field); + void push_seed(const DexField* field, Condition condition); - void push_seed(const DexMethod* method); - - void push_seed_if_instantiable(const DexMethod* method); + void push_seed(const DexMethod* metho, Condition condition); template void record_is_seed(Seed* seed); @@ -260,8 +288,14 @@ class RootSetMarker { */ void mark_external_method_overriders(); + static bool is_rootlike_clinit(const DexMethod* m); + + bool is_rootlike_init(const DexMethod* m) const; + const method_override_graph::Graph& m_method_override_graph; bool m_record_reachability; + bool m_relaxed_keep_class_members; + bool m_remove_no_argument_constructors; ConditionallyMarked* m_cond_marked; ReachableObjects* m_reachable_objects; ConcurrentSet* m_root_set; @@ -273,21 +307,24 @@ class TransitiveClosureMarker { const IgnoreSets& ignore_sets, const method_override_graph::Graph& method_override_graph, bool record_reachability, + bool relaxed_keep_class_members, ConditionallyMarked* cond_marked, ReachableObjects* reachable_objects, InstantiableTypes* instantiable_types, + reachability::DynamicallyReferencedClasses* + dynamically_referenced_classes, MarkWorkerState* worker_state, - Stats* stats, - bool remove_no_argument_constructors = false) + Stats* stats) : m_ignore_sets(ignore_sets), m_method_override_graph(method_override_graph), m_record_reachability(record_reachability), + m_relaxed_keep_class_members(relaxed_keep_class_members), m_cond_marked(cond_marked), m_reachable_objects(reachable_objects), m_instantiable_types(instantiable_types), + m_dynamically_referenced_classes(dynamically_referenced_classes), m_worker_state(worker_state), - m_stats(stats), - m_remove_no_argument_constructors(remove_no_argument_constructors) { + m_stats(stats) { if (s_class_forname == nullptr) { s_class_forname = DexMethod::get_method( "Ljava/lang/Class;.forName:(Ljava/lang/String;)Ljava/lang/Class;"); @@ -334,7 +371,13 @@ class TransitiveClosureMarker { void push(const DexMethodRef* parent, const DexMethodRef* method); - void push_if_instantiable(const DexMethod* method); + void push_if_class_instantiable(const DexField* field); + + void push_if_class_instantiable(const DexMethod* method); + + void push_if_class_retained(const DexField* field); + + void push_if_class_retained(const DexMethod* method); bool has_class_forname(DexMethod* meth); @@ -359,15 +402,24 @@ class TransitiveClosureMarker { void instantiable(DexType* type); + void dynamically_referenced(const DexClass* cls); + void dynamically_referenced( + const std::unordered_set& classes) { + for (auto* cls : classes) { + dynamically_referenced(cls); + } + } + const IgnoreSets& m_ignore_sets; const method_override_graph::Graph& m_method_override_graph; bool m_record_reachability; + bool m_relaxed_keep_class_members; ConditionallyMarked* m_cond_marked; ReachableObjects* m_reachable_objects; InstantiableTypes* m_instantiable_types; + DynamicallyReferencedClasses* m_dynamically_referenced_classes; MarkWorkerState* m_worker_state; Stats* m_stats; - bool m_remove_no_argument_constructors; static DexMethodRef* s_class_forname; }; @@ -381,6 +433,7 @@ std::unique_ptr compute_reachable_objects( const IgnoreSets& ignore_sets, int* num_ignore_check_strings, bool record_reachability = false, + bool relaxed_keep_class_members = false, bool should_mark_all_as_seed = false, std::unique_ptr* out_method_override_graph = nullptr, diff --git a/opt/reachable-natives/ReachableNatives.cpp b/opt/reachable-natives/ReachableNatives.cpp index d95fe9e4e3..e8fe60f6e9 100644 --- a/opt/reachable-natives/ReachableNatives.cpp +++ b/opt/reachable-natives/ReachableNatives.cpp @@ -71,17 +71,16 @@ void ReachableNativesPass::run_pass(DexStoresVector& stores, auto scope = build_class_scope(stores); auto reachable_objects = std::make_unique(); reachability::InstantiableTypes instantiable_types; + reachability::DynamicallyReferencedClasses dynamically_referenced_classes; reachability::ConditionallyMarked cond_marked; auto method_override_graph = method_override_graph::build_graph(scope); ConcurrentSet root_set; - reachability::RootSetMarker root_set_marker(*method_override_graph, - false, - &cond_marked, - reachable_objects.get(), - &root_set); + reachability::RootSetMarker root_set_marker( + *method_override_graph, false, false, false, &cond_marked, + reachable_objects.get(), &root_set); TRACE(NATIVE, 2, "Blanket Native Classes: %zu", g_redex->blanket_native_root_classes.size()); @@ -99,9 +98,10 @@ void ReachableNativesPass::run_pass(DexStoresVector& stores, [&](reachability::MarkWorkerState* worker_state, const reachability::ReachableObject& obj) { reachability::TransitiveClosureMarker transitive_closure_marker( - ignore_sets, *method_override_graph, false, &cond_marked, - reachable_objects.get(), &instantiable_types, worker_state, - &stats_arr[worker_state->worker_id()], false); + ignore_sets, *method_override_graph, false, false, &cond_marked, + reachable_objects.get(), &instantiable_types, + &dynamically_referenced_classes, worker_state, + &stats_arr[worker_state->worker_id()]); transitive_closure_marker.visit(obj); return nullptr; }, diff --git a/opt/remove-unreachable/RemoveUnreachable.cpp b/opt/remove-unreachable/RemoveUnreachable.cpp index 482f615ba2..8990f1c8d7 100644 --- a/opt/remove-unreachable/RemoveUnreachable.cpp +++ b/opt/remove-unreachable/RemoveUnreachable.cpp @@ -194,7 +194,7 @@ void RemoveUnreachablePassBase::run_pass(DexStoresVector& stores, int num_ignore_check_strings = 0; auto reachables = this->compute_reachable_objects( stores, pm, &num_ignore_check_strings, emit_graph_this_run, - m_remove_no_argument_constructors); + m_relaxed_keep_class_members, m_remove_no_argument_constructors); reachability::ObjectCounts before = reachability::count_objects(stores); TRACE(RMU, 1, "before: %lu classes, %lu fields, %lu methods", @@ -281,10 +281,12 @@ RemoveUnreachablePass::compute_reachable_objects( PassManager& /* pm */, int* num_ignore_check_strings, bool emit_graph_this_run, + bool relaxed_keep_class_members, bool remove_no_argument_constructors) { return reachability::compute_reachable_objects( stores, m_ignore_sets, num_ignore_check_strings, emit_graph_this_run, - false, nullptr, remove_no_argument_constructors); + relaxed_keep_class_members, false, nullptr, + remove_no_argument_constructors); } static RemoveUnreachablePass s_pass; diff --git a/opt/remove-unreachable/RemoveUnreachable.h b/opt/remove-unreachable/RemoveUnreachable.h index c3305a5303..43a93160f9 100644 --- a/opt/remove-unreachable/RemoveUnreachable.h +++ b/opt/remove-unreachable/RemoveUnreachable.h @@ -41,6 +41,7 @@ class RemoveUnreachablePassBase : public Pass { false, m_remove_no_argument_constructors); bind("output_full_removed_symbols", false, m_output_full_removed_symbols); + bind("relaxed_keep_class_members", false, m_relaxed_keep_class_members); after_configuration([this] { // To keep the backward compatability of this code, ensure that the // "MemberClasses" annotation is always in system_annos. @@ -56,6 +57,7 @@ class RemoveUnreachablePassBase : public Pass { PassManager& pm, int* num_ignore_check_strings, bool emit_graph_this_run, + bool relaxed_keep_class_members, bool remove_no_argument_constructors) = 0; void write_out_removed_symbols( @@ -69,6 +71,7 @@ class RemoveUnreachablePassBase : public Pass { bool m_always_emit_unreachable_symbols = false; bool m_emit_removed_symbols_references = false; bool m_output_full_removed_symbols = false; + bool m_relaxed_keep_class_members = false; }; class RemoveUnreachablePass : public RemoveUnreachablePassBase { @@ -81,5 +84,6 @@ class RemoveUnreachablePass : public RemoveUnreachablePassBase { PassManager& pm, int* num_ignore_check_strings, bool emit_graph_this_run, + bool relaxed_keep_class_members, bool remove_no_argument_constructors) override; }; diff --git a/opt/remove-unreachable/TypeAnalysisAwareRemoveUnreachable.cpp b/opt/remove-unreachable/TypeAnalysisAwareRemoveUnreachable.cpp index 3f921d57fe..047cc44579 100644 --- a/opt/remove-unreachable/TypeAnalysisAwareRemoveUnreachable.cpp +++ b/opt/remove-unreachable/TypeAnalysisAwareRemoveUnreachable.cpp @@ -44,18 +44,22 @@ class TypeAnaysisAwareClosureMarker final const IgnoreSets& ignore_sets, const method_override_graph::Graph& method_override_graph, bool record_reachability, + bool relaxed_keep_class_members, ConditionallyMarked* cond_marked, ReachableObjects* reachable_objects, InstantiableTypes* instantiable_types, + DynamicallyReferencedClasses* dynamically_referenced_classes, MarkWorkerState* worker_state, Stats* stats, std::shared_ptr gta) : reachability::TransitiveClosureMarker(ignore_sets, method_override_graph, record_reachability, + relaxed_keep_class_members, cond_marked, reachable_objects, instantiable_types, + dynamically_referenced_classes, worker_state, stats), m_gta(std::move(gta)) {} @@ -69,6 +73,9 @@ class TypeAnaysisAwareClosureMarker final gather_methods_on_code(method, refs); // Gather from annotations method->gather_methods_from_annos(refs.methods); + if (m_relaxed_keep_class_members) { + gather_dynamic_references(method->get_code(), &refs); + } return refs; } @@ -104,7 +111,7 @@ class TypeAnaysisAwareClosureMarker final overriding_methods.size(), SHOW(m)); } for (auto* overriding : overriding_methods) { - push_if_instantiable(overriding); + push_if_class_instantiable(overriding); TRACE(REACH, 3, "marking root override: %s", SHOW(overriding)); } } @@ -178,7 +185,8 @@ class TypeAnaysisAwareClosureMarker final for (auto overriding_method : overriding_methods) { TRACE(TRMU, 5, "Gather conditional method ref %s", SHOW(overriding_method)); - refs.cond_methods.push_back(overriding_method); + always_assert(overriding_method->is_virtual()); + refs.vmethods_if_class_instantiable.push_back(overriding_method); } } @@ -229,6 +237,7 @@ std::unique_ptr compute_reachable_objects_with_type_anaysis( const IgnoreSets& ignore_sets, int* num_ignore_check_strings, bool record_reachability, + bool relaxed_keep_class_members, std::shared_ptr gta, bool /*unused*/) { Timer t("Marking"); @@ -241,13 +250,16 @@ std::unique_ptr compute_reachable_objects_with_type_anaysis( auto reachable_objects = std::make_unique(); InstantiableTypes instantiable_types; + reachability::DynamicallyReferencedClasses dynamically_referenced_classes; ConditionallyMarked cond_marked; auto method_override_graph = mog::build_graph(scope); ConcurrentSet root_set; + bool remove_no_argument_constructors = false; RootSetMarker root_set_marker(*method_override_graph, record_reachability, - &cond_marked, reachable_objects.get(), - &root_set); + relaxed_keep_class_members, + remove_no_argument_constructors, &cond_marked, + reachable_objects.get(), &root_set); root_set_marker.mark(scope); size_t num_threads = redex_parallel::default_num_threads(); @@ -256,8 +268,9 @@ std::unique_ptr compute_reachable_objects_with_type_anaysis( [&](MarkWorkerState* worker_state, const ReachableObject& obj) { TypeAnaysisAwareClosureMarker transitive_closure_marker( ignore_sets, *method_override_graph, record_reachability, - &cond_marked, reachable_objects.get(), &instantiable_types, - worker_state, &stats_arr[worker_state->worker_id()], gta); + relaxed_keep_class_members, &cond_marked, reachable_objects.get(), + &instantiable_types, &dynamically_referenced_classes, worker_state, + &stats_arr[worker_state->worker_id()], gta); transitive_closure_marker.visit(obj); return nullptr; }, @@ -282,6 +295,7 @@ TypeAnalysisAwareRemoveUnreachablePass::compute_reachable_objects( PassManager& pm, int* num_ignore_check_strings, bool emit_graph_this_run, + bool relaxed_keep_class_members, bool remove_no_argument_constructors) { // Fetch analysis result auto analysis = pm.template get_preserved_analysis(); @@ -290,8 +304,8 @@ TypeAnalysisAwareRemoveUnreachablePass::compute_reachable_objects( always_assert(gta); return compute_reachable_objects_with_type_anaysis( - stores, m_ignore_sets, num_ignore_check_strings, emit_graph_this_run, gta, - remove_no_argument_constructors); + stores, m_ignore_sets, num_ignore_check_strings, emit_graph_this_run, + relaxed_keep_class_members, gta, remove_no_argument_constructors); } static TypeAnalysisAwareRemoveUnreachablePass s_pass; diff --git a/opt/remove-unreachable/TypeAnalysisAwareRemoveUnreachable.h b/opt/remove-unreachable/TypeAnalysisAwareRemoveUnreachable.h index b0430a45cf..9b30d93ac1 100644 --- a/opt/remove-unreachable/TypeAnalysisAwareRemoveUnreachable.h +++ b/opt/remove-unreachable/TypeAnalysisAwareRemoveUnreachable.h @@ -27,5 +27,6 @@ class TypeAnalysisAwareRemoveUnreachablePass PassManager& pm, int* num_ignore_check_strings, bool emit_graph_this_run, + bool relaxed_keep_class_members, bool remove_no_argument_constructors) override; }; diff --git a/test/integ/ReachabilityTest.cpp b/test/integ/ReachabilityTest.cpp index 2896cdb0d5..f086411234 100644 --- a/test/integ/ReachabilityTest.cpp +++ b/test/integ/ReachabilityTest.cpp @@ -74,8 +74,8 @@ TEST_F(ReachabilityTest, ReachabilityMarkAllTest) { reachability::IgnoreSets ig_sets; auto reachable_objects = reachability::compute_reachable_objects( stores, ig_sets, &num_ignore_check_strings, - /* record_reachability */ false, /* should_mark_all_as_seed */ true, - nullptr); + /* record_reachability */ false, /* relaxed_keep_class_members */ false, + /* should_mark_all_as_seed */ true, nullptr); reachability::sweep(stores, *reachable_objects, nullptr);