check_elf_file: check prebuilts are aligned

WARNING: two things still need to be done
1. this should probably only be enabled when PAGE_SIZE
   is undefined, but I'm curious what other targets
   break now.
2. we may need a per-prebuilt way to disable this,
   like some of the other settings here.

For prebuilts added onto a device, we can check the
page alignment matches the one declared in the build
configuration.

Bug: 342466032
Test: manually, by changing the script to require 64 KB
  alignment, I was able to see its errors on new targets.
Change-Id: Ic118245e64d67204bf5fa740a3e1afb7325b34f5
diff --git a/core/check_elf_file.mk b/core/check_elf_file.mk
index b5be81f..584facc 100644
--- a/core/check_elf_file.mk
+++ b/core/check_elf_file.mk
@@ -10,6 +10,8 @@
 # - LOCAL_BUILT_MODULE
 # - LOCAL_IS_HOST_MODULE
 # - LOCAL_MODULE_CLASS
+# - TARGET_MAX_PAGE_SIZE_SUPPORTED
+# - TARGET_NO_BIONIC_PAGE_SIZE_MACRO
 # - intermediates
 # - my_installed_module_stem
 # - my_prebuilt_src_file
@@ -26,6 +28,15 @@
 # In addition to $(my_check_elf_file_shared_lib_files), some file paths are
 # added by `resolve-shared-libs-for-elf-file-check` from `core/main.mk`.
 $(check_elf_files_stamp): PRIVATE_SHARED_LIBRARY_FILES := $(my_check_elf_file_shared_lib_files)
+# For different page sizes to work, we must support a larger max page size
+# as well as properly reflect page size at runtime. Limit this check, since many
+# devices set the max page size (for future proof) than actually use the
+# larger page size.
+ifeq ($(strip $(TARGET_NO_BIONIC_PAGE_SIZE_MACRO)),true)
+$(check_elf_files_stamp): PRIVATE_MAX_PAGE_SIZE_OPT := --max-page-size=$(TARGET_MAX_PAGE_SIZE_SUPPORTED)
+else
+$(check_elf_files_stamp): PRIVATE_MAX_PAGE_SIZE_OPT :=
+endif
 $(check_elf_files_stamp): $(my_prebuilt_src_file) $(my_check_elf_file_shared_lib_files) $(CHECK_ELF_FILE) $(LLVM_READOBJ)
 	@echo Check prebuilt ELF binary: $<
 	$(hide) mkdir -p $(dir $@)
@@ -33,6 +44,7 @@
 	$(hide) $(CHECK_ELF_FILE) \
 	    --skip-bad-elf-magic \
 	    --skip-unknown-elf-machine \
+	    $(PRIVATE_MAX_PAGE_SIZE_OPT) \
 	    $(if $(PRIVATE_SONAME),--soname $(PRIVATE_SONAME)) \
 	    $(foreach l,$(PRIVATE_SHARED_LIBRARY_FILES),--shared-lib $(l)) \
 	    $(foreach l,$(PRIVATE_SYSTEM_SHARED_LIBRARIES),--system-shared-lib $(l)) \
diff --git a/tools/check_elf_file.py b/tools/check_elf_file.py
index 51ec23b..bed3366 100755
--- a/tools/check_elf_file.py
+++ b/tools/check_elf_file.py
@@ -67,7 +67,7 @@
 
 ELF = collections.namedtuple(
   'ELF',
-  ('dt_soname', 'dt_needed', 'imported', 'exported', 'header'))
+  ('alignments', 'dt_soname', 'dt_needed', 'imported', 'exported', 'header'))
 
 
 def _get_os_name():
@@ -195,7 +195,8 @@
   @classmethod
   def _read_llvm_readobj(cls, elf_file_path, header, llvm_readobj):
     """Run llvm-readobj and parse the output."""
-    cmd = [llvm_readobj, '--dynamic-table', '--dyn-symbols', elf_file_path]
+    cmd = [llvm_readobj, '--program-headers', '--dynamic-table',
+           '--dyn-symbols', elf_file_path]
     out = subprocess.check_output(cmd, text=True)
     lines = out.splitlines()
     return cls._parse_llvm_readobj(elf_file_path, header, lines)
