1#!/usr/bin/env python 2import collections 3import os 4import textwrap 5from gensyscalls import SysCallsTxtParser 6from subprocess import Popen, PIPE 7 8 9BPF_JGE = "BPF_JUMP(BPF_JMP|BPF_JGE|BPF_K, {0}, {1}, {2})" 10BPF_ALLOW = "BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW)" 11 12 13class SyscallRange(object): 14 def __init__(self, name, value): 15 self.names = [name] 16 self.begin = value 17 self.end = self.begin + 1 18 19 def __str__(self): 20 return "(%s, %s, %s)" % (self.begin, self.end, self.names) 21 22 def add(self, name, value): 23 if value != self.end: 24 raise ValueError 25 self.end += 1 26 self.names.append(name) 27 28 29def load_syscall_names_from_file(file_path, architecture): 30 parser = SysCallsTxtParser() 31 parser.parse_open_file(open(file_path)) 32 return set([x["name"] for x in parser.syscalls if x.get(architecture)]) 33 34 35def merge_names(base_names, whitelist_names, blacklist_names): 36 if bool(blacklist_names - base_names): 37 raise RuntimeError("Blacklist item not in bionic - aborting " + str( 38 blacklist_name - base_names)) 39 40 return (base_names - blacklist_names) | whitelist_names 41 42 43def convert_names_to_NRs(names, header_dir, extra_switches): 44 # Run preprocessor over the __NR_syscall symbols, including unistd.h, 45 # to get the actual numbers 46 prefix = "__SECCOMP_" # prefix to ensure no name collisions 47 cpp = Popen(["../../prebuilts/clang/host/linux-x86/clang-stable/bin/clang", 48 "-E", "-nostdinc", "-I" + header_dir, "-Ikernel/uapi/"] 49 + extra_switches 50 + ["-"], 51 stdin=PIPE, stdout=PIPE) 52 cpp.stdin.write("#include <asm/unistd.h>\n") 53 for name in names: 54 # In SYSCALLS.TXT, there are two arm-specific syscalls whose names start 55 # with __ARM__NR_. These we must simply write out as is. 56 if not name.startswith("__ARM_NR_"): 57 cpp.stdin.write(prefix + name + ", __NR_" + name + "\n") 58 else: 59 cpp.stdin.write(prefix + name + ", " + name + "\n") 60 content = cpp.communicate()[0].split("\n") 61 62 # The input is now the preprocessed source file. This will contain a lot 63 # of junk from the preprocessor, but our lines will be in the format: 64 # 65 # __SECCOMP_${NAME}, (0 + value) 66 67 syscalls = [] 68 for line in content: 69 if not line.startswith(prefix): 70 continue 71 72 # We might pick up extra whitespace during preprocessing, so best to strip. 73 name, value = [w.strip() for w in line.split(",")] 74 name = name[len(prefix):] 75 76 # Note that some of the numbers were expressed as base + offset, so we 77 # need to eval, not just int 78 value = eval(value) 79 syscalls.append((name, value)) 80 81 return syscalls 82 83 84def convert_NRs_to_ranges(syscalls): 85 # Sort the values so we convert to ranges and binary chop 86 syscalls = sorted(syscalls, lambda x, y: cmp(x[1], y[1])) 87 88 # Turn into a list of ranges. Keep the names for the comments 89 ranges = [] 90 for name, value in syscalls: 91 if not ranges: 92 ranges.append(SyscallRange(name, value)) 93 continue 94 95 last_range = ranges[-1] 96 if last_range.end == value: 97 last_range.add(name, value) 98 else: 99 ranges.append(SyscallRange(name, value)) 100 return ranges 101 102 103# Converts the sorted ranges of allowed syscalls to a binary tree bpf 104# For a single range, output a simple jump to {fail} or {allow}. We can't set 105# the jump ranges yet, since we don't know the size of the filter, so use a 106# placeholder 107# For multiple ranges, split into two, convert the two halves and output a jump 108# to the correct half 109def convert_to_intermediate_bpf(ranges): 110 if len(ranges) == 1: 111 # We will replace {fail} and {allow} with appropriate range jumps later 112 return [BPF_JGE.format(ranges[0].end, "{fail}", "{allow}") + 113 ", //" + "|".join(ranges[0].names)] 114 else: 115 half = (len(ranges) + 1) / 2 116 first = convert_to_intermediate_bpf(ranges[:half]) 117 second = convert_to_intermediate_bpf(ranges[half:]) 118 jump = [BPF_JGE.format(ranges[half].begin, len(first), 0) + ","] 119 return jump + first + second 120 121 122def convert_ranges_to_bpf(ranges): 123 bpf = convert_to_intermediate_bpf(ranges) 124 125 # Now we know the size of the tree, we can substitute the {fail} and {allow} 126 # placeholders 127 for i, statement in enumerate(bpf): 128 # Replace placeholder with 129 # "distance to jump to fail, distance to jump to allow" 130 # We will add a kill statement and an allow statement after the tree 131 # With bpfs jmp 0 means the next statement, so the distance to the end is 132 # len(bpf) - i - 1, which is where we will put the kill statement, and 133 # then the statement after that is the allow statement 134 if "{fail}" in statement and "{allow}" in statement: 135 bpf[i] = statement.format(fail=str(len(bpf) - i), 136 allow=str(len(bpf) - i - 1)) 137 138 139 # Add the allow calls at the end. If the syscall is not matched, we will 140 # continue. This allows the user to choose to match further syscalls, and 141 # also to choose the action when we want to block 142 bpf.append(BPF_ALLOW + ",") 143 144 # Add check that we aren't off the bottom of the syscalls 145 bpf.insert(0, BPF_JGE.format(ranges[0].begin, 0, str(len(bpf))) + ',') 146 return bpf 147 148 149def convert_bpf_to_output(bpf, architecture, name_modifier): 150 if name_modifier: 151 name_modifier = name_modifier + "_" 152 else: 153 name_modifier = "" 154 header = textwrap.dedent("""\ 155 // Autogenerated file - edit at your peril!! 156 157 #include <linux/filter.h> 158 #include <errno.h> 159 160 #include "seccomp_bpfs.h" 161 const sock_filter {architecture}_{suffix}filter[] = {{ 162 """).format(architecture=architecture,suffix=name_modifier) 163 164 footer = textwrap.dedent("""\ 165 166 }}; 167 168 const size_t {architecture}_{suffix}filter_size = sizeof({architecture}_{suffix}filter) / sizeof(struct sock_filter); 169 """).format(architecture=architecture,suffix=name_modifier) 170 return header + "\n".join(bpf) + footer 171 172 173def construct_bpf(names, architecture, header_dir, extra_switches, 174 name_modifier): 175 syscalls = convert_names_to_NRs(names, header_dir, extra_switches) 176 ranges = convert_NRs_to_ranges(syscalls) 177 bpf = convert_ranges_to_bpf(ranges) 178 return convert_bpf_to_output(bpf, architecture, name_modifier) 179 180 181# final syscalls = base - blacklists + whitelists 182ANDROID_SYSTEM_SYSCALL_FILES = { 183 "base": "SYSCALLS.TXT", 184 "whitelists": [ 185 "SECCOMP_WHITELIST_COMMON.TXT", 186 "SECCOMP_WHITELIST_SYSTEM.TXT"], 187 "blacklists": ["SECCOMP_BLACKLIST_COMMON.TXT"] 188} 189 190ANDROID_APP_SYSCALL_FILES = { 191 "base": "SYSCALLS.TXT", 192 "whitelists": [ 193 "SECCOMP_WHITELIST_COMMON.TXT", 194 "SECCOMP_WHITELIST_APP.TXT"], 195 "blacklists": [ 196 "SECCOMP_BLACKLIST_COMMON.TXT", 197 "SECCOMP_BLACKLIST_APP.TXT"] 198} 199 200ANDROID_GLOBAL_SYSCALL_FILES = { 201 "base": "SYSCALLS.TXT", 202 "whitelists": [ 203 "SECCOMP_WHITELIST_COMMON.TXT", 204 "SECCOMP_WHITELIST_SYSTEM.TXT", 205 "SECCOMP_WHITELIST_APP.TXT", 206 "SECCOMP_WHITELIST_GLOBAL.TXT"], 207 "blacklists": ["SECCOMP_BLACKLIST_COMMON.TXT"] 208} 209 210 211POLICY_CONFIGS = [("arm", "kernel/uapi/asm-arm", []), 212 ("arm64", "kernel/uapi/asm-arm64", []), 213 ("x86", "kernel/uapi/asm-x86", ["-D__i386__"]), 214 ("x86_64", "kernel/uapi/asm-x86", []), 215 ("mips", "kernel/uapi/asm-mips", ["-D_MIPS_SIM=_MIPS_SIM_ABI32"]), 216 ("mips64", "kernel/uapi/asm-mips", ["-D_MIPS_SIM=_MIPS_SIM_ABI64"])] 217 218 219def set_dir(): 220 # Set working directory for predictable results 221 os.chdir(os.path.join(os.environ["ANDROID_BUILD_TOP"], "bionic/libc")) 222 223 224def gen_policy(syscall_files, name_modifier): 225 for arch, header_path, switches in POLICY_CONFIGS: 226 base_names = load_syscall_names_from_file(syscall_files["base"], arch) 227 whitelist_names = set() 228 for f in syscall_files["whitelists"]: 229 whitelist_names |= load_syscall_names_from_file(f, arch) 230 blacklist_names = set() 231 for f in syscall_files["blacklists"]: 232 blacklist_names |= load_syscall_names_from_file(f, arch) 233 234 names = merge_names(base_names, whitelist_names, blacklist_names) 235 output = construct_bpf(names, arch, header_path, switches, name_modifier) 236 237 # And output policy 238 existing = "" 239 filename_modifier = "_" + name_modifier if name_modifier else "" 240 output_path = "seccomp/{}{}_policy.cpp".format(arch, filename_modifier) 241 if os.path.isfile(output_path): 242 existing = open(output_path).read() 243 if output == existing: 244 print "File " + output_path + " not changed." 245 else: 246 with open(output_path, "w") as output_file: 247 output_file.write(output) 248 print "Generated file " + output_path 249 250 251def main(): 252 set_dir() 253 gen_policy(ANDROID_SYSTEM_SYSCALL_FILES, 'system') 254 gen_policy(ANDROID_APP_SYSCALL_FILES, 'app') 255 gen_policy(ANDROID_GLOBAL_SYSCALL_FILES, 'global') 256 257 258if __name__ == "__main__": 259 main() 260