Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python
      2 
      3 import argparse
      4 import collections
      5 import logging
      6 import os
      7 import re
      8 import subprocess
      9 import textwrap
     10 
     11 from gensyscalls import SysCallsTxtParser
     12 
     13 
     14 BPF_JGE = "BPF_JUMP(BPF_JMP|BPF_JGE|BPF_K, {0}, {1}, {2})"
     15 BPF_ALLOW = "BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW)"
     16 
     17 
     18 class 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 
     34 def 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 
     40 def 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 
     48 def 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 
     88 def 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
    113 def 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 
    126 def 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 
    152 def 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 
    177 def 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 
    183 def 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 
    212 def 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 
    251 if __name__ == "__main__":
    252   main()
    253