Autogenerate single policy from syscalls and whitelist
Bug: 35392119
Bug: 34465958
Test: Check boots and same syscalls are blocked as before
Change-Id: I9efa97032c59aebbbfd32e6f0d2d491f6254f0a2
diff --git a/libc/tools/genseccomp.py b/libc/tools/genseccomp.py
index fa6e7e3..7d2b1da 100755
--- a/libc/tools/genseccomp.py
+++ b/libc/tools/genseccomp.py
@@ -5,7 +5,8 @@
from gensyscalls import SysCallsTxtParser
-syscall_file = "SYSCALLS.TXT"
+BPF_JGE = "BPF_JUMP(BPF_JMP|BPF_JGE|BPF_K, {0}, {1}, {2})"
+BPF_ALLOW = "BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW)"
class SyscallRange(object):
@@ -14,6 +15,9 @@
self.begin = value
self.end = self.begin + 1
+ def __str__(self):
+ return "(%s, %s, %s)" % (self.begin, self.end, self.names)
+
def add(self, name, value):
if value != self.end:
raise ValueError
@@ -21,39 +25,21 @@
self.names.append(name)
-def generate_bpf_jge(value, ge_target, less_target):
- return "BPF_JUMP(BPF_JMP|BPF_JGE|BPF_K, {0}, {1}, {2})".format(value, ge_target, less_target)
-
-
-# Converts the sorted ranges of allowed syscalls to a binary tree bpf
-# For a single range, output a simple jump to {fail} or {allow}. We can't set
-# the jump ranges yet, since we don't know the size of the filter, so use a
-# placeholder
-# For multiple ranges, split into two, convert the two halves and output a jump
-# to the correct half
-def convert_to_bpf(ranges):
- if len(ranges) == 1:
- # We will replace {fail} and {allow} with appropriate range jumps later
- return [generate_bpf_jge(ranges[0].end, "{fail}", "{allow}") +
- ", //" + "|".join(ranges[0].names)]
- else:
- half = (len(ranges) + 1) / 2
- first = convert_to_bpf(ranges[:half])
- second = convert_to_bpf(ranges[half:])
- return [generate_bpf_jge(ranges[half].begin, len(first), 0) + ","] + first + second
-
-
-def construct_bpf(architecture, header_dir, output_path):
- parser = SysCallsTxtParser()
- parser.parse_file(syscall_file)
- syscalls = parser.syscalls
+def get_names(syscall_files, architecture):
+ syscalls = []
+ for syscall_file in syscall_files:
+ parser = SysCallsTxtParser()
+ parser.parse_open_file(syscall_file)
+ syscalls += parser.syscalls
# Select only elements matching required architecture
syscalls = [x for x in syscalls if architecture in x and x[architecture]]
# We only want the name
- names = [x["name"] for x in syscalls]
+ return [x["name"] for x in syscalls]
+
+def convert_names_to_NRs(names, header_dir):
# Run preprocessor over the __NR_syscall symbols, including unistd.h,
# to get the actual numbers
prefix = "__SECCOMP_" # prefix to ensure no name collisions
@@ -89,6 +75,10 @@
value = eval(value)
syscalls.append((name, value))
+ return syscalls
+
+
+def convert_NRs_to_ranges(syscalls):
# Sort the values so we convert to ranges and binary chop
syscalls = sorted(syscalls, lambda x, y: cmp(x[1], y[1]))
@@ -104,8 +94,30 @@
last_range.add(name, value)
else:
ranges.append(SyscallRange(name, value))
+ return ranges
- bpf = convert_to_bpf(ranges)
+
+# Converts the sorted ranges of allowed syscalls to a binary tree bpf
+# For a single range, output a simple jump to {fail} or {allow}. We can't set
+# the jump ranges yet, since we don't know the size of the filter, so use a
+# placeholder
+# For multiple ranges, split into two, convert the two halves and output a jump
+# to the correct half
+def convert_to_intermediate_bpf(ranges):
+ if len(ranges) == 1:
+ # We will replace {fail} and {allow} with appropriate range jumps later
+ return [BPF_JGE.format(ranges[0].end, "{fail}", "{allow}") +
+ ", //" + "|".join(ranges[0].names)]
+ else:
+ half = (len(ranges) + 1) / 2
+ first = convert_to_intermediate_bpf(ranges[:half])
+ second = convert_to_intermediate_bpf(ranges[half:])
+ jump = [BPF_JGE.format(ranges[half].begin, len(first), 0) + ","]
+ return jump + first + second
+
+
+def convert_ranges_to_bpf(ranges):
+ bpf = convert_to_intermediate_bpf(ranges)
# Now we know the size of the tree, we can substitute the {fail} and {allow}
# placeholders
@@ -121,16 +133,16 @@
allow=str(len(bpf) - i - 1))
# Add check that we aren't off the bottom of the syscalls
- bpf.insert(0,
- "BPF_JUMP(BPF_JMP|BPF_JGE|BPF_K, " + str(ranges[0].begin) +
- ", 0, " + str(len(bpf)) + "),")
+ bpf.insert(0, BPF_JGE.format(ranges[0].begin, 0, str(len(bpf))) + ',')
# Add the allow calls at the end. If the syscall is not matched, we will
# continue. This allows the user to choose to match further syscalls, and
# also to choose the action when we want to block
- bpf.append("BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW),")
+ bpf.append(BPF_ALLOW + ",")
+ return bpf
- # And output policy
+
+def convert_bpf_to_output(bpf, architecture):
header = textwrap.dedent("""\
// Autogenerated file - edit at your peril!!
@@ -147,25 +159,52 @@
const size_t {architecture}_filter_size = sizeof({architecture}_filter) / sizeof(struct sock_filter);
""").format(architecture=architecture)
- output = header + "\n".join(bpf) + footer
+ return header + "\n".join(bpf) + footer
- existing = ""
- if os.path.isfile(output_path):
- existing = open(output_path).read()
- if output == existing:
- print "File " + output_path + " not changed."
- else:
- with open(output_path, "w") as output_file:
- output_file.write(output)
- print "Generated file " + output_path
+def construct_bpf(syscall_files, architecture, header_dir):
+ names = get_names(syscall_files, architecture)
+ syscalls = convert_names_to_NRs(names, header_dir)
+ ranges = convert_NRs_to_ranges(syscalls)
+ bpf = convert_ranges_to_bpf(ranges)
+ return convert_bpf_to_output(bpf, architecture)
+
+
+android_syscall_files = ["SYSCALLS.TXT", "SECCOMP_WHITELIST.TXT"]
+arm_headers = "kernel/uapi/asm-arm"
+arm64_headers = "kernel/uapi/asm-arm64"
+arm_architecture = "arm"
+arm64_architecture = "arm64"
+
+
+ANDROID_SYSCALL_FILES = ["SYSCALLS.TXT", "SECCOMP_WHITELIST.TXT"]
+
+POLICY_CONFIGS = [("arm", "kernel/uapi/asm-arm"),
+ ("arm64", "kernel/uapi/asm-arm64")]
+
+
+def set_dir():
+ # Set working directory for predictable results
+ os.chdir(os.path.join(os.environ["ANDROID_BUILD_TOP"], "bionic/libc"))
def main():
- # Set working directory for predictable results
- os.chdir(os.path.join(os.environ["ANDROID_BUILD_TOP"], "bionic/libc"))
- construct_bpf("arm", "kernel/uapi/asm-arm", "seccomp/arm_policy.c")
- construct_bpf("arm64", "kernel/uapi/asm-arm64", "seccomp/arm64_policy.c")
+ set_dir()
+ for arch, header_path in POLICY_CONFIGS:
+ files = [open(filename) for filename in ANDROID_SYSCALL_FILES]
+ output = construct_bpf(files, arch, header_path)
+
+ # And output policy
+ existing = ""
+ output_path = "seccomp/{}_policy.c".format(arch)
+ if os.path.isfile(output_path):
+ existing = open(output_path).read()
+ if output == existing:
+ print "File " + output_path + " not changed."
+ else:
+ with open(output_path, "w") as output_file:
+ output_file.write(output)
+ print "Generated file " + output_path
if __name__ == "__main__":