blob: 9aa6ec7f849acd03bf3c82cdade1f52475dfefae [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2025, The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Checks a nanoapp's shared object file for unresolvable external symbols.
This script inspects a given nanoapp .so file to ensure that it only references
symbols that are explicitly allowed. It builds a comprehensive list of allowed
symbols by:
1. Parsing standard CHRE API header files.
2. Parsing platform-specific extension headers.
3. Reading platform-specific lists of exported symbols.
4. Reading an optional user-provided file of additional allowed symbols.
It then compares the nanoapp's undefined symbols (extracted using the corresponding
elf reader of the target) against this allowed list and reports any discrepancies,
exiting with an error if unresolvable symbols are found.
"""
import argparse
import os
import re
import subprocess
import warnings
from os import listdir
from os.path import join
import pyclibrary
from shell_util import warning, log_w, success
# The number of rows to discard from the output of the elf reader
NUM_ROWS_TO_DISCARD = 4
def _get_env_list(env_var_name: str):
return os.getenv(env_var_name).split(":") if env_var_name in os.environ else []
def _get_known_exported_functions() -> list:
"""Extracts function names from ADD_EXPORTED_SYMBOL or ADD_EXPORTED_C_SYMBOL macros.
Returns:
A list of unique exported function names.
"""
exported_names = []
# Regex to match both macro forms:
# 1. ADD_EXPORTED_SYMBOL(internal_name, "external_name") - Captures 'external_name'
# 2. ADD_EXPORTED_C_SYMBOL(function_name) - Captures 'function_name'
regex = r'ADD_EXPORTED_SYMBOL\s*\([^,]+,\s*"([^"]+)"\)|' \
r'ADD_EXPORTED_C_SYMBOL\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)'
exported_macro_files = _get_env_list("EXPORTED_MACRO_FILES")
for file in exported_macro_files:
symbols = []
with open(file, 'r') as f:
for line in f:
matches = re.search(regex, line)
if matches:
name_from_symbol, name_from_c_symbol = matches.groups()
symbols.append(name_from_symbol if name_from_symbol else name_from_c_symbol)
print(f"{len(symbols)} dynamic symbols found in {file}")
exported_names.extend(symbols)
return exported_names
def _get_allowed_symbols() -> list:
"""Gathers all allowed symbols from CHRE API and platform sources.
This function aggregates symbols from several sources:
- Standard CHRE API header files.
- Platform-specific extension header files defined in PLATFORM_INFO.
- Symbols exported via macros, found by _get_known_exported_functions.
- A platform-specific symbol list file (e.g., dl_base_symbols.lst).
Returns:
A list of all allowed symbol names.
"""
chre_api_path = f"{os.environ['ANDROID_BUILD_TOP']}/system/chre/chre_api/include/chre_api"
header_files = []
for f in listdir(chre_api_path + '/chre'):
header_files.append(join(chre_api_path + '/chre', f))
header_files.append(join(chre_api_path, 'chre.h'))
header_files.extend(_get_env_list('EXTERNAL_SYMBOL_DECLARATIONS'))
fnames = []
# suppress warnings from pyclibrary parsing headers
warnings.simplefilter('ignore', SyntaxWarning)
try:
for h in header_files:
# pyclibrary cannot find functions that
# - have a vararg parameter
# - have a trailing macro
# so removing them from the header file before parsing
pyc_parser = pyclibrary.CParser(h, replace={r', \.\.\.': '', r'[A-Z_]+;': ';'})
fnames.extend(list(pyc_parser.defs['functions'].keys()))
finally:
warnings.simplefilter('default', SyntaxWarning)
print(f"{len(fnames)} dynamic symbols found in chre header files")
fnames.extend(_get_known_exported_functions())
lst_files = _get_env_list("EXTERNAL_SYMBOL_LISTS")
for lst_file in lst_files:
with open(lst_file) as f:
platform_external_symbols = [s.strip() for s in f.readlines()]
print(
f"{len(platform_external_symbols)} dynamic symbols found in {lst_file}")
fnames.extend(platform_external_symbols)
return fnames
def _get_symbols_from_nanoapp(file_name: str) -> list:
"""Extracts undefined dynamic symbols from a nanoapp .so file.
Uses the ELF reader specified by the CHRE_TARGET_ELF_READER environment
variable to inspect the dynamic symbol table of the given file.
Args:
file_name: The path to the nanoapp .so file.
Returns:
A list of undefined symbol names found in the nanoapp.
"""
elf_reader = os.environ['CHRE_TARGET_ELF_READER']
readelf_cmd = f"{elf_reader} --dyn-syms --wide {file_name}"
out = subprocess.check_output(readelf_cmd.split(), text=True)
symbols = []
for line in out.splitlines()[NUM_ROWS_TO_DISCARD:]:
words = line.split()
idx_type, symbol_name = words[-2:]
if "UND" == idx_type:
symbols.append(symbol_name)
print(f"{len(symbols)} dynamic symbols found in {file_name}")
return symbols
def _get_allowed_symbols_from_file(filename: str) -> list:
"""Reads a list of allowed symbols from a text file.
Each line in the file is treated as a single symbol.
Args:
filename: The path to the file containing allowed symbols.
Returns:
A list of symbol names.
"""
with open(filename) as f:
return [s.strip() for s in f.readlines()]
def _disallowed_symbols(observed_symbols: list, allowed_symbols: list) -> list:
"""Compares observed symbols against a list of allowed symbols.
Calculates the set difference between observed and allowed symbols. It also
handles basic wildcard matching for allowed symbols that end with '*'.
Args:
observed_symbols: A list of symbols found in the nanoapp.
allowed_symbols: A list of all allowed symbols.
Returns:
A list of symbols that are observed but not allowed.
"""
diff_list = list(set(observed_symbols) - set(allowed_symbols))
# See if any of the wildcard ending allowed symbols will cause some more of
# the observed symbols to be allowed
for allowed in allowed_symbols:
if '*' in allowed:
prefix = allowed[:allowed.find('*')]
diff_list = [sym for sym in diff_list if not sym.startswith(prefix)]
return diff_list
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description="Check nanoapp for illegal" +
" symbols in file provided by --nanoapp argument")
parser.add_argument('--nanoapp', type=str, help="nanoapp file", required=True)
parser.add_argument('--allowed_symbols_file', type=str,
help="a file containing a list of allowed symbols", required=False)
args = parser.parse_args()
nanoapp_filename = args.nanoapp
print("\n------- Checking unresolvable external symbols -------")
specific_allowed_symbols_file = args.allowed_symbols_file
observed_symbols = _get_symbols_from_nanoapp(nanoapp_filename)
allowed_symbols = _get_allowed_symbols()
if specific_allowed_symbols_file is not None:
allowed_symbols += _get_allowed_symbols_from_file(
specific_allowed_symbols_file)
disallowed_symbols_list = _disallowed_symbols(observed_symbols,
allowed_symbols)
if len(disallowed_symbols_list) > 0:
warning(f"{len(disallowed_symbols_list)} unresolvable symbol(s) found:\n")
for sym in disallowed_symbols_list:
log_w(f" - {sym}")
else:
success(f"All the dynamic symbols are resolvable!")