check_symbol_exports.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. #!/usr/bin/env python3
  2. # Copyright (c) 2017 Google Inc.
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Ensures that all externally visible functions in the library have an appropriate name
  15. Appropriate function names are:
  16. - names starting with spv,
  17. - anything in a namespace,
  18. - functions added by the protobuf compiler,
  19. - and weak definitions of new and delete."""
  20. import os.path
  21. import re
  22. import subprocess
  23. import sys
  24. PROG = 'check_symbol_exports'
  25. def command_output(cmd, directory):
  26. """Runs a command in a directory and returns its standard output stream.
  27. Captures the standard error stream.
  28. Raises a RuntimeError if the command fails to launch or otherwise fails.
  29. """
  30. p = subprocess.Popen(cmd,
  31. cwd=directory,
  32. stdout=subprocess.PIPE,
  33. stderr=subprocess.PIPE,
  34. universal_newlines=True)
  35. (stdout, _) = p.communicate()
  36. if p.returncode != 0:
  37. raise RuntimeError('Failed to run %s in %s' % (cmd, directory))
  38. return stdout
  39. def check_library(library):
  40. """Scans the given library file for global exports. If all such
  41. exports are namespaced or begin with spv (in either C or C++ styles)
  42. then return 0. Otherwise emit a message and return 1."""
  43. # The pattern for an externally visible symbol record
  44. symbol_pattern = re.compile(r'^[0-aA-Fa-f]+ +([wg]) *F \.text.*[0-9A-Fa-f]+ +(.*)')
  45. # Ok patterns are as follows, assuming Itanium name mangling:
  46. # spv[A-Z] : extern "C" symbol starting with spv
  47. # _ZN : something in a namespace
  48. # _ZSt : something in the standard namespace
  49. # _ZZN : something in a local scope and namespace
  50. # _Z[0-9]+spv[A-Z_] : C++ symbol starting with spv[A-Z_]
  51. symbol_ok_pattern = re.compile(r'^(spv[A-Z]|_ZN|_ZSt|_ZZN|_Z[0-9]+spv[A-Z_])')
  52. # In addition, the following pattern allowlists global functions that are added
  53. # by the protobuf compiler:
  54. # - AddDescriptors_spvtoolsfuzz_2eproto()
  55. # - InitDefaults_spvtoolsfuzz_2eproto()
  56. symbol_allowlist_pattern = re.compile(r'_Z[0-9]+.*spvtoolsfuzz_2eproto.*')
  57. symbol_is_new_or_delete = re.compile(r'^(_Zna|_Znw|_Zdl|_Zda)')
  58. # Compilaion for Arm has various thunks for constructors, destructors, vtables.
  59. # They are weak.
  60. symbol_is_thunk = re.compile(r'^_ZT')
  61. # This occurs in NDK builds.
  62. symbol_is_hidden = re.compile(r'^\.hidden ')
  63. seen = set()
  64. result = 0
  65. for line in command_output(['objdump', '-t', library], '.').split('\n'):
  66. match = symbol_pattern.search(line)
  67. if match:
  68. linkage = match.group(1)
  69. symbol = match.group(2)
  70. if symbol not in seen:
  71. seen.add(symbol)
  72. #print("look at '{}'".format(symbol))
  73. if not (symbol_is_new_or_delete.match(symbol) and linkage == 'w'):
  74. if not (symbol_is_thunk.match(symbol) and linkage == 'w'):
  75. if not (symbol_allowlist_pattern.match(symbol) or
  76. symbol_ok_pattern.match(symbol) or
  77. symbol_is_hidden.match(symbol)):
  78. print('{}: error: Unescaped exported symbol: {}'.format(PROG, symbol))
  79. result = 1
  80. return result
  81. def main():
  82. import argparse
  83. parser = argparse.ArgumentParser(description='Check global names exported from a library')
  84. parser.add_argument('library', help='The static library to examine')
  85. args = parser.parse_args()
  86. if not os.path.isfile(args.library):
  87. print('{}: error: {} does not exist'.format(PROG, args.library))
  88. sys.exit(1)
  89. if os.name == 'posix':
  90. status = check_library(args.library)
  91. sys.exit(status)
  92. else:
  93. print('Passing test since not on Posix')
  94. sys.exit(0)
  95. if __name__ == '__main__':
  96. main()