16k compat: Combine memory protection logic for RX|RW and RWX modes

In both modes the initial mapping is RW, and execute permission is only
applied after linking is done.

In RX|RW compat mode RX protection is applied to code (.text, .plt, ...)
and GNU RELRO segments.

In RWX compat mode RWX protection is applied to the whole ELF mapping.

Test: Manually verify that both RX|RW and RWX binaries load correctly.
Bug: 424598884
Change-Id: I96f5f07f984a0ab9fbd9d868d3531b0bfba3ac97
diff --git a/linker/linker.cpp b/linker/linker.cpp
index 8117930..206a39f 100644
--- a/linker/linker.cpp
+++ b/linker/linker.cpp
@@ -645,9 +645,10 @@
     si_->set_gap_size(elf_reader.gap_size());
     si_->set_should_pad_segments(elf_reader.should_pad_segments());
     si_->set_should_use_16kib_app_compat(elf_reader.should_use_16kib_app_compat());
+    si_->set_should_16kib_app_compat_use_rwx(elf_reader.should_16kib_app_compat_use_rwx());
     if (si_->should_use_16kib_app_compat()) {
-      si_->set_compat_relro_start(elf_reader.compat_relro_start());
-      si_->set_compat_relro_size(elf_reader.compat_relro_size());
+      si_->set_compat_code_start(elf_reader.compat_code_start());
+      si_->set_compat_code_size(elf_reader.compat_code_size());
     }
 
     return true;
@@ -3422,6 +3423,12 @@
     return false;
   }
 
