tools/compile_seccomp_policy: Add support for syscall groups

This change adds support for the 'function@libc' and 'group@systemd' syntax to
make writing policy files much easier.

Bug: chromium:856315
Test: ./tools/parser_unittest.py

Change-Id: Ia1d51d30c68346b4390b0e5c23d0f7e929f08c70
diff --git a/tools/arch.py b/tools/arch.py
index 6f2dfb2..ac2f32b 100644
--- a/tools/arch.py
+++ b/tools/arch.py
@@ -21,9 +21,10 @@
 
 
 class Arch(
-        collections.namedtuple(
-            'Arch',
-            ['arch_nr', 'arch_name', 'bits', 'syscalls', 'constants'])):
+        collections.namedtuple('Arch', [
+            'arch_nr', 'arch_name', 'bits', 'syscalls', 'constants',
+            'syscall_groups'
+        ])):
     """Holds architecture-specific information."""
 
     def truncate_word(self, value):
@@ -51,4 +52,5 @@
                 bits=constants['bits'],
                 syscalls=constants['syscalls'],
                 constants=constants['constants'],
+                syscall_groups=constants.get('syscall_groups', {}),
             )
diff --git a/tools/parser.py b/tools/parser.py
index 41d2f52..65297f9 100644
--- a/tools/parser.py
+++ b/tools/parser.py
@@ -57,7 +57,7 @@
     ('ARGUMENT', r'arg[0-9]+'),
     ('RETURN', r'return'),
     ('ACTION', r'allow|kill-process|kill-thread|kill|trap|trace|log'),
-    ('IDENTIFIER', r'[a-zA-Z_][a-zA-Z_0-9@]*'),
+    ('IDENTIFIER', r'[a-zA-Z_][a-zA-Z_0-9-@]*'),
 )
 _TOKEN_RE = re.compile('|'.join(
     r'(?P<%s>%s)' % pair for pair in _TOKEN_SPECIFICATION))
@@ -481,7 +481,7 @@
         return metadata
 
     # syscall-descriptor = syscall-name , [ metadata ]
-    #                    | libc-function , [ metadata ]
+    #                    | syscall-group-name , [ metadata ]
     #                    ;
     def _parse_syscall_descriptor(self, tokens):
         if not tokens:
@@ -490,11 +490,28 @@
         if syscall_descriptor.type != 'IDENTIFIER':
             self._parser_state.error(
                 'invalid syscall descriptor', token=syscall_descriptor)
-        # TODO(lhchavez): Support libc function names.
         if tokens and tokens[0].type == 'LBRACKET':
             metadata = self._parse_metadata(tokens)
             if 'arch' in metadata and self._arch.arch_name not in metadata['arch']:
                 return ()
+        if '@' in syscall_descriptor.value:
+            # This is a syscall group.
+            subtokens = syscall_descriptor.value.split('@')
+            if len(subtokens) != 2:
+                self._parser_state.error(
+                    'invalid syscall group name', token=syscall_descriptor)
+            syscall_group_name, syscall_namespace_name = subtokens
+            if syscall_namespace_name not in self._arch.syscall_groups:
+                self._parser_state.error(
+                    'nonexistent syscall group namespace',
+                    token=syscall_descriptor)
+            syscall_namespace = self._arch.syscall_groups[
+                syscall_namespace_name]
+            if syscall_group_name not in syscall_namespace:
+                self._parser_state.error(
+                    'nonexistent syscall group', token=syscall_descriptor)
+            return (Syscall(name, self._arch.syscalls[name])
+                    for name in syscall_namespace[syscall_group_name])
         if syscall_descriptor.value not in self._arch.syscalls:
             self._parser_state.error(
                 'nonexistent syscall', token=syscall_descriptor)
diff --git a/tools/parser_unittest.py b/tools/parser_unittest.py
index 4fba590..f13a109 100755
--- a/tools/parser_unittest.py
+++ b/tools/parser_unittest.py
@@ -398,6 +398,24 @@
             ), [
                 parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()),
             ]))
+        self.assertEqual(
+            self.parser.parse_filter_statement(
+                self._tokenize('io@libc: arg0 == 0')),
+            parser.ParsedFilterStatement((
+                parser.Syscall('read', 0),
+                parser.Syscall('write', 1),
+            ), [
+                parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()),
+            ]))
+        self.assertEqual(
+            self.parser.parse_filter_statement(
+                self._tokenize('file-io@systemd: arg0 == 0')),
+            parser.ParsedFilterStatement((
+                parser.Syscall('read', 0),
+                parser.Syscall('write', 1),
+            ), [
+                parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()),
+            ]))
 
     def test_parse_metadata(self):
         """Accept valid filter statements with metadata."""
@@ -418,6 +436,11 @@
     def test_parse_unclosed_brace(self):
         """Reject unclosed brace."""
         with self.assertRaisesRegex(parser.ParseException, 'unclosed brace'):
+            self.parser.parse_filter(self._tokenize('{ allow'))
+
+    def test_parse_invalid_syscall_group(self):
+        """Reject invalid syscall groups."""
+        with self.assertRaisesRegex(parser.ParseException, 'unclosed brace'):
             self.parser.parse_filter_statement(
                 self._tokenize('{ read, write: arg0 == 0'))
 
diff --git a/tools/testdata/arch_64.json b/tools/testdata/arch_64.json
index 10c9855..385da94 100644
--- a/tools/testdata/arch_64.json
+++ b/tools/testdata/arch_64.json
@@ -109,5 +109,19 @@
     "O_RDONLY": 0,
     "PROT_WRITE": 2,
     "PROT_EXEC": 4
+  },
+  "syscall_groups": {
+    "libc": {
+      "io": [
+        "read",
+        "write"
+      ]
+    },
+    "systemd": {
+      "file-io": [
+        "read",
+        "write"
+      ]
+    }
   }
 }