hiddenapi: Handle visibility through inheritance
Framework uses a pattern in which a public method is defined in
a package-private parent class, but exposed outside of package
via a public child class. Improve the definition of stub member
visibility in `hiddenapi` to understand this pattern.
Bug: 76424618
Test: m appcompat, SpannableStringInternal.length() public
Change-Id: Ie9c1653142a4991d6de5460be5abd074aa03b0a0
diff --git a/tools/hiddenapi/hiddenapi.cc b/tools/hiddenapi/hiddenapi.cc
index 0381381..e4bec06 100644
--- a/tools/hiddenapi/hiddenapi.cc
+++ b/tools/hiddenapi/hiddenapi.cc
@@ -75,7 +75,9 @@
UsageError("");
UsageError(" Command \"list\": dump lists of public and private API");
UsageError(" --boot-dex=<filename>: dex file which belongs to boot class path");
- UsageError(" --stub-dex=<filename>: dex/apk file which belongs to SDK API stubs");
+ UsageError(" --stub-classpath=<filenames>: colon-separated list of dex/apk files");
+ UsageError(" which form API stubs of boot class path. Multiple classpaths can");
+ UsageError(" be specified");
UsageError("");
UsageError(" --out-public=<filename>: output file for a list of all public APIs");
UsageError(" --out-private=<filename>: output file for a list of all private APIs");
@@ -121,7 +123,7 @@
return list;
}
- inline bool IsVisible() const { return HasAccessFlags(kAccPublic); }
+ inline bool IsPublic() const { return HasAccessFlags(kAccPublic); }
inline bool Equals(const DexClass& other) const {
bool equals = GetDescriptor() == other.GetDescriptor();
@@ -178,11 +180,10 @@
inline bool IsMethod() const { return it_.IsAtMethod(); }
inline bool IsVirtualMethod() const { return it_.IsAtVirtualMethod(); }
+ inline bool IsConstructor() const { return IsMethod() && HasAccessFlags(kAccConstructor); }
- // Returns true if the member is public/protected and is in a public class.
- inline bool IsVisible() const {
- return GetDeclaringClass().IsVisible() &&
- (HasAccessFlags(kAccPublic) || HasAccessFlags(kAccProtected));
+ inline bool IsPublicOrProtected() const {
+ return HasAccessFlags(kAccPublic) || HasAccessFlags(kAccProtected);
}
// Constructs a string with a unique signature of this class member.
@@ -344,6 +345,24 @@
return ForEachResolvableMember_Impl(other, fn) != ResolutionResult::kNotFound;
}
+ // Returns true if this class contains at least one member matching `other`.
+ bool HasMatchingMember(const DexMember& other) {
+ return ForEachMatchingMember(
+ other, [](const DexMember&) { return true; }) != ResolutionResult::kNotFound;
+ }
+
+ // Recursively iterates over all subclasses of this class and invokes `fn`
+ // on each one. If `fn` returns false for a particular subclass, exploring its
+ // subclasses is skipped.
+ template<typename Fn>
+ void ForEachSubClass(Fn fn) {
+ for (HierarchyClass* subclass : extended_by_) {
+ if (fn(subclass)) {
+ subclass->ForEachSubClass(fn);
+ }
+ }
+ }
+
private:
// Result of resolution which takes into account whether the member was found
// for the first time or not. This is just a performance optimization to prevent
@@ -438,7 +457,7 @@
class Hierarchy FINAL {
public:
- explicit Hierarchy(ClassPath& class_path) : class_path_(class_path) {
+ explicit Hierarchy(ClassPath& classpath) : classpath_(classpath) {
BuildClassHierarchy();
}
@@ -454,6 +473,48 @@
return (klass != nullptr) && klass->ForEachResolvableMember(other, fn);
}
+ // Returns true if `member`, which belongs to this classpath, is visible to
+ // code in child class loaders.
+ bool IsMemberVisible(const DexMember& member) {
+ if (!member.IsPublicOrProtected()) {
+ // Member is private or package-private. Cannot be visible.
+ return false;
+ } else if (member.GetDeclaringClass().IsPublic()) {
+ // Member is public or protected, and class is public. It must be visible.
+ return true;
+ } else if (member.IsConstructor()) {
+ // Member is public or protected constructor and class is not public.
+ // Must be hidden because it cannot be implicitly exposed by a subclass.
+ return false;
+ } else {
+ // Member is public or protected method, but class is not public. Check if
+ // it is exposed through a public subclass.
+ // Example code (`foo` exposed by ClassB):
+ // class ClassA { public void foo() { ... } }
+ // public class ClassB extends ClassA {}
+ HierarchyClass* klass = FindClass(member.GetDeclaringClass().GetDescriptor());
+ CHECK(klass != nullptr);
+ bool visible = false;
+ klass->ForEachSubClass([&visible, &member](HierarchyClass* subclass) {
+ if (subclass->HasMatchingMember(member)) {
+ // There is a member which matches `member` in `subclass`, either
+ // a virtual method overriding `member` or a field overshadowing
+ // `member`. In either case, `member` remains hidden.
+ CHECK(member.IsVirtualMethod() || !member.IsMethod());
+ return false; // do not explore deeper
+ } else if (subclass->GetOneDexClass().IsPublic()) {
+ // `subclass` inherits and exposes `member`.
+ visible = true;
+ return false; // do not explore deeper
+ } else {
+ // `subclass` inherits `member` but does not expose it.
+ return true; // explore deeper
+ }
+ });
+ return visible;
+ }
+ }
+
private:
HierarchyClass* FindClass(const std::string& descriptor) {
auto it = classes_.find(descriptor);
@@ -467,7 +528,7 @@
void BuildClassHierarchy() {
// Create one HierarchyClass entry in `classes_` per class descriptor
// and add all DexClass objects with the same descriptor to that entry.
- class_path_.ForEachDexClass([this](DexClass& klass) {
+ classpath_.ForEachDexClass([this](DexClass& klass) {
classes_[klass.GetDescriptor()].AddDexClass(klass);
});
@@ -494,7 +555,7 @@
}
}
- ClassPath& class_path_;
+ ClassPath& classpath_;
std::map<std::string, HierarchyClass> classes_;
};
@@ -547,8 +608,9 @@
const StringPiece option(argv[i]);
if (option.starts_with("--boot-dex=")) {
boot_dex_paths_.push_back(option.substr(strlen("--boot-dex=")).ToString());
- } else if (option.starts_with("--stub-dex=")) {
- stub_dex_paths_.push_back(option.substr(strlen("--stub-dex=")).ToString());
+ } else if (option.starts_with("--stub-classpath=")) {
+ stub_classpaths_.push_back(android::base::Split(
+ option.substr(strlen("--stub-classpath=")).ToString(), ":"));
} else if (option.starts_with("--out-public=")) {
out_public_path_ = option.substr(strlen("--out-public=")).ToString();
} else if (option.starts_with("--out-private=")) {
@@ -578,10 +640,10 @@
OpenApiFile(blacklist_path_, api_list, HiddenApiAccessFlags::kBlacklist);
// Open all dex files.
- ClassPath boot_class_path(boot_dex_paths_, /* open_writable */ true);
+ ClassPath boot_classpath(boot_dex_paths_, /* open_writable */ true);
// Set access flags of all members.
- boot_class_path.ForEachDexMember([&api_list](DexMember& boot_member) {
+ boot_classpath.ForEachDexMember([&api_list](DexMember& boot_member) {
auto it = api_list.find(boot_member.GetApiEntry());
if (it == api_list.end()) {
boot_member.SetHidden(HiddenApiAccessFlags::kWhitelist);
@@ -590,7 +652,7 @@
}
});
- boot_class_path.UpdateDexChecksums();
+ boot_classpath.UpdateDexChecksums();
}
void OpenApiFile(const std::string& path,
@@ -614,7 +676,7 @@
void ListApi() {
if (boot_dex_paths_.empty()) {
Usage("No boot DEX files specified");
- } else if (stub_dex_paths_.empty()) {
+ } else if (stub_classpaths_.empty()) {
Usage("No stub DEX files specified");
} else if (out_public_path_.empty()) {
Usage("No public API output path specified");
@@ -630,39 +692,42 @@
std::set<std::string> unresolved;
// Open all dex files.
- ClassPath stub_class_path(stub_dex_paths_, /* open_writable */ false);
- ClassPath boot_class_path(boot_dex_paths_, /* open_writable */ false);
- Hierarchy boot_hierarchy(boot_class_path);
+ ClassPath boot_classpath(boot_dex_paths_, /* open_writable */ false);
+ Hierarchy boot_hierarchy(boot_classpath);
// Mark all boot dex members private.
- boot_class_path.ForEachDexMember([&boot_members](DexMember& boot_member) {
+ boot_classpath.ForEachDexMember([&boot_members](DexMember& boot_member) {
boot_members[boot_member.GetApiEntry()] = false;
});
// Resolve each SDK dex member against the framework and mark it white.
- stub_class_path.ForEachDexMember(
- [&boot_hierarchy, &boot_members, &unresolved](DexMember& stub_member) {
- if (!stub_member.IsVisible()) {
- // Typically fake constructors and inner-class `this` fields.
- return;
- }
- bool resolved = boot_hierarchy.ForEachResolvableMember(
- stub_member,
- [&boot_members](DexMember& boot_member) {
- std::string entry = boot_member.GetApiEntry();
- auto it = boot_members.find(entry);
- CHECK(it != boot_members.end());
- if (it->second) {
- return false; // has been marked before
- } else {
- it->second = true;
- return true; // marked for the first time
- }
- });
- if (!resolved) {
- unresolved.insert(stub_member.GetApiEntry());
- }
- });
+ for (const std::vector<std::string>& stub_classpath_dex : stub_classpaths_) {
+ ClassPath stub_classpath(stub_classpath_dex, /* open_writable */ false);
+ Hierarchy stub_hierarchy(stub_classpath);
+ stub_classpath.ForEachDexMember(
+ [&stub_hierarchy, &boot_hierarchy, &boot_members, &unresolved](DexMember& stub_member) {
+ if (!stub_hierarchy.IsMemberVisible(stub_member)) {
+ // Typically fake constructors and inner-class `this` fields.
+ return;
+ }
+ bool resolved = boot_hierarchy.ForEachResolvableMember(
+ stub_member,
+ [&boot_members](DexMember& boot_member) {
+ std::string entry = boot_member.GetApiEntry();
+ auto it = boot_members.find(entry);
+ CHECK(it != boot_members.end());
+ if (it->second) {
+ return false; // has been marked before
+ } else {
+ it->second = true;
+ return true; // marked for the first time
+ }
+ });
+ if (!resolved) {
+ unresolved.insert(stub_member.GetApiEntry());
+ }
+ });
+ }
// Print errors.
for (const std::string& str : unresolved) {
@@ -685,7 +750,10 @@
// Paths to DEX files which should be processed.
std::vector<std::string> boot_dex_paths_;
- std::vector<std::string> stub_dex_paths_;
+
+ // Set of public API stub classpaths. Each classpath is formed by a list
+ // of DEX/APK files in the order they appear on the classpath.
+ std::vector<std::vector<std::string>> stub_classpaths_;
// Paths to text files which contain the lists of API members.
std::string light_greylist_path_;