Resolve symlinks when checking class loader context.

There is some inconsistency in the build system. When /system_ext is
a symlink to /system/system_ext and there is a shared library in
/system_ext, the oat file generated by dexpreopt has the dex location
starting with /system/system_ext, while the metadata in
/system_ext/etc/permissions has the dex location starting with
/system_ext. This inconsitency causes the class loader context to fail.

This CL works around the inconsistency by resolving symlinks when
checking class loader context.

Bug: 247914536
Test: m test-art-host-gtest-art_runtime_tests
Change-Id: I7a177bda08e179c3d5417108be6411a84fdd5c40
diff --git a/libartbase/base/common_art_test.cc b/libartbase/base/common_art_test.cc
index ce02299..1aba547 100644
--- a/libartbase/base/common_art_test.cc
+++ b/libartbase/base/common_art_test.cc
@@ -73,15 +73,7 @@
 
 ScratchDir::~ScratchDir() {
   if (!keep_files_) {
-    // Recursively delete the directory and all its content.
-    nftw(path_.c_str(), [](const char* name, const struct stat*, int type, struct FTW *) {
-      if (type == FTW_F) {
-        unlink(name);
-      } else if (type == FTW_DP) {
-        rmdir(name);
-      }
-      return 0;
-    }, 256 /* max open file descriptors */, FTW_DEPTH);
+    std::filesystem::remove_all(path_);
   }
 }
 
diff --git a/runtime/class_loader_context.cc b/runtime/class_loader_context.cc
index 31c310e..2ab905b 100644
--- a/runtime/class_loader_context.cc
+++ b/runtime/class_loader_context.cc
@@ -18,9 +18,9 @@
 
 #include <algorithm>
 
-#include <android-base/parseint.h>
-#include <android-base/strings.h>
-
+#include "android-base/file.h"
+#include "android-base/parseint.h"
+#include "android-base/strings.h"
 #include "art_field-inl.h"
 #include "base/casts.h"
 #include "base/dchecked_vector.h"
@@ -1346,6 +1346,28 @@
          (std::string_view(path).substr(/*pos*/ path.size() - suffix.size()) == suffix);
 }
 
+// Resolves symlinks and returns the canonicalized absolute path. Returns relative path as is.
+static std::string ResolveIfAbsolutePath(const std::string& path) {
+  if (!IsAbsoluteLocation(path)) {
+    return path;
+  }
+
+  std::string filename = path;
+  std::string multi_dex_suffix;
+  size_t pos = filename.find(DexFileLoader::kMultiDexSeparator);
+  if (pos != std::string::npos) {
+    multi_dex_suffix = filename.substr(pos);
+    filename.resize(pos);
+  }
+
+  std::string resolved_filename;
+  if (!android::base::Realpath(filename, &resolved_filename)) {
+    PLOG(ERROR) << "Unable to resolve path '" << path << "'";
+    return path;
+  }
+  return resolved_filename + multi_dex_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) {
@@ -1356,28 +1378,30 @@
   bool is_dex_name_absolute = IsAbsoluteLocation(actual_dex_name);
   bool is_expected_dex_name_absolute = IsAbsoluteLocation(expected_dex_name);
   bool dex_names_match = false;
+  std::string resolved_actual_dex_name = ResolveIfAbsolutePath(actual_dex_name);
+  std::string resolved_expected_dex_name = ResolveIfAbsolutePath(expected_dex_name);
 
   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);
+    dex_names_match = (resolved_actual_dex_name == resolved_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);
+        AbsolutePathHasRelativeSuffix(resolved_actual_dex_name, resolved_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);
+        AbsolutePathHasRelativeSuffix(resolved_expected_dex_name, resolved_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);
+    dex_names_match = (resolved_actual_dex_name == resolved_expected_dex_name);
   }
 
   return dex_names_match;
diff --git a/runtime/class_loader_context_test.cc b/runtime/class_loader_context_test.cc
index 073b4b6..fd14476 100644
--- a/runtime/class_loader_context_test.cc
+++ b/runtime/class_loader_context_test.cc
@@ -18,6 +18,10 @@
 
 #include <gtest/gtest.h>
 
+#include <filesystem>
+#include <fstream>
+
+#include "android-base/stringprintf.h"
 #include "android-base/strings.h"
 #include "art_field-inl.h"
 #include "base/dchecked_vector.h"
@@ -42,6 +46,19 @@
 
 class ClassLoaderContextTest : public CommonRuntimeTest {
  public:
+  void SetUp() override {
+    CommonRuntimeTest::SetUp();
+    scratch_dir_ = std::make_unique<ScratchDir>();
+    scratch_path_ = scratch_dir_->GetPath();
+    // Remove the trailing '/';
+    scratch_path_.resize(scratch_path_.length() - 1);
+  }
+
+  void TearDown() override {
+    scratch_dir_.reset();
+    CommonRuntimeTest::TearDown();
+  }
+
   void VerifyContextSize(ClassLoaderContext* context, size_t expected_size) {
     ASSERT_TRUE(context != nullptr);
     ASSERT_EQ(expected_size, context->GetParentChainSize());
@@ -340,6 +357,9 @@
     return true;
   }
 
+  std::unique_ptr<ScratchDir> scratch_dir_;
+  std::string scratch_path_;
+
  private:
   void VerifyClassLoaderInfo(ClassLoaderContext* context,
                              size_t index,
@@ -1600,6 +1620,28 @@
             ClassLoaderContext::VerificationResult::kVerifies);
 }
 
+TEST_F(ClassLoaderContextTest, VerifyClassLoaderContextMatchAfterResolvingSymlinks) {
+  {
+    std::ofstream ofs(scratch_path_ + "/foo.jar");
+    ASSERT_TRUE(ofs);
+  }
+  std::filesystem::create_directory_symlink(scratch_path_, scratch_path_ + "/bar");
+
+  std::string context_spec =
+      android::base::StringPrintf("PCL[%s/foo.jar*123:%s/foo.jar!classes2.dex*456]",
+                                  scratch_path_.c_str(),
+                                  scratch_path_.c_str());
+  std::unique_ptr<ClassLoaderContext> context = ParseContextWithChecksums(context_spec);
+  PretendContextOpenedDexFilesForChecksums(context.get());
+
+  std::string context_spec_with_symlinks =
+      android::base::StringPrintf("PCL[%s/bar/foo.jar*123:%s/bar/foo.jar!classes2.dex*456]",
+                                  scratch_path_.c_str(),
+                                  scratch_path_.c_str());
+  ASSERT_EQ(context->VerifyClassLoaderContextMatch(context_spec_with_symlinks),
+            ClassLoaderContext::VerificationResult::kVerifies);
+}
+
 TEST_F(ClassLoaderContextTest, VerifyClassLoaderContextMatchAfterEncoding) {
   jobject class_loader_a = LoadDexInPathClassLoader("ForClassLoaderA", nullptr);
   jobject class_loader_b = LoadDexInDelegateLastClassLoader("ForClassLoaderB", class_loader_a);