aboutsummaryrefslogtreecommitdiff
path: root/seccomp_tools/mass_seccomp_editor/mass_seccomp_editor.py
diff options
context:
space:
mode:
Diffstat (limited to 'seccomp_tools/mass_seccomp_editor/mass_seccomp_editor.py')
-rwxr-xr-xseccomp_tools/mass_seccomp_editor/mass_seccomp_editor.py273
1 files changed, 273 insertions, 0 deletions
diff --git a/seccomp_tools/mass_seccomp_editor/mass_seccomp_editor.py b/seccomp_tools/mass_seccomp_editor/mass_seccomp_editor.py
new file mode 100755
index 00000000..d8dd7626
--- /dev/null
+++ b/seccomp_tools/mass_seccomp_editor/mass_seccomp_editor.py
@@ -0,0 +1,273 @@
+#!/usr/bin/env python3
+
+# Copyright 2021 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Script to make mass, CrOS-wide seccomp changes."""
+
+import argparse
+import re
+import subprocess
+import sys
+import shutil
+from typing import Any, Iterable, Optional
+from dataclasses import dataclass, field
+
+# Pre-compiled regexes.
+AMD64_RE = re.compile(r'.*(amd|x86_)64.*\.policy')
+X86_RE = re.compile(r'.*x86.*\.policy')
+AARCH64_RE = re.compile(r'.*a(arch|rm)64.*\.policy')
+ARM_RE = re.compile(r'.*arm(v7)?.*\.policy')
+
+
+@dataclass(frozen=True)
+class Policies:
+ """Dataclass to hold lists of policies which match certain types."""
+ arm: list[str] = field(default_factory=list)
+ x86_64: list[str] = field(default_factory=list)
+ x86: list[str] = field(default_factory=list)
+ arm64: list[str] = field(default_factory=list)
+ none: list[str] = field(default_factory=list)
+
+ def to_dict(self) -> dict[str, list[str]]:
+ """Convert this class to a dictionary."""
+ return {**self.__dict__}
+
+
+def main():
+ """Run the program from cmd line"""
+ args = parse_args()
+ if all(x is None for x in [args.all, args.b64, args.b32, args.none]):
+ print('Require at least one of {--all, --b64, --b32, --none}',
+ file=sys.stderr)
+ sys.exit(1)
+ matches, success = find_potential_policy_files(args.packages)
+
+ separated = Policies()
+
+ for m in matches:
+ if AMD64_RE.match(m):
+ separated.x86_64.append(m)
+ continue
+ if X86_RE.match(m):
+ separated.x86.append(m)
+ continue
+ if AARCH64_RE.match(m):
+ separated.arm64.append(m)
+ continue
+ if ARM_RE.match(m):
+ separated.arm.append(m)
+ continue
+ separated.none.append(m)
+
+ syscall_lookup_table = _make_syscall_lookup_table(args)
+
+ for (type_, val) in separated.to_dict().items():
+ for fp in val:
+ syscalls = syscall_lookup_table[type_]
+ missing = check_missing_syscalls(syscalls, fp)
+ if missing is None:
+ print(f'E ({type_}) {fp}')
+ elif len(missing) == 0:
+ print(f'_ ({type_}) {fp}')
+ else:
+ missing_str = ','.join(missing)
+ print(f'M ({type_}) {fp} :: {missing_str}')
+
+ if not args.edit:
+ sys.exit(0 if success else 2)
+
+ for (type_, val) in separated.to_dict().items():
+ for fp in val:
+ syscalls = syscall_lookup_table[type_]
+ if args.force:
+ _confirm_add(fp, syscalls, args.yes)
+ continue
+ missing = check_missing_syscalls(syscalls, fp)
+ if missing is None or len(missing) == 0:
+ print(f'Already good for {fp} ({type_})')
+ else:
+ _confirm_add(fp, missing, args.yes)
+
+ sys.exit(0 if success else 2)
+
+
+def _make_syscall_lookup_table(args: Any) -> dict[str, list[str]]:
+ """Make lookup table, segmented by all/b32/b64/none policies.
+
+ Args:
+ args: Direct output from parse_args.
+
+ Returns:
+ dict of syscalls we want to search for in each policy file,
+ where the key is the policy file arch, and the value is
+ a list of syscalls as strings.
+ """
+ syscall_lookup_table = Policies().to_dict()
+ if args.all:
+ split_syscalls = [x.strip() for x in args.all.split(',')]
+ for v in syscall_lookup_table.values():
+ v.extend(split_syscalls)
+ if args.b32:
+ split_syscalls = [x.strip() for x in args.b32.split(',')]
+ syscall_lookup_table['x86'].extend(split_syscalls)
+ syscall_lookup_table['arm'].extend(split_syscalls)
+ if args.b64:
+ split_syscalls = [x.strip() for x in args.b64.split(',')]
+ syscall_lookup_table['x86_64'].extend(split_syscalls)
+ syscall_lookup_table['arm64'].extend(split_syscalls)
+ if args.none:
+ split_syscalls = [x.strip() for x in args.none.split(',')]
+ syscall_lookup_table['none'].extend(split_syscalls)
+ return syscall_lookup_table
+
+
+def _confirm_add(fp: str, syscalls: Iterable[str], noninteractive=None):
+ """Interactive confirmation check you wish to add a syscall.
+
+ Args:
+ fp: filepath of the file to edit.
+ syscalls: list-like of syscalls to add to append to the files.
+ noninteractive: Just add the syscalls without asking.
+ """
+ if noninteractive:
+ _update_seccomp(fp, list(syscalls))
+ return
+ syscalls_str = ','.join(syscalls)
+ user_input = input(f'Add {syscalls_str} for {fp}? [y/N]> ')
+ if user_input.lower().startswith('y'):
+ _update_seccomp(fp, list(syscalls))
+ print('Edited!')
+ else:
+ print(f'Skipping {fp}')
+
+
+def check_missing_syscalls(syscalls: list[str], fp: str) -> Optional[set[str]]:
+ """Return which specified syscalls are missing in the given file."""
+ missing_syscalls = set(syscalls)
+ with open(fp) as f:
+ try:
+ lines = f.readlines()
+ for syscall in syscalls:
+ for line in lines:
+ if re.match(syscall + r':\s*1', line):
+ missing_syscalls.remove(syscall)
+ except UnicodeDecodeError:
+ return None
+ return missing_syscalls
+
+
+def _update_seccomp(fp: str, missing_syscalls: list[str]):
+ """Update the seccomp of the file based on the seccomp change type."""
+ with open(fp, 'a') as f:
+ sorted_syscalls = sorted(missing_syscalls)
+ for to_write in sorted_syscalls:
+ f.write(to_write + ': 1\n')
+
+
+def _search_cmd(query: str, use_fd=True) -> list[str]:
+ if use_fd and shutil.which('fdfind') is not None:
+ return [
+ 'fdfind',
+ '-t',
+ 'f',
+ '--full-path',
+ f'^.*{query}.*\\.policy$',
+ ]
+ return [
+ 'find',
+ '.',
+ '-regex',
+ f'^.*{query}.*\\.policy$',
+ '-type',
+ 'f',
+ ]
+
+
+def find_potential_policy_files(packages: list[str]) -> tuple[list[str], bool]:
+ """Find potentially related policy files to the given packages.
+
+ Returns:
+ (policy_files, successful): A list of policy file paths, and a boolean
+ indicating whether all queries were successful in finding at least
+ one related policy file.
+ """
+ all_queries_succeeded = True
+ matches = []
+ for p in packages:
+ # It's quite common that hyphens are translated to underscores
+ # and similarly common that underscores are translated to hyphens.
+ # We make them agnostic here.
+ hyphen_agnostic = re.sub(r'[-_]', '[-_]', p)
+ cmd = subprocess.run(
+ _search_cmd(hyphen_agnostic),
+ stdout=subprocess.PIPE,
+ check=True,
+ )
+ new_matches = [a for a in cmd.stdout.decode('utf-8').split('\n') if a]
+ if not new_matches:
+ print(f'WARNING: No matches found for {p}', file=sys.stderr)
+ all_queries_succeeded = False
+ else:
+ matches.extend(new_matches)
+ return matches, all_queries_succeeded
+
+
+def parse_args() -> Any:
+ """Handle command line arguments."""
+ parser = argparse.ArgumentParser(
+ description='Check for missing syscalls in'
+ ' seccomp policy files, or make'
+ ' mass seccomp changes.\n\n'
+ 'The format of this output follows the template:\n'
+ ' status (arch) local/policy/filepath :: syscall,syscall,syscall\n'
+ 'Where the status can be "_" for present, "M" for missing,'
+ ' or "E" for Error\n\n'
+ 'Example:\n'
+ ' mass_seccomp_editor.py --all fstatfs --b32 fstatfs64'
+ ' modemmanager\n\n'
+ 'Exit Codes:\n'
+ " '0' for successfully found specific policy files\n"
+ " '1' for python-related error.\n"
+ " '2' for no matched policy files for a given query.",
+ formatter_class=argparse.RawTextHelpFormatter,
+ )
+ parser.add_argument('packages', nargs='+')
+ parser.add_argument(
+ '--all',
+ type=str,
+ metavar='syscalls',
+ help='comma separated syscalls to check in all policy files')
+ parser.add_argument(
+ '--b64',
+ type=str,
+ metavar='syscalls',
+ help='Comma separated syscalls to check in 64bit architectures')
+ parser.add_argument(
+ '--b32',
+ type=str,
+ metavar='syscalls',
+ help='Comma separated syscalls to check in 32bit architectures')
+ parser.add_argument(
+ '--none',
+ type=str,
+ metavar='syscalls',
+ help='Comma separated syscalls to check in unknown architectures')
+ parser.add_argument('--edit',
+ action='store_true',
+ help='Make changes to the listed files,'
+ ' rather than just printing out what is missing')
+ parser.add_argument('-y',
+ '--yes',
+ action='store_true',
+ help='Say "Y" to all interactive checks')
+ parser.add_argument('--force',
+ action='store_true',
+ help='Edit all files, regardless of missing status.'
+ ' Does nothing without --edit.')
+ return parser.parse_args()
+
+
+if __name__ == '__main__':
+ main()