minijail: Make denylist compiler return Errno
Make 2 big changes to the denylist compiler. Instead of '0' and '!'
indicating a denylist policy, have an explicit header, '@denylist' in a
policy to indicate a denylist policy. Also, require denylist policies to
specifically define returning a specific Errno for each syscall entry.
Bug: None
Test: Fix tests and run unit tests
Change-Id: Iea2d94b67df8362b7268b08ff8fe8a814b5e0db1
diff --git a/tools/compiler.py b/tools/compiler.py
index a0e5183..e7ab842 100644
--- a/tools/compiler.py
+++ b/tools/compiler.py
@@ -289,7 +289,8 @@
visitor = bpf.FlatteningVisitor(
arch=self._arch, kill_action=kill_action)
if denylist:
- accept_action = kill_action
+ # Default action for a denylist policy is return EPERM
+ accept_action = bpf.ReturnErrno(self._arch.constants['EPERM'])
reject_action = bpf.Allow()
else:
accept_action = bpf.Allow()
diff --git a/tools/parser.py b/tools/parser.py
index b7e5f69..9e8dcd5 100644
--- a/tools/parser.py
+++ b/tools/parser.py
@@ -42,6 +42,7 @@
('DEFAULT', r'@default\b'),
('INCLUDE', r'@include\b'),
('FREQUENCY', r'@frequency\b'),
+ ('DENYLIST', r'@denylist$'),
('PATH', r'(?:\.)?/\S+'),
('NUMERIC_CONSTANT', r'-?0[xX][0-9a-fA-F]+|-?0[Oo][0-7]+|-?[0-9]+'),
('COLON', r':'),
@@ -59,7 +60,6 @@
('BITWISE_OR', r'\|'),
('OP', r'&|\bin\b|==|!=|<=|<|>=|>'),
('EQUAL', r'='),
- ('NOT', r'!'),
('ARGUMENT', r'\barg[0-9]+\b'),
('RETURN', r'\breturn\b'),
('ACTION',
@@ -401,7 +401,7 @@
# action = 'allow' | '1'
# | 'kill-process'
# | 'kill-thread'
- # | 'kill' | '0'
+ # | 'kill'
# | 'trap'
# | 'trace'
# | 'log'
@@ -412,9 +412,9 @@
if not tokens:
self._parser_state.error('missing action')
action_token = tokens.pop(0)
- # The only valid denylist action is 0
+ # denylist policies must specify a return for every line.
if self._denylist:
- if action_token.type != 'NUMERIC_CONSTANT':
+ if action_token.type != 'RETURN':
self._parser_state.error('invalid denylist policy')
if action_token.type == 'ACTION':
@@ -437,13 +437,7 @@
elif action_token.type == 'NUMERIC_CONSTANT':
constant = self._parse_single_constant(action_token)
if constant == 1:
- if self._denylist:
- self._parser_state.error('invalid denylist policy')
return bpf.Allow()
- if constant == 0:
- if not self._denylist:
- self._parser_state.error('invalid allowlist policy')
- return self._kill_action
elif action_token.type == 'RETURN':
if not tokens:
self._parser_state.error('missing return value')
@@ -458,23 +452,16 @@
if not tokens:
self._parser_state.error('missing filter')
if tokens[0].type == 'ARGUMENT':
- if self._denylist:
- self._parser_state.error('invalid denylist policy')
- action = bpf.Allow()
- elif tokens[0].type == 'NOT':
- if not self._denylist:
- self._parser_state.error('invalid allowlist policy')
- tokens.pop(0)
- action = self._kill_action
+ # Only argument expressions can start with an ARGUMENT token.
+ argument_expression = self.parse_argument_expression(tokens)
+ if tokens and tokens[0].type == 'SEMICOLON':
+ tokens.pop(0)
+ action = self.parse_action(tokens)
+ else:
+ action = bpf.Allow()
+ return Filter(argument_expression, action)
else:
return Filter(None, self.parse_action(tokens))
- # Parse an argument expression, either allowlist or denylist.
- # Only argument expressions can start with an ARGUMENT token.
- argument_expression = self.parse_argument_expression(tokens)
- if tokens and tokens[0].type == 'SEMICOLON':
- tokens.pop(0)
- action = self.parse_action(tokens)
- return Filter(argument_expression, action)
# filter = '{' , single-filter , [ { ',' , single-filter } ] , '}'
# | single-filter
@@ -722,6 +709,7 @@
self._parser_states.append(ParserState(filename))
try:
statements = []
+ denylist_header = False
with open(filename) as policy_file:
for tokens in self._parser_state.tokenize(policy_file):
if tokens[0].type == 'INCLUDE':
@@ -735,6 +723,14 @@
elif tokens[0].type == 'DEFAULT':
self._default_action = self._parse_default_statement(
tokens)
+ elif tokens[0].type == 'DENYLIST':
+ tokens.pop()
+ if not self._denylist:
+ self._parser_state.error('policy is denylist, but '
+ 'flag --denylist not '
+ 'passed in.')
+ else:
+ denylist_header = True
else:
statement = self.parse_filter_statement(tokens)
if statement is None:
@@ -746,6 +742,9 @@
if tokens:
self._parser_state.error(
'extra tokens', token=tokens[0])
+ if self._denylist and not denylist_header:
+ self._parser_state.error('policy must contain @denylist flag to'
+ ' be compiled with --denylist flag.')
return statements
finally:
self._parser_states.pop()
diff --git a/tools/parser_unittest.py b/tools/parser_unittest.py
index 0aa17e1..72eee54 100755
--- a/tools/parser_unittest.py
+++ b/tools/parser_unittest.py
@@ -58,14 +58,12 @@
('INCLUDE', '@include'),
('PATH', './minijail.policy'),
])
- """A ! is a valid symbol for tokenizing no matter the policy type."""
self.assertEqual(
[(token.type, token.value) for token in TokenizerTests._tokenize(
- 'read: ! arg0 in ~0xffff || arg0 & (1|2) && arg0 == 0o755; '
+ 'read: arg0 in ~0xffff || arg0 & (1|2) && arg0 == 0o755; '
'return ENOSYS # ignored')], [
('IDENTIFIER', 'read'),
('COLON', ':'),
- ('NOT', '!'),
('ARGUMENT', 'arg0'),
('OP', 'in'),
('BITWISE_COMPLEMENT', '~'),
@@ -396,18 +394,6 @@
with self.assertRaisesRegex(parser.ParseException, 'unclosed brace'):
self.parser.parse_filter(self._tokenize('{ allow'))
- def test_parse_allowlist_with_not(self):
- """Reject a policy with a !"""
- with self.assertRaisesRegex(parser.ParseException,
- 'invalid allowlist policy'):
- self.parser.parse_filter(self._tokenize('! allow'))
-
- def test_parse_allowlist_with_zero(self):
- """Reject a policy with a 0"""
- with self.assertRaisesRegex(parser.ParseException,
- 'invalid allowlist policy'):
- self.parser.parse_filter(self._tokenize('0'))
-
class ParseFilterDenylistTests(unittest.TestCase):
"""Tests for PolicyParser.parse_filter with a denylist policy."""
@@ -423,27 +409,14 @@
return list(self.parser._parser_state.tokenize([line]))[0]
def test_parse_filter(self):
- """Accept only filters prefaced by a ! or just the token 0."""
+ """Accept only filters that return an errno."""
self.assertEqual(
- self.parser.parse_filter(self._tokenize('! arg0 == 0')), [
- parser.Filter([[parser.Atom(0, '==', 0)]], self.kill_action),
- ])
- self.assertEqual(
- self.parser.parse_filter(self._tokenize('0')), [
- parser.Filter(None, self.kill_action),
+ self.parser.parse_filter(self._tokenize('arg0 == 0; return ENOSYS')),
+ [
+ parser.Filter([[parser.Atom(0, '==', 0)]],
+ bpf.ReturnErrno(self.arch.constants['ENOSYS'])),
])
- def test_parse_allowlist_without_not(self):
- """Reject a denylist policy without a !"""
- with self.assertRaisesRegex(parser.ParseException,
- 'invalid denylist policy'):
- self.parser.parse_filter(self._tokenize('allow'))
-
- def test_parse_allowlist_with_zero(self):
- """Reject a denylistpolicy with a 1"""
- with self.assertRaisesRegex(parser.ParseException,
- 'invalid denylist policy'):
- self.parser.parse_filter(self._tokenize('1'))
class ParseFilterStatementTests(unittest.TestCase):
"""Tests for PolicyParser.parse_filter_statement."""
@@ -918,6 +891,18 @@
r'applied')):
self.parser.parse_file(path)
+ def test_parse_allowlist_denylist_header(self):
+ """Reject trying to compile denylist policy file as allowlist."""
+ with self.assertRaisesRegex(parser.ParseException,
+ r'policy is denylist, but flag --denylist '
+ 'not passed in'):
+ path = self._write_file(
+ 'test.policy', """
+ @denylist
+ """)
+ self.parser.parse_file(path)
+
+
class ParseFileDenylistTests(unittest.TestCase):
"""Tests for PolicyParser.parse_file."""
@@ -943,8 +928,9 @@
path = self._write_file(
'test.policy', """
# Comment.
- read: 0
- write: 0
+ @denylist
+ read: return ENOSYS
+ write: return ENOSYS
""")
self.assertEqual(
@@ -956,13 +942,15 @@
syscall=parser.Syscall('read', 0),
frequency=1,
filters=[
- parser.Filter(None, self.kill_action),
+ parser.Filter(None, bpf.ReturnErrno(
+ self.arch.constants['ENOSYS'])),
]),
parser.FilterStatement(
syscall=parser.Syscall('write', 1),
frequency=1,
filters=[
- parser.Filter(None, self.kill_action),
+ parser.Filter(None, bpf.ReturnErrno(
+ self.arch.constants['ENOSYS'])),
]),
]))
@@ -971,8 +959,9 @@
path = self._write_file(
'test.policy', """
# Comment.
- read: 0
- write: ! arg0 == 0
+ @denylist
+ read: return ENOSYS
+ write: arg0 == 0 ; return ENOSYS
""")
self.assertEqual(
@@ -984,17 +973,30 @@
syscall=parser.Syscall('read', 0),
frequency=1,
filters=[
- parser.Filter(None, self.kill_action),
+ parser.Filter(None, bpf.ReturnErrno(
+ self.arch.constants['ENOSYS'])),
]),
parser.FilterStatement(
syscall=parser.Syscall('write', 1),
frequency=1,
filters=[
parser.Filter([[parser.Atom(0, '==', 0)]],
- self.kill_action),
+ bpf.ReturnErrno(self.arch.constants['ENOSYS'])),
parser.Filter(None, bpf.Allow()),
]),
]))
+
+ def test_parse_denylist_no_header(self):
+ """Reject trying to compile denylist policy file as allowlist."""
+ with self.assertRaisesRegex(parser.ParseException,
+ r'policy must contain @denylist flag to be '
+ 'compiled with --denylist flag'):
+ path = self._write_file(
+ 'test.policy', """
+ read: return ENOSYS
+ """)
+ self.parser.parse_file(path)
+
if __name__ == '__main__':
unittest.main()