blob: 89eeb445952ac5e6ac2a044e0c5ecbc1dec999d1 [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
Elliott Hughesae03b122019-09-17 16:37:05 -070011from gensyscalls import SupportedArchitectures, 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
19class SyscallRange(object):
20 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))
38 return set([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 = []
44 with open(file_path) as f:
45 for line in f:
46 m = format_re.match(line)
47 if not m:
48 continue
49 try:
50 name = m.group(1)
51 priorities.append(name)
52 except:
53 logging.debug('Failed to parse %s from %s', (line, file_path))
54 pass
55
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.
Paul Lawrenceeabc3522016-11-11 11:33:42 -080088
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -070089 constant_re = re.compile(
90 r'^\s*#define\s+([A-Za-z_][A-Za-z0-9_]+)\s+(.+)\s*$')
91 token_re = re.compile(r'\b[A-Za-z_][A-Za-z0-9_]+\b')
92 constants = {}
93 with open(names_path) as f:
94 for line in f:
95 m = constant_re.match(line)
96 if not m:
97 continue
98 try:
99 name = m.group(1)
100 # eval() takes care of any arithmetic that may be done
101 value = eval(token_re.sub(lambda x: str(constants[x.group(0)]),
102 m.group(2)))
103
104 constants[name] = value
105 except:
106 logging.debug('Failed to parse %s', line)
107 pass
108
109 syscalls = {}
110 for name, value in constants.iteritems():
111 if not name.startswith("__NR_") and not name.startswith("__ARM_NR"):
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800112 continue
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700113 if name.startswith("__NR_"):
114 # Remote the __NR_ prefix
115 name = name[len("__NR_"):]
116 syscalls[name] = value
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800117
Paul Lawrence7ea40902017-02-14 13:32:23 -0800118 return syscalls
119
120
121def convert_NRs_to_ranges(syscalls):
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800122 # Sort the values so we convert to ranges and binary chop
123 syscalls = sorted(syscalls, lambda x, y: cmp(x[1], y[1]))
124
125 # Turn into a list of ranges. Keep the names for the comments
126 ranges = []
127 for name, value in syscalls:
128 if not ranges:
129 ranges.append(SyscallRange(name, value))
130 continue
131
132 last_range = ranges[-1]
133 if last_range.end == value:
134 last_range.add(name, value)
135 else:
136 ranges.append(SyscallRange(name, value))
Paul Lawrence7ea40902017-02-14 13:32:23 -0800137 return ranges
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800138
Paul Lawrence7ea40902017-02-14 13:32:23 -0800139
140# Converts the sorted ranges of allowed syscalls to a binary tree bpf
141# For a single range, output a simple jump to {fail} or {allow}. We can't set
142# the jump ranges yet, since we don't know the size of the filter, so use a
143# placeholder
144# For multiple ranges, split into two, convert the two halves and output a jump
145# to the correct half
146def convert_to_intermediate_bpf(ranges):
147 if len(ranges) == 1:
148 # We will replace {fail} and {allow} with appropriate range jumps later
149 return [BPF_JGE.format(ranges[0].end, "{fail}", "{allow}") +
150 ", //" + "|".join(ranges[0].names)]
151 else:
152 half = (len(ranges) + 1) / 2
153 first = convert_to_intermediate_bpf(ranges[:half])
154 second = convert_to_intermediate_bpf(ranges[half:])
155 jump = [BPF_JGE.format(ranges[half].begin, len(first), 0) + ","]
156 return jump + first + second
157
158
Bram Bonnéacadd092020-05-06 13:49:55 +0200159# Converts the prioritized syscalls to a bpf list that is prepended to the
160# tree generated by convert_to_intermediate_bpf(). If we hit one of these
161# syscalls, shortcut to the allow statement at the bottom of the tree
162# immediately
163def convert_priority_to_intermediate_bpf(priority_syscalls):
164 result = []
165 for i, syscall in enumerate(priority_syscalls):
166 result.append(BPF_JEQ.format(syscall[1], "{allow}", 0) +
167 ", //" + syscall[0])
168 return result
169
170
171def convert_ranges_to_bpf(ranges, priority_syscalls):
172 bpf = convert_priority_to_intermediate_bpf(priority_syscalls) + \
173 convert_to_intermediate_bpf(ranges)
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800174
175 # Now we know the size of the tree, we can substitute the {fail} and {allow}
176 # placeholders
177 for i, statement in enumerate(bpf):
178 # Replace placeholder with
179 # "distance to jump to fail, distance to jump to allow"
180 # We will add a kill statement and an allow statement after the tree
181 # With bpfs jmp 0 means the next statement, so the distance to the end is
182 # len(bpf) - i - 1, which is where we will put the kill statement, and
183 # then the statement after that is the allow statement
Bram Bonnéacadd092020-05-06 13:49:55 +0200184 bpf[i] = statement.format(fail=str(len(bpf) - i),
185 allow=str(len(bpf) - i - 1))
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800186
Paul Lawrencebe8a2af2017-01-25 15:20:52 -0800187 # Add the allow calls at the end. If the syscall is not matched, we will
188 # continue. This allows the user to choose to match further syscalls, and
189 # also to choose the action when we want to block
Paul Lawrence7ea40902017-02-14 13:32:23 -0800190 bpf.append(BPF_ALLOW + ",")
Paul Lawrence65b47c92017-03-22 08:03:51 -0700191
192 # Add check that we aren't off the bottom of the syscalls
193 bpf.insert(0, BPF_JGE.format(ranges[0].begin, 0, str(len(bpf))) + ',')
Paul Lawrence7ea40902017-02-14 13:32:23 -0800194 return bpf
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800195
Paul Lawrence7ea40902017-02-14 13:32:23 -0800196
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800197def convert_bpf_to_output(bpf, architecture, name_modifier):
198 if name_modifier:
199 name_modifier = name_modifier + "_"
200 else:
201 name_modifier = ""
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800202 header = textwrap.dedent("""\
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700203 // File autogenerated by {self_path} - edit at your peril!!
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800204
205 #include <linux/filter.h>
206 #include <errno.h>
207
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700208 #include "seccomp/seccomp_bpfs.h"
Steve Muckleaa3f96c2017-07-20 13:11:54 -0700209 const sock_filter {architecture}_{suffix}filter[] = {{
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700210 """).format(self_path=os.path.basename(__file__), architecture=architecture,
211 suffix=name_modifier)
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800212
213 footer = textwrap.dedent("""\
214
215 }};
216
Steve Muckleaa3f96c2017-07-20 13:11:54 -0700217 const size_t {architecture}_{suffix}filter_size = sizeof({architecture}_{suffix}filter) / sizeof(struct sock_filter);
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800218 """).format(architecture=architecture,suffix=name_modifier)
Paul Lawrence7ea40902017-02-14 13:32:23 -0800219 return header + "\n".join(bpf) + footer
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800220
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800221
Bram Bonnéacadd092020-05-06 13:49:55 +0200222def construct_bpf(syscalls, architecture, name_modifier, priorities):
223 priority_syscalls, other_syscalls = \
224 extract_priority_syscalls(syscalls, priorities)
225 ranges = convert_NRs_to_ranges(other_syscalls)
226 bpf = convert_ranges_to_bpf(ranges, priority_syscalls)
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800227 return convert_bpf_to_output(bpf, architecture, name_modifier)
Paul Lawrence7ea40902017-02-14 13:32:23 -0800228
229
Bram Bonnéacadd092020-05-06 13:49:55 +0200230def gen_policy(name_modifier, out_dir, base_syscall_file, syscall_files, syscall_NRs, priority_file):
Elliott Hughesae03b122019-09-17 16:37:05 -0700231 for arch in SupportedArchitectures:
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700232 base_names = load_syscall_names_from_file(base_syscall_file, arch)
Victor Hsiehdbb86702020-06-15 09:29:07 -0700233 allowlist_names = set()
234 blocklist_names = set()
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700235 for f in syscall_files:
Victor Hsiehdbb86702020-06-15 09:29:07 -0700236 if "blocklist" in f.lower():
237 blocklist_names |= load_syscall_names_from_file(f, arch)
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700238 else:
Victor Hsiehdbb86702020-06-15 09:29:07 -0700239 allowlist_names |= load_syscall_names_from_file(f, arch)
Bram Bonnéacadd092020-05-06 13:49:55 +0200240 priorities = []
241 if priority_file:
242 priorities = load_syscall_priorities_from_file(priority_file)
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800243
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700244 allowed_syscalls = []
Victor Hsiehdbb86702020-06-15 09:29:07 -0700245 for name in merge_names(base_names, allowlist_names, blocklist_names):
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700246 try:
247 allowed_syscalls.append((name, syscall_NRs[arch][name]))
248 except:
249 logging.exception("Failed to find %s in %s", name, arch)
250 raise
Bram Bonnéacadd092020-05-06 13:49:55 +0200251 output = construct_bpf(allowed_syscalls, arch, name_modifier, priorities)
Paul Lawrence7ea40902017-02-14 13:32:23 -0800252
253 # And output policy
254 existing = ""
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800255 filename_modifier = "_" + name_modifier if name_modifier else ""
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700256 output_path = os.path.join(out_dir,
257 "{}{}_policy.cpp".format(arch, filename_modifier))
258 with open(output_path, "w") as output_file:
259 output_file.write(output)
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800260
Steve Muckleaa3f96c2017-07-20 13:11:54 -0700261
262def main():
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700263 parser = argparse.ArgumentParser(
264 description="Generates a seccomp-bpf policy")
265 parser.add_argument("--verbose", "-v", help="Enables verbose logging.")
266 parser.add_argument("--name-modifier",
267 help=("Specifies the name modifier for the policy. "
Elliott Hughesae03b122019-09-17 16:37:05 -0700268 "One of {app,system}."))
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700269 parser.add_argument("--out-dir",
270 help="The output directory for the policy files")
271 parser.add_argument("base_file", metavar="base-file", type=str,
272 help="The path of the base syscall list (SYSCALLS.TXT).")
273 parser.add_argument("files", metavar="FILE", type=str, nargs="+",
274 help=("The path of the input files. In order to "
275 "simplify the build rules, it can take any of the "
276 "following files: \n"
Victor Hsiehdbb86702020-06-15 09:29:07 -0700277 "* /blocklist.*\.txt$/ syscall blocklist.\n"
278 "* /allowlist.*\.txt$/ syscall allowlist.\n"
Bram Bonnéacadd092020-05-06 13:49:55 +0200279 "* /priority.txt$/ priorities for bpf rules.\n"
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700280 "* otherwise, syscall name-number mapping.\n"))
281 args = parser.parse_args()
282
283 if args.verbose:
284 logging.basicConfig(level=logging.DEBUG)
285 else:
286 logging.basicConfig(level=logging.INFO)
287
288 syscall_files = []
Bram Bonnéacadd092020-05-06 13:49:55 +0200289 priority_file = None
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700290 syscall_NRs = {}
291 for filename in args.files:
292 if filename.lower().endswith('.txt'):
Bram Bonnéacadd092020-05-06 13:49:55 +0200293 if filename.lower().endswith('priority.txt'):
294 priority_file = filename
295 else:
296 syscall_files.append(filename)
Luis Hector Chavezfa09b3c2018-08-03 20:53:28 -0700297 else:
298 m = re.search(r"libseccomp_gen_syscall_nrs_([^/]+)", filename)
299 syscall_NRs[m.group(1)] = parse_syscall_NRs(filename)
300
301 gen_policy(name_modifier=args.name_modifier, out_dir=args.out_dir,
302 syscall_NRs=syscall_NRs, base_syscall_file=args.base_file,
Bram Bonnéacadd092020-05-06 13:49:55 +0200303 syscall_files=syscall_files, priority_file=priority_file)
Victor Hsieh4f02dd52017-12-20 09:19:22 -0800304
Steve Muckleaa3f96c2017-07-20 13:11:54 -0700305
Paul Lawrenceeabc3522016-11-11 11:33:42 -0800306if __name__ == "__main__":
307 main()