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__":