#!/usr/bin/env python3 # # Copyright (C) 2021 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """A linter for the Minijail seccomp policy file.""" import argparse import re import sys from typing import List, NamedTuple # The syscalls we have determined are more dangerous and need justification # for inclusion in a policy. DANGEROUS_SYSCALLS = ( 'clone', 'mount', 'setns', 'kill', 'execve', 'execveat', 'bpf', 'socket', 'ptrace', 'swapon', 'swapoff', # TODO(b/193169195): Add argument granularity for the below syscalls. 'prctl', 'ioctl', # 'mmap', # 'mprotect', # 'mmap2', ) class CheckPolicyReturn(NamedTuple): """Represents a return value from check_seccomp_policy Contains a message to print to the user and a list of errors that were found in the file. """ message: str errors: List[str] def parse_args(argv): """Return the parsed CLI arguments for this tool.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( '--denylist', action='store_true', help='Check as a denylist policy rather than the default allowlist.') parser.add_argument( '--dangerous-syscalls', action='store', default=','.join(DANGEROUS_SYSCALLS), help='Comma-separated list of dangerous sycalls (overrides default).' ) parser.add_argument('policy', help='The seccomp policy.', type=argparse.FileType('r', encoding='utf-8')) return parser.parse_args(argv), parser def check_seccomp_policy(check_file, dangerous_syscalls): """Fail if the seccomp policy file has dangerous, undocumented syscalls. Takes in a file object and a set of dangerous syscalls as arguments. """ found_syscalls = set() errors = [] msg = '' contains_dangerous_syscall = False prev_line_comment = False for line_num, line in enumerate(check_file): if re.match(r'^\s*#', line): prev_line_comment = True elif re.match(r'^\s*$', line): # Empty lines shouldn't reset prev_line_comment. continue else: match = re.match(fr'^\s*(\w*)\s*:', line) if match: syscall = match.group(1) if syscall in found_syscalls: errors.append(f'{check_file.name}, line {line_num}: repeat ' f'syscall: {syscall}') else: found_syscalls.add(syscall) for dangerous in dangerous_syscalls: if dangerous == syscall: # Dangerous syscalls must be preceded with a # comment. contains_dangerous_syscall = True if not prev_line_comment: errors.append(f'{check_file.name}, line ' f'{line_num}: {syscall} syscall ' 'is a dangerous syscall so ' 'requires a comment on the ' 'preceding line') prev_line_comment = False else: # This line is probably a continuation from the previous line. # TODO(b/203216289): Support line breaks. pass if contains_dangerous_syscall: msg = (f'seccomp: {check_file.name} contains dangerous syscalls, so' ' requires review from chromeos-security@') else: msg = (f'seccomp: {check_file.name} does not contain any dangerous' ' syscalls, so does not require review from' ' chromeos-security@') if errors: return CheckPolicyReturn(msg, errors) return CheckPolicyReturn(msg, errors) def main(argv=None): """Main entrypoint.""" if argv is None: argv = sys.argv[1:] opts, _arg_parser = parse_args(argv) check = check_seccomp_policy(opts.policy, set(opts.dangerous_syscalls.split(','))) formatted_items = '' if check.errors: item_prefix = '\n * ' formatted_items = item_prefix + item_prefix.join(check.errors) print('* ' + check.message + formatted_items) return 1 if check.errors else 0 if __name__ == '__main__': sys.exit(main(sys.argv[1:]))