#!/usr/bin/env python3 # Copyright 2020 The ChromiumOS Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """A linter for the Minijail seccomp policy file.""" import argparse import re import sys from typing import List, NamedTuple, Optional, Set # The syscalls we have determined are more dangerous and need justification # for inclusion in a policy. DANGEROUS_SYSCALLS = ( "clone", "mount", "setns", "kill", "execve", "execveat", "getrandom", "bpf", "socket", "ptrace", "swapon", "swapoff", # TODO(b/193169195): Add argument granularity for the below syscalls. "prctl", "ioctl", "mmap", "mmap2", "mprotect", ) # If a dangerous syscall uses these rules, then it's considered safe. SYSCALL_SAFE_RULES = { "getrandom": ("arg2 in ~GRND_RANDOM",), "mmap": ( "arg2 == PROT_READ || arg2 == PROT_NONE", "arg2 in ~PROT_EXEC", "arg2 in ~PROT_EXEC || arg2 in ~PROT_WRITE", ), "mmap2": ( "arg2 == PROT_READ || arg2 == PROT_NONE", "arg2 in ~PROT_EXEC", "arg2 in ~PROT_EXEC || arg2 in ~PROT_WRITE", ), "mprotect": ( "arg2 == PROT_READ || arg2 == PROT_NONE", "arg2 in ~PROT_EXEC", "arg2 in ~PROT_EXEC || arg2 in ~PROT_WRITE", ), } GLOBAL_SAFE_RULES = ( "kill", "kill-process", "kill-thread", "return 1", ) 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( "--assume-filename", help="The filename when parsing stdin.", ) 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: Set[str], filename: Optional[str] = None ): """Fail if the seccomp policy file has dangerous, undocumented syscalls. Takes in a file object and a set of dangerous syscalls as arguments. """ if filename is None: filename = check_file.name 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(r"^\s*(\w*)\s*:\s*(.*)\s*", line) if match: syscall = match.group(1) rule = match.group(2) err_prefix = f"{filename}:{line_num}:{syscall}:" if syscall in found_syscalls: errors.append(f"{err_prefix} duplicate entry found") else: found_syscalls.add(syscall) if syscall in dangerous_syscalls: contains_dangerous_syscall = True if not prev_line_comment: # Dangerous syscalls must be commented. safe_rules = SYSCALL_SAFE_RULES.get(syscall, ()) if rule in GLOBAL_SAFE_RULES or rule in safe_rules: pass elif safe_rules: # Dangerous syscalls with known safe rules must # use those rules. errors.append( f"{err_prefix} syscall is dangerous and " f"should use one of the rules: {safe_rules}" ) else: errors.append( f"{err_prefix} syscall is dangerous and " "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: {filename} contains dangerous syscalls, so" " requires review from chromeos-security@" ) else: msg = ( f"seccomp: {filename} 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) filename = opts.assume_filename if opts.assume_filename else opts.policy check = check_seccomp_policy( opts.policy, set(opts.dangerous_syscalls.split(",")), filename=filename ) 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:]))