blob: f13897359efe8aaa794a94d270c5af5868046c6c [file] [log] [blame] [edit]
#!/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:]))