Introduce hwasan mode for linker

This mode instructs the linker to search for libraries in hwasan
subdirectories of all library search paths. This is set up to contain a
hwasan-enabled copy of libc, which is needed for HWASan programs to
operate. There are two ways this mode can be enabled:

* for native binaries, by using the linker_hwasan64 symlink as its
  interpreter
* for apps: by setting the LD_HWASAN environment variable in wrap.sh

Bug: 276930343
Change-Id: I0f4117a50091616f26947fbe37a28ee573b97ad0
diff --git a/linker/Android.bp b/linker/Android.bp
index 83077c6..020bd7d 100644
--- a/linker/Android.bp
+++ b/linker/Android.bp
@@ -374,6 +374,11 @@
     ],
 
     symlinks: ["linker_asan"],
+    arch: {
+        arm64: {
+            symlinks: ["linker_hwasan"],
+        },
+    },
     multilib: {
         lib64: {
             suffix: "64",
diff --git a/linker/linker.cpp b/linker/linker.cpp
index c5a822a..17b574f 100644
--- a/linker/linker.cpp
+++ b/linker/linker.cpp
@@ -34,6 +34,7 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/auxv.h>
 #include <sys/mman.h>
 #include <sys/param.h>
 #include <sys/vfs.h>
@@ -133,6 +134,36 @@
   nullptr
 };
 
+#if defined(__aarch64__)
+static const char* const kHwasanSystemLibDir  = "/system/lib64/hwasan";
+static const char* const kHwasanOdmLibDir     = "/odm/lib64/hwasan";
+static const char* const kHwasanVendorLibDir  = "/vendor/lib64/hwasan";
+
+// HWASan is only supported on aarch64.
+static const char* const kHwsanDefaultLdPaths[] = {
+  kHwasanSystemLibDir,
+  kSystemLibDir,
+  kHwasanOdmLibDir,
+  kOdmLibDir,
+  kHwasanVendorLibDir,
+  kVendorLibDir,
+  nullptr
+};
+
+// Is HWASAN enabled?
+static bool g_is_hwasan = false;
+#else
+static const char* const kHwsanDefaultLdPaths[] = {
+  kSystemLibDir,
+  kOdmLibDir,
+  kVendorLibDir,
+  nullptr
+};
+
+// Never any HWASan. Help the compiler remove the code we don't need.
+constexpr bool g_is_hwasan = false;
+#endif
+
 // Is ASAN enabled?
 static bool g_is_asan = false;
 
@@ -2134,26 +2165,46 @@
   }
   // End Workaround for dlopen(/system/lib/<soname>) when .so is in /apex.
 
-  std::string asan_name_holder;
+  std::string translated_name_holder;
 
+  assert(!g_is_hwasan || !g_is_asan);
   const char* translated_name = name;
   if (g_is_asan && translated_name != nullptr && translated_name[0] == '/') {
     char original_path[PATH_MAX];
     if (realpath(name, original_path) != nullptr) {
-      asan_name_holder = std::string(kAsanLibDirPrefix) + original_path;
-      if (file_exists(asan_name_holder.c_str())) {
+      translated_name_holder = std::string(kAsanLibDirPrefix) + original_path;
+      if (file_exists(translated_name_holder.c_str())) {
         soinfo* si = nullptr;
         if (find_loaded_library_by_realpath(ns, original_path, true, &si)) {
           PRINT("linker_asan dlopen NOT translating \"%s\" -> \"%s\": library already loaded", name,
-                asan_name_holder.c_str());
+                translated_name_holder.c_str());
         } else {
           PRINT("linker_asan dlopen translating \"%s\" -> \"%s\"", name, translated_name);
-          translated_name = asan_name_holder.c_str();
+          translated_name = translated_name_holder.c_str();
+        }
+      }
+    }
+  } else if (g_is_hwasan && translated_name != nullptr && translated_name[0] == '/') {
+    char original_path[PATH_MAX];
+    if (realpath(name, original_path) != nullptr) {
+      // Keep this the same as CreateHwasanPath in system/linkerconfig/modules/namespace.cc.
+      std::string path(original_path);
+      auto slash = path.rfind('/');
+      if (slash != std::string::npos || slash != path.size() - 1) {
+        translated_name_holder = path.substr(0, slash) + "/hwasan" + path.substr(slash);
+      }
+      if (!translated_name_holder.empty() && file_exists(translated_name_holder.c_str())) {
+        soinfo* si = nullptr;
+        if (find_loaded_library_by_realpath(ns, original_path, true, &si)) {
+          PRINT("linker_hwasan dlopen NOT translating \"%s\" -> \"%s\": library already loaded", name,
+                translated_name_holder.c_str());
+        } else {
+          PRINT("linker_hwasan dlopen translating \"%s\" -> \"%s\"", name, translated_name);
+          translated_name = translated_name_holder.c_str();
         }
       }
     }
   }
-
   ProtectedDataGuard guard;
   soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
   loading_trace.End();
@@ -3335,9 +3386,10 @@
   return true;
 }
 
-static std::vector<android_namespace_t*> init_default_namespace_no_config(bool is_asan) {
+static std::vector<android_namespace_t*> init_default_namespace_no_config(bool is_asan, bool is_hwasan) {
   g_default_namespace.set_isolated(false);
-  auto default_ld_paths = is_asan ? kAsanDefaultLdPaths : kDefaultLdPaths;
+  auto default_ld_paths = is_asan ? kAsanDefaultLdPaths : (
+    is_hwasan ? kHwsanDefaultLdPaths : kDefaultLdPaths);
 
   char real_path[PATH_MAX];
   std::vector<std::string> ld_default_paths;
@@ -3441,6 +3493,7 @@
   return kLdConfigFilePath;
 }
 
+
 std::vector<android_namespace_t*> init_default_namespaces(const char* executable_path) {
   g_default_namespace.set_name("(default)");
 
@@ -3454,6 +3507,16 @@
               (strcmp(bname, "linker_asan") == 0 ||
                strcmp(bname, "linker_asan64") == 0);
 
+#if defined(__aarch64__)
+  // HWASan is only supported on AArch64.
+  // The AT_SECURE restriction is because this is a debug feature that does
+  // not need to work on secure binaries, it doesn't hurt to disallow the
+  // environment variable for them, as it impacts the program execution.
+  char* hwasan_env = getenv("LD_HWASAN");
+  g_is_hwasan = (bname != nullptr &&
+              strcmp(bname, "linker_hwasan64") == 0) ||
+              (hwasan_env != nullptr && !getauxval(AT_SECURE) && strcmp(hwasan_env, "1") == 0);
+#endif
   const Config* config = nullptr;
 
   {
@@ -3461,7 +3524,7 @@
     INFO("[ Reading linker config \"%s\" ]", ld_config_file_path.c_str());
     ScopedTrace trace(("linker config " + ld_config_file_path).c_str());
     std::string error_msg;
-    if (!Config::read_binary_config(ld_config_file_path.c_str(), executable_path, g_is_asan,
+    if (!Config::read_binary_config(ld_config_file_path.c_str(), executable_path, g_is_asan, g_is_hwasan,
                                     &config, &error_msg)) {
       if (!error_msg.empty()) {
         DL_WARN("Warning: couldn't read '%s' for '%s' (using default configuration instead): %s",
@@ -3472,7 +3535,7 @@
   }
 
   if (config == nullptr) {
-    return init_default_namespace_no_config(g_is_asan);
+    return init_default_namespace_no_config(g_is_asan, g_is_hwasan);
   }
 
   const auto& namespace_configs = config->namespace_configs();
diff --git a/linker/linker_config.cpp b/linker/linker_config.cpp
index 1771e87..ad40c50 100644
--- a/linker/linker_config.cpp
+++ b/linker/linker_config.cpp
@@ -463,6 +463,7 @@
 bool Config::read_binary_config(const char* ld_config_file_path,
                                       const char* binary_realpath,
                                       bool is_asan,
+                                      bool is_hwasan,
                                       const Config** config,
                                       std::string* error_msg) {
   g_config.clear();
@@ -579,6 +580,8 @@
     // these are affected by is_asan flag
     if (is_asan) {
       property_name_prefix += ".asan";
+    } else if (is_hwasan) {
+      property_name_prefix += ".hwasan";
     }
 
     // search paths are resolved (canonicalized). This is required mainly for
diff --git a/linker/linker_config.h b/linker/linker_config.h
index fe23ec1..09fea45 100644
--- a/linker/linker_config.h
+++ b/linker/linker_config.h
@@ -166,6 +166,7 @@
   static bool read_binary_config(const char* ld_config_file_path,
                                  const char* binary_realpath,
                                  bool is_asan,
+                                 bool is_hwasan,
                                  const Config** config,
                                  std::string* error_msg);
 
diff --git a/linker/linker_config_test.cpp b/linker/linker_config_test.cpp
index acdf641..7e947f3 100644
--- a/linker/linker_config_test.cpp
+++ b/linker/linker_config_test.cpp
@@ -40,6 +40,7 @@
 #include <android-base/file.h>
 #include <android-base/scopeguard.h>
 #include <android-base/stringprintf.h>
+#include <vector>
 
 #if defined(__LP64__)
 #define ARCH_SUFFIX "64"
@@ -64,6 +65,10 @@
   "namespace.default.asan.search.paths = /data\n"
   "namespace.default.asan.search.paths += /vendor/${LIB}\n"
   "namespace.default.asan.permitted.paths = /data:/vendor\n"
+  "namespace.default.hwasan.search.paths = /vendor/${LIB}/hwasan\n"
+  "namespace.default.hwasan.search.paths += /vendor/${LIB}\n"
+  "namespace.default.hwasan.permitted.paths = /vendor/${LIB}/hwasan\n"
+  "namespace.default.hwasan.permitted.paths += /vendor/${LIB}\n"
   "namespace.default.links = system\n"
   "namespace.default.links += vndk\n"
   // irregular whitespaces are added intentionally for testing purpose
@@ -77,11 +82,17 @@
   "namespace.system.permitted.paths = /system/${LIB}\n"
   "namespace.system.asan.search.paths = /data:/system/${LIB}\n"
   "namespace.system.asan.permitted.paths = /data:/system\n"
+  "namespace.system.hwasan.search.paths = /system/${LIB}/hwasan\n"
+  "namespace.system.hwasan.search.paths += /system/${LIB}\n"
+  "namespace.system.hwasan.permitted.paths = /system/${LIB}/hwasan\n"
+  "namespace.system.hwasan.permitted.paths += /system/${LIB}\n"
   "namespace.vndk.isolated = tr\n"
   "namespace.vndk.isolated += ue\n" // should be ignored and return as 'false'.
   "namespace.vndk.search.paths = /system/${LIB}/vndk\n"
   "namespace.vndk.asan.search.paths = /data\n"
   "namespace.vndk.asan.search.paths += /system/${LIB}/vndk\n"
+  "namespace.vndk.hwasan.search.paths = /system/${LIB}/vndk/hwasan\n"
+  "namespace.vndk.hwasan.search.paths += /system/${LIB}/vndk\n"
   "namespace.vndk.links = default\n"
   "namespace.vndk.link.default.allow_all_shared_libs = true\n"
   "namespace.vndk.link.vndk_in_system.allow_all_shared_libs = true\n"
@@ -107,26 +118,50 @@
   return resolved_paths;
 }
 
-static void run_linker_config_smoke_test(bool is_asan) {
-  const std::vector<std::string> kExpectedDefaultSearchPath =
-      resolve_paths(is_asan ? std::vector<std::string>({ "/data", "/vendor/lib" ARCH_SUFFIX }) :
-                              std::vector<std::string>({ "/vendor/lib" ARCH_SUFFIX }));
+enum class SmokeTestType {
+  None,
+  Asan,
+  Hwasan,
+};
 
-  const std::vector<std::string> kExpectedDefaultPermittedPath =
-      resolve_paths(is_asan ? std::vector<std::string>({ "/data", "/vendor" }) :
-                              std::vector<std::string>({ "/vendor/lib" ARCH_SUFFIX }));
+static void run_linker_config_smoke_test(SmokeTestType type) {
+  std::vector<std::string> expected_default_search_path;
+  std::vector<std::string> expected_default_permitted_path;
+  std::vector<std::string> expected_system_search_path;
+  std::vector<std::string> expected_system_permitted_path;
+  std::vector<std::string> expected_vndk_search_path;
 
-  const std::vector<std::string> kExpectedSystemSearchPath =
-      resolve_paths(is_asan ? std::vector<std::string>({ "/data", "/system/lib" ARCH_SUFFIX }) :
-                              std::vector<std::string>({ "/system/lib" ARCH_SUFFIX }));
+  switch (type) {
+    case SmokeTestType::None:
+      expected_default_search_path = { "/vendor/lib" ARCH_SUFFIX };
+      expected_default_permitted_path = { "/vendor/lib" ARCH_SUFFIX };
+      expected_system_search_path = { "/system/lib" ARCH_SUFFIX };
+      expected_system_permitted_path = { "/system/lib" ARCH_SUFFIX };
+      expected_vndk_search_path = { "/system/lib" ARCH_SUFFIX "/vndk" };
+      break;
+    case SmokeTestType::Asan:
+      expected_default_search_path = { "/data", "/vendor/lib" ARCH_SUFFIX };
+      expected_default_permitted_path = { "/data", "/vendor" };
+      expected_system_search_path = { "/data", "/system/lib" ARCH_SUFFIX };
+      expected_system_permitted_path = { "/data", "/system" };
+      expected_vndk_search_path = { "/data", "/system/lib" ARCH_SUFFIX "/vndk" };
+      break;
+    case SmokeTestType::Hwasan:
+      expected_default_search_path = { "/vendor/lib" ARCH_SUFFIX "/hwasan", "/vendor/lib" ARCH_SUFFIX };
+      expected_default_permitted_path = { "/vendor/lib" ARCH_SUFFIX "/hwasan", "/vendor/lib" ARCH_SUFFIX };
+      expected_system_search_path = { "/system/lib" ARCH_SUFFIX "/hwasan" , "/system/lib" ARCH_SUFFIX };
+      expected_system_permitted_path = { "/system/lib" ARCH_SUFFIX "/hwasan", "/system/lib" ARCH_SUFFIX };
+      expected_vndk_search_path = { "/system/lib" ARCH_SUFFIX "/vndk/hwasan", "/system/lib" ARCH_SUFFIX "/vndk" };
+      break;
+  }
 
-  const std::vector<std::string> kExpectedSystemPermittedPath =
-      resolve_paths(is_asan ? std::vector<std::string>({ "/data", "/system" }) :
-                              std::vector<std::string>({ "/system/lib" ARCH_SUFFIX }));
-
-  const std::vector<std::string> kExpectedVndkSearchPath =
-      resolve_paths(is_asan ? std::vector<std::string>({ "/data", "/system/lib" ARCH_SUFFIX "/vndk"}) :
-                              std::vector<std::string>({ "/system/lib" ARCH_SUFFIX "/vndk"}));
+  expected_default_search_path = resolve_paths(expected_default_search_path);
+  // expected_default_permitted_path is skipped on purpose, permitted paths
+  // do not get resolved in linker_config.cpp
+  expected_system_search_path = resolve_paths(expected_system_search_path);
+  // expected_system_permitted_path is skipped on purpose, permitted paths
+  // do not get resolved in linker_config.cpp
+  expected_vndk_search_path = resolve_paths(expected_vndk_search_path);
 
   TemporaryFile tmp_file;
   close(tmp_file.fd);
@@ -149,7 +184,8 @@
   std::string error_msg;
   ASSERT_TRUE(Config::read_binary_config(tmp_file.path,
                                          executable_path.c_str(),
-                                         is_asan,
+                                         type == SmokeTestType::Asan,
+                                         type == SmokeTestType::Hwasan,
                                          &config,
                                          &error_msg)) << error_msg;
   ASSERT_TRUE(config != nullptr);
@@ -162,8 +198,8 @@
 
   ASSERT_TRUE(default_ns_config->isolated());
   ASSERT_FALSE(default_ns_config->visible());
-  ASSERT_EQ(kExpectedDefaultSearchPath, default_ns_config->search_paths());
-  ASSERT_EQ(kExpectedDefaultPermittedPath, default_ns_config->permitted_paths());
+  ASSERT_EQ(expected_default_search_path, default_ns_config->search_paths());
+  ASSERT_EQ(expected_default_permitted_path, default_ns_config->permitted_paths());
 
   const auto& default_ns_links = default_ns_config->links();
   ASSERT_EQ(2U, default_ns_links.size());
@@ -202,14 +238,14 @@
 
   ASSERT_TRUE(ns_system->isolated());
   ASSERT_TRUE(ns_system->visible());
-  ASSERT_EQ(kExpectedSystemSearchPath, ns_system->search_paths());
-  ASSERT_EQ(kExpectedSystemPermittedPath, ns_system->permitted_paths());
+  ASSERT_EQ(expected_system_search_path, ns_system->search_paths());
+  ASSERT_EQ(expected_system_permitted_path, ns_system->permitted_paths());
 
   ASSERT_TRUE(ns_vndk != nullptr) << "vndk namespace was not found";
 
   ASSERT_FALSE(ns_vndk->isolated()); // malformed bool property
   ASSERT_FALSE(ns_vndk->visible()); // undefined bool property
-  ASSERT_EQ(kExpectedVndkSearchPath, ns_vndk->search_paths());
+  ASSERT_EQ(expected_vndk_search_path, ns_vndk->search_paths());
 
   const auto& ns_vndk_links = ns_vndk->links();
   ASSERT_EQ(1U, ns_vndk_links.size());
@@ -223,11 +259,15 @@
 }
 
 TEST(linker_config, smoke) {
-  run_linker_config_smoke_test(false);
+  run_linker_config_smoke_test(SmokeTestType::None);
 }
 
 TEST(linker_config, asan_smoke) {
-  run_linker_config_smoke_test(true);
+  run_linker_config_smoke_test(SmokeTestType::Asan);
+}
+
+TEST(linker_config, hwasan_smoke) {
+  run_linker_config_smoke_test(SmokeTestType::Hwasan);
 }
 
 TEST(linker_config, ns_link_shared_libs_invalid_settings) {
@@ -259,6 +299,7 @@
   ASSERT_FALSE(Config::read_binary_config(tmp_file.path,
                                           executable_path.c_str(),
                                           false,
+                                          false,
                                           &config,
                                           &error_msg));
   ASSERT_TRUE(config == nullptr);
@@ -304,6 +345,7 @@
   ASSERT_TRUE(Config::read_binary_config(tmp_file.path,
                                          executable_path.c_str(),
                                          false,
+                                         false,
                                          &config,
                                          &error_msg)) << error_msg;