blob: 05b66284425898b8607a8832833d4e3a3ecc10a9 [file] [log] [blame]
Luis Hector Chavezd4ce4492018-12-04 20:00:32 -08001#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2018 The Android Open Source Project
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17"""A parser for the Minijail policy file."""
18
19from __future__ import absolute_import
20from __future__ import division
21from __future__ import print_function
22
23import collections
24import re
25
26Token = collections.namedtuple('token',
27 ['type', 'value', 'filename', 'line', 'column'])
28
29# A regex that can tokenize a Minijail policy file line.
30_TOKEN_SPECIFICATION = (
31 ('COMMENT', r'#.*$'),
32 ('WHITESPACE', r'\s+'),
33 ('INCLUDE', r'@include'),
34 ('PATH', r'(?:\.)?/\S+'),
35 ('NUMERIC_CONSTANT', r'-?0[xX][0-9a-fA-F]+|-?0[Oo][0-7]+|-?[0-9]+'),
36 ('COLON', r':'),
37 ('SEMICOLON', r';'),
38 ('COMMA', r','),
39 ('BITWISE_COMPLEMENT', r'~'),
40 ('LPAREN', r'\('),
41 ('RPAREN', r'\)'),
42 ('LBRACE', r'\{'),
43 ('RBRACE', r'\}'),
44 ('RBRACKET', r'\]'),
45 ('LBRACKET', r'\['),
46 ('OR', r'\|\|'),
47 ('AND', r'&&'),
48 ('BITWISE_OR', r'\|'),
49 ('OP', r'&|in|==|!=|<=|<|>=|>'),
50 ('EQUAL', r'='),
51 ('ARGUMENT', r'arg[0-9]+'),
52 ('RETURN', r'return'),
53 ('ACTION', r'allow|kill-process|kill-thread|kill|trap|trace|log'),
54 ('IDENTIFIER', r'[a-zA-Z_][a-zA-Z_0-9@]*'),
55)
56_TOKEN_RE = re.compile('|'.join(
57 r'(?P<%s>%s)' % pair for pair in _TOKEN_SPECIFICATION))
58
59
60class ParseException(Exception):
61 """An exception that is raised when parsing fails."""
62
63 # pylint: disable=too-many-arguments
64 def __init__(self, message, filename, line, line_number=1, token=None):
65 if token:
66 column = token.column
67 length = len(token.value)
68 else:
69 column = len(line)
70 length = 1
71
72 message = ('%s(%d:%d): %s') % (filename, line_number, column + 1,
73 message)
74 message += '\n %s' % line
75 message += '\n %s%s' % (' ' * column, '^' * length)
76 super().__init__(message)
77
78
79class ParserState:
80 """Stores the state of the Parser to provide better diagnostics."""
81
82 def __init__(self, filename):
83 self._filename = filename
84 self._line = ''
85 self._line_number = 0
86
87 @property
88 def filename(self):
89 """Return the name of the file being processed."""
90 return self._filename
91
92 @property
93 def line(self):
94 """Return the current line being processed."""
95 return self._line
96
97 @property
98 def line_number(self):
99 """Return the current line number being processed."""
100 return self._line_number
101
102 def set_line(self, line):
103 """Update the current line being processed."""
104 self._line = line
105 self._line_number += 1
106
107 def error(self, message, token=None):
108 """Raise a ParserException with the provided message."""
109 raise ParseException(message, self.filename, self.line,
110 self.line_number, token)
111
112 def tokenize(self):
113 """Return a list of tokens for the current line."""
114 tokens = []
115
116 last_end = 0
117 for token in _TOKEN_RE.finditer(self.line):
118 if token.start() != last_end:
119 self.error(
120 'invalid token',
121 token=Token('INVALID', self.line[last_end:token.start()],
122 self.filename, self.line_number, last_end))
123 last_end = token.end()
124
125 # Omit whitespace and comments now to avoid sprinkling this logic
126 # elsewhere.
127 if token.lastgroup in ('WHITESPACE', 'COMMENT'):
128 continue
129 tokens.append(
130 Token(token.lastgroup, token.group(), self.filename,
131 self.line_number, token.start()))
132 if last_end != len(self.line):
133 self.error(
134 'invalid token',
135 token=Token('INVALID', self.line[last_end:], self.filename,
136 self.line_number, last_end))
137 return tokens
138
139
140# pylint: disable=too-few-public-methods
141class PolicyParser:
142 """A parser for the Minijail seccomp policy file format."""
143
144 def __init__(self, arch):
145 self._parser_states = [ParserState("<memory>")]
146 self._arch = arch
147
148 @property
149 def _parser_state(self):
150 return self._parser_states[-1]
151
152 # single-constant = identifier
153 # | numeric-constant
154 # ;
155 def _parse_single_constant(self, token):
156 if token.type == 'IDENTIFIER':
157 if token.value not in self._arch.constants:
158 self._parser_state.error('invalid constant', token=token)
159 single_constant = self._arch.constants[token.value]
160 elif token.type == 'NUMERIC_CONSTANT':
161 try:
162 single_constant = int(token.value, base=0)
163 except ValueError:
164 self._parser_state.error('invalid constant', token=token)
165 else:
166 self._parser_state.error('invalid constant', token=token)
167 if single_constant > self._arch.max_unsigned:
168 self._parser_state.error('unsigned overflow', token=token)
169 elif single_constant < self._arch.min_signed:
170 self._parser_state.error('signed underflow', token=token)
171 elif single_constant < 0:
172 # This converts the constant to an unsigned representation of the
173 # same value, since BPF only uses unsigned values.
174 single_constant = self._arch.truncate_word(single_constant)
175 return single_constant
176
177 # constant = [ '~' ] , '(' , value , ')'
178 # | [ '~' ] , single-constant
179 # ;
180 def _parse_constant(self, tokens):
181 negate = False
182 if tokens[0].type == 'BITWISE_COMPLEMENT':
183 negate = True
184 tokens.pop(0)
185 if not tokens:
186 self._parser_state.error('empty complement')
187 if tokens[0].type == 'BITWISE_COMPLEMENT':
188 self._parser_state.error(
189 'invalid double complement', token=tokens[0])
190 if tokens[0].type == 'LPAREN':
191 last_open_paren = tokens.pop(0)
192 single_value = self.parse_value(tokens)
193 if not tokens or tokens[0].type != 'RPAREN':
194 self._parser_state.error(
195 'unclosed parenthesis', token=last_open_paren)
196 else:
197 single_value = self._parse_single_constant(tokens[0])
198 tokens.pop(0)
199 if negate:
200 single_value = self._arch.truncate_word(~single_value)
201 return single_value
202
203 # value = constant , [ { '|' , constant } ]
204 # ;
205 def parse_value(self, tokens):
206 """Parse constants separated bitwise OR operator |.
207
208 Constants can be:
209
210 - A number that can be parsed with int(..., base=0)
211 - A named constant expression.
212 - A parenthesized, valid constant expression.
213 - A valid constant expression prefixed with the unary bitwise
214 complement operator ~.
215 - A series of valid constant expressions separated by bitwise
216 OR operator |.
217
218 If there is an error parsing any of the constants, the whole process
219 fails.
220 """
221
222 value = 0
223 while tokens:
224 value |= self._parse_constant(tokens)
225 if not tokens or tokens[0].type != 'BITWISE_OR':
226 break
227 tokens.pop(0)
228 else:
229 self._parser_state.error('empty constant')
230 return value