• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright 2020 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""A linter for the Minijail seccomp policy file."""
7
8import argparse
9import re
10import sys
11from typing import List, NamedTuple, Optional, Set
12
13
14# The syscalls we have determined are more dangerous and need justification
15# for inclusion in a policy.
16DANGEROUS_SYSCALLS = (
17    "clone",
18    "mount",
19    "setns",
20    "kill",
21    "execve",
22    "execveat",
23    "getrandom",
24    "bpf",
25    "socket",
26    "ptrace",
27    "swapon",
28    "swapoff",
29    # TODO(b/193169195): Add argument granularity for the below syscalls.
30    "prctl",
31    "ioctl",
32    "mmap",
33    "mmap2",
34    "mprotect",
35)
36
37
38# If a dangerous syscall uses these rules, then it's considered safe.
39SYSCALL_SAFE_RULES = {
40    "getrandom": ("arg2 in ~GRND_RANDOM",),
41    "mmap": (
42        "arg2 == PROT_READ || arg2 == PROT_NONE",
43        "arg2 in ~PROT_EXEC",
44        "arg2 in ~PROT_EXEC || arg2 in ~PROT_WRITE",
45    ),
46    "mmap2": (
47        "arg2 == PROT_READ || arg2 == PROT_NONE",
48        "arg2 in ~PROT_EXEC",
49        "arg2 in ~PROT_EXEC || arg2 in ~PROT_WRITE",
50    ),
51    "mprotect": (
52        "arg2 == PROT_READ || arg2 == PROT_NONE",
53        "arg2 in ~PROT_EXEC",
54        "arg2 in ~PROT_EXEC || arg2 in ~PROT_WRITE",
55    ),
56}
57
58GLOBAL_SAFE_RULES = (
59    "kill",
60    "kill-process",
61    "kill-thread",
62    "return 1",
63)
64
65
66class CheckPolicyReturn(NamedTuple):
67    """Represents a return value from check_seccomp_policy
68
69    Contains a message to print to the user and a list of errors that were
70    found in the file.
71    """
72
73    message: str
74    errors: List[str]
75
76
77def parse_args(argv):
78    """Return the parsed CLI arguments for this tool."""
79    parser = argparse.ArgumentParser(description=__doc__)
80    parser.add_argument(
81        "--denylist",
82        action="store_true",
83        help="Check as a denylist policy rather than the default allowlist.",
84    )
85    parser.add_argument(
86        "--dangerous-syscalls",
87        action="store",
88        default=",".join(DANGEROUS_SYSCALLS),
89        help="Comma-separated list of dangerous sycalls (overrides default).",
90    )
91    parser.add_argument(
92        "--assume-filename",
93        help="The filename when parsing stdin.",
94    )
95    parser.add_argument(
96        "policy",
97        help="The seccomp policy.",
98        type=argparse.FileType("r", encoding="utf-8"),
99    )
100    return parser.parse_args(argv), parser
101
102
103def check_seccomp_policy(
104    check_file, dangerous_syscalls: Set[str], filename: Optional[str] = None
105):
106    """Fail if the seccomp policy file has dangerous, undocumented syscalls.
107
108    Takes in a file object and a set of dangerous syscalls as arguments.
109    """
110
111    if filename is None:
112        filename = check_file.name
113    found_syscalls = set()
114    errors = []
115    msg = ""
116    contains_dangerous_syscall = False
117    prev_line_comment = False
118
119    for line_num, line in enumerate(check_file):
120        if re.match(r"^\s*#", line):
121            prev_line_comment = True
122        elif re.match(r"^\s*$", line):
123            # Empty lines shouldn't reset prev_line_comment.
124            continue
125        else:
126            match = re.match(r"^\s*(\w*)\s*:\s*(.*)\s*", line)
127            if match:
128                syscall = match.group(1)
129                rule = match.group(2)
130                err_prefix = f"{filename}:{line_num}:{syscall}:"
131                if syscall in found_syscalls:
132                    errors.append(f"{err_prefix} duplicate entry found")
133                else:
134                    found_syscalls.add(syscall)
135                    if syscall in dangerous_syscalls:
136                        contains_dangerous_syscall = True
137                        if not prev_line_comment:
138                            # Dangerous syscalls must be commented.
139                            safe_rules = SYSCALL_SAFE_RULES.get(syscall, ())
140                            if rule in GLOBAL_SAFE_RULES or rule in safe_rules:
141                                pass
142                            elif safe_rules:
143                                # Dangerous syscalls with known safe rules must
144                                # use those rules.
145                                errors.append(
146                                    f"{err_prefix} syscall is dangerous and "
147                                    f"should use one of the rules: {safe_rules}"
148                                )
149                            else:
150                                errors.append(
151                                    f"{err_prefix} syscall is dangerous and "
152                                    "requires a comment on the preceding line"
153                                )
154                prev_line_comment = False
155            else:
156                # This line is probably a continuation from the previous line.
157                # TODO(b/203216289): Support line breaks.
158                pass
159
160    if contains_dangerous_syscall:
161        msg = (
162            f"seccomp: {filename} contains dangerous syscalls, so"
163            " requires review from chromeos-security@"
164        )
165    else:
166        msg = (
167            f"seccomp: {filename} does not contain any dangerous"
168            " syscalls, so does not require review from"
169            " chromeos-security@"
170        )
171
172    if errors:
173        return CheckPolicyReturn(msg, errors)
174
175    return CheckPolicyReturn(msg, errors)
176
177
178def main(argv=None):
179    """Main entrypoint."""
180
181    if argv is None:
182        argv = sys.argv[1:]
183
184    opts, _arg_parser = parse_args(argv)
185
186    filename = opts.assume_filename if opts.assume_filename else opts.policy
187    check = check_seccomp_policy(
188        opts.policy, set(opts.dangerous_syscalls.split(",")), filename=filename
189    )
190
191    formatted_items = ""
192    if check.errors:
193        item_prefix = "\n    * "
194        formatted_items = item_prefix + item_prefix.join(check.errors)
195
196    print("* " + check.message + formatted_items)
197
198    return 1 if check.errors else 0
199
200
201if __name__ == "__main__":
202    sys.exit(main(sys.argv[1:]))
203