Skip to content

Commit c45f486

Browse files
authored
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.
2 parents c567291 + 3c419c1 commit c45f486

File tree

2 files changed

+168
-2
lines changed

2 files changed

+168
-2
lines changed

lit.cfg

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ llvm_bin_dir = lit_config.params.get("llvm-bin-dir")
139139
if llvm_bin_dir is None:
140140
lit_config.fatal("'--param llvm_bin_dir=PATH' is required")
141141
filecheck_path = os.path.join(llvm_bin_dir, 'FileCheck')
142+
readelf_path = os.path.join(llvm_bin_dir, 'llvm-readelf')
143+
lit_config.note("testing using 'FileCheck': %r" % (filecheck_path,))
144+
lit_config.note("testing using 'readelf': %r" % (readelf_path,))
145+
142146

143147
# Use the default Swift src layout if swiftpm is not provided as a
144148
# param
@@ -150,7 +154,6 @@ if os.path.exists(swiftpm_srcdir):
150154
config.substitutions.append( ('%{swiftpm_srcdir}', swiftpm_srcdir) )
151155

152156
# Find the tools we need.
153-
lit_config.note("testing using 'FileCheck': %r" % (filecheck_path,))
154157

155158
swift_path = lit_config.params.get(
156159
"swift",
@@ -182,11 +185,13 @@ if not os.path.exists(lldb_path):
182185

183186
# Define our supported substitutions.
184187
config.substitutions.append( ('%{package_path}', package_path) )
188+
config.substitutions.append( ('%{python}', sys.executable) )
185189
config.substitutions.append( ('%{not}', os.path.join(srcroot, "not")) )
186190
config.substitutions.append( ('%{lldb}', lldb_path) )
187191
config.substitutions.append( ('%{swift}', swift_path) )
188192
config.substitutions.append( ('%{swiftc}', swiftc_path) )
189193
config.substitutions.append( ('%{FileCheck}', filecheck_path) )
194+
config.substitutions.append( ('%{readelf}', readelf_path) )
190195

191196
# Add substitutions for swiftpm executables.
192197
swiftpm_build = lit_config.params.get("swiftpm-build")
@@ -203,6 +208,6 @@ else:
203208
###
204209

205210
# Protected against unquoted use of substitutions.
206-
for name in ('swift-build', 'FileCheck'):
211+
for name in ('swift-build', 'FileCheck', 'readelf'):
207212
config.substitutions.append((' {0} '.format(name),
208213
' unquoted-command-name-{0} '.format(name)))
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
2+
# REQUIRES: platform=Linux
3+
# RUN: rm -rf %T && mkdir -p %t
4+
# RUN: %{python} %s '%{package_path}' '%T' '%{readelf}'
5+
6+
# Test that all linux libraries that we provide do not have any load
7+
# commands that are both writeable and executable.
8+
9+
import argparse
10+
import re
11+
import sys
12+
import subprocess
13+
14+
# For each library, we want to run llvm-readelf on it and verify that none of
15+
# the flag fields say that the load commands are both writable and
16+
# executable. Our target outputs look like this:
17+
#
18+
# ----
19+
# There are 7 program headers, starting at offset 64
20+
#
21+
# Program Headers:
22+
# Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
23+
# PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x000188 0x000188 R 0x8
24+
# LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x9839a0 0x9839a0 R E 0x1000
25+
# LOAD 0x983a60 0x0000000000984a60 0x0000000000984a60 0x07ad78 0x0a3da9 RW 0x1000
26+
# DYNAMIC 0x9b5b88 0x00000000009b6b88 0x00000000009b6b88 0x0002f0 0x0002f0 RW 0x8
27+
# GNU_EH_FRAME 0x95ecd4 0x000000000095ecd4 0x000000000095ecd4 0x024ccc 0x024ccc R 0x4
28+
# GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x0
29+
# GNU_RELRO 0x983a60 0x0000000000984a60 0x0000000000984a60 0x0345a0 0x0345a0 RW 0x10
30+
# ----
31+
#
32+
# TODO: Evaluate if parallelism helps here. We /could/ use libdispatch to work
33+
# in parallel over all artifacts.
34+
class ParseState(object):
35+
firstLine = 0
36+
programHeadersLine = 1
37+
dataHeader = 2
38+
data = 3
39+
40+
def __init__(self, state=None):
41+
if state is None:
42+
state = ParseState.firstLine
43+
self.value = state
44+
45+
@property
46+
def regex_string(self):
47+
if self.value == ParseState.firstLine:
48+
return "There are (\d+) program headers"
49+
if self.value == ParseState.programHeadersLine:
50+
return "Program Headers:"
51+
if self.value == ParseState.dataHeader:
52+
return "\\s+Type"
53+
if self.value == ParseState.data:
54+
name = "(\w+)"
55+
hex_pattern = "0x[0-9a-fA-F]+"
56+
ws = "\s"
57+
col = "{}+{}".format(ws, hex_pattern)
58+
return "^{ws}*{name}{col}{col}{col}{col}{col} (.+) 0x".format(**
59+
{'ws': ws, 'name': name, 'col': col})
60+
raise RuntimeError('Invalid ParseState value')
61+
62+
@property
63+
def regex(self):
64+
return re.compile(self.regex_string)
65+
66+
@property
67+
def next(self):
68+
if self.value == ParseState.firstLine:
69+
return ParseState(ParseState.programHeadersLine)
70+
if self.value == ParseState.programHeadersLine:
71+
return ParseState(ParseState.dataHeader)
72+
if self.value == ParseState.dataHeader:
73+
return ParseState(ParseState.data)
74+
if self.value == ParseState.data:
75+
return self
76+
raise RuntimeError('Invalid ParseState value')
77+
78+
def matches(self, input_string):
79+
return self.regex.match(input_string)
80+
81+
def process_library(args, lib):
82+
assert(len(lib) > 0)
83+
84+
numberOfLines = None
85+
numberOfLinesSeen = 0
86+
87+
print("Visiting lib: {}".format(lib))
88+
lines = list(reversed(subprocess.check_output([args.read_elf, "-program-headers", lib]).split("\n")[:-1]))
89+
p = ParseState()
90+
91+
# Until we finish parsing or run out of lines to parse...
92+
while len(lines) > 0:
93+
l = lines.pop()
94+
print("DUMP: '{}'".format(l))
95+
assert(p is not None)
96+
curState = p
97+
98+
m = curState.matches(l)
99+
if m is None:
100+
continue
101+
102+
p = curState.next
103+
if curState.value == ParseState.firstLine:
104+
numberOfLines = int(m.group(1))
105+
continue
106+
107+
if curState.value == ParseState.programHeadersLine:
108+
continue
109+
110+
if curState.value == ParseState.dataHeader:
111+
continue
112+
113+
if curState.value == ParseState.data:
114+
val = m.group(1)
115+
if val == "LOAD":
116+
flags = m.group(2)
117+
print("Found LOAD command! Flags: '{}'. Full match: '{}'".format(flags, l))
118+
if "W" in flags and "E" in flags:
119+
raise RuntimeError("Found a load command that loads something executable and writeable")
120+
121+
# If we haven't seen enough lines, continue.
122+
assert(numberOfLines is not None)
123+
if numberOfLinesSeen != numberOfLines - 1:
124+
numberOfLinesSeen += 1
125+
continue
126+
127+
# If we have seen enough lines, be sure to not only break out
128+
# of the switch, but additionally break out of the whole
129+
# parsing loop. We could go through the rest of the output from
130+
# llvm-readelf, but there isn't any point.
131+
p = None
132+
break
133+
134+
# If we ran out of lines to parse without finishing parsing, we failed.
135+
assert(p is None)
136+
assert(numberOfLines is not None)
137+
assert(numberOfLinesSeen == numberOfLines - 1)
138+
139+
def get_libraries(package_path):
140+
cmd = [
141+
"/usr/bin/find",
142+
package_path,
143+
"-iname",
144+
"*.so"
145+
]
146+
return subprocess.check_output(cmd).split("\n")[:-1]
147+
148+
def main():
149+
parser = argparse.ArgumentParser()
150+
parser.add_argument('package_path')
151+
parser.add_argument('tmp_dir')
152+
parser.add_argument('read_elf')
153+
args = parser.parse_args()
154+
155+
libraries = get_libraries(args.package_path)
156+
for l in libraries:
157+
process_library(args, l)
158+
sys.exit(0)
159+
160+
if __name__ == "__main__":
161+
main()

0 commit comments

Comments
 (0)