blob: 365e1980bcecccde3c3f10e90eea8bc7422d7892 [file] [log] [blame]
Paul Lawrenceeabc3522016-11-11 11:33:42 -08001#!/usr/bin/env python
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -07002
3import argparse
Paul Lawrence89fa81f2017-02-17 10:22:03 -08004import collections
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -07005import logging
Paul Lawrenceeabc3522016-11-11 11:33:42 -08006import os
Luis Hector Chavezfd3f6d72018-08-03 10:38:41 -07007import re
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -07008import subprocess
Paul Lawrenceeabc3522016-11-11 11:33:42 -08009import textwrap
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -070010
Paul Lawrenceeabc3522016-11-11 11:33:42 -080011from gensyscalls import SysCallsTxtParser
12
13
Paul Lawrence7ea40902017-02-14 13:32:23 -080014BPF_JGE = "BPF_JUMP(BPF_JMP|BPF_JGE|BPF_K, {0}, {1}, {2})"
15BPF_ALLOW = "BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW)"
Paul Lawrenceeabc3522016-11-11 11:33:42 -080016
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
Paul Lawrence7ea40902017-02-14 13:32:23 -080024 def __str__(self):
25 return "(%s, %s, %s)" % (self.begin, self.end, self.names)
26
Paul Lawrenceeabc3522016-11-11 11:33:42 -080027 def add(self, name, value):
28 if value != self.end:
29 raise ValueError
30 self.end += 1
31 self.names.append(name)
32
33
Victor Hsieh4f02dd52017-12-20 09:19:22 -080034def 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)])
Paul Lawrence3dd3d552017-04-12 10:02:54 -070038
Steve Muckleaa3f96c2017-07-20 13:11:54 -070039
Victor Hsieh4f02dd52017-12-20 09:19:22 -080040def 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(
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -070043 blacklist_names - base_names))
Paul Lawrence3dd3d552017-04-12 10:02:54 -070044
Victor Hsieh4f02dd52017-12-20 09:19:22 -080045 return (base_names - blacklist_names) | whitelist_names
Paul Lawrenceeabc3522016-11-11 11:33:42 -080046
Paul Lawrence7ea40902017-02-14 13:32:23 -080047
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -070048def parse_syscall_NRs(names_path):
Paul Lawrenceeabc3522016-11-11 11:33:42 -080049 # 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 #
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -070052 # #define __(ARM_)?NR_${NAME} ${VALUE}
53 #
54 # Where ${VALUE} is a preprocessor expression.
Paul Lawrenceeabc3522016-11-11 11:33:42 -080055
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -070056 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"):
Paul Lawrenceeabc3522016-11-11 11:33:42 -080079 continue
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -070080 if name.startswith("__NR_"):
81 # Remote the __NR_ prefix
82 name = name[len("__NR_"):]
83 syscalls[name] = value
Paul Lawrenceeabc3522016-11-11 11:33:42 -080084
Paul Lawrence7ea40902017-02-14 13:32:23 -080085 return syscalls
86
87
88def convert_NRs_to_ranges(syscalls):
Paul Lawrenceeabc3522016-11-11 11:33:42 -080089 # 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))
Paul Lawrence7ea40902017-02-14 13:32:23 -0800104 return ranges
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800105
Paul Lawrence7ea40902017-02-14 13:32:23 -0800106
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)
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800128
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:
Paul Lawrencebe8a2af2017-01-25 15:20:52 -0800139 bpf[i] = statement.format(fail=str(len(bpf) - i),
140 allow=str(len(bpf) - i - 1))
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800141
Paul Lawrencebe8a2af2017-01-25 15:20:52 -0800142 # 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
Paul Lawrence7ea40902017-02-14 13:32:23 -0800145 bpf.append(BPF_ALLOW + ",")
Paul Lawrence65b47c92017-03-22 08:03:51 -0700146
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))) + ',')
Paul Lawrence7ea40902017-02-14 13:32:23 -0800149 return bpf
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800150
Paul Lawrence7ea40902017-02-14 13:32:23 -0800151
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800152def convert_bpf_to_output(bpf, architecture, name_modifier):
153 if name_modifier:
154 name_modifier = name_modifier + "_"
155 else:
156 name_modifier = ""
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800157 header = textwrap.dedent("""\
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700158 // File autogenerated by {self_path} - edit at your peril!!
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800159
160 #include <linux/filter.h>
161 #include <errno.h>
162
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700163 #include "seccomp/seccomp_bpfs.h"
Steve Muckleaa3f96c2017-07-20 13:11:54 -0700164 const sock_filter {architecture}_{suffix}filter[] = {{
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700165 """).format(self_path=os.path.basename(__file__), architecture=architecture,
166 suffix=name_modifier)
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800167
168 footer = textwrap.dedent("""\
169
170 }};
171
Steve Muckleaa3f96c2017-07-20 13:11:54 -0700172 const size_t {architecture}_{suffix}filter_size = sizeof({architecture}_{suffix}filter) / sizeof(struct sock_filter);
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800173 """).format(architecture=architecture,suffix=name_modifier)
Paul Lawrence7ea40902017-02-14 13:32:23 -0800174 return header + "\n".join(bpf) + footer
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800175
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800176
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700177def construct_bpf(syscalls, architecture, name_modifier):
Paul Lawrence7ea40902017-02-14 13:32:23 -0800178 ranges = convert_NRs_to_ranges(syscalls)
179 bpf = convert_ranges_to_bpf(ranges)
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800180 return convert_bpf_to_output(bpf, architecture, name_modifier)
Paul Lawrence7ea40902017-02-14 13:32:23 -0800181
182
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700183def 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)
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800186 whitelist_names = set()
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800187 blacklist_names = set()
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700188 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)
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800193
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700194 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)
Paul Lawrence7ea40902017-02-14 13:32:23 -0800202
203 # And output policy
204 existing = ""
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800205 filename_modifier = "_" + name_modifier if name_modifier else ""
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700206 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)
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800210
Steve Muckleaa3f96c2017-07-20 13:11:54 -0700211
212def main():
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700213 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)
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800249
Steve Muckleaa3f96c2017-07-20 13:11:54 -0700250
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800251if __name__ == "__main__":
252 main()