summaryrefslogtreecommitdiffstats
path: root/config/check_vanilla_allocations.py
diff options
context:
space:
mode:
Diffstat (limited to 'config/check_vanilla_allocations.py')
-rw-r--r--config/check_vanilla_allocations.py191
1 files changed, 191 insertions, 0 deletions
diff --git a/config/check_vanilla_allocations.py b/config/check_vanilla_allocations.py
new file mode 100644
index 000000000..2a3be879e
--- /dev/null
+++ b/config/check_vanilla_allocations.py
@@ -0,0 +1,191 @@
+# vim: set ts=8 sts=4 et sw=4 tw=79:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#----------------------------------------------------------------------------
+# All heap allocations in SpiderMonkey must go through js_malloc, js_calloc,
+# js_realloc, and js_free. This is so that any embedder who uses a custom
+# allocator (by defining JS_USE_CUSTOM_ALLOCATOR) will see all heap allocation
+# go through that custom allocator.
+#
+# Therefore, the presence of any calls to "vanilla" allocation/free functions
+# (e.g. malloc(), free()) is a bug.
+#
+# This script checks for the presence of such disallowed vanilla
+# allocation/free function in SpiderMonkey when it's built as a library. It
+# relies on |nm| from the GNU binutils, and so only works on Linux, but one
+# platform is good enough to catch almost all violations.
+#
+# This checking is only 100% reliable in a JS_USE_CUSTOM_ALLOCATOR build in
+# which the default definitions of js_malloc et al (in Utility.h) -- which call
+# malloc et al -- are replaced with empty definitions. This is because the
+# presence and possible inlining of the default js_malloc et al can cause
+# malloc/calloc/realloc/free calls show up in unpredictable places.
+#
+# Unfortunately, that configuration cannot be tested on Mozilla's standard
+# testing infrastructure. Instead, by default this script only tests that none
+# of the other vanilla allocation/free functions (operator new, memalign, etc)
+# are present. If given the --aggressive flag, it will also check for
+# malloc/calloc/realloc/free.
+#
+# Note: We don't check for |operator delete| and |operator delete[]|. These
+# can be present somehow due to virtual destructors, but this is not too
+# because vanilla delete/delete[] calls don't make sense without corresponding
+# vanilla new/new[] calls, and any explicit calls will be caught by Valgrind's
+# mismatched alloc/free checking.
+#----------------------------------------------------------------------------
+
+from __future__ import print_function
+
+import argparse
+import re
+import subprocess
+import sys
+
+# The obvious way to implement this script is to search for occurrences of
+# malloc et al, succeed if none are found, and fail is some are found.
+# However, "none are found" does not necessarily mean "none are present" --
+# this script could be buggy. (Or the output format of |nm| might change in
+# the future.)
+#
+# So jsutil.cpp deliberately contains a (never-called) function that contains a
+# single use of all the vanilla allocation/free functions. And this script
+# fails if it (a) finds uses of those functions in files other than jsutil.cpp,
+# *or* (b) fails to find them in jsutil.cpp.
+
+# Tracks overall success of the test.
+has_failed = False
+
+
+def fail(msg):
+ print('TEST-UNEXPECTED-FAIL | check_vanilla_allocations.py |', msg)
+ global has_failed
+ has_failed = True
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--aggressive', action='store_true',
+ help='also check for malloc, calloc, realloc and free')
+ parser.add_argument('file', type=str,
+ help='name of the file to check')
+ args = parser.parse_args()
+
+ # Run |nm|. Options:
+ # -u: show only undefined symbols
+ # -C: demangle symbol names
+ # -A: show an object filename for each undefined symbol
+ cmd = ['nm', '-u', '-C', '-A', args.file]
+ lines = subprocess.check_output(cmd, universal_newlines=True,
+ stderr=subprocess.PIPE).split('\n')
+
+ # alloc_fns contains all the vanilla allocation/free functions that we look
+ # for. Regexp chars are escaped appropriately.
+
+ alloc_fns = [
+ # Matches |operator new(unsigned T)|, where |T| is |int| or |long|.
+ r'operator new\(unsigned',
+
+ # Matches |operator new[](unsigned T)|, where |T| is |int| or |long|.
+ r'operator new\[\]\(unsigned',
+
+ r'memalign',
+ # These three aren't available on all Linux configurations.
+ #r'posix_memalign',
+ #r'aligned_alloc',
+ #r'valloc',
+ ]
+
+ if args.aggressive:
+ alloc_fns += [
+ r'malloc',
+ r'calloc',
+ r'realloc',
+ r'free',
+ r'strdup'
+ ]
+
+ # This is like alloc_fns, but regexp chars are not escaped.
+ alloc_fns_unescaped = [fn.translate(None, r'\\') for fn in alloc_fns]
+
+ # This regexp matches the relevant lines in the output of |nm|, which look
+ # like the following.
+ #
+ # js/src/libjs_static.a:jsutil.o: U malloc
+ #
+ alloc_fns_re = r'([^:/ ]+):\s+U (' + r'|'.join(alloc_fns) + r')'
+
+ # This tracks which allocation/free functions have been seen in jsutil.cpp.
+ jsutil_cpp = set([])
+
+ # Would it be helpful to emit detailed line number information after a failure?
+ emit_line_info = False
+
+ for line in lines:
+ m = re.search(alloc_fns_re, line)
+ if m is None:
+ continue
+
+ filename = m.group(1)
+ fn = m.group(2)
+ if filename == 'jsutil.o':
+ jsutil_cpp.add(fn)
+ else:
+ # An allocation is present in a non-special file. Fail!
+ fail("'" + fn + "' present in " + filename)
+ # Try to give more precise information about the offending code.
+ emit_line_info = True
+
+
+ # Check that all functions we expect are used in jsutil.cpp. (This will
+ # fail if the function-detection code breaks at any point.)
+ for fn in alloc_fns_unescaped:
+ if fn not in jsutil_cpp:
+ fail("'" + fn + "' isn't used as expected in jsutil.cpp")
+ else:
+ jsutil_cpp.remove(fn)
+
+ # This should never happen, but check just in case.
+ if jsutil_cpp:
+ fail('unexpected allocation fns used in jsutil.cpp: ' +
+ ', '.join(jsutil_cpp))
+
+ # If we found any improper references to allocation functions, try to use
+ # DWARF debug info to get more accurate line number information about the
+ # bad calls. This is a lot slower than 'nm -A', and it is not always
+ # precise when building with --enable-optimized.
+ if emit_line_info:
+ print('check_vanilla_allocations.py: Source lines with allocation calls:')
+ print('check_vanilla_allocations.py: Accurate in unoptimized builds; jsutil.cpp expected.')
+
+ # Run |nm|. Options:
+ # -u: show only undefined symbols
+ # -C: demangle symbol names
+ # -l: show line number information for each undefined symbol
+ cmd = ['nm', '-u', '-C', '-l', args.file]
+ lines = subprocess.check_output(cmd, universal_newlines=True,
+ stderr=subprocess.PIPE).split('\n')
+
+ # This regexp matches the relevant lines in the output of |nm -l|,
+ # which look like the following.
+ #
+ # U malloc jsutil.cpp:117
+ #
+ alloc_lines_re = r'U ((' + r'|'.join(alloc_fns) + r').*)\s+(\S+:\d+)$'
+
+ for line in lines:
+ m = re.search(alloc_lines_re, line)
+ if m:
+ print('check_vanilla_allocations.py:', m.group(1), 'called at', m.group(3))
+
+ if has_failed:
+ sys.exit(1)
+
+ print('TEST-PASS | check_vanilla_allocations.py | ok')
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ main()
+