Log an error when an app loads duplicate dex files
Creating a class loader with duplicate dex files in its classpath is most
likely an unintended bug. That leads to rejecting any compiled code and
hurts performance by extracting in memory.
Test: run-test gtest
Bug: 149410951
Change-Id: Ieebb69c6bd03acbe95dd8bedb6101d70390b92d8
diff --git a/runtime/class_loader_context.cc b/runtime/class_loader_context.cc
index 98db62e..d2119db 100644
--- a/runtime/class_loader_context.cc
+++ b/runtime/class_loader_context.cc
@@ -1277,6 +1277,43 @@
(std::string_view(path).substr(/*pos*/ path.size() - suffix.size()) == suffix);
}
+// Returns true if the given dex names are mathing, false otherwise.
+static bool AreDexNameMatching(const std::string& actual_dex_name,
+ const std::string& expected_dex_name) {
+ // Compute the dex location that must be compared.
+ // We shouldn't do a naive comparison `actual_dex_name == expected_dex_name`
+ // because even if they refer to the same file, one could be encoded as a relative location
+ // and the other as an absolute one.
+ bool is_dex_name_absolute = IsAbsoluteLocation(actual_dex_name);
+ bool is_expected_dex_name_absolute = IsAbsoluteLocation(expected_dex_name);
+ bool dex_names_match = false;
+
+ if (is_dex_name_absolute == is_expected_dex_name_absolute) {
+ // If both locations are absolute or relative then compare them as they are.
+ // This is usually the case for: shared libraries and secondary dex files.
+ dex_names_match = (actual_dex_name == expected_dex_name);
+ } else if (is_dex_name_absolute) {
+ // The runtime name is absolute but the compiled name (the expected one) is relative.
+ // This is the case for split apks which depend on base or on other splits.
+ dex_names_match =
+ AbsolutePathHasRelativeSuffix(actual_dex_name, expected_dex_name);
+ } else if (is_expected_dex_name_absolute) {
+ // The runtime name is relative but the compiled name is absolute.
+ // There is no expected use case that would end up here as dex files are always loaded
+ // with their absolute location. However, be tolerant and do the best effort (in case
+ // there are unexpected new use case...).
+ dex_names_match =
+ AbsolutePathHasRelativeSuffix(expected_dex_name, actual_dex_name);
+ } else {
+ // Both locations are relative. In this case there's not much we can be sure about
+ // except that the names are the same. The checksum will ensure that the files are
+ // are same. This should not happen outside testing and manual invocations.
+ dex_names_match = (actual_dex_name == expected_dex_name);
+ }
+
+ return dex_names_match;
+}
+
bool ClassLoaderContext::ClassLoaderInfoMatch(
const ClassLoaderInfo& info,
const ClassLoaderInfo& expected_info,
@@ -1305,37 +1342,7 @@
if (verify_names) {
for (size_t k = 0; k < info.classpath.size(); k++) {
- // Compute the dex location that must be compared.
- // We shouldn't do a naive comparison `info.classpath[k] == expected_info.classpath[k]`
- // because even if they refer to the same file, one could be encoded as a relative location
- // and the other as an absolute one.
- bool is_dex_name_absolute = IsAbsoluteLocation(info.classpath[k]);
- bool is_expected_dex_name_absolute = IsAbsoluteLocation(expected_info.classpath[k]);
- bool dex_names_match = false;
-
-
- if (is_dex_name_absolute == is_expected_dex_name_absolute) {
- // If both locations are absolute or relative then compare them as they are.
- // This is usually the case for: shared libraries and secondary dex files.
- dex_names_match = (info.classpath[k] == expected_info.classpath[k]);
- } else if (is_dex_name_absolute) {
- // The runtime name is absolute but the compiled name (the expected one) is relative.
- // This is the case for split apks which depend on base or on other splits.
- dex_names_match =
- AbsolutePathHasRelativeSuffix(info.classpath[k], expected_info.classpath[k]);
- } else if (is_expected_dex_name_absolute) {
- // The runtime name is relative but the compiled name is absolute.
- // There is no expected use case that would end up here as dex files are always loaded
- // with their absolute location. However, be tolerant and do the best effort (in case
- // there are unexpected new use case...).
- dex_names_match =
- AbsolutePathHasRelativeSuffix(expected_info.classpath[k], info.classpath[k]);
- } else {
- // Both locations are relative. In this case there's not much we can be sure about
- // except that the names are the same. The checksum will ensure that the files are
- // are same. This should not happen outside testing and manual invocations.
- dex_names_match = (info.classpath[k] == expected_info.classpath[k]);
- }
+ bool dex_names_match = AreDexNameMatching(info.classpath[k], expected_info.classpath[k]);
// Compare the locations.
if (!dex_names_match) {
@@ -1393,4 +1400,34 @@
}
}
+std::vector<const DexFile*> ClassLoaderContext::CheckForDuplicateDexFiles(
+ const std::vector<const DexFile*>& dex_files_to_check) {
+ DCHECK(dex_files_open_attempted_);
+ DCHECK(dex_files_open_result_);
+
+ std::vector<const DexFile*> result;
+
+ if (special_shared_library_) {
+ return result;
+ }
+
+ std::vector<ClassLoaderInfo*> work_list;
+ work_list.push_back(class_loader_chain_.get());
+ while (!work_list.empty()) {
+ ClassLoaderInfo* info = work_list.back();
+ work_list.pop_back();
+ for (size_t k = 0; k < info->classpath.size(); k++) {
+ for (const DexFile* dex_file : dex_files_to_check) {
+ if (info->checksums[k] == dex_file->GetLocationChecksum()
+ && AreDexNameMatching(info->classpath[k], dex_file->GetLocation())) {
+ result.push_back(dex_file);
+ }
+ }
+ }
+ AddToWorkList(info, work_list);
+ }
+
+ return result;
+}
+
} // namespace art
diff --git a/runtime/class_loader_context.h b/runtime/class_loader_context.h
index 42ecd3d..31fd092 100644
--- a/runtime/class_loader_context.h
+++ b/runtime/class_loader_context.h
@@ -166,6 +166,11 @@
bool verify_names = true,
bool verify_checksums = true) const;
+ // Checks if any of the given dex files is already loaded in the current class loader context.
+ // Returns the list of duplicate dex files (empty if there are no duplicates).
+ std::vector<const DexFile*> CheckForDuplicateDexFiles(
+ const std::vector<const DexFile*>& dex_files);
+
// Creates the class loader context from the given string.
// The format: ClassLoaderType1[ClasspathElem1:ClasspathElem2...];ClassLoaderType2[...]...
// ClassLoaderType is either "PCL" (PathClassLoader) or "DLC" (DelegateLastClassLoader).
diff --git a/runtime/class_loader_context_test.cc b/runtime/class_loader_context_test.cc
index 4d7e390..e2e6075 100644
--- a/runtime/class_loader_context_test.cc
+++ b/runtime/class_loader_context_test.cc
@@ -1637,4 +1637,23 @@
ClassLoaderContext::VerificationResult::kVerifies);
}
+TEST_F(ClassLoaderContextTest, CheckForDuplicateDexFiles) {
+ jobject class_loader_a = LoadDexInPathClassLoader("Main", nullptr);
+ jobject class_loader_b =
+ LoadDexInInMemoryDexClassLoader("MyClass", class_loader_a);
+
+ std::unique_ptr<ClassLoaderContext> context =
+ CreateContextForClassLoader(class_loader_b);
+
+ std::vector<const DexFile*> result = context->CheckForDuplicateDexFiles(
+ std::vector<const DexFile*>());
+ ASSERT_EQ(0u, result.size());
+
+ std::vector<std::unique_ptr<const DexFile>> dex1 = OpenTestDexFiles("Main");
+ std::vector<const DexFile*> dex1_raw = MakeNonOwningPointerVector(dex1);
+ result = context->CheckForDuplicateDexFiles(dex1_raw);
+ ASSERT_EQ(1u, result.size());
+ ASSERT_EQ(dex1_raw[0], result[0]);
+}
+
} // namespace art
diff --git a/runtime/oat_file_manager.cc b/runtime/oat_file_manager.cc
index 22ebc73..a35f8f4 100644
--- a/runtime/oat_file_manager.cc
+++ b/runtime/oat_file_manager.cc
@@ -412,7 +412,7 @@
// Mismatched context, do the actual collision check.
break;
case ClassLoaderContext::VerificationResult::kVerifies:
- return CheckCollisionResult::kNoCollisions;
+ return CheckCollisionResult::kClassLoaderContextMatches;
}
// The class loader context does not match. Perform a full duplicate classes check.
@@ -434,7 +434,8 @@
if (kEnableAppImage && (!runtime->IsJavaDebuggable() || source_oat_file->IsDebuggable())) {
// If we verified the class loader context (skipping due to the special marker doesn't
// count), then also avoid the collision check.
- bool load_image = check_collision_result == CheckCollisionResult::kNoCollisions;
+ bool load_image = check_collision_result == CheckCollisionResult::kNoCollisions
+ || check_collision_result == CheckCollisionResult::kClassLoaderContextMatches;
// If we skipped the collision check, we need to reverify to be sure its OK to load the
// image.
if (!load_image &&
@@ -655,6 +656,41 @@
Runtime::Current()->GetJit()->RegisterDexFiles(dex_files, class_loader);
}
+ // Verify if any of the dex files being loaded is already in the class path.
+ // If so, report an error with the current stack trace.
+ // Most likely the developer didn't intend to do this because it will waste
+ // performance and memory.
+ // We perform the check only if the class loader context match failed or did
+ // not run (e.g. not that we do not run the check if we don't have an oat file).
+ if (context != nullptr
+ && check_collision_result != CheckCollisionResult::kClassLoaderContextMatches) {
+ std::vector<const DexFile*> already_exists_in_classpath =
+ context->CheckForDuplicateDexFiles(MakeNonOwningPointerVector(dex_files));
+ if (!already_exists_in_classpath.empty()) {
+ std::string duplicates = already_exists_in_classpath[0]->GetLocation();
+ for (size_t i = 1; i < already_exists_in_classpath.size(); i++) {
+ duplicates += "," + already_exists_in_classpath[i]->GetLocation();
+ }
+
+ std::ostringstream out;
+ out << "Trying to load dex files which is already loaded in the same ClassLoader hierarchy.\n"
+ << "This is a strong indication of bad ClassLoader construct which leads to poor "
+ << "performance and wastes memory.\n"
+ << "The list of duplicate dex files is: " << duplicates << "\n"
+ << "The current class loader context is: " << context->EncodeContextForOatFile("") << "\n"
+ << "Java stack trace:\n";
+
+ {
+ ScopedObjectAccess soa(self);
+ self->DumpJavaStack(out);
+ }
+
+ // We log this as an ERROR to stress the fact that this is most likely unintended.
+ // Note that ART cannot do anything about it. It is up to the app to fix their logic.
+ // Here we are trying to give a heads up on why the app might have performance issues.
+ LOG(ERROR) << out.str();
+ }
+ }
return dex_files;
}
diff --git a/runtime/oat_file_manager.h b/runtime/oat_file_manager.h
index 71eab16..9c3a38a 100644
--- a/runtime/oat_file_manager.h
+++ b/runtime/oat_file_manager.h
@@ -147,6 +147,7 @@
kSkippedVerificationDisabled,
kNoCollisions,
kPerformedHasCollisions,
+ kClassLoaderContextMatches
};
std::vector<std::unique_ptr<const DexFile>> OpenDexFilesFromOat_Impl(
diff --git a/test/1002-notify-startup/check b/test/1002-notify-startup/check
new file mode 100644
index 0000000..9d8f464
--- /dev/null
+++ b/test/1002-notify-startup/check
@@ -0,0 +1,20 @@
+#!/bin/bash
+#
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Oat file manager will complain about duplicate dex files. Ignore.
+sed -e '/.*oat_file_manager.*/d' "$2" > "$2.tmp"
+
+diff --strip-trailing-cr -q "$1" "$2.tmp" >/dev/null
diff --git a/test/146-bad-interface/check b/test/146-bad-interface/check
new file mode 100644
index 0000000..9d8f464
--- /dev/null
+++ b/test/146-bad-interface/check
@@ -0,0 +1,20 @@
+#!/bin/bash
+#
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Oat file manager will complain about duplicate dex files. Ignore.
+sed -e '/.*oat_file_manager.*/d' "$2" > "$2.tmp"
+
+diff --strip-trailing-cr -q "$1" "$2.tmp" >/dev/null
diff --git a/test/688-shared-library/check b/test/688-shared-library/check
index 0b6c9e4..55847cd 100644
--- a/test/688-shared-library/check
+++ b/test/688-shared-library/check
@@ -16,6 +16,7 @@
# Finalizers of DexFile will complain not being able to close
# the main dex file, as it's still open. That's OK to ignore.
-sed -e '/^E\/System/d' "$2" > "$2.tmp"
+# Oat file manager will also complain about duplicate dex files. Ignore.
+sed -e '/^E\/System/d' "$2" | sed -e '/.*oat_file_manager.*/d' > "$2.tmp"
diff --strip-trailing-cr -q "$1" "$2.tmp" >/dev/null
diff --git a/test/726-load-duplicate-dex-files/check b/test/726-load-duplicate-dex-files/check
new file mode 100644
index 0000000..117bd01
--- /dev/null
+++ b/test/726-load-duplicate-dex-files/check
@@ -0,0 +1,28 @@
+#!/bin/bash
+#
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Content somewhere inside the output.
+grep 'Trying to load dex files which is already loaded in the same ClassLoader hierarchy.' "$2" >/dev/null
+MSG_FOUND=$?
+
+if [[ "$HEAD" != "$EXPECTED_HEAD" || $MSG_FOUND -ne 0 ]] ; then
+ # Print out the log and return with error.
+ cat "$2"
+ exit 1
+fi
+
+# Success.
+exit 0
diff --git a/test/726-load-duplicate-dex-files/expected.txt b/test/726-load-duplicate-dex-files/expected.txt
new file mode 100644
index 0000000..ef3043a
--- /dev/null
+++ b/test/726-load-duplicate-dex-files/expected.txt
@@ -0,0 +1 @@
+Trying to load dex files which is already loaded in the same ClassLoader hierarchy.
diff --git a/test/726-load-duplicate-dex-files/info.txt b/test/726-load-duplicate-dex-files/info.txt
new file mode 100644
index 0000000..c055acf
--- /dev/null
+++ b/test/726-load-duplicate-dex-files/info.txt
@@ -0,0 +1 @@
+Tests that duplicate dex files get reported.
diff --git a/test/726-load-duplicate-dex-files/src/Main.java b/test/726-load-duplicate-dex-files/src/Main.java
new file mode 100644
index 0000000..f156fb9
--- /dev/null
+++ b/test/726-load-duplicate-dex-files/src/Main.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import dalvik.system.PathClassLoader;
+
+class Main {
+ static final String DEX_FILE = System.getenv("DEX_LOCATION")
+ + "/726-load-duplicate-dex-files.jar";
+ static final String DEX_FILES = DEX_FILE + ":" + DEX_FILE;
+
+ public static void main(String[] args) throws Exception {
+ // Adding duplicate dex files to the classpath will trigger a runtime warning.
+ PathClassLoader p = new PathClassLoader(DEX_FILES, Main.class.getClassLoader());
+ }
+}