[lldb/Test] Fix ASan/TSan workaround for Xcode Python 3
authorJonas Devlieghere <jonas@devlieghere.com>
Fri, 12 Jun 2020 02:29:26 +0000 (19:29 -0700)
committerJonas Devlieghere <jonas@devlieghere.com>
Fri, 12 Jun 2020 02:36:42 +0000 (19:36 -0700)
The Python 3 interpreter in Xcode has a relative RPATH and dyld fails to
load it when we copy it into the build directory.

This patch adds an additional check that the copied binary can be
executed. If it doesn't, we assume we're dealing with the Xcode python
interpreter and return the path to the real executable. That is
sufficient for the sanitizers because only system binaries need to be
copied to work around SIP.

This patch also moves all that logic out of LLDBTest and into the lit
configuration so that it's executed only once per test run, instead of
once for every test. Although I didn't benchmark the difference this
should result in a mild speedup.

Differential revision: https://reviews.llvm.org/D81696

lldb/test/API/lit.cfg.py
lldb/test/API/lit.site.cfg.py.in
lldb/test/API/lldbtest.py

index 288d667..632d883 100644 (file)
@@ -21,6 +21,17 @@ config.test_source_root = os.path.dirname(__file__)
 config.test_exec_root = config.test_source_root
 
 
 config.test_exec_root = config.test_source_root
 
 
