|
| 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