1#!/usr/bin/env python
2#
3# Copyright (C) 2023 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""apex_elf_checker checks if ELF files in the APEX
17
18Usage: apex_elf_checker [--unwanted <names>] <apex>
19
20  --unwanted <names>
21
22    Fail if any of ELF files in APEX has any of unwanted names in NEEDED `
23"""
24
25import argparse
26import os
27import re
28import subprocess
29import sys
30import tempfile
31
32
33_DYNAMIC_SECTION_NEEDED_PATTERN = re.compile(
34    '^  0x[0-9a-fA-F]+\\s+NEEDED\\s+Shared library: \\[(.*)\\]$'
35)
36
37
38_ELF_MAGIC = b'\x7fELF'
39
40
41def ParseArgs():
42  parser = argparse.ArgumentParser()
43  parser.add_argument('apex', help='Path to the APEX')
44  parser.add_argument(
45      '--tool_path',
46      help='Tools are searched in TOOL_PATH/bin. Colon-separated list of paths',
47  )
48  parser.add_argument(
49      '--unwanted',
50      help='Names not allowed in DT_NEEDED. Colon-separated list of names',
51  )
52  return parser.parse_args()
53
54
55def InitTools(tool_path):
56  if tool_path is None:
57    exec_path = os.path.realpath(sys.argv[0])
58    if exec_path.endswith('.py'):
59      script_name = os.path.basename(exec_path)[:-3]
60      sys.exit(
61          f'Do not invoke {exec_path} directly. Instead, use {script_name}'
62      )
63    tool_path = os.environ['PATH']
64
65  def ToolPath(name):
66    for p in tool_path.split(':'):
67      path = os.path.join(p, name)
68      if os.path.exists(path):
69        return path
70    sys.exit(f'Required tool({name}) not found in {tool_path}')
71
72  return {
73      tool: ToolPath(tool)
74      for tool in [
75          'deapexer',
76          'debugfs_static',
77          'fsck.erofs',
78          'llvm-readelf',
79      ]
80  }
81
82
83def IsElfFile(path):
84  with open(path, 'rb') as f:
85    buf = bytearray(len(_ELF_MAGIC))
86    f.readinto(buf)
87    return buf == _ELF_MAGIC
88
89
90def ParseElfNeeded(path, tools):
91  output = subprocess.check_output(
92      [tools['llvm-readelf'], '-d', '--elf-output-style', 'LLVM', path],
93      text=True,
94      stderr=subprocess.PIPE,
95  )
96
97  needed = []
98  for line in output.splitlines():
99    match = _DYNAMIC_SECTION_NEEDED_PATTERN.match(line)
100    if match:
101      needed.append(match.group(1))
102  return needed
103
104
105def ScanElfFiles(work_dir):
106  for parent, _, files in os.walk(work_dir):
107    for file in files:
108      path = os.path.join(parent, file)
109      # Skip symlinks for APEXes with symlink optimization
110      if os.path.islink(path):
111        continue
112      if IsElfFile(path):
113        yield path
114
115
116def CheckElfFiles(args, tools):
117  with tempfile.TemporaryDirectory() as work_dir:
118    subprocess.check_output(
119        [
120            tools['deapexer'],
121            '--debugfs_path',
122            tools['debugfs_static'],
123            '--fsckerofs_path',
124            tools['fsck.erofs'],
125            'extract',
126            args.apex,
127            work_dir,
128        ],
129        text=True,
130        stderr=subprocess.PIPE,
131    )
132
133    if args.unwanted:
134      unwanted = set(args.unwanted.split(':'))
135      for file in ScanElfFiles(work_dir):
136        needed = set(ParseElfNeeded(file, tools))
137        if unwanted & needed:
138          sys.exit(
139              f'{os.path.relpath(file, work_dir)} has unwanted NEEDED:'
140              f' {",".join(unwanted & needed)}'
141          )
142
143
144def main():
145  args = ParseArgs()
146  tools = InitTools(args.tool_path)
147  try:
148    CheckElfFiles(args, tools)
149  except subprocess.CalledProcessError as e:
150    sys.exit('Result:' + str(e.stderr))
151
152
153if __name__ == '__main__':
154  main()
155