+def mkdir_p(path):
+    import errno
+    try:
+        os.makedirs(path)
+    except OSError as e:
+        if e.errno != errno.EEXIST:
+            raise
+    if not os.path.isdir(path):
+        raise OSError(errno.ENOTDIR, "%s is not a directory"%path)
+
+
 def find_sanitizer_runtime(name):
   import subprocess
   resource_dir = subprocess.check_output(
 def find_sanitizer_runtime(name):
   import subprocess
   resource_dir = subprocess.check_output(
@@ -38,6 +49,43 @@ def find_shlibpath_var():
     yield 'PATH'
 
 
     yield 'PATH'
 
 
+# On macOS, we can't do the DYLD_INSERT_LIBRARIES trick with a shim python
+# binary as the ASan interceptors get loaded too late. Also, when SIP is
+# enabled, we can't inject libraries into system binaries at all, so we need a
+# copy of the "real" python to work with.
+def find_python_interpreter():
+  # Avoid doing any work if we already copied the binary.
+  copied_python = os.path.join(config.lldb_build_directory, 'copied-python')
+  if os.path.isfile(copied_python):
+    return copied_python
+
+  # Find the "real" python binary.
+  import shutil, subprocess
+  real_python = subprocess.check_output([
+      config.python_executable,
+      os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                   'get_darwin_real_python.py')
+  ]).decode('utf-8').strip()
+
+  shutil.copy(real_python, copied_python)
+
+  # Now make sure the copied Python works. The Python in Xcode has a relative
+  # RPATH and cannot be copied.
+  try:
+    # We don't care about the output, just make sure it runs.
+    subprocess.check_output([copied_python, '-V'], stderr=subprocess.STDOUT)
+  except subprocess.CalledProcessError:
+    # The copied Python didn't work. Assume we're dealing with the Python
+    # interpreter in Xcode. Given that this is not a system binary SIP
+    # won't prevent us form injecting the interceptors so we get away with
+    # not copying the executable.
+    os.remove(copied_python)
+    return real_python
+
+  # The copied Python works.
+  return copied_python
+
+
 if 'Address' in config.llvm_use_sanitizer:
   config.environment['ASAN_OPTIONS'] = 'detect_stack_use_after_return=1'
   if 'Darwin' in config.host_os and 'x86' in config.host_triple:
 if 'Address' in config.llvm_use_sanitizer:
   config.environment['ASAN_OPTIONS'] = 'detect_stack_use_after_return=1'
   if 'Darwin' in config.host_os and 'x86' in config.host_triple:
@@ -49,6 +97,9 @@ if 'Thread' in config.llvm_use_sanitizer:
     config.environment['DYLD_INSERT_LIBRARIES'] = find_sanitizer_runtime(
         'libclang_rt.tsan_osx_dynamic.dylib')
 
     config.environment['DYLD_INSERT_LIBRARIES'] = find_sanitizer_runtime(
         'libclang_rt.tsan_osx_dynamic.dylib')
 
+if 'DYLD_INSERT_LIBRARIES' in config.environment and platform.system() == 'Darwin':
+  config.python_executable = find_python_interpreter()
+
 # Shared library build of LLVM may require LD_LIBRARY_PATH or equivalent.
 if config.shared_libs:
   for shlibpath_var in find_shlibpath_var():
 # Shared library build of LLVM may require LD_LIBRARY_PATH or equivalent.
 if config.shared_libs:
   for shlibpath_var in find_shlibpath_var():
index e5d5c00..e97f867 100644 (file)
@@ -18,6 +18,7 @@ config.shared_libs = @LLVM_ENABLE_SHARED_LIBS@
 config.llvm_use_sanitizer = "@LLVM_USE_SANITIZER@"
 config.target_triple = "@TARGET_TRIPLE@"
 config.lldb_build_directory = "@LLDB_TEST_BUILD_DIRECTORY@"
 config.llvm_use_sanitizer = "@LLVM_USE_SANITIZER@"
 config.target_triple = "@TARGET_TRIPLE@"
 config.lldb_build_directory = "@LLDB_TEST_BUILD_DIRECTORY@"
+config.lldb_reproducer_directory = os.path.join("@LLDB_TEST_BUILD_DIRECTORY@", "reproducers")
 config.python_executable = "@PYTHON_EXECUTABLE@"
 config.dotest_path = "@LLDB_SOURCE_DIR@/test/API/dotest.py"
 config.dotest_args_str = "@LLDB_DOTEST_ARGS@"
 config.python_executable = "@PYTHON_EXECUTABLE@"
 config.dotest_path = "@LLDB_SOURCE_DIR@/test/API/dotest.py"
 config.dotest_args_str = "@LLDB_DOTEST_ARGS@"
index fcc13a0..94eb8eb 100644 (file)
@@ -10,24 +10,6 @@ import lit.TestRunner
 import lit.util
 from lit.formats.base import TestFormat
 
 import lit.util
 from lit.formats.base import TestFormat
 
-def getBuildDir(cmd):
-    found = False
-    for arg in cmd:
-        if found:
-            return arg
-        if arg == '--build-dir':
-            found = True
-    return None
-
-def mkdir_p(path):
-    import errno
-    try:
-        os.makedirs(path)
-    except OSError as e:
-        if e.errno != errno.EEXIST:
-            raise
-    if not os.path.isdir(path):
-        raise OSError(errno.ENOTDIR, "%s is not a directory"%path)
 
 class LLDBTest(TestFormat):
     def __init__(self, dotest_cmd):
 
 class LLDBTest(TestFormat):
     def __init__(self, dotest_cmd):
@@ -73,33 +55,10 @@ class LLDBTest(TestFormat):
         # python exe as the first parameter of the command.
         cmd = [executable] + self.dotest_cmd + [testPath, '-p', testFile]
 
         # python exe as the first parameter of the command.
         cmd = [executable] + self.dotest_cmd + [testPath, '-p', testFile]
 
-        builddir = getBuildDir(cmd)
-        mkdir_p(builddir)
-
-        # On macOS, we can't do the DYLD_INSERT_LIBRARIES trick with a shim
-        # python binary as the ASan interceptors get loaded too late. Also,
-        # when SIP is enabled, we can't inject libraries into system binaries
-        # at all, so we need a copy of the "real" python to work with.
-        #
-        # Find the "real" python binary, copy it, and invoke it.
-        if 'DYLD_INSERT_LIBRARIES' in test.config.environment and \
-                platform.system() == 'Darwin':
-            copied_python = os.path.join(builddir, 'copied-system-python')
-            if not os.path.isfile(copied_python):
-                import shutil, subprocess
-                python = subprocess.check_output([
-                    executable,
-                    os.path.join(os.path.dirname(os.path.realpath(__file__)),
-                        'get_darwin_real_python.py')
-                ]).decode('utf-8').strip()
-                shutil.copy(python, copied_python)
-            cmd[0] = copied_python
-
         if 'lldb-repro-capture' in test.config.available_features or \
            'lldb-repro-replay' in test.config.available_features:
         if 'lldb-repro-capture' in test.config.available_features or \
            'lldb-repro-replay' in test.config.available_features:
-            reproducer_root = os.path.join(builddir, 'reproducers')
-            mkdir_p(reproducer_root)
-            reproducer_path = os.path.join(reproducer_root, testFile)
+            reproducer_path = os.path.join(
+                test.config.lldb_reproducer_directory, testFile)
             if 'lldb-repro-capture' in test.config.available_features:
                 cmd.extend(['--capture-path', reproducer_path])
             else:
             if 'lldb-repro-capture' in test.config.available_features:
                 cmd.extend(['--capture-path', reproducer_path])
             else: