opt/remove-builders/RemoveBuilders.cpp (327 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 "RemoveBuilders.h"
#include <boost/regex.hpp>
#include <tuple>
#include "ConfigFiles.h"
#include "Dataflow.h"
#include "DexUtil.h"
#include "IRCode.h"
#include "PassManager.h"
#include "RemoveBuildersHelper.h"
#include "Resolver.h"
#include "Walkers.h"
namespace {
constexpr const char* METRIC_CLASSES_REMOVED = "classes_removed";
constexpr const char* METRIC_FIELDS_REMOVED = "fields_removed";
constexpr const char* METRIC_METHODS_REMOVED = "methods_removed";
constexpr const char* METRIC_METHODS_CLEARED = "methods_cleared";
struct builder_counters {
size_t classes_removed;
size_t fields_removed;
size_t methods_removed;
size_t methods_cleared;
};
builder_counters b_counter;
// checks if the `this` argument on an instance method ever gets passed to
// a method that doesn't belong to the same instance, or if it gets stored
// in a field, or if it escapes as a return value.
bool this_arg_escapes(DexMethod* method, bool enable_buildee_constr_change) {
always_assert(method != nullptr);
always_assert(!(method->get_access() & ACC_STATIC));
auto code = method->get_code();
auto ii = InstructionIterable(code);
auto this_insn = ii.begin()->insn;
always_assert(this_insn->opcode() == IOPCODE_LOAD_PARAM_OBJECT);
auto regs_size = code->get_registers_size();
auto this_cls = method->get_class();
code->build_cfg(/* editable */ false);
const auto& blocks = code->cfg().blocks_reverse_post_deprecated();
std::function<void(IRList::iterator, TaintedRegs*)> trans =
[&](const IRList::iterator& it, TaintedRegs* tregs) {
auto* insn = it->insn;
if (insn == this_insn) {
tregs->m_reg_set[insn->dest()] = 1;
} else {
transfer_object_reach(this_cls, regs_size, insn, tregs->m_reg_set);
}
};
auto taint_map = forwards_dataflow(blocks, TaintedRegs(regs_size + 1), trans);
return tainted_reg_escapes(
this_cls, method, *taint_map, enable_buildee_constr_change);
}
bool this_arg_escapes(DexClass* cls, bool enable_buildee_constr_change) {
always_assert(cls != nullptr);
bool result = false;
for (DexMethod* m : cls->get_dmethods()) {
if (!m->get_code()) {
continue;
}
if (!(m->get_access() & ACC_STATIC) &&
this_arg_escapes(m, enable_buildee_constr_change)) {
TRACE(BUILDERS,
3,
"this escapes in %s",
m->get_deobfuscated_name().c_str());
result = true;
}
}
for (DexMethod* m : cls->get_vmethods()) {
if (!m->get_code()) {
continue;
}
if (this_arg_escapes(m, enable_buildee_constr_change)) {
TRACE(BUILDERS,
3,
"this escapes in %s",
m->get_deobfuscated_name().c_str());
result = true;
}
}
return result;
}
std::vector<DexMethod*> get_static_methods(
const std::vector<DexMethod*>& dmethods) {
std::vector<DexMethod*> static_methods;
for (const auto& dmethod : dmethods) {
if (is_static(dmethod)) {
static_methods.emplace_back(dmethod);
}
}
return static_methods;
}
/**
* First pass through what "trivial builder" means:
* - is a builder
* - it doesn't escape stack
* - has no static methods
* - has no static fields
*/
std::unordered_set<DexClass*> get_trivial_builders(
const std::unordered_set<DexType*>& builders,
const std::unordered_set<DexType*>& stack_only_builders) {
std::unordered_set<DexClass*> trivial_builders;
for (DexType* builder_type : builders) {
DexClass* builder_class = type_class(builder_type);
// Filter out builders that escape the stack.
if (stack_only_builders.find(builder_type) == stack_only_builders.end()) {
continue;
}
// Filter out builders that do "extra work".
bool has_static_methods =
!get_static_methods(builder_class->get_dmethods()).empty();
if (has_static_methods || !builder_class->get_sfields().empty()) {
continue;
}
DexType* buildee_type = get_buildee(builder_class->get_type());
if (!buildee_type) {
continue;
}
trivial_builders.emplace(builder_class);
}
return trivial_builders;
}
void gather_removal_builder_stats(
const std::unordered_set<DexClass*>& builders,
const std::unordered_set<DexClass*>& kept_builders) {
for (DexClass* builder : builders) {
if (kept_builders.find(builder) == kept_builders.end()) {
b_counter.classes_removed++;
b_counter.methods_removed +=
builder->get_vmethods().size() + builder->get_dmethods().size();
b_counter.fields_removed += builder->get_ifields().size();
}
}
}
std::unordered_set<DexClass*> get_builders_with_subclasses(Scope& classes) {
std::unordered_set<DexClass*> builders_with_subclasses;
for (const auto& cls : classes) {
DexType* super_type = cls->get_super_class();
if (!super_type) {
continue;
}
DexClass* super_cls = type_class(super_type);
if (!super_cls) {
continue;
}
if (has_builder_name(super_type)) {
builders_with_subclasses.emplace(super_cls);
}
}
return builders_with_subclasses;
}
} // namespace
std::vector<DexType*> RemoveBuildersPass::created_builders(DexMethod* m) {
always_assert(m != nullptr);
std::vector<DexType*> builders;
auto code = m->get_code();
if (!code) {
return builders;
}
for (auto& mie : InstructionIterable(code)) {
auto insn = mie.insn;
if (insn->opcode() == OPCODE_NEW_INSTANCE) {
DexType* cls = insn->get_type();
if (m_builders.find(cls) != m_builders.end()) {
builders.emplace_back(cls);
}
}
}
return builders;
}
// checks if any instances of :builder that get created in the method ever get
// passed to a method (aside from when its own instance methods get invoked),
// or if they get stored in a field, or if they escape as a return value.
bool RemoveBuildersPass::escapes_stack(DexType* builder, DexMethod* method) {
always_assert(builder != nullptr);
always_assert(method != nullptr);
auto code = method->get_code();
code->build_cfg(/* editable */ false);
const auto& blocks = code->cfg().blocks_reverse_post_deprecated();
auto regs_size = method->get_code()->get_registers_size();
auto taint_map = get_tainted_regs(regs_size, blocks, builder);
return tainted_reg_escapes(
builder, method, *taint_map, m_enable_buildee_constr_change);
}
void RemoveBuildersPass::run_pass(DexStoresVector& stores,
ConfigFiles& conf,
PassManager& mgr) {
if (mgr.no_proguard_rules()) {
TRACE(BUILDERS,
1,
"RemoveBuildersPass did not run because no Proguard configuration "
"was provided.");
return;
}
// Initialize couters.
b_counter = {0, 0, 0, 0};
auto obj_type = type::java_lang_Object();
auto scope = build_class_scope(stores);
for (DexClass* cls : scope) {
if (is_annotation(cls) || is_interface(cls) ||
cls->get_super_class() != obj_type) {
continue;
}
if (has_builder_name(cls->get_type())) {
m_builders.emplace(cls->get_type());
}
}
std::unordered_set<DexType*> escaped_builders;
walk::methods(scope, [&](DexMethod* m) {
auto builders = created_builders(m);
for (DexType* builder : builders) {
if (escapes_stack(builder, m)) {
TRACE(BUILDERS,
3,
"%s escapes in %s",
SHOW(builder),
m->get_deobfuscated_name().c_str());
escaped_builders.emplace(builder);
}
}
});
std::unordered_set<DexType*> stack_only_builders;
for (DexType* builder : m_builders) {
if (escaped_builders.find(builder) == escaped_builders.end()) {
stack_only_builders.emplace(builder);
}
}
std::unordered_set<DexType*> builders_and_supers;
for (DexType* builder : stack_only_builders) {
DexType* cls = builder;
while (cls != nullptr && cls != obj_type) {
builders_and_supers.emplace(cls);
cls = type_class(cls)->get_super_class();
}
}
std::unordered_set<DexType*> this_escapes;
for (DexType* cls_ty : builders_and_supers) {
DexClass* cls = type_class(cls_ty);
if (cls->is_external() ||
this_arg_escapes(cls, m_enable_buildee_constr_change)) {
this_escapes.emplace(cls_ty);
}
}
// set of builders that neither escape the stack nor pass their 'this' arg
// to another function
std::unordered_set<DexType*> no_escapes;
for (DexType* builder : stack_only_builders) {
DexType* cls = builder;
bool hierarchy_has_escape = false;
while (cls != nullptr) {
if (this_escapes.find(cls) != this_escapes.end()) {
hierarchy_has_escape = true;
break;
}
cls = type_class(cls)->get_super_class();
}
if (!hierarchy_has_escape) {
no_escapes.emplace(builder);
}
}
size_t dmethod_count = 0;
size_t vmethod_count = 0;
size_t build_count = 0;
for (DexType* builder : no_escapes) {
auto cls = type_class(builder);
auto buildee = get_buildee(builder);
dmethod_count += cls->get_dmethods().size();
vmethod_count += cls->get_vmethods().size();
for (DexMethod* m : cls->get_vmethods()) {
if (m->get_proto()->get_rtype() == buildee) {
build_count++;
}
}
}
std::unordered_set<DexClass*> trivial_builders =
get_trivial_builders(m_builders, no_escapes);
std::unordered_set<DexClass*> kept_builders =
get_builders_with_subclasses(scope);
init_classes::InitClassesWithSideEffects init_classes_with_side_effects(
scope, conf.create_init_class_insns());
BuilderTransform b_transform(init_classes_with_side_effects,
conf.get_inliner_config(),
scope,
stores,
false);
// Inline non init methods.
std::unordered_set<DexClass*> removed_builders;
walk::methods(scope, [&](DexMethod* method) {
auto builders = created_builders(method);
for (DexType* builder : builders) {
if (method->get_class() == builder) {
continue;
}
DexClass* builder_cls = type_class(builder);
// Filter out builders that we cannot remove.
if (kept_builders.find(builder_cls) != kept_builders.end()) {
continue;
}
if (m_blocklist.find(builder) != m_blocklist.end()) {
TRACE(BUILDERS, 2, "Skipping excluded type %s", SHOW(builder));
continue;
}
// Check it is a trivial one.
if (trivial_builders.find(builder_cls) != trivial_builders.end()) {
DexMethod* method_copy = DexMethod::make_method_from(
method,
method->get_class(),
DexString::make_string(method->get_name()->str() +
"$redex_builders"));
bool was_not_removed =
!b_transform.inline_methods(
method, builder, &get_non_init_methods) ||
!remove_builder_from(method, builder_cls, b_transform);
if (was_not_removed) {
kept_builders.emplace(builder_cls);
method->set_code(method_copy->release_code());
} else {
b_counter.methods_cleared++;
removed_builders.emplace(builder_cls);
}
DexMethod::erase_method(method_copy);
}
}
});
// No need to remove the builders here, since `RemoveUnreachable` will
// take care of it.
gather_removal_builder_stats(removed_builders, kept_builders);
mgr.set_metric("total_builders", m_builders.size());
mgr.set_metric("stack_only_builders", stack_only_builders.size());
mgr.set_metric("no_escapes", no_escapes.size());
mgr.incr_metric(METRIC_CLASSES_REMOVED, b_counter.classes_removed);
mgr.incr_metric(METRIC_METHODS_REMOVED, b_counter.methods_removed);
mgr.incr_metric(METRIC_FIELDS_REMOVED, b_counter.fields_removed);
mgr.incr_metric(METRIC_METHODS_CLEARED, b_counter.methods_cleared);
TRACE(BUILDERS, 1, "Total builders: %zu", m_builders.size());
TRACE(BUILDERS, 1, "Stack-only builders: %zu", stack_only_builders.size());
TRACE(BUILDERS,
1,
"Stack-only builders that don't let `this` escape: %zu",
no_escapes.size());
TRACE(BUILDERS, 1, "Stats for unescaping builders:");
TRACE(BUILDERS, 1, "\tdmethods: %zu", dmethod_count);
TRACE(BUILDERS, 1, "\tvmethods: %zu", vmethod_count);
TRACE(BUILDERS, 1, "\tbuild methods: %zu", build_count);
TRACE(BUILDERS, 1, "Trivial builders: %zu", trivial_builders.size());
TRACE(BUILDERS, 1, "Classes removed: %zu", b_counter.classes_removed);
TRACE(BUILDERS, 1, "Methods removed: %zu", b_counter.methods_removed);
TRACE(BUILDERS, 1, "Fields removed: %zu", b_counter.fields_removed);
TRACE(BUILDERS, 1, "Methods cleared: %zu", b_counter.methods_cleared);
}
static RemoveBuildersPass s_pass;