Mike Frysinger | b9743c6 | 2020-02-20 02:53:55 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # -*- coding: utf-8 -*- |
| 3 | # Copyright 2020 The Chromium OS Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
| 7 | """Make sure packages don't create random paths outside of existing norms.""" |
| 8 | |
| 9 | from __future__ import print_function |
| 10 | |
| 11 | import argparse |
| 12 | import fnmatch |
| 13 | import logging # pylint: disable=cros-logging-import |
| 14 | import os |
| 15 | import sys |
| 16 | |
| 17 | |
| 18 | # NB: Do not add any new entries here without wider discussion. |
| 19 | |
| 20 | |
| 21 | # Paths that are allowed in the / dir. |
| 22 | VALID_ROOT = { |
| 23 | 'bin', 'boot', 'dev', 'etc', 'home', 'lib', 'lib32', 'lib64', 'media', |
| 24 | 'mnt', 'opt', 'proc', 'root', 'run', 'sbin', 'sys', 'tmp', 'usr', 'var', |
| 25 | } |
| 26 | |
| 27 | # Paths that are allowed in the / dir for boards. |
| 28 | VALID_BOARD_ROOT = { |
| 29 | 'build', 'firmware', |
| 30 | # TODO(): We should clean this up. |
| 31 | 'postinst', |
| 32 | } |
| 33 | |
| 34 | # Paths that are allowed in the / dir for the SDK chroot. |
| 35 | VALID_HOST_ROOT = set() |
| 36 | |
| 37 | # Paths under / that should not have any subdirs. |
| 38 | NOSUBDIRS_ROOT = { |
| 39 | 'bin', 'dev', 'proc', 'sbin', 'sys', 'tmp', |
| 40 | } |
| 41 | |
| 42 | # Paths that are allowed in the /usr dir. |
| 43 | VALID_USR = { |
| 44 | 'bin', 'include', 'lib', 'lib32', 'lib64', 'libexec', 'sbin', 'share', |
| 45 | 'src', |
| 46 | } |
| 47 | |
| 48 | # Paths that are allowed in the /usr dir for boards. |
| 49 | VALID_BOARD_USR = { |
| 50 | # Boards install into /usr/local for test images. |
| 51 | 'local', |
| 52 | } |
| 53 | |
| 54 | # Paths that are allowed in the /usr dir for the SDK chroot. |
| 55 | VALID_HOST_USR = set() |
| 56 | |
| 57 | # Paths under /usr that should not have any subdirs. |
| 58 | NOSUBDIRS_USR = { |
| 59 | 'bin', 'sbin', |
| 60 | } |
| 61 | |
| 62 | # Valid toolchain targets. We don't want to add any more non-standard ones. |
| 63 | # targets that use *-cros-* as the vendor are OK to add more. |
| 64 | KNOWN_TARGETS = { |
| 65 | 'arm-none-eabi', |
| 66 | 'i686-pc-linux-gnu', |
| 67 | 'x86_64-pc-linux-gnu', |
| 68 | 'arm*-cros-eabi', |
| 69 | '*-cros-linux-gnu*', |
| 70 | } |
| 71 | |
| 72 | # These packages need fixing. |
| 73 | # NB: Do *not* add more packages here. |
| 74 | BAD_ROOT_PACKAGES = { |
| 75 | # TODO(crbug.com/1003107): Delete this. |
| 76 | 'dev-go/go-tools', |
| 77 | } |
| 78 | |
| 79 | # These SDK packages need cleanup. |
| 80 | # NB: Do *not* add more packages here. |
| 81 | BAD_HOST_USR_LOCAL_PACKAGES = { |
| 82 | 'app-crypt/nss', |
| 83 | } |
| 84 | |
| 85 | # Ignore some packages installing into /var for now. |
| 86 | # NB: Do *not* add more packages here. |
| 87 | BAD_VAR_PACKAGES = { |
| 88 | 'app-accessibility/brltty', |
| 89 | 'app-admin/eselect', |
| 90 | 'app-admin/puppet', |
| 91 | 'app-admin/rsyslog', |
| 92 | 'app-admin/sudo', |
| 93 | 'app-admin/sysstat', |
| 94 | 'app-admin/webapp-config', |
| 95 | 'app-crypt/mit-krb5', |
| 96 | 'app-crypt/trousers', |
| 97 | 'app-emulation/containerd', |
| 98 | 'app-emulation/lxc', |
Mike Frysinger | f3828e9 | 2020-03-06 13:17:34 -0500 | [diff] [blame] | 99 | # https://crbug.com/1059308 |
| 100 | 'chromeos-base/chromeos-bsp-initramfs-rialto', |
Mike Frysinger | b9743c6 | 2020-02-20 02:53:55 -0500 | [diff] [blame] | 101 | 'chromeos-base/chromeos-initramfs', |
| 102 | # https://crbug.com/1054646 |
| 103 | 'chromeos-base/devserver', |
| 104 | # https://crbug.com/1007402 |
| 105 | 'chromeos-base/factory', |
| 106 | 'chromeos-base/factory-board', |
| 107 | 'dev-python/django', |
| 108 | 'media-gfx/sane-backends', |
| 109 | 'media-sound/alsa-utils', |
| 110 | 'net-analyzer/netperf', |
| 111 | 'net-dns/dnsmasq', |
| 112 | 'net-firewall/iptables', |
Mike Frysinger | 88028fe | 2020-03-04 18:53:39 -0500 | [diff] [blame] | 113 | 'net-firewall/nftables', |
Mike Frysinger | b9743c6 | 2020-02-20 02:53:55 -0500 | [diff] [blame] | 114 | 'net-fs/samba', |
Mike Frysinger | 0669e00 | 2020-03-02 16:56:24 -0500 | [diff] [blame] | 115 | 'net-misc/chrony', |
Mike Frysinger | b9743c6 | 2020-02-20 02:53:55 -0500 | [diff] [blame] | 116 | 'net-misc/dhcpcd', |
| 117 | 'net-misc/openssh', |
| 118 | 'net-print/cups', |
| 119 | 'sys-apps/dbus', |
| 120 | 'sys-apps/fwupd', |
| 121 | 'sys-apps/iproute2', |
| 122 | 'sys-apps/journald', |
| 123 | 'sys-apps/portage', |
| 124 | 'sys-apps/sandbox', |
| 125 | 'sys-apps/systemd', |
| 126 | 'sys-apps/usbguard', |
| 127 | 'sys-kernel/loonix-initramfs', |
| 128 | 'sys-libs/glibc', |
| 129 | 'sys-process/audit', |
| 130 | 'www-servers/nginx', |
| 131 | 'x11-base/xwayland', |
| 132 | } |
| 133 | |
| 134 | # Ignore some packages installing into /run for now. |
| 135 | # NB: Do *not* add more packages here. |
| 136 | BAD_RUN_PACKAGES = { |
| 137 | 'app-accessibility/brltty', |
| 138 | 'net-fs/samba', |
| 139 | } |
| 140 | |
| 141 | # Ignore some packages installing into /tmp for now. |
| 142 | # NB: Do *not* add more packages here. |
| 143 | BAD_TMP_PACKAGES = { |
| 144 | # https://crbug.com/1057059 |
| 145 | 'chromeos-base/chromeos-bsp-caroline-private', |
Mike Frysinger | 833a822 | 2020-03-02 14:04:36 -0500 | [diff] [blame] | 146 | 'chromeos-base/chromeos-bsp-elm-private', |
Mike Frysinger | 7dd8d68 | 2020-03-03 11:18:29 -0500 | [diff] [blame] | 147 | 'chromeos-base/chromeos-config-bsp', |
Mike Frysinger | 833a822 | 2020-03-02 14:04:36 -0500 | [diff] [blame] | 148 | 'chromeos-base/chromeos-config-bsp-coral', |
| 149 | 'chromeos-base/chromeos-config-bsp-coral-private', |
| 150 | 'chromeos-base/chromeos-config-bsp-nami', |
| 151 | 'chromeos-base/chromeos-config-bsp-scarlet-private', |
Mike Frysinger | b9743c6 | 2020-02-20 02:53:55 -0500 | [diff] [blame] | 152 | 'chromeos-base/cros-config-test', |
| 153 | } |
| 154 | |
| 155 | |
| 156 | def has_subdirs(path): |
| 157 | """See if |path| has any subdirs.""" |
| 158 | # These checks are helpful for manually running the script when debugging. |
| 159 | if os.path.ismount(path): |
| 160 | logging.warning('Ignoring mounted dir for subdir check: %s', path) |
| 161 | return False |
| 162 | |
| 163 | if os.path.join(os.getenv('SYSROOT', '/'), 'tmp') == path: |
| 164 | logging.warning('Ignoring live dir: %s', path) |
| 165 | return False |
| 166 | |
| 167 | for _, dirs, _ in os.walk(path): |
| 168 | if dirs: |
| 169 | logging.error('Subdirs found in a dir that should be empty:\n %s\n' |
| 170 | ' |-- %s', path, '\n |-- '.join(sorted(dirs))) |
| 171 | return True |
| 172 | break |
| 173 | |
| 174 | return False |
| 175 | |
| 176 | |
| 177 | def check_usr(usr, host=False): |
| 178 | """Check the /usr filesystem at |usr|.""" |
| 179 | ret = True |
| 180 | |
| 181 | # Not all packages install into /usr. |
| 182 | if not os.path.exists(usr): |
| 183 | return ret |
| 184 | |
| 185 | atom = get_current_package() |
| 186 | paths = set(os.listdir(usr)) |
| 187 | unknown = paths - VALID_USR |
| 188 | for target in KNOWN_TARGETS: |
| 189 | unknown = set(x for x in unknown if not fnmatch.fnmatch(x, target)) |
| 190 | if host: |
| 191 | unknown -= VALID_HOST_USR |
| 192 | |
| 193 | if atom in BAD_HOST_USR_LOCAL_PACKAGES: |
| 194 | logging.warning('Ignoring known bad /usr/local install for now') |
| 195 | unknown -= {'local'} |
| 196 | else: |
| 197 | unknown -= VALID_BOARD_USR |
| 198 | |
| 199 | if atom in {'chromeos-base/ap-daemons'}: |
| 200 | logging.warning('Ignoring known bad /usr install for now') |
| 201 | unknown -= {'www'} |
| 202 | |
| 203 | if unknown: |
| 204 | logging.error('Paths are not allowed in the /usr dir: %s', sorted(unknown)) |
| 205 | ret = False |
| 206 | |
| 207 | for path in NOSUBDIRS_USR: |
| 208 | if has_subdirs(os.path.join(usr, path)): |
| 209 | ret = False |
| 210 | |
| 211 | return ret |
| 212 | |
| 213 | |
| 214 | def check_root(root, host=False): |
| 215 | """Check the filesystem |root|.""" |
| 216 | ret = True |
| 217 | |
| 218 | atom = get_current_package() |
| 219 | paths = set(os.listdir(root)) |
| 220 | unknown = paths - VALID_ROOT |
| 221 | if host: |
| 222 | unknown -= VALID_HOST_ROOT |
| 223 | else: |
| 224 | unknown -= VALID_BOARD_ROOT |
| 225 | |
| 226 | if atom in BAD_ROOT_PACKAGES: |
| 227 | logging.warning('Ignoring known bad / install for now') |
| 228 | elif unknown: |
| 229 | logging.error('Paths are not allowed in the root dir:\n %s\n |-- %s', |
| 230 | root, '\n |-- '.join(sorted(unknown))) |
| 231 | ret = False |
| 232 | |
| 233 | # Some of these may have subdirs at runtime, but not from package installs. |
| 234 | for path in NOSUBDIRS_ROOT: |
| 235 | if has_subdirs(os.path.join(root, path)): |
| 236 | if path == 'tmp' and atom in BAD_TMP_PACKAGES: |
| 237 | logging.warning('Ignoring known bad /tmp install for now') |
| 238 | else: |
| 239 | ret = False |
| 240 | |
| 241 | # Special case /var due to so many misuses currently. |
| 242 | if has_subdirs(os.path.join(root, 'var')): |
| 243 | if atom in BAD_VAR_PACKAGES: |
| 244 | logging.warning('Ignoring known bad /var install for now') |
| 245 | elif os.environ.get('PORTAGE_REPO_NAME') == 'portage-stable': |
| 246 | logging.warning('Ignoring bad /var install with portage-stable package ' |
| 247 | 'for now') |
| 248 | else: |
| 249 | ret = False |
| 250 | else: |
| 251 | if atom in BAD_VAR_PACKAGES: |
| 252 | logging.warning('Package has improved; please update BAD_VAR_PACKAGES') |
| 253 | |
| 254 | # Special case /run due to so many misuses currently. |
| 255 | if has_subdirs(os.path.join(root, 'run')): |
| 256 | if atom in BAD_RUN_PACKAGES: |
| 257 | logging.warning('Ignoring known bad /run install for now') |
| 258 | elif os.environ.get('PORTAGE_REPO_NAME') == 'portage-stable': |
| 259 | logging.warning('Ignoring bad /run install with portage-stable package ' |
| 260 | 'for now') |
| 261 | else: |
| 262 | ret = False |
| 263 | else: |
| 264 | if atom in BAD_RUN_PACKAGES: |
| 265 | logging.warning('Package has improved; please update BAD_RUN_PACKAGES') |
| 266 | |
| 267 | if not check_usr(os.path.join(root, 'usr'), host): |
| 268 | ret = False |
| 269 | |
| 270 | return ret |
| 271 | |
| 272 | |
| 273 | def get_current_package(): |
| 274 | """Figure out what package is being built currently.""" |
| 275 | if 'CATEGORY' in os.environ and 'PN' in os.environ: |
| 276 | return f'{os.environ.get("CATEGORY")}/{os.environ.get("PN")}' |
| 277 | else: |
| 278 | return None |
| 279 | |
| 280 | |
| 281 | def get_parser(): |
| 282 | """Get a CLI parser.""" |
| 283 | parser = argparse.ArgumentParser(description=__doc__) |
| 284 | parser.add_argument('--host', default=None, action='store_true', |
| 285 | help='the filesystem is the host SDK, not board sysroot') |
| 286 | parser.add_argument('--board', dest='host', action='store_false', |
| 287 | help='the filesystem is a board sysroot') |
| 288 | parser.add_argument('root', nargs='?', |
| 289 | help='the rootfs to scan') |
| 290 | return parser |
| 291 | |
| 292 | |
| 293 | def main(argv): |
| 294 | """The main func!""" |
| 295 | parser = get_parser() |
| 296 | opts = parser.parse_args(argv) |
| 297 | |
| 298 | # Default to common portage env vars. |
| 299 | if opts.root is None: |
| 300 | for var in ('ED', 'D', 'ROOT'): |
| 301 | if var in os.environ: |
| 302 | logging.debug('Scanning filesystem root via $%s', var) |
| 303 | opts.root = os.environ[var] |
| 304 | break |
| 305 | if not opts.root: |
| 306 | parser.error('Need a valid rootfs to scan, but unable to detect one') |
| 307 | |
| 308 | if opts.host is None: |
| 309 | if os.getenv('BOARD') == 'amd64-host': |
| 310 | opts.host = True |
| 311 | else: |
| 312 | opts.host = not bool(os.getenv('SYSROOT')) |
| 313 | |
| 314 | if not check_root(opts.root, opts.host): |
| 315 | logging.critical( |
| 316 | "This package does not conform to CrOS's filesystem conventions. " |
| 317 | 'Please review the paths flagged above and adjust its layout.') |
| 318 | return 1 |
| 319 | else: |
| 320 | return 0 |
| 321 | |
| 322 | |
| 323 | if __name__ == '__main__': |
| 324 | sys.exit(main(sys.argv[1:])) |