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