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