Merge pull request #28 from gottesmm/pr-0a55d87a0272d595f447879875576af64c1da6ea
Add a new test that makes sure that on Linux, all libraries in a snapshot do not load any memory as writeable and executable.
diff --git a/lit.cfg b/lit.cfg
index d62de07..79adc57 100644
--- a/lit.cfg
+++ b/lit.cfg
@@ -139,6 +139,10 @@
if llvm_bin_dir is None:
lit_config.fatal("'--param llvm_bin_dir=PATH' is required")
filecheck_path = os.path.join(llvm_bin_dir, 'FileCheck')
+readelf_path = os.path.join(llvm_bin_dir, 'llvm-readelf')
+lit_config.note("testing using 'FileCheck': %r" % (filecheck_path,))
+lit_config.note("testing using 'readelf': %r" % (readelf_path,))
+
# Use the default Swift src layout if swiftpm is not provided as a
# param
@@ -150,7 +154,6 @@
config.substitutions.append( ('%{swiftpm_srcdir}', swiftpm_srcdir) )
# Find the tools we need.
-lit_config.note("testing using 'FileCheck': %r" % (filecheck_path,))
swift_path = lit_config.params.get(
"swift",
@@ -182,11 +185,13 @@
# Define our supported substitutions.
config.substitutions.append( ('%{package_path}', package_path) )
+config.substitutions.append( ('%{python}', sys.executable) )
config.substitutions.append( ('%{not}', os.path.join(srcroot, "not")) )
config.substitutions.append( ('%{lldb}', lldb_path) )
config.substitutions.append( ('%{swift}', swift_path) )
config.substitutions.append( ('%{swiftc}', swiftc_path) )
config.substitutions.append( ('%{FileCheck}', filecheck_path) )
+config.substitutions.append( ('%{readelf}', readelf_path) )
# Add substitutions for swiftpm executables.
swiftpm_build = lit_config.params.get("swiftpm-build")
@@ -203,6 +208,6 @@
###
# Protected against unquoted use of substitutions.
-for name in ('swift-build', 'FileCheck'):
+for name in ('swift-build', 'FileCheck', 'readelf'):
config.substitutions.append((' {0} '.format(name),
' unquoted-command-name-{0} '.format(name)))
diff --git a/test-snapshot-binaries/linux_load_commands.py b/test-snapshot-binaries/linux_load_commands.py
new file mode 100644
index 0000000..f7d044e
--- /dev/null
+++ b/test-snapshot-binaries/linux_load_commands.py
@@ -0,0 +1,161 @@
+
+# REQUIRES: platform=Linux
+# RUN: rm -rf %T && mkdir -p %t
+# RUN: %{python} %s '%{package_path}' '%T' '%{readelf}'
+
+# Test that all linux libraries that we provide do not have any load
+# commands that are both writeable and executable.
+
+import argparse
+import re
+import sys
+import subprocess
+
+# For each library, we want to run llvm-readelf on it and verify that none of
+# the flag fields say that the load commands are both writable and
+# executable. Our target outputs look like this:
+#
+# ----
+# There are 7 program headers, starting at offset 64
+#
+# Program Headers:
+# Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
+# PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x000188 0x000188 R 0x8
+# LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x9839a0 0x9839a0 R E 0x1000
+# LOAD 0x983a60 0x0000000000984a60 0x0000000000984a60 0x07ad78 0x0a3da9 RW 0x1000
+# DYNAMIC 0x9b5b88 0x00000000009b6b88 0x00000000009b6b88 0x0002f0 0x0002f0 RW 0x8
+# GNU_EH_FRAME 0x95ecd4 0x000000000095ecd4 0x000000000095ecd4 0x024ccc 0x024ccc R 0x4
+# GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x0
+# GNU_RELRO 0x983a60 0x0000000000984a60 0x0000000000984a60 0x0345a0 0x0345a0 RW 0x10
+# ----
+#
+# TODO: Evaluate if parallelism helps here. We /could/ use libdispatch to work
+# in parallel over all artifacts.
+class ParseState(object):
+ firstLine = 0
+ programHeadersLine = 1
+ dataHeader = 2
+ data = 3
+
+ def __init__(self, state=None):
+ if state is None:
+ state = ParseState.firstLine
+ self.value = state
+
+ @property
+ def regex_string(self):
+ if self.value == ParseState.firstLine:
+ return "There are (\d+) program headers"
+ if self.value == ParseState.programHeadersLine:
+ return "Program Headers:"
+ if self.value == ParseState.dataHeader:
+ return "\\s+Type"
+ if self.value == ParseState.data:
+ name = "(\w+)"
+ hex_pattern = "0x[0-9a-fA-F]+"
+ ws = "\s"
+ col = "{}+{}".format(ws, hex_pattern)
+ return "^{ws}*{name}{col}{col}{col}{col}{col} (.+) 0x".format(**
+ {'ws': ws, 'name': name, 'col': col})
+ raise RuntimeError('Invalid ParseState value')
+
+ @property
+ def regex(self):
+ return re.compile(self.regex_string)
+
+ @property
+ def next(self):
+ if self.value == ParseState.firstLine:
+ return ParseState(ParseState.programHeadersLine)
+ if self.value == ParseState.programHeadersLine:
+ return ParseState(ParseState.dataHeader)
+ if self.value == ParseState.dataHeader:
+ return ParseState(ParseState.data)
+ if self.value == ParseState.data:
+ return self
+ raise RuntimeError('Invalid ParseState value')
+
+ def matches(self, input_string):
+ return self.regex.match(input_string)
+
+def process_library(args, lib):
+ assert(len(lib) > 0)
+
+ numberOfLines = None
+ numberOfLinesSeen = 0
+
+ print("Visiting lib: {}".format(lib))
+ lines = list(reversed(subprocess.check_output([args.read_elf, "-program-headers", lib]).split("\n")[:-1]))
+ p = ParseState()
+
+ # Until we finish parsing or run out of lines to parse...
+ while len(lines) > 0:
+ l = lines.pop()
+ print("DUMP: '{}'".format(l))
+ assert(p is not None)
+ curState = p
+
+ m = curState.matches(l)
+ if m is None:
+ continue
+
+ p = curState.next
+ if curState.value == ParseState.firstLine:
+ numberOfLines = int(m.group(1))
+ continue
+
+ if curState.value == ParseState.programHeadersLine:
+ continue
+
+ if curState.value == ParseState.dataHeader:
+ continue
+
+ if curState.value == ParseState.data:
+ val = m.group(1)
+ if val == "LOAD":
+ flags = m.group(2)
+ print("Found LOAD command! Flags: '{}'. Full match: '{}'".format(flags, l))
+ if "W" in flags and "E" in flags:
+ raise RuntimeError("Found a load command that loads something executable and writeable")
+
+ # If we haven't seen enough lines, continue.
+ assert(numberOfLines is not None)
+ if numberOfLinesSeen != numberOfLines - 1:
+ numberOfLinesSeen += 1
+ continue
+
+ # If we have seen enough lines, be sure to not only break out
+ # of the switch, but additionally break out of the whole
+ # parsing loop. We could go through the rest of the output from
+ # llvm-readelf, but there isn't any point.
+ p = None
+ break
+
+ # If we ran out of lines to parse without finishing parsing, we failed.
+ assert(p is None)
+ assert(numberOfLines is not None)
+ assert(numberOfLinesSeen == numberOfLines - 1)
+
+def get_libraries(package_path):
+ cmd = [
+ "/usr/bin/find",
+ package_path,
+ "-iname",
+ "*.so"
+ ]
+ return subprocess.check_output(cmd).split("\n")[:-1]
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('package_path')
+ parser.add_argument('tmp_dir')
+ parser.add_argument('read_elf')
+ args = parser.parse_args()
+
+ libraries = get_libraries(args.package_path)
+ for l in libraries:
+ process_library(args, l)
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()