OSDN Git Service

Allow sharing the RELRO section via a file.
authorTorne (Richard Coles) <torne@google.com>
Thu, 27 Feb 2014 13:18:00 +0000 (13:18 +0000)
committerTorne (Richard Coles) <torne@google.com>
Thu, 17 Apr 2014 13:30:46 +0000 (14:30 +0100)
Add flags and a file descriptor to android_dlopen_ext() to allow writing
the RELRO section of the loaded library to a file after relocation
processing, and to allow mapping identical pages from the file over the
top of relocated memory in another process. Explicitly comparing the
pages is required in case a page contains a reference to a symbol
defined in another library loaded at a random base address.

Bug: 13005501
Change-Id: Ibb5b2d384edfaa5acf3e97a5f8b6115c10497a1e

libc/include/android/dlext.h
linker/linker.cpp
linker/linker_phdr.cpp
linker/linker_phdr.h
tests/dlext_test.cpp

index 3ae0965..90962fa 100644 (file)
@@ -36,15 +36,31 @@ enum {
    */
   ANDROID_DLEXT_RESERVED_ADDRESS_HINT = 0x2,
 
+  /* When set, write the GNU RELRO section of the mapped library to relro_fd
+   * after relocation has been performed, to allow it to be reused by another
+   * process loading the same library at the same address. This implies
+   * ANDROID_DLEXT_USE_RELRO.
+   */
+  ANDROID_DLEXT_WRITE_RELRO           = 0x4,
+
+  /* When set, compare the GNU RELRO section of the mapped library to relro_fd
+   * after relocation has been performed, and replace any relocated pages that
+   * are identical with a version mapped from the file.
+   */
+  ANDROID_DLEXT_USE_RELRO             = 0x8,
+
   /* Mask of valid bits */
   ANDROID_DLEXT_VALID_FLAG_BITS       = ANDROID_DLEXT_RESERVED_ADDRESS |
-                                        ANDROID_DLEXT_RESERVED_ADDRESS_HINT,
+                                        ANDROID_DLEXT_RESERVED_ADDRESS_HINT |
+                                        ANDROID_DLEXT_WRITE_RELRO |
+                                        ANDROID_DLEXT_USE_RELRO,
 };
 
 typedef struct {
   int     flags;
   void*   reserved_addr;
   size_t  reserved_size;
+  int     relro_fd;
 } android_dlextinfo;
 
 extern void* android_dlopen_ext(const char* filename, int flag, const android_dlextinfo* extinfo);
index 85ef63b..40237f3 100755 (executable)
@@ -65,7 +65,7 @@
  *   and NOEXEC
  */
 
-static bool soinfo_link_image(soinfo* si);
+static bool soinfo_link_image(soinfo* si, const android_dlextinfo* extinfo);
 static ElfW(Addr) get_elf_exec_load_bias(const ElfW(Ehdr)* elf);
 
 // We can't use malloc(3) in the dynamic linker. We use a linked list of anonymous
