check_vanilla_allocations.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. # This Source Code Form is subject to the terms of the Mozilla Public
  2. # License, v. 2.0. If a copy of the MPL was not distributed with this
  3. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  4. #----------------------------------------------------------------------------
  5. # All heap allocations in SpiderMonkey must go through js_malloc, js_calloc,
  6. # js_realloc, and js_free. This is so that any embedder who uses a custom
  7. # allocator (by defining JS_USE_CUSTOM_ALLOCATOR) will see all heap allocation
  8. # go through that custom allocator.
  9. #
  10. # Therefore, the presence of any calls to "vanilla" allocation/free functions
  11. # (e.g. malloc(), free()) is a bug.
  12. #
  13. # This script checks for the presence of such disallowed vanilla
  14. # allocation/free function in SpiderMonkey when it's built as a library. It
  15. # relies on |nm| from the GNU binutils, and so only works on Linux, but one
  16. # platform is good enough to catch almost all violations.
  17. #
  18. # This checking is only 100% reliable in a JS_USE_CUSTOM_ALLOCATOR build in
  19. # which the default definitions of js_malloc et al (in Utility.h) -- which call
  20. # malloc et al -- are replaced with empty definitions. This is because the
  21. # presence and possible inlining of the default js_malloc et al can cause
  22. # malloc/calloc/realloc/free calls show up in unpredictable places.
  23. #
  24. # Unfortunately, that configuration cannot be tested on Mozilla's standard
  25. # testing infrastructure. Instead, by default this script only tests that none
  26. # of the other vanilla allocation/free functions (operator new, memalign, etc)
  27. # are present. If given the --aggressive flag, it will also check for
  28. # malloc/calloc/realloc/free.
  29. #
  30. # Note: We don't check for |operator delete| and |operator delete[]|. These
  31. # can be present somehow due to virtual destructors, but this is not too
  32. # because vanilla delete/delete[] calls don't make sense without corresponding
  33. # vanilla new/new[] calls, and any explicit calls will be caught by Valgrind's
  34. # mismatched alloc/free checking.
  35. #----------------------------------------------------------------------------
  36. from __future__ import print_function
  37. import argparse
  38. import re
  39. import subprocess
  40. import sys
  41. # The obvious way to implement this script is to search for occurrences of
  42. # malloc et al, succeed if none are found, and fail is some are found.
  43. # However, "none are found" does not necessarily mean "none are present" --
  44. # this script could be buggy. (Or the output format of |nm| might change in
  45. # the future.)
  46. #
  47. # So jsutil.cpp deliberately contains a (never-called) function that contains a
  48. # single use of all the vanilla allocation/free functions. And this script
  49. # fails if it (a) finds uses of those functions in files other than jsutil.cpp,
  50. # *or* (b) fails to find them in jsutil.cpp.
  51. # Tracks overall success of the test.
  52. has_failed = False
  53. def fail(msg):
  54. print('TEST-UNEXPECTED-FAIL | check_vanilla_allocations.py |', msg)
  55. global has_failed
  56. has_failed = True
  57. def main():
  58. parser = argparse.ArgumentParser()
  59. parser.add_argument('--aggressive', action='store_true',
  60. help='also check for malloc, calloc, realloc and free')
  61. parser.add_argument('file', type=str,
  62. help='name of the file to check')
  63. args = parser.parse_args()
  64. # Run |nm|. Options:
  65. # -u: show only undefined symbols
  66. # -C: demangle symbol names
  67. # -A: show an object filename for each undefined symbol
  68. cmd = ['nm', '-u', '-C', '-A', args.file]
  69. lines = subprocess.check_output(cmd, universal_newlines=True,
  70. stderr=subprocess.PIPE).split('\n')
  71. # alloc_fns contains all the vanilla allocation/free functions that we look
  72. # for. Regexp chars are escaped appropriately.
  73. alloc_fns = [
  74. # Matches |operator new(unsigned T)|, where |T| is |int| or |long|.
  75. r'operator new\(unsigned',
  76. # Matches |operator new[](unsigned T)|, where |T| is |int| or |long|.
  77. r'operator new\[\]\(unsigned',
  78. r'memalign',
  79. # These three aren't available on all Linux configurations.
  80. #r'posix_memalign',
  81. #r'aligned_alloc',
  82. #r'valloc',
  83. ]
  84. if args.aggressive:
  85. alloc_fns += [
  86. r'malloc',
  87. r'calloc',
  88. r'realloc',
  89. r'free',
  90. r'strdup'
  91. ]
  92. # This is like alloc_fns, but regexp chars are not escaped.
  93. alloc_fns_unescaped = [fn.translate(None, r'\\') for fn in alloc_fns]
  94. # This regexp matches the relevant lines in the output of |nm|, which look
  95. # like the following.
  96. #
  97. # js/src/libjs_static.a:jsutil.o: U malloc
  98. #
  99. alloc_fns_re = r'([^:/ ]+):\s+U (' + r'|'.join(alloc_fns) + r')'
  100. # This tracks which allocation/free functions have been seen in jsutil.cpp.
  101. jsutil_cpp = set([])
  102. # Would it be helpful to emit detailed line number information after a failure?
  103. emit_line_info = False
  104. for line in lines:
  105. m = re.search(alloc_fns_re, line)
  106. if m is None:
  107. continue
  108. filename = m.group(1)
  109. fn = m.group(2)
  110. if filename == 'jsutil.o':
  111. jsutil_cpp.add(fn)
  112. else:
  113. # An allocation is present in a non-special file. Fail!
  114. fail("'" + fn + "' present in " + filename)
  115. # Try to give more precise information about the offending code.
  116. emit_line_info = True
  117. # Check that all functions we expect are used in jsutil.cpp. (This will
  118. # fail if the function-detection code breaks at any point.)
  119. for fn in alloc_fns_unescaped:
  120. if fn not in jsutil_cpp:
  121. fail("'" + fn + "' isn't used as expected in jsutil.cpp")
  122. else:
  123. jsutil_cpp.remove(fn)
  124. # This should never happen, but check just in case.
  125. if jsutil_cpp:
  126. fail('unexpected allocation fns used in jsutil.cpp: ' +
  127. ', '.join(jsutil_cpp))
  128. # If we found any improper references to allocation functions, try to use
  129. # DWARF debug info to get more accurate line number information about the
  130. # bad calls. This is a lot slower than 'nm -A', and it is not always
  131. # precise when building with --enable-optimized.
  132. if emit_line_info:
  133. print('check_vanilla_allocations.py: Source lines with allocation calls:')
  134. print('check_vanilla_allocations.py: Accurate in unoptimized builds; jsutil.cpp expected.')
  135. # Run |nm|. Options:
  136. # -u: show only undefined symbols
  137. # -C: demangle symbol names
  138. # -l: show line number information for each undefined symbol
  139. cmd = ['nm', '-u', '-C', '-l', args.file]
  140. lines = subprocess.check_output(cmd, universal_newlines=True,
  141. stderr=subprocess.PIPE).split('\n')
  142. # This regexp matches the relevant lines in the output of |nm -l|,
  143. # which look like the following.
  144. #
  145. # U malloc jsutil.cpp:117
  146. #
  147. alloc_lines_re = r'U ((' + r'|'.join(alloc_fns) + r').*)\s+(\S+:\d+)$'
  148. for line in lines:
  149. m = re.search(alloc_lines_re, line)
  150. if m:
  151. print('check_vanilla_allocations.py:', m.group(1), 'called at', m.group(3))
  152. if has_failed:
  153. sys.exit(1)
  154. print('TEST-PASS | check_vanilla_allocations.py | ok')
  155. sys.exit(0)
  156. if __name__ == '__main__':
  157. main()