1#!/usr/bin/env python 2 3import argparse 4import collections 5import logging 6import os 7import re 8import subprocess 9import textwrap 10 11from gensyscalls import SysCallsTxtParser 12 13 14BPF_JGE = "BPF_JUMP(BPF_JMP|BPF_JGE|BPF_K, {0}, {1}, {2})" 15BPF_ALLOW = "BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW)" 16 17 18class SyscallRange(object): 19 def __init__(self, name, value): 20 self.names = [name] 21 self.begin = value 22 self.end = self.begin + 1 23 24 def __str__(self): 25 return "(%s, %s, %s)" % (self.begin, self.end, self.names) 26 27 def add(self, name, value): 28 if value != self.end: 29 raise ValueError 30 self.end += 1 31 self.names.append(name) 32 33 34def load_syscall_names_from_file(file_path, architecture): 35 parser = SysCallsTxtParser() 36 parser.parse_open_file(open(file_path)) 37 return set([x["name"] for x in parser.syscalls if x.get(architecture)]) 38 39 40def merge_names(base_names, whitelist_names, blacklist_names): 41 if bool(blacklist_names - base_names): 42 raise RuntimeError("Blacklist item not in bionic - aborting " + str( 43 blacklist_names - base_names)) 44 45 return (base_names - blacklist_names) | whitelist_names 46 47 48def parse_syscall_NRs(names_path): 49 # The input is now the preprocessed source file. This will contain a lot 50 # of junk from the preprocessor, but our lines will be in the format: 51 # 52 # #define __(ARM_)?NR_${NAME} ${VALUE} 53 # 54 # Where ${VALUE} is a preprocessor expression. 55 56 constant_re = re.compile( 57 r'^\s*#define\s+([A-Za-z_][A-Za-z0-9_]+)\s+(.+)\s*$') 58 token_re = re.compile(r'\b[A-Za-z_][A-Za-z0-9_]+\b') 59 constants = {} 60 with open(names_path) as f: 61 for line in f: 62 m = constant_re.match(line) 63 if not m: 64 continue 65 try: 66 name = m.group(1) 67 # eval() takes care of any arithmetic that may be done 68 value = eval(token_re.sub(lambda x: str(constants[x.group(0)]), 69 m.group(2))) 70 71 constants[name] = value 72 except: 73 logging.debug('Failed to parse %s', line) 74 pass 75 76 syscalls = {} 77 for name, value in constants.iteritems(): 78 if not name.startswith("__NR_") and not name.startswith("__ARM_NR"): 79 continue 80 if name.startswith("__NR_"): 81 # Remote the __NR_ prefix 82 name = name[len("__NR_"):] 83 syscalls[name] = value 84 85 return syscalls 86 87 88def convert_NRs_to_ranges(syscalls): 89 # Sort the values so we convert to ranges and binary chop 90 syscalls = sorted(syscalls, lambda x, y: cmp(x[1], y[1])) 91 92 # Turn into a list of ranges. Keep the names for the comments 93 ranges = [] 94 for name, value in syscalls: 95 if not ranges: 96 ranges.append(SyscallRange(name, value)) 97 continue 98 99 last_range = ranges[-1] 100 if last_range.end == value: 101 last_range.add(name, value) 102 else: 103 ranges.append(SyscallRange(name, value)) 104 return ranges 105 106 107# Converts the sorted ranges of allowed syscalls to a binary tree bpf 108# For a single range, output a simple jump to {fail} or {allow}. We can't set 109# the jump ranges yet, since we don't know the size of the filter, so use a 110# placeholder 111# For multiple ranges, split into two, convert the two halves and output a jump 112# to the correct half 113def convert_to_intermediate_bpf(ranges): 114 if len(ranges) == 1: 115 # We will replace {fail} and {allow} with appropriate range jumps later 116 return [BPF_JGE.format(ranges[0].end, "{fail}", "{allow}") + 117 ", //" + "|".join(ranges[0].names)] 118 else: 119 half = (len(ranges) + 1) / 2 120 first = convert_to_intermediate_bpf(ranges[:half]) 121 second = convert_to_intermediate_bpf(ranges[half:]) 122 jump = [BPF_JGE.format(ranges[half].begin, len(first), 0) + ","] 123 return jump + first + second 124 125 126def convert_ranges_to_bpf(ranges): 127 bpf = convert_to_intermediate_bpf(ranges) 128 129 # Now we know the size of the tree, we can substitute the {fail} and {allow} 130 # placeholders 131 for i, statement in enumerate(bpf): 132 # Replace placeholder with 133 # "distance to jump to fail, distance to jump to allow" 134 # We will add a kill statement and an allow statement after the tree 135 # With bpfs jmp 0 means the next statement, so the distance to the end is 136 # len(bpf) - i - 1, which is where we will put the kill statement, and 137 # then the statement after that is the allow statement 138 if "{fail}" in statement and "{allow}" in statement: 139 bpf[i] = statement.format(fail=str(len(bpf) - i), 140 allow=str(len(bpf) - i - 1)) 141 142 # Add the allow calls at the end. If the syscall is not matched, we will 143 # continue. This allows the user to choose to match further syscalls, and 144 # also to choose the action when we want to block 145 bpf.append(BPF_ALLOW + ",") 146 147 # Add check that we aren't off the bottom of the syscalls 148 bpf.insert(0, BPF_JGE.format(ranges[0].begin, 0, str(len(bpf))) + ',') 149 return bpf 150 151 152def convert_bpf_to_output(bpf, architecture, name_modifier): 153 if name_modifier: 154 name_modifier = name_modifier + "_" 155 else: 156 name_modifier = "" 157 header = textwrap.dedent("""\ 158 // File autogenerated by {self_path} - edit at your peril!! 159 160 #include <linux/filter.h> 161 #include <errno.h> 162 163 #include "seccomp/seccomp_bpfs.h" 164 const sock_filter {architecture}_{suffix}filter[] = {{ 165 """).format(self_path=os.path.basename(__file__), architecture=architecture, 166 suffix=name_modifier) 167 168 footer = textwrap.dedent("""\ 169 170 }}; 171 172 const size_t {architecture}_{suffix}filter_size = sizeof({architecture}_{suffix}filter) / sizeof(struct sock_filter); 173 """).format(architecture=architecture,suffix=name_modifier) 174 return header + "\n".join(bpf) + footer 175 176 177def construct_bpf(syscalls, architecture, name_modifier): 178 ranges = convert_NRs_to_ranges(syscalls) 179 bpf = convert_ranges_to_bpf(ranges) 180 return convert_bpf_to_output(bpf, architecture, name_modifier) 181 182 183def gen_policy(name_modifier, out_dir, base_syscall_file, syscall_files, syscall_NRs): 184 for arch in ('arm', 'arm64', 'mips', 'mips64', 'x86', 'x86_64'): 185 base_names = load_syscall_names_from_file(base_syscall_file, arch) 186 whitelist_names = set() 187 blacklist_names = set() 188 for f in syscall_files: 189 if "blacklist" in f.lower(): 190 blacklist_names |= load_syscall_names_from_file(f, arch) 191 else: 192 whitelist_names |= load_syscall_names_from_file(f, arch) 193 194 allowed_syscalls = [] 195 for name in merge_names(base_names, whitelist_names, blacklist_names): 196 try: 197 allowed_syscalls.append((name, syscall_NRs[arch][name])) 198 except: 199 logging.exception("Failed to find %s in %s", name, arch) 200 raise 201 output = construct_bpf(allowed_syscalls, arch, name_modifier) 202 203 # And output policy 204 existing = "" 205 filename_modifier = "_" + name_modifier if name_modifier else "" 206 output_path = os.path.join(out_dir, 207 "{}{}_policy.cpp".format(arch, filename_modifier)) 208 with open(output_path, "w") as output_file: 209 output_file.write(output) 210 211 212def main(): 213 parser = argparse.ArgumentParser( 214 description="Generates a seccomp-bpf policy") 215 parser.add_argument("--verbose", "-v", help="Enables verbose logging.") 216 parser.add_argument("--name-modifier", 217 help=("Specifies the name modifier for the policy. " 218 "One of {app,global,system}.")) 219 parser.add_argument("--out-dir", 220 help="The output directory for the policy files") 221 parser.add_argument("base_file", metavar="base-file", type=str, 222 help="The path of the base syscall list (SYSCALLS.TXT).") 223 parser.add_argument("files", metavar="FILE", type=str, nargs="+", 224 help=("The path of the input files. In order to " 225 "simplify the build rules, it can take any of the " 226 "following files: \n" 227 "* /blacklist.*\.txt$/ syscall blacklist.\n" 228 "* /whitelist.*\.txt$/ syscall whitelist.\n" 229 "* otherwise, syscall name-number mapping.\n")) 230 args = parser.parse_args() 231 232 if args.verbose: 233 logging.basicConfig(level=logging.DEBUG) 234 else: 235 logging.basicConfig(level=logging.INFO) 236 237 syscall_files = [] 238 syscall_NRs = {} 239 for filename in args.files: 240 if filename.lower().endswith('.txt'): 241 syscall_files.append(filename) 242 else: 243 m = re.search(r"libseccomp_gen_syscall_nrs_([^/]+)", filename) 244 syscall_NRs[m.group(1)] = parse_syscall_NRs(filename) 245 246 gen_policy(name_modifier=args.name_modifier, out_dir=args.out_dir, 247 syscall_NRs=syscall_NRs, base_syscall_file=args.base_file, 248 syscall_files=args.files) 249 250 251if __name__ == "__main__": 252 main() 253