opt/rebindrefs/ReBindRefs.cpp (251 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 "ReBindRefs.h"
#include <boost/algorithm/string.hpp>
#include <functional>
#include <string>
#include <vector>
#include "ApiLevelChecker.h"
#include "ConfigFiles.h"
#include "Debug.h"
#include "DexClass.h"
#include "DexUtil.h"
#include "IRInstruction.h"
#include "PassManager.h"
#include "Resolver.h"
#include "Show.h"
#include "Trace.h"
#include "Util.h"
#include "Walkers.h"
namespace {
DexMethodRef* object_array_clone() {
static DexMethodRef* clone = DexMethod::make_method(
"[Ljava/lang/Object;", "clone", "Ljava/lang/Object;", {});
return clone;
}
bool is_array_clone(DexMethodRef* mref, DexType* mtype) {
static auto clone = DexString::make_string("clone");
return type::is_array(mtype) && mref->get_name() == clone &&
!type::is_primitive(type::get_array_element_type(mtype));
}
inline bool match(const DexString* name,
const DexProto* proto,
const DexMethod* cls_meth) {
return name == cls_meth->get_name() && proto == cls_meth->get_proto();
}
// Only looking at the public, protected and private bits.
template <typename DexMember>
DexAccessFlags get_visibility(const DexMember* member) {
return member->get_access() & VISIBILITY_MASK;
}
struct Rebinder {
private:
template <typename T>
struct RefStats {
int count = 0;
std::unordered_set<T> in;
std::unordered_set<T> out;
void insert(T tin, T tout) {
++count;
in.emplace(tin);
out.emplace(tout);
}
void insert(T tin) { insert(tin, T()); }
void print(const char* tag, PassManager& mgr) const {
TRACE(BIND, 1, "%11s [call sites: %6d, old refs: %6lu, new refs: %6lu]",
tag, count, in.size(), out.size());
using std::string;
string tagStr{tag};
string count_metric = tagStr + string("_candidates");
string rebound_metric = tagStr + string("_rebound");
mgr.incr_metric(count_metric, count);
auto rebound =
static_cast<ssize_t>(in.size()) - static_cast<ssize_t>(out.size());
mgr.incr_metric(rebound_metric, rebound);
}
RefStats& operator+=(const RefStats& rhs) {
count += rhs.count;
if (!rhs.in.empty()) {
in.insert(rhs.in.begin(), rhs.in.end());
}
if (!rhs.out.empty()) {
out.insert(rhs.out.begin(), rhs.out.end());
}
return *this;
}
};
public:
struct RebinderRefs {
RefStats<DexMethodRef*> mrefs;
RefStats<DexMethodRef*> array_clone_refs;
void print(PassManager& mgr) const {
mrefs.print("method_refs", mgr);
array_clone_refs.print("array_clone", mgr);
}
RebinderRefs& operator+=(const RebinderRefs& rhs) {
mrefs += rhs.mrefs;
array_clone_refs += rhs.array_clone_refs;
return *this;
}
};
Rebinder(Scope& scope,
bool rebind_to_external,
std::vector<std::string>& excluded_externals,
const api::AndroidSDK& min_sdk_sdk)
: m_scope(scope),
m_rebind_to_external(rebind_to_external),
m_excluded_externals(excluded_externals),
m_min_sdk_sdk(min_sdk_sdk) {}
RebinderRefs rewrite_refs() {
return walk::parallel::methods<RebinderRefs>(
m_scope, [&](DexMethod* method) {
if (!method || !method->get_code()) {
return RebinderRefs();
}
bool is_support_lib = api::is_support_lib_type(method->get_class());
RebinderRefs rebinder_refs;
for (auto& mie : InstructionIterable(method->get_code())) {
auto insn = mie.insn;
switch (insn->opcode()) {
case OPCODE_INVOKE_VIRTUAL: {
rebind_invoke_virtual(is_support_lib, insn, rebinder_refs);
break;
}
case OPCODE_INVOKE_SUPER:
case OPCODE_INVOKE_INTERFACE:
case OPCODE_INVOKE_STATIC: {
// Do nothing for now.
break;
}
default:
break;
}
}
return rebinder_refs;
});
}
private:
/**
* Java allows relaxing visibility down the hierarchy chain so while
* rebinding we don't want to bind to a method up the hierarchy that would
* not be visible.
* Walk up the hierarchy chain as long as the method is visible.
*/
DexMethod* bind_to_visible_ancestor(const DexClass* cls,
const DexString* name,
const DexProto* proto) {
auto leaf_impl = resolve_virtual(cls, name, proto);
if (!leaf_impl) {
return nullptr;
}
auto leaf_vis = get_visibility(leaf_impl);
if (!is_public(leaf_vis)) {
return leaf_impl;
}
DexMethod* top_impl = leaf_impl;
// The resolved leaf impl can only be PUBLIC at this point.
while (cls) {
for (const auto& cls_meth : cls->get_vmethods()) {
if (match(name, proto, cls_meth)) {
auto curr_vis = get_visibility(cls_meth);
auto curr_cls_vis = get_visibility(cls);
if (is_private(curr_vis) || is_package_private(curr_vis)) {
return top_impl;
}
bool is_external = cls->is_external() || cls_meth->is_external();
if (is_external &&
(!is_public(curr_vis) || !is_public(curr_cls_vis))) {
return top_impl;
}
// We can only rebind PUBLIC to PUBLIC here.
if (leaf_vis != curr_vis) {
return top_impl;
}
top_impl = cls_meth;
break;
}
}
cls = type_class(cls->get_super_class());
}
return top_impl;
}
void rebind_invoke_virtual(bool is_support_lib,
IRInstruction* mop,
RebinderRefs& rebinder_refs) {
const auto mref = mop->get_method();
auto mtype = mref->get_class();
if (is_array_clone(mref, mtype)) {
rebind_method_opcode(is_support_lib, mop, mref,
rebind_array_clone(mref, rebinder_refs),
rebinder_refs);
return;
}
// leave java.lang.String alone not to interfere with OP_EXECUTE_INLINE
// and possibly any smart handling of String
if (mtype == type::java_lang_String()) {
return;
}
auto cls = type_class(mtype);
auto real_ref =
bind_to_visible_ancestor(cls, mref->get_name(), mref->get_proto());
rebind_method_opcode(is_support_lib, mop, mref, real_ref, rebinder_refs);
}
bool is_excluded_external(const std::string& name) {
for (auto& excluded : m_excluded_externals) {
if (boost::starts_with(name, excluded)) {
return true;
}
}
return false;
}
void rebind_method_opcode(bool is_support_lib,
IRInstruction* mop,
DexMethodRef* mref,
DexMethodRef* real_ref,
RebinderRefs& rebinder_refs) {
if (!real_ref || real_ref == mref) {
return;
}
auto cls = type_class(real_ref->get_class());
bool is_external = cls && cls->is_external();
if (is_external && !m_rebind_to_external) {
TRACE(BIND, 4, "external %s", SHOW(real_ref));
return;
}
// If the rebind target is in the exluded external list, stop.
if (is_external && is_excluded_external(show(real_ref))) {
TRACE(BIND, 4, "excluded external %s", SHOW(real_ref));
return;
}
// If the caller is in support libraries Android support library or Android
// X, we don't rebind if the target is in Android SDK. That means we do
// rebind for JDK classes since we know it's safe to do so.
if (is_external && api::is_android_sdk_type(real_ref->get_class()) &&
is_support_lib) {
TRACE(BIND, 4, "support lib external %s", SHOW(real_ref));
return;
}
if (is_external && real_ref->is_def()) {
auto target_def = real_ref->as_def();
if (!m_min_sdk_sdk.has_method(target_def)) {
TRACE(BIND, 4, "Bailed on mismatch with min_sdk %s", SHOW(target_def));
return;
}
}
// Bail out if the def is non public external
if (is_external && !is_public(cls)) {
TRACE(BIND, 4, "non-public external %s", SHOW(real_ref));
return;
}
TRACE(BIND, 2, "Rebinding %s\n\t=>%s", SHOW(mref), SHOW(real_ref));
rebinder_refs.mrefs.insert(mref, real_ref);
mop->set_method(real_ref);
if (cls != nullptr && !is_public(cls)) {
always_assert(!is_external);
set_public(cls);
}
}
DexMethodRef* rebind_array_clone(DexMethodRef* mref,
RebinderRefs& rebinder_refs) {
DexMethodRef* real_ref = object_array_clone();
rebinder_refs.array_clone_refs.insert(mref, real_ref);
return real_ref;
}
Scope& m_scope;
bool m_rebind_to_external;
std::vector<std::string> m_excluded_externals;
const api::AndroidSDK& m_min_sdk_sdk;
};
} // namespace
void ReBindRefsPass::run_pass(DexStoresVector& stores,
ConfigFiles& /* conf */,
PassManager& mgr) {
always_assert(m_min_sdk_api);
Scope scope = build_class_scope(stores);
Rebinder rb(scope, m_refine_to_external, m_excluded_externals,
*m_min_sdk_api);
auto stats = rb.rewrite_refs();
stats.print(mgr);
}
static ReBindRefsPass s_pass;