• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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