blob: 8a07caffc625f7891d97cf1ee0486574ac7fb287 [file] [log] [blame]
Elliott Hughes6b586e72021-04-15 13:39:08 -07001#!/usr/bin/env python3
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -07002
3import argparse
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -07004import logging
Elliott Hughesbc6999f2021-02-03 13:13:57 -08005import operator
Paul Lawrenceeabc3522016-11-11 11:33:42 -08006import os
Luis Hector Chavezfd3f6d72018-08-03 10:38:41 -07007import re
Elliott Hughes704772b2022-10-10 17:06:43 +00008import sys
Paul Lawrenceeabc3522016-11-11 11:33:42 -08009import textwrap
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -070010
Elliott Hughes704772b2022-10-10 17:06:43 +000011from gensyscalls import SysCallsTxtParser
Paul Lawrenceeabc3522016-11-11 11:33:42 -080012
13
Paul Lawrence7ea40902017-02-14 13:32:23 -080014BPF_JGE = "BPF_JUMP(BPF_JMP|BPF_JGE|BPF_K, {0}, {1}, {2})"
Bram Bonnéacadd092020-05-06 13:49:55 +020015BPF_JEQ = "BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, {0}, {1}, {2})"
Paul Lawrence7ea40902017-02-14 13:32:23 -080016BPF_ALLOW = "BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW)"
Paul Lawrenceeabc3522016-11-11 11:33:42 -080017
18
Elliott Hughesbc6999f2021-02-03 13:13:57 -080019class SyscallRange:
Paul Lawrenceeabc3522016-11-11 11:33:42 -080020 def __init__(self, name, value):
21 self.names = [name]
22 self.begin = value
23 self.end = self.begin + 1
24
Paul Lawrence7ea40902017-02-14 13:32:23 -080025 def __str__(self):
26 return "(%s, %s, %s)" % (self.begin, self.end, self.names)
27
Paul Lawrenceeabc3522016-11-11 11:33:42 -080028 def add(self, name, value):
29 if value != self.end:
30 raise ValueError
31 self.end += 1
32 self.names.append(name)
33
34
Victor Hsieh4f02dd52017-12-20 09:19:22 -080035def load_syscall_names_from_file(file_path, architecture):
36 parser = SysCallsTxtParser()
37 parser.parse_open_file(open(file_path))
Elliott Hughesbc6999f2021-02-03 13:13:57 -080038 return {x["name"] for x in parser.syscalls if x.get(architecture)}
Paul Lawrence3dd3d552017-04-12 10:02:54 -070039
Steve Muckleaa3f96c2017-07-20 13:11:54 -070040
Bram Bonnéacadd092020-05-06 13:49:55 +020041def load_syscall_priorities_from_file(file_path):
42 format_re = re.compile(r'^\s*([A-Za-z_][A-Za-z0-9_]+)\s*$')
43 priorities = []
Elliott Hughesbc6999f2021-02-03 13:13:57 -080044 with open(file_path) as priority_file:
45 for line in priority_file:
46 match = format_re.match(line)
47 if match is None:
Bram Bonnéacadd092020-05-06 13:49:55 +020048 continue
49 try:
Elliott Hughesbc6999f2021-02-03 13:13:57 -080050 name = match.group(1)
Bram Bonnéacadd092020-05-06 13:49:55 +020051 priorities.append(name)
Elliott Hughesbc6999f2021-02-03 13:13:57 -080052 except IndexError:
53 # TODO: This should be impossible becauase it wouldn't have matched?
54 logging.exception('Failed to parse %s from %s', line, file_path)
Bram Bonnéacadd092020-05-06 13:49:55 +020055
56 return priorities
57
58
Victor Hsiehdbb86702020-06-15 09:29:07 -070059def merge_names(base_names, allowlist_names, blocklist_names):
60 if bool(blocklist_names - base_names):
61 raise RuntimeError("blocklist item not in bionic - aborting " + str(
62 blocklist_names - base_names))
Paul Lawrence3dd3d552017-04-12 10:02:54 -070063
Victor Hsiehdbb86702020-06-15 09:29:07 -070064 return (base_names - blocklist_names) | allowlist_names
Paul Lawrenceeabc3522016-11-11 11:33:42 -080065
Paul Lawrence7ea40902017-02-14 13:32:23 -080066
Bram Bonnéacadd092020-05-06 13:49:55 +020067def extract_priority_syscalls(syscalls, priorities):
68 # Extract syscalls that are not in the priority list
69 other_syscalls = \
70 [syscall for syscall in syscalls if syscall[0] not in priorities]
71 # For prioritized syscalls, keep the order in which they appear in th
72 # priority list
73 syscall_dict = {syscall[0]: syscall[1] for syscall in syscalls}
74 priority_syscalls = []
75 for name in priorities:
76 if name in syscall_dict.keys():
77 priority_syscalls.append((name, syscall_dict[name]))
78 return priority_syscalls, other_syscalls
79
80
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -070081def parse_syscall_NRs(names_path):
Paul Lawrenceeabc3522016-11-11 11:33:42 -080082 # The input is now the preprocessed source file. This will contain a lot
83 # of junk from the preprocessor, but our lines will be in the format:
84 #
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -070085 # #define __(ARM_)?NR_${NAME} ${VALUE}
86 #
87 # Where ${VALUE} is a preprocessor expression.
Elliott Hughes704772b2022-10-10 17:06:43 +000088 #
89 # Newer architectures have things like this though:
90 #
91 # #define __NR3264_fcntl 25
92 # #define __NR_fcntl __NR3264_fcntl
93 #
94 # So we need to keep track of the __NR3264_* constants and substitute them.
Paul Lawrenceeabc3522016-11-11 11:33:42 -080095
Elliott Hughes704772b2022-10-10 17:06:43 +000096 line_re = re.compile(r'^# \d+ ".*".*')
97 undef_re = re.compile(r'^#undef\s.*')
98 define_re = re.compile(r'^\s*#define\s+([A-Za-z0-9_(,)]+)(?:\s+(.+))?\s*$')
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -070099 token_re = re.compile(r'\b[A-Za-z_][A-Za-z0-9_]+\b')
100 constants = {}
Elliott Hughes704772b2022-10-10 17:06:43 +0000101 nr3264s = {}
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700102 with open(names_path) as f:
103 for line in f:
Elliott Hughes704772b2022-10-10 17:06:43 +0000104 line = line.strip()
105 m = define_re.match(line)
106 if m:
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700107 name = m.group(1)
Elliott Hughes704772b2022-10-10 17:06:43 +0000108 value = m.group(2)
109 if name.startswith('__NR3264'):
110 nr3264s[name] = value
111 elif name.startswith('__NR_') or name.startswith('__ARM_NR_'):
112 if value in nr3264s:
113 value = nr3264s[value]
114 # eval() takes care of any arithmetic that may be done
115 value = eval(token_re.sub(lambda x: str(constants[x.group(0)]), value))
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700116
Elliott Hughes704772b2022-10-10 17:06:43 +0000117 constants[name] = value
118 else:
119 if not line_re.match(line) and not undef_re.match(line) and line:
120 print('%s: failed to parse line `%s`' % (names_path, line))
121 sys.exit(1)
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700122
123 syscalls = {}
Elliott Hughesbc6999f2021-02-03 13:13:57 -0800124 for name, value in constants.items():
Elliott Hughes704772b2022-10-10 17:06:43 +0000125 # Remove the __NR_ prefix.
126 # TODO: why not __ARM_NR too?
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700127 if name.startswith("__NR_"):
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700128 name = name[len("__NR_"):]
129 syscalls[name] = value
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800130
Paul Lawrence7ea40902017-02-14 13:32:23 -0800131 return syscalls
132
133
134def convert_NRs_to_ranges(syscalls):
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800135 # Sort the values so we convert to ranges and binary chop
Elliott Hughesbc6999f2021-02-03 13:13:57 -0800136 syscalls = sorted(syscalls, key=operator.itemgetter(1))
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800137
138 # Turn into a list of ranges. Keep the names for the comments
139 ranges = []
140 for name, value in syscalls:
141 if not ranges:
142 ranges.append(SyscallRange(name, value))
143 continue
144
145 last_range = ranges[-1]
146 if last_range.end == value:
147 last_range.add(name, value)
148 else:
149 ranges.append(SyscallRange(name, value))
Paul Lawrence7ea40902017-02-14 13:32:23 -0800150 return ranges
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800151
Paul Lawrence7ea40902017-02-14 13:32:23 -0800152
153# Converts the sorted ranges of allowed syscalls to a binary tree bpf
154# For a single range, output a simple jump to {fail} or {allow}. We can't set
155# the jump ranges yet, since we don't know the size of the filter, so use a
156# placeholder
157# For multiple ranges, split into two, convert the two halves and output a jump
158# to the correct half
159def convert_to_intermediate_bpf(ranges):
160 if len(ranges) == 1:
161 # We will replace {fail} and {allow} with appropriate range jumps later
162 return [BPF_JGE.format(ranges[0].end, "{fail}", "{allow}") +
163 ", //" + "|".join(ranges[0].names)]
Elliott Hughesbc6999f2021-02-03 13:13:57 -0800164
165 half = (len(ranges) + 1) // 2
166 first = convert_to_intermediate_bpf(ranges[:half])
167 second = convert_to_intermediate_bpf(ranges[half:])
168 jump = [BPF_JGE.format(ranges[half].begin, len(first), 0) + ","]
169 return jump + first + second
Paul Lawrence7ea40902017-02-14 13:32:23 -0800170
171
Bram Bonnéacadd092020-05-06 13:49:55 +0200172# Converts the prioritized syscalls to a bpf list that is prepended to the
173# tree generated by convert_to_intermediate_bpf(). If we hit one of these
174# syscalls, shortcut to the allow statement at the bottom of the tree
175# immediately
176def convert_priority_to_intermediate_bpf(priority_syscalls):
177 result = []
Elliott Hughesbc6999f2021-02-03 13:13:57 -0800178 for syscall in priority_syscalls:
Bram Bonnéacadd092020-05-06 13:49:55 +0200179 result.append(BPF_JEQ.format(syscall[1], "{allow}", 0) +
180 ", //" + syscall[0])
181 return result
182
183
184def convert_ranges_to_bpf(ranges, priority_syscalls):
185 bpf = convert_priority_to_intermediate_bpf(priority_syscalls) + \
186 convert_to_intermediate_bpf(ranges)
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800187
188 # Now we know the size of the tree, we can substitute the {fail} and {allow}
189 # placeholders
190 for i, statement in enumerate(bpf):
191 # Replace placeholder with
192 # "distance to jump to fail, distance to jump to allow"
193 # We will add a kill statement and an allow statement after the tree
194 # With bpfs jmp 0 means the next statement, so the distance to the end is
195 # len(bpf) - i - 1, which is where we will put the kill statement, and
196 # then the statement after that is the allow statement
Bram Bonnéacadd092020-05-06 13:49:55 +0200197 bpf[i] = statement.format(fail=str(len(bpf) - i),
198 allow=str(len(bpf) - i - 1))
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800199
Paul Lawrencebe8a2af2017-01-25 15:20:52 -0800200 # Add the allow calls at the end. If the syscall is not matched, we will
201 # continue. This allows the user to choose to match further syscalls, and
202 # also to choose the action when we want to block
Paul Lawrence7ea40902017-02-14 13:32:23 -0800203 bpf.append(BPF_ALLOW + ",")
Paul Lawrence65b47c92017-03-22 08:03:51 -0700204
205 # Add check that we aren't off the bottom of the syscalls
206 bpf.insert(0, BPF_JGE.format(ranges[0].begin, 0, str(len(bpf))) + ',')
Paul Lawrence7ea40902017-02-14 13:32:23 -0800207 return bpf
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800208
Paul Lawrence7ea40902017-02-14 13:32:23 -0800209
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800210def convert_bpf_to_output(bpf, architecture, name_modifier):
211 if name_modifier:
212 name_modifier = name_modifier + "_"
213 else:
214 name_modifier = ""
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800215 header = textwrap.dedent("""\
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700216 // File autogenerated by {self_path} - edit at your peril!!
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800217
218 #include <linux/filter.h>
219 #include <errno.h>
220
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700221 #include "seccomp/seccomp_bpfs.h"
Steve Muckleaa3f96c2017-07-20 13:11:54 -0700222 const sock_filter {architecture}_{suffix}filter[] = {{
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700223 """).format(self_path=os.path.basename(__file__), architecture=architecture,
224 suffix=name_modifier)
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800225
226 footer = textwrap.dedent("""\
227
228 }};
229
Steve Muckleaa3f96c2017-07-20 13:11:54 -0700230 const size_t {architecture}_{suffix}filter_size = sizeof({architecture}_{suffix}filter) / sizeof(struct sock_filter);
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800231 """).format(architecture=architecture,suffix=name_modifier)
Paul Lawrence7ea40902017-02-14 13:32:23 -0800232 return header + "\n".join(bpf) + footer
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800233
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800234
Bram Bonnéacadd092020-05-06 13:49:55 +0200235def construct_bpf(syscalls, architecture, name_modifier, priorities):
236 priority_syscalls, other_syscalls = \
237 extract_priority_syscalls(syscalls, priorities)
238 ranges = convert_NRs_to_ranges(other_syscalls)
239 bpf = convert_ranges_to_bpf(ranges, priority_syscalls)
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800240 return convert_bpf_to_output(bpf, architecture, name_modifier)
Paul Lawrence7ea40902017-02-14 13:32:23 -0800241
242
Elliott Hughesbc6999f2021-02-03 13:13:57 -0800243def gen_policy(name_modifier, out_dir, base_syscall_file, syscall_files,
244 syscall_NRs, priority_file):
Elliott Hughes704772b2022-10-10 17:06:43 +0000245 for arch in syscall_NRs.keys():
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700246 base_names = load_syscall_names_from_file(base_syscall_file, arch)
Victor Hsiehdbb86702020-06-15 09:29:07 -0700247 allowlist_names = set()
248 blocklist_names = set()
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700249 for f in syscall_files:
Victor Hsiehdbb86702020-06-15 09:29:07 -0700250 if "blocklist" in f.lower():
251 blocklist_names |= load_syscall_names_from_file(f, arch)
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700252 else:
Victor Hsiehdbb86702020-06-15 09:29:07 -0700253 allowlist_names |= load_syscall_names_from_file(f, arch)
Bram Bonnéacadd092020-05-06 13:49:55 +0200254 priorities = []
255 if priority_file:
256 priorities = load_syscall_priorities_from_file(priority_file)
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800257
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700258 allowed_syscalls = []
Elliott Hughes704772b2022-10-10 17:06:43 +0000259 for name in sorted(merge_names(base_names, allowlist_names, blocklist_names)):
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700260 try:
261 allowed_syscalls.append((name, syscall_NRs[arch][name]))
262 except:
Elliott Hughes704772b2022-10-10 17:06:43 +0000263 logging.exception("Failed to find %s in %s (%s)", name, arch, syscall_NRs[arch])
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700264 raise
Bram Bonnéacadd092020-05-06 13:49:55 +0200265 output = construct_bpf(allowed_syscalls, arch, name_modifier, priorities)
Paul Lawrence7ea40902017-02-14 13:32:23 -0800266
267 # And output policy
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800268 filename_modifier = "_" + name_modifier if name_modifier else ""
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700269 output_path = os.path.join(out_dir,
270 "{}{}_policy.cpp".format(arch, filename_modifier))
271 with open(output_path, "w") as output_file:
272 output_file.write(output)
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800273
Steve Muckleaa3f96c2017-07-20 13:11:54 -0700274
275def main():
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700276 parser = argparse.ArgumentParser(
277 description="Generates a seccomp-bpf policy")
278 parser.add_argument("--verbose", "-v", help="Enables verbose logging.")
279 parser.add_argument("--name-modifier",
280 help=("Specifies the name modifier for the policy. "
Elliott Hughesae03b122019-09-17 16:37:05 -0700281 "One of {app,system}."))
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700282 parser.add_argument("--out-dir",
283 help="The output directory for the policy files")
284 parser.add_argument("base_file", metavar="base-file", type=str,
285 help="The path of the base syscall list (SYSCALLS.TXT).")
286 parser.add_argument("files", metavar="FILE", type=str, nargs="+",
287 help=("The path of the input files. In order to "
288 "simplify the build rules, it can take any of the "
289 "following files: \n"
Elliott Hughesbc6999f2021-02-03 13:13:57 -0800290 "* /blocklist.*\\.txt$/ syscall blocklist.\n"
291 "* /allowlist.*\\.txt$/ syscall allowlist.\n"
Bram Bonnéacadd092020-05-06 13:49:55 +0200292 "* /priority.txt$/ priorities for bpf rules.\n"
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700293 "* otherwise, syscall name-number mapping.\n"))
294 args = parser.parse_args()
295
296 if args.verbose:
297 logging.basicConfig(level=logging.DEBUG)
298 else:
299 logging.basicConfig(level=logging.INFO)
300
301 syscall_files = []
Bram Bonnéacadd092020-05-06 13:49:55 +0200302 priority_file = None
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700303 syscall_NRs = {}
304 for filename in args.files:
305 if filename.lower().endswith('.txt'):
Bram Bonnéacadd092020-05-06 13:49:55 +0200306 if filename.lower().endswith('priority.txt'):
307 priority_file = filename
308 else:
309 syscall_files.append(filename)
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700310 else:
311 m = re.search(r"libseccomp_gen_syscall_nrs_([^/]+)", filename)
312 syscall_NRs[m.group(1)] = parse_syscall_NRs(filename)
313
314 gen_policy(name_modifier=args.name_modifier, out_dir=args.out_dir,
315 syscall_NRs=syscall_NRs, base_syscall_file=args.base_file,
Bram Bonnéacadd092020-05-06 13:49:55 +0200316 syscall_files=syscall_files, priority_file=priority_file)
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800317
Steve Muckleaa3f96c2017-07-20 13:11:54 -0700318
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800319if __name__ == "__main__":
320 main()