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