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