xref: /aosp_15_r20/external/minijail/tools/seccomp_policy_lint.py (revision 4b9c6d91573e8b3a96609339b46361b5476dd0f9)
1#!/usr/bin/env python3
2#
3# Copyright (C) 2021 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""A linter for the Minijail seccomp policy file."""
17
18import argparse
19import re
20import sys
21
22from typing import List, NamedTuple
23
24# The syscalls we have determined are more dangerous and need justification
25# for inclusion in a policy.
26DANGEROUS_SYSCALLS = (
27    'clone',
28    'mount',
29    'setns',
30    'kill',
31    'execve',
32    'execveat',
33    'bpf',
34    'socket',
35    'ptrace',
36    'swapon',
37    'swapoff',
38    # TODO(b/193169195): Add argument granularity for the below syscalls.
39    'prctl',
40    'ioctl',
41#   'mmap',
42#   'mprotect',
43#   'mmap2',
44)
45
46class CheckPolicyReturn(NamedTuple):
47    """Represents a return value from check_seccomp_policy
48
49    Contains a message to print to the user and a list of errors that were
50    found in the file.
51    """
52    message: str
53    errors: List[str]
54
55def parse_args(argv):
56    """Return the parsed CLI arguments for this tool."""
57    parser = argparse.ArgumentParser(description=__doc__)
58    parser.add_argument(
59        '--denylist',
60        action='store_true',
61        help='Check as a denylist policy rather than the default allowlist.')
62    parser.add_argument(
63        '--dangerous-syscalls',
64        action='store',
65        default=','.join(DANGEROUS_SYSCALLS),
66        help='Comma-separated list of dangerous sycalls (overrides default).'
67    )
68    parser.add_argument('policy',
69                            help='The seccomp policy.',
70                            type=argparse.FileType('r', encoding='utf-8'))
71    return parser.parse_args(argv), parser
72
73def check_seccomp_policy(check_file, dangerous_syscalls):
74    """Fail if the seccomp policy file has dangerous, undocumented syscalls.
75
76    Takes in a file object and a set of dangerous syscalls as arguments.
77    """
78
79    found_syscalls = set()
80    errors = []
81    msg = ''
82    contains_dangerous_syscall = False
83    prev_line_comment = False
84
85    for line_num, line in enumerate(check_file):
86        if re.match(r'^\s*#', line):
87            prev_line_comment = True
88        elif re.match(r'^\s*$', line):
89            # Empty lines shouldn't reset prev_line_comment.
90            continue
91        else:
92            match = re.match(fr'^\s*(\w*)\s*:', line)
93            if match:
94                syscall = match.group(1)
95                if syscall in found_syscalls:
96                    errors.append(f'{check_file.name}, line {line_num}: repeat '
97                                  f'syscall: {syscall}')
98                else:
99                    found_syscalls.add(syscall)
100                    for dangerous in dangerous_syscalls:
101                        if dangerous == syscall:
102                            # Dangerous syscalls must be preceded with a
103                            # comment.
104                            contains_dangerous_syscall = True
105                            if not prev_line_comment:
106                                errors.append(f'{check_file.name}, line '
107                                              f'{line_num}: {syscall} syscall '
108                                              'is a dangerous syscall so '
109                                              'requires a comment on the '
110                                              'preceding line')
111                prev_line_comment = False
112            else:
113                # This line is probably a continuation from the previous line.
114                # TODO(b/203216289): Support line breaks.
115                pass
116
117    if contains_dangerous_syscall:
118        msg = (f'seccomp: {check_file.name} contains dangerous syscalls, so'
119               ' requires review from chromeos-security@')
120    else:
121        msg = (f'seccomp: {check_file.name} does not contain any dangerous'
122               ' syscalls, so does not require review from'
123               ' chromeos-security@')
124
125    if errors:
126        return CheckPolicyReturn(msg, errors)
127
128    return CheckPolicyReturn(msg, errors)
129
130def main(argv=None):
131    """Main entrypoint."""
132
133    if argv is None:
134        argv = sys.argv[1:]
135
136    opts, _arg_parser = parse_args(argv)
137
138    check = check_seccomp_policy(opts.policy,
139                                 set(opts.dangerous_syscalls.split(',')))
140
141    formatted_items = ''
142    if check.errors:
143        item_prefix = '\n    * '
144        formatted_items = item_prefix + item_prefix.join(check.errors)
145
146    print('* ' + check.message + formatted_items)
147
148    return 1 if check.errors else 0
149
150if __name__ == '__main__':
151    sys.exit(main(sys.argv[1:]))
152