opt/obfuscate/ObfuscateUtils.h (603 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. */ #pragma once #include "ClassHierarchy.h" #include "DexAccess.h" #include "DexAnnotation.h" #include "DexClass.h" #include "DexUtil.h" #include "ReachableClasses.h" #include "Show.h" #include "Trace.h" #include <list> #include <type_traits> namespace obfuscate_utils { void compute_identifier(int value, std::string* res); } // namespace obfuscate_utils // Type for the map of descriptor -> [newname -> oldname] // This map is used for reverse lookup to find naming collisions using NameMapping = std::unordered_map<std::string, std::unordered_map<std::string, std::string>>; // Renames a field in the Dex void rename_field(DexField* field, const std::string& new_name); void rename_method(DexMethod* method, const std::string& new_name); // Whether or not the configs allow for us to obfuscate the member // We don't want to obfuscate seeds. Keep members shouldn't be obfuscated // unless we are explicitly told to do so with the allowobfuscation flag // an element being a seed trumps allowobfuscation. template <class T> bool should_rename_elem(const T* member) { auto cls = type_class(member->get_class()); return can_rename(member) && !member->is_external() && cls != nullptr && !cls->is_external(); } /* * Allows us to wrap Dex elements with a new name that we intend to assign them * T should be a pointer to a Dex element (e.g. DexField*). We cannot just * assign names as we create them because of collisions and issues around * vmethods (requires some additional information). Additionally, some record * of the old name is necessary to fix up ref opcodes. */ template <class T, typename std::enable_if<std::is_pointer<T>::value, int>::type = 0> class DexNameWrapper { protected: T dex_elem; // the new name that we're trying to give this element bool has_new_name{false}; std::string name{"INVALID_DEFAULT_NAME"}; public: using const_type = typename std::add_const<T>::type; // Default constructor is only ever used for map template to work correctly // we have added asserts to make sure there are no nullptrs returned. DexNameWrapper() = default; virtual ~DexNameWrapper() {} DexNameWrapper(DexNameWrapper&& other) noexcept = default; DexNameWrapper(DexNameWrapper const& other) = default; // This is the constructor that should be used, creates a new wrapper on // the pointer it is passed explicit DexNameWrapper(T dex_elem) : dex_elem(dex_elem) {} DexNameWrapper& operator=(DexNameWrapper const& other) = default; DexNameWrapper& operator=(DexNameWrapper&& other) noexcept = default; inline T get() { always_assert(dex_elem != nullptr); return dex_elem; } inline const_type get() const { always_assert(dex_elem != nullptr); return dex_elem; } virtual bool name_has_changed() { return has_new_name; } virtual const char* get_name() { return has_new_name ? this->name.c_str() : this->get()->get_name()->c_str(); } virtual void set_name(const std::string& new_name) { has_new_name = true; name = new_name; // Uncomment this line for debug renaming // name = std::string(SHOW(dex_elem->get_name())) + "_" + new_name; } // Meant to be overridden, but we don't want this class to be abstract virtual void mark_unrenamable() { not_reached(); } virtual void mark_renamable() { not_reached(); } virtual bool should_rename() { not_reached(); } virtual std::string get_printable() { std::ostringstream res; res << " 0x" << this->get() << ": " << show(dex_elem) << " -> " << get_name(); return res.str(); } bool is_modified() { return this->name_has_changed() && this->should_rename(); } virtual bool should_commit() { return !type_class(get()->get_class())->is_external(); } }; using DexFieldWrapper = DexNameWrapper<DexField*>; using DexMethodWrapper = DexNameWrapper<DexMethod*>; class FieldNameWrapper : public DexFieldWrapper { public: FieldNameWrapper() = default; explicit FieldNameWrapper(DexField* elem) : DexFieldWrapper(elem) {} bool should_rename() override { return true; } }; class MethodNameWrapper : public DexMethodWrapper { private: int n_links = 1; // Allows us to link wrappers together to show groups of wrappers that // have to be renamed together. If there is a non-null next pointer, any calls // looking for information will be forwarded to the last element in the chain MethodNameWrapper* next; bool renamable = true; // Updates our links union-find style so that finding the end of the chain // is cheap inline void update_link() { if (next != nullptr) { next->update_link(); next = find_end_link(); } } MethodNameWrapper* find_end_link() { if (next == nullptr) return this; return next->find_end_link(); } public: MethodNameWrapper() = default; explicit MethodNameWrapper(DexMethod* dex_elem) : DexNameWrapper<DexMethod*>(dex_elem), next(nullptr) { // Make sure on construction any external elements are unrenamable renamable = should_rename_elem(dex_elem); } bool name_has_changed() override { if (next == nullptr) return has_new_name; update_link(); return next->name_has_changed(); } const char* get_name() override { if (next == nullptr) return DexMethodWrapper::get_name(); update_link(); return next->get_name(); } void set_name(const std::string& new_name) override { if (next == nullptr) { DexMethodWrapper::set_name(new_name); return; } next->set_name(new_name); update_link(); } void link(MethodNameWrapper* other) { always_assert(other != nullptr); always_assert(other != this); always_assert( strcmp(SHOW(get()->get_name()), SHOW(other->get()->get_name())) == 0); auto this_end = find_end_link(); // Make sure they aren't already linked if (this_end == other->find_end_link()) return; if (next == nullptr) { next = other; // Make sure if either isn't renamable, we mark the end result as not // renamable if (!renamable || !other->should_rename()) { TRACE(OBFUSCATE, 4, "Elem %s marking\n\t%s unrenamable", SHOW(renamable ? other->get() : this->get()), SHOW(renamable ? this->get() : other->get())); other->mark_unrenamable(); } } else { this_end->next = other; other->n_links += this_end->n_links; update_link(); } } int get_num_links() { update_link(); return find_end_link()->n_links; } bool should_rename() override { if (next == nullptr) return renamable; update_link(); return next->should_rename(); } bool is_linked() { return next != nullptr; } void mark_unrenamable() override { if (next == nullptr) { TRACE(OBFUSCATE, 4, "Elem %s marked unrenamable", SHOW(this->get())); renamable = false; return; } update_link(); next->mark_unrenamable(); } bool should_commit() override { return true; } std::string get_printable() override { std::ostringstream res; res << " 0x" << this << ": " << show(dex_elem) << " -> " << get_name() << " => 0x" << next; return res.str(); } }; /* * Interface for factories for new obfuscated names. */ template <class T> class NameGenerator { protected: int ctr{0}; // Set of ids to avoid (these ids were marked as do not rename and we cannot // conflict with) const std::unordered_set<std::string>& ids_to_avoid; // Set of ids we used while assigning names std::unordered_set<std::string>& used_ids; // Gets the next name that is not in the used_ids set std::string next_name() { std::string res; do { res.clear(); obfuscate_utils::compute_identifier(ctr++, &res); TRACE(OBFUSCATE, 4, "NameGenerator looking for a name, trying: %s", res.c_str()); } while (ids_to_avoid.count(res) > 0 || used_ids.count(res) > 0); return res; } public: NameGenerator(const std::unordered_set<std::string>& ids_to_avoid, std::unordered_set<std::string>& used_ids) : ids_to_avoid(ids_to_avoid), used_ids(used_ids) {} virtual ~NameGenerator() = default; // We want to rename the DexField pointed to by this wrapper. // The new name will be recorded in the wrapper virtual void find_new_name(DexNameWrapper<T>* wrap) = 0; // Function for saying when we're done figuring out which things we want // renamed and we can start actually renaming things virtual void bind_names() {} int next_ctr() { return ctr; } }; // We define various "biased" comparators: We re-order fields and methods // to place those which are "similar" close to each other, e.g. sharing the // same static value/proto/type/access. // This increases compressibility of associated metadata structures. // The re-ordering happens before renaming, after which all tables are again in // sorted-by-renamed-names order, while the underlying items got reshuffled. struct proto_access_biased_dexmethods_comparator { bool operator()(const DexMethod* a, const DexMethod* b) const { // First checking proto, then access yields the biggest compressed wins for // many apps. if (a->get_proto() != b->get_proto()) { return compare_dexprotos(a->get_proto(), b->get_proto()); } if (a->get_access() != b->get_access()) { return a->get_access() < b->get_access(); } return compare_dexmethods(a, b); } }; class MethodNameGenerator : public NameGenerator<DexMethod*> { private: // Use ordered maps here to get deterministic names, and to sort for best // compressibility std::map<DexMethod*, DexMethodWrapper*, proto_access_biased_dexmethods_comparator> methods; public: MethodNameGenerator(const std::unordered_set<std::string>& ids_to_avoid, std::unordered_set<std::string>& used_ids) : NameGenerator<DexMethod*>(ids_to_avoid, used_ids) {} void find_new_name(DexMethodWrapper* wrap) override { DexMethod* method = wrap->get(); methods.emplace(method, wrap); } void bind_names() override { for (auto p : methods) { auto wrap = p.second; always_assert(!wrap->is_modified()); do { std::string new_name(this->next_name()); wrap->set_name(new_name); this->used_ids.insert(new_name); TRACE(OBFUSCATE, 3, "\tTrying method name %s for %s", wrap->get_name(), SHOW(wrap->get())); } while (DexMethod::get_method(wrap->get()->get_class(), DexString::make_string(wrap->get_name()), wrap->get()->get_proto()) != nullptr); // Keep spinning on a name until you find one that isn't used at all TRACE(OBFUSCATE, 2, "\tIntending to rename method %s (%s) to %s ids to avoid %zu", SHOW(wrap->get()), SHOW(wrap->get()->get_name()), wrap->get_name(), ids_to_avoid.size()); } } }; // We sort static fields according to their static values. This reduces the // number of needed encoded static values for static fields, and it increases // the compressibility of associated metadata structures. struct static_value_biased_dexfields_comparator { bool operator()(const DexField* a, const DexField* b) const { // We prefer instance fields over static fields if (is_static(a) != is_static(b)) { return !is_static(a); } auto eva = a->get_static_value(); auto evb = b->get_static_value(); auto is_eva_relevant = eva && !eva->is_zero(); auto is_evb_relevant = evb && !evb->is_zero(); always_assert(!is_eva_relevant || is_static(a)); always_assert(!is_evb_relevant || is_static(b)); // We prefer fields that have relevant static values if (is_eva_relevant != is_evb_relevant) { return is_eva_relevant; } if (is_eva_relevant) { // We are biasing the comparator to the static value --- its type, // and then its actual value. If the type/value is indistinguishtable, // we still fall back to a more basic comparator at the very end of this // function, so that different fields are still properly distinguished. if (eva->evtype() != evb->evtype()) { return eva->evtype() < evb->evtype(); } switch (eva->evtype()) { case DEVT_STRING: { auto evastring = static_cast<DexEncodedValueString*>(eva)->string(); auto evbstring = static_cast<DexEncodedValueString*>(evb)->string(); if (evastring != evbstring) { return compare_dexstrings(evastring, evbstring); } break; } case DEVT_TYPE: { auto evatype = static_cast<DexEncodedValueType*>(eva)->type(); auto evbtype = static_cast<DexEncodedValueType*>(evb)->type(); if (evatype != evbtype) { return compare_dextypes(evatype, evbtype); } break; } case DEVT_FIELD: { auto evafield = static_cast<DexEncodedValueField*>(eva)->field(); auto evbfield = static_cast<DexEncodedValueField*>(evb)->field(); if (evafield != evbfield) { return compare_dexfields(evafield, evbfield); } break; } case DEVT_METHOD: { auto evamethod = static_cast<DexEncodedValueMethod*>(eva)->method(); auto evbmethod = static_cast<DexEncodedValueMethod*>(evb)->method(); if (evamethod != evbmethod) { return compare_dexmethods(evamethod, evbmethod); } break; } case DEVT_ARRAY: { auto evaarray = static_cast<DexEncodedValueArray*>(eva); auto evbarray = static_cast<DexEncodedValueArray*>(evb); if (evaarray->is_static_val() != evbarray->is_static_val()) { return evaarray->is_static_val() < evbarray->is_static_val(); } auto evavalues = evaarray->evalues(); auto evbvalues = evbarray->evalues(); if (evavalues->size() != evbvalues->size()) { return evavalues->size() < evbvalues->size(); } // TODO: deep-inspect array, but likely little impact break; } case DEVT_ANNOTATION: // TODO: deep-inspect this, but doesn't seem to occur in practice break; default: if (eva->value() != evb->value()) { return eva->value() < evb->value(); } } } // Now we are comparing fields, ignoring any static values. // First checking access, then type yields the biggest compressed wins for // many apps. if (a->get_access() != b->get_access()) { return a->get_access() < b->get_access(); } if (a->get_type() != b->get_type()) { return compare_dextypes(a->get_type(), b->get_type()); } return compare_dexfields(a, b); } }; // Will collect all the wrappers of fields to rename then rename them all at // once making sure to put static final fields with a nullptr value at the end // (for proper writing of dexes) class FieldNameGenerator : public NameGenerator<DexField*> { private: // Use ordered maps here to get deterministic names, and to sort for best // compressibility std:: map<DexField*, DexFieldWrapper*, static_value_biased_dexfields_comparator> fields; public: FieldNameGenerator(const std::unordered_set<std::string>& ids_to_avoid, std::unordered_set<std::string>& used_ids) : NameGenerator<DexField*>(ids_to_avoid, used_ids) {} void find_new_name(DexFieldWrapper* wrap) override { DexField* field = wrap->get(); fields.emplace(field, wrap); } void bind_names() override { for (auto p : fields) { auto wrap = p.second; always_assert(!wrap->is_modified()); do { std::string new_name(this->next_name()); wrap->set_name(new_name); this->used_ids.insert(new_name); TRACE(OBFUSCATE, 2, "\tTrying field name %s for %s", wrap->get_name(), SHOW(wrap->get())); } while (DexField::get_field(wrap->get()->get_class(), DexString::make_string(wrap->get_name()), wrap->get()->get_type()) != nullptr); // Keep spinning on a name until you find one that isn't used at all TRACE(OBFUSCATE, 2, "\tIntending to rename elem %s (%s) (renamable %s) to %s", SHOW(wrap->get()), SHOW(wrap->get()->get_name()), should_rename_elem(wrap->get()) ? "true" : "false", wrap->get_name()); } } }; // T - DexField/DexElem - what we're managing // R - DexFieldRef/DexElemRef - the ref to what we're managing // S - DexFieldSpec/DexElemSpec - the spec to what we're managing // K - DexType/DexProto - the type of the key we're using in the map template <class T, class R, class S, class K> class DexElemManager { protected: // Map from class_name -> type -> old_name -> // DexNameWrapper (contains new name) // Note: unique_ptr necessary here to avoid object slicing std::unordered_map< DexType*, std::unordered_map< K, std::unordered_map<const DexString*, std::unique_ptr<DexNameWrapper<T>>>>> elements; using RefCtrFn = std::function<S(const std::string&)>; using SigGetFn = std::function<K(R)>; using ElemCtrFn = std::function<DexNameWrapper<T>*(T&)>; SigGetFn sig_getter_fn; RefCtrFn ref_getter_fn; ElemCtrFn elemCtr; bool mark_all_unrenamable; public: DexElemManager(ElemCtrFn elem_ctr, SigGetFn get_sig, RefCtrFn ref_ctr) : sig_getter_fn(get_sig), ref_getter_fn(ref_ctr), elemCtr(elem_ctr), mark_all_unrenamable(false) {} // NOLINTNEXTLINE(performance-noexcept-move-constructor) DexElemManager(DexElemManager&& other) = default; virtual ~DexElemManager() {} // void lock_elements() { mark_all_unrenamable = true; } // void unlock_elements() { mark_all_unrenamable = false; } inline bool contains_elem(DexType* cls, K sig, const DexString* name) { return elements.count(cls) > 0 && elements[cls].count(sig) > 0 && elements[cls][sig].count(name) > 0; } inline bool contains_elem(R elem) { return contains_elem(elem->get_class(), sig_getter_fn(elem), elem->get_name()); } inline DexNameWrapper<T>* emplace(T elem) { elements[elem->get_class()][sig_getter_fn(elem)][elem->get_name()] = std::unique_ptr<DexNameWrapper<T>>(elemCtr(elem)); if (mark_all_unrenamable) elements[elem->get_class()][sig_getter_fn(elem)][elem->get_name()] ->mark_unrenamable(); return elements[elem->get_class()][sig_getter_fn(elem)][elem->get_name()] .get(); } // Mirrors the map get operator, but ensures we create correct wrappers // if they don't exist inline DexNameWrapper<T>* operator[](T elem) { return contains_elem(elem) ? elements[elem->get_class()] [sig_getter_fn(elem)][elem->get_name()] .get() : emplace(elem); } // Commits all the renamings in elements to the dex by modifying the // underlying DexFields. Does in-place modification. Returns the number // of elements renamed int commit_renamings_to_dex() { std::unordered_set<T> renamed_elems; int renamings = 0; for (auto& class_itr : this->elements) { for (auto& type_itr : class_itr.second) { for (auto& name_wrap : type_itr.second) { auto& wrap = name_wrap.second; // need both because of methods ?? if (!wrap->is_modified() || !should_rename_elem(wrap->get()) || !wrap->should_commit() || strcmp(wrap->get()->get_name()->c_str(), wrap->get_name()) == 0) { TRACE(OBFUSCATE, 2, "Not committing %s to %s", SHOW(wrap->get()), wrap->get_name()); continue; } auto elem = wrap->get(); TRACE(OBFUSCATE, 2, "\tRenaming the elem 0x%p %s%s to %s external: %s can_rename: " "%s\n", elem, SHOW(sig_getter_fn(elem)), SHOW(elem), wrap->get_name(), type_class(elem->get_class())->is_external() ? "true" : "false", can_rename(elem) ? "true" : "false"); if (renamed_elems.count(elem) > 0) { TRACE(OBFUSCATE, 2, "Found elem we've already renamed %s", SHOW(elem)); } renamed_elems.insert(elem); elem->change(ref_getter_fn(wrap->get_name()), false /* rename on collision */); renamings++; } } } return renamings; } private: // Returns the def for that class and ref if it exists, nullptr otherwise T find_def(R ref, DexType* cls) { if (cls == nullptr) return nullptr; if (contains_elem(cls, sig_getter_fn(ref), ref->get_name())) { DexNameWrapper<T>* wrap = elements[cls][sig_getter_fn(ref)][ref->get_name()].get(); if (wrap->is_modified()) return wrap->get(); } return nullptr; } /** * Look up in the class and all its interfaces. */ T find_def_in_class_and_intf(R ref, DexClass* cls) { if (cls == nullptr) return nullptr; auto found_def = find_def(ref, cls->get_type()); if (found_def != nullptr) return found_def; for (auto& intf : *cls->get_interfaces()) { auto found = find_def_in_class_and_intf(ref, type_class(intf)); if (found != nullptr) return found; } return nullptr; } public: // Does a lookup over the fields we renamed in the dex to see what the // reference should be reset with. Returns nullptr if there is no mapping. // Note: we also have to look in superclasses in the case that this is a ref T def_of_ref(R ref) { DexClass* cls = type_class(ref->get_class()); while (cls && !cls->is_external()) { auto found = find_def_in_class_and_intf(ref, cls); if (found) { return found; } cls = type_class(cls->get_super_class()); } return nullptr; } // Debug print of the mapping virtual void print_elements() { TRACE(OBFUSCATE, 4, "Elem Ptr: (type/proto) class:old name -> new name"); for (auto& class_itr : elements) { for (auto& type_itr : class_itr.second) { for (auto& name_wrap : type_itr.second) { TRACE(OBFUSCATE, 2, " (%s) %s", SHOW(type_itr.first), name_wrap.second->get_printable().c_str()); /*SHOW(class_itr.first), SHOW(name_wrap.first), name_wrap.second->get_name());*/ } } } } }; using DexFieldManager = DexElemManager<DexField*, DexFieldRef*, DexFieldSpec, DexType*>; DexFieldManager new_dex_field_manager(); using DexMethodManager = DexElemManager<DexMethod*, DexMethodRef*, DexMethodSpec, DexProto*>; DexMethodManager new_dex_method_manager(); // Look at a list of members and check if there is a renamable member template <class T, class R, class S, class K> bool contains_renamable_elem(const std::vector<T>& elems, DexElemManager<T, R, S, K>& name_mapping) { for (T e : elems) if (should_rename_elem(e) && !name_mapping[e]->name_has_changed() && name_mapping[e]->should_rename()) return true; return false; } // Static state of the renamer (wrapper for args for obfuscation) template <class T> class RenamingContext { public: const std::vector<T>& elems; NameGenerator<T>& name_gen; RenamingContext(std::vector<T>& elems, NameGenerator<T>& name_gen) : elems(elems), name_gen(name_gen) {} virtual ~RenamingContext() {} // Whether or not on this pass we should rename the member virtual bool can_rename_elem(T elem) const { return can_rename(elem); } }; using FieldRenamingContext = RenamingContext<DexField*>; // Method renaming context is special because we have to make sure we don't // rename <init> or <clinit> ever regardless of configs class MethodRenamingContext : public RenamingContext<DexMethod*> { const DexString* initstr = DexString::get_string("<init>"); const DexString* clinitstr = DexString::get_string("<clinit>"); DexMethodManager& name_mapping; public: MethodRenamingContext(std::vector<DexMethod*>& elems, NameGenerator<DexMethod*>& name_gen, DexMethodManager& name_mapping) : RenamingContext<DexMethod*>(elems, name_gen), name_mapping(name_mapping) {} // For methods we have to make sure we don't rename <init> or <clinit> ever bool can_rename_elem(DexMethod* elem) const override { return should_rename_elem(elem) && elem->get_name() != initstr && elem->get_name() != clinitstr && name_mapping[elem]->should_rename(); } }; // State of the renaming that we need to modify as we rename more fields template <class T, class R, class S, class K> class ObfuscationState { public: // Ids that we've used in renaming std::unordered_set<std::string> used_ids; // Ids to avoid in renaming std::unordered_set<std::string> ids_to_avoid; virtual ~ObfuscationState() = default; virtual void populate_ids_to_avoid(DexClass* base, DexElemManager<T, R, S, K>& name_manager, const ClassHierarchy& ch) = 0; }; class FieldObfuscationState : public ObfuscationState<DexField*, DexFieldRef*, DexFieldSpec, DexType*> { public: void populate_ids_to_avoid(DexClass* base, DexFieldManager& name_manager, const ClassHierarchy& /* unused */) override { for (auto f : base->get_ifields()) ids_to_avoid.insert(name_manager[f]->get_name()); for (auto f : base->get_sfields()) ids_to_avoid.insert(name_manager[f]->get_name()); } }; enum HierarchyDirection { VisitNeither = 0, VisitSuperClasses = (1 << 0), VisitSubClasses = (1 << 1), VisitBoth = VisitSuperClasses | VisitSubClasses }; // Walks the class hierarchy starting at this class and including // superclasses (including external ones) and/or subclasses based on // the specified HierarchyDirection. // T - type of element we're visiting // Visitor - the lambda to call on each element // get_list - the function to get a list of elements to operate on from a class template <typename Visitor> void walk_hierarchy(DexClass* cls, Visitor on_member, bool visit_private, HierarchyDirection h_dir, const ClassHierarchy& ch) { if (!cls) return; auto visit = [&](DexClass* cls) { for (const auto meth : const_cast<const DexClass*>(cls)->get_dmethods()) { if (!is_private(meth) || visit_private) on_member(const_cast<DexMethod*>(meth)); } for (const auto meth : const_cast<const DexClass*>(cls)->get_vmethods()) { if (!is_private(meth) || visit_private) on_member(const_cast<DexMethod*>(meth)); } }; visit(cls); // TODO: revisit for methods to be careful around Object // We shouldn't need to visit object because we shouldn't ever be renaming // to the name of a java.lang.Object method if (h_dir & HierarchyDirection::VisitSuperClasses) { auto clazz = cls; while (clazz) { visit(clazz); if (clazz->get_super_class() == nullptr) break; clazz = type_class(clazz->get_super_class()); } } if (h_dir & HierarchyDirection::VisitSubClasses) { for (auto subcls_type : get_children(ch, cls->get_type())) { walk_hierarchy(type_class(subcls_type), on_member, visit_private, HierarchyDirection::VisitSubClasses, ch); } } } class MethodObfuscationState : public ObfuscationState<DexMethod*, DexMethodRef*, DexMethodSpec, DexProto*> { public: // Essentially what this does is walks the hierarchy collecting names of // public methods in superclasses and any methods in this class (and // subclasses if visitSubclasses is specified) void populate_ids_to_avoid(DexClass* base, DexMethodManager& name_manager, const ClassHierarchy& ch) override { auto visit_member = [&](DexMethod* m) { auto wrap(name_manager[m]); if (wrap->name_has_changed()) ids_to_avoid.insert(SHOW(wrap->get()->get_name())); ids_to_avoid.insert(wrap->get_name()); }; walk_hierarchy(base, visit_member, false, HierarchyDirection::VisitSuperClasses, ch); walk_hierarchy(base, visit_member, true, HierarchyDirection::VisitSubClasses, ch); } };