+  // Now that we've finished linking we can apply execute permission to code segments in compat
+  // loaded binaries, and remove write permission from .text and GNU RELRO in RX|RW compat mode.
+  if (!protect_16kib_app_compat_code()) {
+    return false;
+  }
+
   if (should_tag_memtag_globals()) {
     std::list<std::string>* vma_names_ptr = vma_names();
     // should_tag_memtag_globals -> __aarch64__ -> vma_names() != nullptr
@@ -3452,18 +3459,29 @@
 
 bool soinfo::protect_relro() {
   if (should_use_16kib_app_compat_) {
-    if (phdr_table_protect_gnu_relro_16kib_compat(compat_relro_start_, compat_relro_size_) < 0) {
-      DL_ERR("can't enable COMPAT GNU RELRO protection for \"%s\": %s", get_realpath(),
-             strerror(errno));
-      return false;
-    }
-  } else {
-    if (phdr_table_protect_gnu_relro(phdr, phnum, load_bias, should_pad_segments_,
-                                     should_use_16kib_app_compat_) < 0) {
-      DL_ERR("can't enable GNU RELRO protection for \"%s\": %m", get_realpath());
-      return false;
-    }
+    return true;
   }
+
+  if (phdr_table_protect_gnu_relro(phdr, phnum, load_bias, should_pad_segments_) < 0) {
+    DL_ERR("can't enable GNU RELRO protection for \"%s\": %m", get_realpath());
+    return false;
+  }
+
+  return true;
+}
+
+bool soinfo::protect_16kib_app_compat_code() {
+  if (!should_use_16kib_app_compat_) {
+    return true;
+  }
+
+  if (phdr_table_protect_16kib_app_compat_code(compat_code_start_, compat_code_size_,
+                                               should_16kib_app_compat_use_rwx_) < 0) {
+    DL_ERR("failed to set execute permission for compat loaded binary \"%s\": %s", get_realpath(),
+           strerror(errno));
+    return false;
+  }
+
   return true;
 }
 
diff --git a/linker/linker_phdr.cpp b/linker/linker_phdr.cpp
index 62faaf0..c6b1127 100644
--- a/linker/linker_phdr.cpp
+++ b/linker/linker_phdr.cpp
@@ -1314,8 +1314,7 @@
 static inline void _extend_gnu_relro_prot_end(const ElfW(Phdr)* relro_phdr,
                                               const ElfW(Phdr)* phdr_table, size_t phdr_count,
                                               ElfW(Addr) load_bias, ElfW(Addr)* seg_page_end,
-                                              bool should_pad_segments,
-                                              bool should_use_16kib_app_compat) {
+                                              bool should_pad_segments) {
   // Find the index and phdr of the LOAD containing the GNU_RELRO segment
   for (size_t index = 0; index < phdr_count; ++index) {
     const ElfW(Phdr)* phdr = &phdr_table[index];
@@ -1363,7 +1362,7 @@
       // mprotect will only RO protect a part of the extended RW LOAD segment, which
       // will leave an extra split RW VMA (the gap).
       _extend_load_segment_vma(phdr_table, phdr_count, index, &p_memsz, &p_filesz,
-                               should_pad_segments, should_use_16kib_app_compat);
+                               should_pad_segments, /*should_use_16kib_app_compat=*/false);
 
       *seg_page_end = page_end(phdr->p_vaddr + p_memsz + load_bias);
       return;
@@ -1376,8 +1375,7 @@
  */
 static int _phdr_table_set_gnu_relro_prot(const ElfW(Phdr)* phdr_table, size_t phdr_count,
                                           ElfW(Addr) load_bias, int prot_flags,
-                                          bool should_pad_segments,
-                                          bool should_use_16kib_app_compat) {
+                                          bool should_pad_segments) {
   const ElfW(Phdr)* phdr = phdr_table;
   const ElfW(Phdr)* phdr_limit = phdr + phdr_count;
 
@@ -1405,7 +1403,7 @@
     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;
     _extend_gnu_relro_prot_end(phdr, phdr_table, phdr_count, load_bias, &seg_page_end,
-                               should_pad_segments, should_use_16kib_app_compat);
+                               should_pad_segments);
 
     int ret = mprotect(reinterpret_cast<void*>(seg_page_start),
                        seg_page_end - seg_page_start,
@@ -1436,24 +1434,9 @@
  *   0 on success, -1 on failure (error code in errno).
  */
 int phdr_table_protect_gnu_relro(const ElfW(Phdr)* phdr_table, size_t phdr_count,
-                                 ElfW(Addr) load_bias, bool should_pad_segments,
-                                 bool should_use_16kib_app_compat) {
+                                 ElfW(Addr) load_bias, bool should_pad_segments) {
   return _phdr_table_set_gnu_relro_prot(phdr_table, phdr_count, load_bias, PROT_READ,
-                                        should_pad_segments, should_use_16kib_app_compat);
-}
-
-/*
- * Apply RX protection to the compat relro region of the ELF being loaded in
- * 16KiB compat mode.
- *
- * Input:
- *   start  -> start address of the compat relro region.
- *   size   -> size of the compat relro region in bytes.
- * Return:
- *   0 on success, -1 on failure (error code in errno).
- */
-int phdr_table_protect_gnu_relro_16kib_compat(ElfW(Addr) start, ElfW(Addr) size) {
-  return mprotect(reinterpret_cast<void*>(start), size, PROT_READ | PROT_EXEC);
+                                        should_pad_segments);
 }
 
 /* Serialize the GNU relro segments to the given file descriptor. This can be
diff --git a/linker/linker_phdr.h b/linker/linker_phdr.h
index 399e3ed..d78f7c1 100644
--- a/linker/linker_phdr.h
+++ b/linker/linker_phdr.h
@@ -70,8 +70,9 @@
   ElfW(Addr) entry_point() const { return header_.e_entry + load_bias_; }
   bool should_pad_segments() const { return should_pad_segments_; }
   bool should_use_16kib_app_compat() const { return should_use_16kib_app_compat_; }
-  ElfW(Addr) compat_relro_start() const { return compat_relro_start_; }
-  ElfW(Addr) compat_relro_size() const { return compat_relro_size_; }
+  bool should_16kib_app_compat_use_rwx() const { return should_16kib_app_compat_use_rwx_; }
+  ElfW(Addr) compat_code_start() const { return compat_code_start_; }
+  ElfW(Addr) compat_code_size() const { return compat_code_size_; }
   const GnuPropertySection* note_gnu_property() const { return &note_gnu_property_; }
 
  private:
@@ -94,7 +95,7 @@
   void FixMinAlignFor16KiB();
   void LabelCompatVma();
   void SetupRXRWAppCompat(ElfW(Addr) rx_rw_boundary);
-  [[nodiscard]] bool SetupRWXAppCompat();
+  void SetupRWXAppCompat();
   [[nodiscard]] bool Setup16KiBAppCompat();
   [[nodiscard]] bool LoadSegments();
   [[nodiscard]] bool FindPhdr();
@@ -153,12 +154,17 @@
   // Use app compat mode when loading 4KiB max-page-size ELFs on 16KiB page-size devices?
   bool should_use_16kib_app_compat_ = false;
 
+  // Map ELF segments RWX in app compat mode?
+  bool should_16kib_app_compat_use_rwx_ = false;
+
   // Should fail hard on 16KiB related dlopen() errors?
   bool dlopen_16kib_err_is_fatal_ = false;
 
-  // RELRO region for 16KiB compat loading
-  ElfW(Addr) compat_relro_start_ = 0;
-  ElfW(Addr) compat_relro_size_ = 0;
+  // Region that needs execute permission for 16KiB compat loading.
+  // RX|RW compat mode: Contains code and GNU RELRO sections.
+  // RWX compat mode: Contains the whole ELF.
+  ElfW(Addr) compat_code_start_ = 0;
+  ElfW(Addr) compat_code_size_ = 0;
 
   // Only used by AArch64 at the moment.
   GnuPropertySection note_gnu_property_ __unused;
@@ -177,10 +183,10 @@
                                   bool should_use_16kib_app_compat);
 
 int phdr_table_protect_gnu_relro(const ElfW(Phdr)* phdr_table, size_t phdr_count,
-                                 ElfW(Addr) load_bias, bool should_pad_segments,
-                                 bool should_use_16kib_app_compat);
+                                 ElfW(Addr) load_bias, bool should_pad_segments);
 
-int phdr_table_protect_gnu_relro_16kib_compat(ElfW(Addr) start, ElfW(Addr) size);
+int phdr_table_protect_16kib_app_compat_code(ElfW(Addr) start, ElfW(Addr) size,
+                                             bool should_16kib_app_compat_use_rwx);
 
 int phdr_table_serialize_gnu_relro(const ElfW(Phdr)* phdr_table, size_t phdr_count,
                                    ElfW(Addr) load_bias, int fd, size_t* file_offset);
diff --git a/linker/linker_phdr_16kib_compat.cpp b/linker/linker_phdr_16kib_compat.cpp
index 570f766..7721dac 100644
--- a/linker/linker_phdr_16kib_compat.cpp
+++ b/linker/linker_phdr_16kib_compat.cpp
@@ -360,29 +360,20 @@
 
   // RW region (.data, .bss ...)
   ElfW(Addr) rw_start = load_bias_ + rx_rw_boundary;
-  ElfW(Addr) rw_size = load_size_ - (rw_start - reinterpret_cast<ElfW(Addr)>(load_start_));
+  CHECK(rw_start % page_size() == 0);
 
-  CHECK(rw_start % getpagesize() == 0);
-  CHECK(rw_size % getpagesize() == 0);
-
-  // Compat RELRO (RX) region (.text, .data.relro, ...)
-  compat_relro_size_ = load_size_ - rw_size;
-  compat_relro_start_ = reinterpret_cast<ElfW(Addr)>(load_start_);
+  // Compat code and RELRO (RX) region (.text, .data.relro, ...)
+  compat_code_start_ = load_start();
+  compat_code_size_ = rw_start - load_start();
 }
 
-bool ElfReader::SetupRWXAppCompat() {
+void ElfReader::SetupRWXAppCompat() {
   // Warn and fallback to RWX mapping
   const std::string layout = elf_layout(phdr_table_, phdr_num_);
   DL_WARN("\"%s\": RX|RW compat loading failed, falling back to RWX compat: load segments [%s]",
           name_.c_str(), layout.c_str());
-
-  // There is no RELRO protection in this mode
-  CHECK(!compat_relro_start_);
-  CHECK(!compat_relro_size_);
-
-  // Make the reserved mapping RWX; the ELF contents will be read
-  // into it, instead of mapped over it.
-  return mprotect(load_start_, load_size_, PROT_READ | PROT_WRITE | PROT_EXEC) != -1;
+  compat_code_start_ = load_start();
+  compat_code_size_ = load_size();
 }
 
 bool ElfReader::Setup16KiBAppCompat() {
@@ -393,9 +384,9 @@
   ElfW(Addr) rx_rw_boundary;  // Permission boundary for RX|RW compat mode
   if (IsEligibleForRXRWAppCompat(&rx_rw_boundary)) {
     SetupRXRWAppCompat(rx_rw_boundary);
-  } else if (!SetupRWXAppCompat()) {
-    DL_ERR_AND_LOG("\"%s\": mprotect failed to setup RWX app compat: %m", name_.c_str());
-    return false;
+  } else {
+    should_16kib_app_compat_use_rwx_ = true;
+    SetupRWXAppCompat();
   }
 
   LabelCompatVma();
@@ -479,3 +470,23 @@
     }
   }
 }
+
+/*
+ * Apply RX or RWX protection to the code region of the ELF being loaded in
+ * 16KiB compat mode.
+ *
+ * Input:
+ *   start                           -> start address of the compat code region.
+ *   size                            -> size of the compat code region in bytes.
+ *   should_16kib_app_compat_use_rwx -> use RWX or RX permission.
+ * Return:
+ *   0 on success, -1 on failure (error code in errno).
+ */
+int phdr_table_protect_16kib_app_compat_code(ElfW(Addr) start, ElfW(Addr) size,
+                                             bool should_16kib_app_compat_use_rwx) {
+  int prot = PROT_READ | PROT_EXEC;
+  if (should_16kib_app_compat_use_rwx) {
+    prot |= PROT_WRITE;
+  }
+  return mprotect(reinterpret_cast<void*>(start), size, prot);
+}
diff --git a/linker/linker_soinfo.h b/linker/linker_soinfo.h
index 8f56c9f..d107c73 100644
--- a/linker/linker_soinfo.h
+++ b/linker/linker_soinfo.h
@@ -254,6 +254,7 @@
   bool link_image(const SymbolLookupList& lookup_list, soinfo* local_group_root,
                   const android_dlextinfo* extinfo, size_t* relro_fd_offset);
   bool protect_relro();
+  bool protect_16kib_app_compat_code();
 
   void tag_globals(bool deterministic_memtag_globals);
   ElfW(Addr) apply_memtag_if_mte_globals(ElfW(Addr) sym_addr) const;
@@ -406,11 +407,16 @@
   }
   bool should_use_16kib_app_compat() const { return should_use_16kib_app_compat_; }
 
-  void set_compat_relro_start(ElfW(Addr) start) { compat_relro_start_ = start; }
-  ElfW(Addr) compat_relro_start() const { return compat_relro_start_; }
+  void set_should_16kib_app_compat_use_rwx(bool should_16kib_app_compat_use_rwx) {
+    should_16kib_app_compat_use_rwx_ = should_16kib_app_compat_use_rwx;
+  }
+  bool should_16kib_app_compat_use_rwx() const { return should_16kib_app_compat_use_rwx_; }
 
-  void set_compat_relro_size(ElfW(Addr) size) { compat_relro_size_ = size; }
-  ElfW(Addr) compat_relro_size() const { return compat_relro_start_; }
+  void set_compat_code_start(ElfW(Addr) start) { compat_code_start_ = start; }
+  ElfW(Addr) compat_code_start() const { return compat_code_start_; }
+
+  void set_compat_code_size(ElfW(Addr) size) { compat_code_size_ = size; }
+  ElfW(Addr) compat_code_size() const { return compat_code_size_; }
 
  private:
   bool is_image_linked() const;
@@ -503,9 +509,14 @@
   // Use app compat mode when loading 4KiB max-page-size ELFs on 16KiB page-size devices?
   bool should_use_16kib_app_compat_ = false;
 
-  // RELRO region for 16KiB compat loading
-  ElfW(Addr) compat_relro_start_ = 0;
-  ElfW(Addr) compat_relro_size_ = 0;
+  // Map ELF segments RWX in app compat mode?
+  bool should_16kib_app_compat_use_rwx_ = false;
+
+  // Region that needs execute permission for 16KiB compat loading.
+  // RX|RW compat mode: Contains code and GNU RELRO sections.
+  // RWX compat mode: Contains the whole ELF.
+  ElfW(Addr) compat_code_start_ = 0;
+  ElfW(Addr) compat_code_size_ = 0;
 };
 
 // This function is used by dlvsym() to calculate hash of sym_ver