@@ -760,7 +760,7 @@ static soinfo* find_library_internal(const char* name, const android_dlextinfo*
   TRACE("[ find_library_internal base=%p size=%zu name='%s' ]",
         reinterpret_cast<void*>(si->base), si->size, si->name);
 
-  if (!soinfo_link_image(si)) {
+  if (!soinfo_link_image(si, extinfo)) {
     munmap(reinterpret_cast<void*>(si->base), si->size);
     soinfo_free(si);
     return NULL;
@@ -1566,7 +1566,7 @@ static int nullify_closed_stdio() {
     return return_value;
 }
 
-static bool soinfo_link_image(soinfo* si) {
+static bool soinfo_link_image(soinfo* si, const android_dlextinfo* extinfo) {
     /* "base" might wrap around UINT32_MAX. */
     ElfW(Addr) base = si->load_bias;
     const ElfW(Phdr)* phdr = si->phdr;
@@ -1902,6 +1902,23 @@ static bool soinfo_link_image(soinfo* si) {
         return false;
     }
 
+    /* Handle serializing/sharing the RELRO segment */
+    if (extinfo && (extinfo->flags & ANDROID_DLEXT_WRITE_RELRO)) {
+      if (phdr_table_serialize_gnu_relro(si->phdr, si->phnum, si->load_bias,
+                                         extinfo->relro_fd) < 0) {
+        DL_ERR("failed serializing GNU RELRO section for \"%s\": %s",
+               si->name, strerror(errno));
+        return false;
+      }
+    } else if (extinfo && (extinfo->flags & ANDROID_DLEXT_USE_RELRO)) {
+      if (phdr_table_map_gnu_relro(si->phdr, si->phnum, si->load_bias,
+                                   extinfo->relro_fd) < 0) {
+        DL_ERR("failed mapping GNU RELRO section for \"%s\": %s",
+               si->name, strerror(errno));
+        return false;
+      }
+    }
+
     notify_gdb_of_load(si);
     return true;
 }
@@ -2055,7 +2072,7 @@ static ElfW(Addr) __linker_init_post_relocation(KernelArgumentBlock& args, ElfW(
 
     somain = si;
 
-    if (!soinfo_link_image(si)) {
+    if (!soinfo_link_image(si, NULL)) {
         __libc_format_fd(2, "CANNOT LINK EXECUTABLE: %s\n", linker_get_error_buffer());
         exit(EXIT_FAILURE);
     }
@@ -2172,7 +2189,7 @@ extern "C" ElfW(Addr) __linker_init(void* raw_args) {
   linker_so.phnum = elf_hdr->e_phnum;
   linker_so.flags |= FLAG_LINKER;
 
-  if (!soinfo_link_image(&linker_so)) {
+  if (!soinfo_link_image(&linker_so, NULL)) {
     // It would be nice to print an error message, but if the linker
     // can't link itself, there's no guarantee that we'll be able to
     // call write() (because it involves a GOT reference). We may as
index 7e97aa9..cfeab96 100644 (file)
@@ -31,6 +31,9 @@
 #include <errno.h>
 #include <machine/exec.h>
 #include <sys/mman.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
 
 #include "linker.h"
 #include "linker_debug.h"
@@ -523,6 +526,129 @@ int phdr_table_protect_gnu_relro(const ElfW(Phdr)* phdr_table, size_t phdr_count
   return _phdr_table_set_gnu_relro_prot(phdr_table, phdr_count, load_bias, PROT_READ);
 }
 
+/* Serialize the GNU relro segments to the given file descriptor. This can be
+ * performed after relocations to allow another process to later share the
+ * relocated segment, if it was loaded at the same address.
+ *
+ * Input:
+ *   phdr_table  -> program header table
+ *   phdr_count  -> number of entries in tables
+ *   load_bias   -> load bias
+ *   fd          -> writable file descriptor to use
+ * Return:
+ *   0 on error, -1 on failure (error code in errno).
+ */
+int phdr_table_serialize_gnu_relro(const ElfW(Phdr)* phdr_table, size_t phdr_count, ElfW(Addr) load_bias,
+                                   int fd) {
+  const ElfW(Phdr)* phdr = phdr_table;
+  const ElfW(Phdr)* phdr_limit = phdr + phdr_count;
+  ssize_t file_offset = 0;
+
+  for (phdr = phdr_table; phdr < phdr_limit; phdr++) {
+    if (phdr->p_type != PT_GNU_RELRO) {
+      continue;
+    }
+
+    ElfW(Addr) seg_page_start = PAGE_START(phdr->p_vaddr) + load_bias;
+    ElfW(Addr) seg_page_end   = PAGE_END(phdr->p_vaddr + phdr->p_memsz) + load_bias;
+    ssize_t size = seg_page_end - seg_page_start;
+
+    ssize_t written = TEMP_FAILURE_RETRY(write(fd, reinterpret_cast<void*>(seg_page_start), size));
+    if (written != size) {
+      return -1;
+    }
+    void* map = mmap(reinterpret_cast<void*>(seg_page_start), size, PROT_READ,
+                     MAP_PRIVATE|MAP_FIXED, fd, file_offset);
+    if (map == MAP_FAILED) {
+      return -1;
+    }
+    file_offset += size;
+  }
+  return 0;
+}
+
+/* Where possible, replace the GNU relro segments with mappings of the given
+ * file descriptor. This can be performed after relocations to allow a file
+ * previously created by phdr_table_serialize_gnu_relro in another process to
+ * replace the dirty relocated pages, saving memory, if it was loaded at the
+ * same address. We have to compare the data before we map over it, since some
+ * parts of the relro segment may not be identical due to other libraries in
+ * the process being loaded at different addresses.
+ *
+ * Input:
+ *   phdr_table  -> program header table
+ *   phdr_count  -> number of entries in tables
+ *   load_bias   -> load bias
+ *   fd          -> readable file descriptor to use
+ * Return:
+ *   0 on error, -1 on failure (error code in errno).
+ */
+int phdr_table_map_gnu_relro(const ElfW(Phdr)* phdr_table, size_t phdr_count, ElfW(Addr) load_bias,
+                             int fd) {
+  // Map the file at a temporary location so we can compare its contents.
+  struct stat file_stat;
+  if (TEMP_FAILURE_RETRY(fstat(fd, &file_stat)) != 0) {
+    return -1;
+  }
+  off_t file_size = file_stat.st_size;
+  void* temp_mapping = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
+  if (temp_mapping == MAP_FAILED) {
+    return -1;
+  }
+  size_t file_offset = 0;
+
+  // Iterate over the relro segments and compare/remap the pages.
+  const ElfW(Phdr)* phdr = phdr_table;
+  const ElfW(Phdr)* phdr_limit = phdr + phdr_count;
+
+  for (phdr = phdr_table; phdr < phdr_limit; phdr++) {
+    if (phdr->p_type != PT_GNU_RELRO) {
+      continue;
+    }
+
+    ElfW(Addr) seg_page_start = PAGE_START(phdr->p_vaddr) + load_bias;
+    ElfW(Addr) seg_page_end   = PAGE_END(phdr->p_vaddr + phdr->p_memsz) + load_bias;
+
+    char* file_base = static_cast<char*>(temp_mapping) + file_offset;
+    char* mem_base = reinterpret_cast<char*>(seg_page_start);
+    size_t match_offset = 0;
+    size_t size = seg_page_end - seg_page_start;
+
+    while (match_offset < size) {
+      // Skip over dissimilar pages.
+      while (match_offset < size &&
+             memcmp(mem_base + match_offset, file_base + match_offset, PAGE_SIZE) != 0) {
+        match_offset += PAGE_SIZE;
+      }
+
+      // Count similar pages.
+      size_t mismatch_offset = match_offset;
+      while (mismatch_offset < size &&
+             memcmp(mem_base + mismatch_offset, file_base + mismatch_offset, PAGE_SIZE) == 0) {
+        mismatch_offset += PAGE_SIZE;
+      }
+
+      // Map over similar pages.
+      if (mismatch_offset > match_offset) {
+        void* map = mmap(mem_base + match_offset, mismatch_offset - match_offset,
+                         PROT_READ, MAP_PRIVATE|MAP_FIXED, fd, match_offset);
+        if (map == MAP_FAILED) {
+          munmap(temp_mapping, file_size);
+          return -1;
+        }
+      }
+
+      match_offset = mismatch_offset;
+    }
+
+    // Add to the base file offset in case there are multiple relro segments.
+    file_offset += size;
+  }
+  munmap(temp_mapping, file_size);
+  return 0;
+}
+
+
 #if defined(__arm__)
 
 #  ifndef PT_ARM_EXIDX
index 430c6ec..611f1a7 100644 (file)
@@ -89,6 +89,11 @@ int phdr_table_unprotect_segments(const ElfW(Phdr)* phdr_table, size_t phdr_coun
 
 int phdr_table_protect_gnu_relro(const ElfW(Phdr)* phdr_table, size_t phdr_count, ElfW(Addr) load_bias);
 
+int phdr_table_serialize_gnu_relro(const ElfW(Phdr)* phdr_table, size_t phdr_count, ElfW(Addr) load_bias,
+                                   int fd);
+
+int phdr_table_map_gnu_relro(const ElfW(Phdr)* phdr_table, size_t phdr_count, ElfW(Addr) load_bias,
+                             int fd);
 
 #if defined(__arm__)
 int phdr_table_get_arm_exidx(const ElfW(Phdr)* phdr_table, size_t phdr_count, ElfW(Addr) load_bias,
index 299b408..14dff2b 100644 (file)
 #include <gtest/gtest.h>
 
 #include <dlfcn.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
 #include <android/dlext.h>
 #include <sys/mman.h>
+#include <sys/wait.h>
 
 
 #define ASSERT_DL_NOTNULL(ptr) \
 #define ASSERT_DL_ZERO(i) \
     ASSERT_EQ(0, i) << "dlerror: " << dlerror()
 
+#define ASSERT_NOERROR(i) \
+    ASSERT_NE(-1, i) << "errno: " << strerror(errno)
+
 
 typedef int (*fn)(void);
 #define LIBNAME "libdlext_test.so"
 #define LIBSIZE 1024*1024 // how much address space to reserve for it
+#define RELRO_FILE "/data/local/tmp/libdlext_test.relro"
 
 
 class DlExtTest : public ::testing::Test {
@@ -134,3 +144,49 @@ TEST_F(DlExtTest, ReservedHintTooSmall) {
                             reinterpret_cast<char*>(start) + PAGE_SIZE));
   EXPECT_EQ(4, f());
 }
+
+TEST_F(DlExtTest, RelroShareChildWrites) {
+  void* start = mmap(NULL, LIBSIZE, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS,
+                     -1, 0);
+  ASSERT_TRUE(start != MAP_FAILED);
+  android_dlextinfo extinfo;
+  extinfo.reserved_addr = start;
+  extinfo.reserved_size = LIBSIZE;
+
+  int relro_fd;
+  relro_fd = open(RELRO_FILE, O_CREAT | O_RDWR | O_TRUNC, 0644);
+  extinfo.flags = ANDROID_DLEXT_RESERVED_ADDRESS | ANDROID_DLEXT_WRITE_RELRO;
+  ASSERT_NOERROR(relro_fd);
+  extinfo.relro_fd = relro_fd;
+
+  pid_t pid = fork();
+  if (pid == 0) {
+    // child process
+    void* handle = android_dlopen_ext(LIBNAME, RTLD_NOW, &extinfo);
+    if (handle == NULL) {
+      fprintf(stderr, "in child: %s\n", dlerror());
+      exit(1);
+    }
+    exit(0);
+  }
+
+  // continuing in parent
+  ASSERT_NOERROR(close(relro_fd));
+  ASSERT_NOERROR(pid);
+  int status;
+  ASSERT_EQ(pid, waitpid(pid, &status, 0));
+  ASSERT_TRUE(WIFEXITED(status));
+  ASSERT_EQ(0, WEXITSTATUS(status));
+
+  relro_fd = open(RELRO_FILE, O_RDONLY);
+  ASSERT_NOERROR(relro_fd);
+  extinfo.flags = ANDROID_DLEXT_RESERVED_ADDRESS | ANDROID_DLEXT_USE_RELRO;
+  extinfo.relro_fd = relro_fd;
+  handle_ = android_dlopen_ext(LIBNAME, RTLD_NOW, &extinfo);
+  ASSERT_NOERROR(close(relro_fd));
+
+  ASSERT_DL_NOTNULL(handle_);
+  fn f = reinterpret_cast<fn>(dlsym(handle_, "getRandomNumber"));
+  ASSERT_DL_NOTNULL(f);
+  EXPECT_EQ(4, f());
+}