@@ -205,9 +206,56 @@
   def _parse_llvm_readobj(cls, elf_file_path, header, lines):
     """Parse the output of llvm-readobj."""
     lines_it = iter(lines)
+    alignments = cls._parse_program_headers(lines_it)
     dt_soname, dt_needed = cls._parse_dynamic_table(elf_file_path, lines_it)
     imported, exported = cls._parse_dynamic_symbols(lines_it)
-    return ELF(dt_soname, dt_needed, imported, exported, header)
+    return ELF(alignments, dt_soname, dt_needed, imported, exported, header)
+
+
+  _PROGRAM_HEADERS_START_PATTERN = 'ProgramHeaders ['
+  _PROGRAM_HEADERS_END_PATTERN = ']'
+  _PROGRAM_HEADER_START_PATTERN = 'ProgramHeader {'
+  _PROGRAM_HEADER_TYPE_PATTERN = re.compile('^\\s+Type:\\s+(.*)$')
+  _PROGRAM_HEADER_ALIGN_PATTERN = re.compile('^\\s+Alignment:\\s+(.*)$')
+  _PROGRAM_HEADER_END_PATTERN = '}'
+
+
+  @classmethod
+  def _parse_program_headers(cls, lines_it):
+    """Parse the dynamic table section."""
+    alignments = []
+
+    if not cls._find_prefix(cls._PROGRAM_HEADERS_START_PATTERN, lines_it):
+      raise ELFError()
+
+    for line in lines_it:
+      # Parse each program header
+      if line.strip() == cls._PROGRAM_HEADER_START_PATTERN:
+        p_align = None
+        p_type = None
+        for line in lines_it:
+          if line.strip() == cls._PROGRAM_HEADER_END_PATTERN:
+            if not p_align:
+              raise ELFError("Could not parse alignment from program header!")
+            if not p_type:
+              raise ELFError("Could not parse type from program header!")
+
+            if p_type.startswith("PT_LOAD "):
+              alignments.append(int(p_align))
+            break
+
+          match = cls._PROGRAM_HEADER_TYPE_PATTERN.match(line)
+          if match:
+            p_type = match.group(1)
+
+          match = cls._PROGRAM_HEADER_ALIGN_PATTERN.match(line)
+          if match:
+            p_align = match.group(1)
+
+      if line == cls._PROGRAM_HEADERS_END_PATTERN:
+        break
+
+    return alignments
 
 
   _DYNAMIC_SECTION_START_PATTERN = 'DynamicSection ['
@@ -434,6 +482,19 @@
 
       sys.exit(2)
 
+  def check_max_page_size(self, max_page_size):
+    for alignment in self._file_under_test.alignments:
+      if alignment % max_page_size != 0:
+        self._error(f'Load segment has alignment {alignment} but '
+                    f'{max_page_size} required.')
+        self._note()
+        self._note('Fix suggestions:')
+        self._note(f'  use linker flag "-Wl,-z,max-page-size={max_page_size}" '
+                   f'when compiling this lib')
+
+        # TODO: instead of exiting immediately, we may want to collect the
+        # errors from all checks and emit them at once
+        sys.exit(2)
 
   @staticmethod
   def _find_symbol(lib, name, version):
@@ -514,6 +575,8 @@
                       help='Ignore the input file with unknown machine ID')
   parser.add_argument('--allow-undefined-symbols', action='store_true',
                       help='Ignore unresolved undefined symbols')
+  parser.add_argument('--max-page-size', action='store', type=int,
+                      help='Required page size alignment support')
 
   # Other options
   parser.add_argument('--llvm-readobj',
@@ -542,6 +605,9 @@
 
   checker.check_dt_needed(args.system_shared_lib)
 
+  if args.max_page_size:
+    checker.check_max_page_size(args.max_page_size)
+
   if not args.allow_undefined_symbols:
     checker.check_symbols()