|
| 1 | +# |
| 2 | +# Copyright (c) nexB Inc. and others. All rights reserved. |
| 3 | +# ScanCode is a trademark of nexB Inc. |
| 4 | +# SPDX-License-Identifier: Apache-2.0 |
| 5 | +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. |
| 6 | +# See https://github.com/nexB/scancode-toolkit for support or download. |
| 7 | +# See https://aboutcode.org for more information about nexB OSS projects. |
| 8 | +# |
| 9 | + |
| 10 | +from typing import Dict |
| 11 | +from typing import List |
| 12 | + |
| 13 | +import attr |
| 14 | + |
| 15 | +from commoncode.cliutils import OTHER_SCAN_GROUP |
| 16 | +from commoncode.cliutils import PluggableCommandLineOption |
| 17 | +from elftools.common.exceptions import ELFError |
| 18 | +from elftools.elf.elffile import ELFFile |
| 19 | +from plugincode.scan import ScanPlugin |
| 20 | +from plugincode.scan import scan_impl |
| 21 | + |
| 22 | + |
| 23 | +@scan_impl |
| 24 | +class LinuxKernelModuleScanner(ScanPlugin): |
| 25 | + """ |
| 26 | + Scan Linux kernel module files for metadata stored in their ELF |
| 27 | + '.modinfo' section. |
| 28 | + """ |
| 29 | + |
| 30 | + resource_attributes = dict( |
| 31 | + linux_kernel_module=attr.ib(default=None, repr=False), |
| 32 | + ) |
| 33 | + |
| 34 | + run_order = 9 |
| 35 | + sort_order = 9 |
| 36 | + |
| 37 | + options = [ |
| 38 | + PluggableCommandLineOption( |
| 39 | + ('--lkm',), |
| 40 | + is_flag=True, |
| 41 | + default=False, |
| 42 | + help='Scan Linux kernel module files for .modinfo metadata.', |
| 43 | + help_group=OTHER_SCAN_GROUP, |
| 44 | + ) |
| 45 | + ] |
| 46 | + |
| 47 | + def is_enabled(self, lkm, **kwargs): |
| 48 | + return lkm |
| 49 | + |
| 50 | + def get_scanner(self, **kwargs): |
| 51 | + return scan_linux_kernel_module |
| 52 | + |
| 53 | + |
| 54 | +def scan_linux_kernel_module(location, **kwargs): |
| 55 | + """ |
| 56 | + Return a mapping of Linux kernel module metadata found in the '.ko' file |
| 57 | + at 'location'. Return an empty mapping for other files and for files that |
| 58 | + do not contain usable '.modinfo' metadata. |
| 59 | + """ |
| 60 | + if not location.lower().endswith('.ko'): |
| 61 | + return {} |
| 62 | + |
| 63 | + metadata = extract_modinfo(location) |
| 64 | + if not metadata: |
| 65 | + return {} |
| 66 | + |
| 67 | + # The .modinfo "depends" value is a comma-separated list of required |
| 68 | + # kernel module names, not a list of package-management dependencies. |
| 69 | + metadata['depends'] = get_dependency_names(metadata) |
| 70 | + |
| 71 | + return dict(linux_kernel_module=metadata) |
| 72 | + |
| 73 | + |
| 74 | +def extract_modinfo(location: str) -> Dict[str, List[str]]: |
| 75 | + """ |
| 76 | + Extract '.modinfo' metadata from the Linux kernel module file at 'location'. |
| 77 | +
|
| 78 | + Return '.modinfo' metadata as a mapping of keys to lists of values. |
| 79 | +
|
| 80 | + Multiple values are preserved because fields such as 'author', 'alias', |
| 81 | + and 'firmware' may occur more than once. |
| 82 | + """ |
| 83 | + metadata: Dict[str, List[str]] = {} |
| 84 | + |
| 85 | + try: |
| 86 | + with open(location, 'rb') as module_file: |
| 87 | + elf_file = ELFFile(module_file) |
| 88 | + modinfo_section = elf_file.get_section_by_name('.modinfo') |
| 89 | + if modinfo_section is None: |
| 90 | + return {} |
| 91 | + |
| 92 | + raw_bytes = modinfo_section.data() |
| 93 | + |
| 94 | + except (ELFError, OSError): |
| 95 | + return {} |
| 96 | + |
| 97 | + # Entries in .modinfo are NUL-terminated key=value strings. |
| 98 | + for raw_entry in raw_bytes.split(b'\x00'): |
| 99 | + if not raw_entry: |
| 100 | + continue |
| 101 | + |
| 102 | + entry = raw_entry.decode('utf-8', errors='replace') |
| 103 | + if '=' not in entry: |
| 104 | + continue |
| 105 | + |
| 106 | + key, value = entry.split('=', 1) |
| 107 | + if not key: |
| 108 | + continue |
| 109 | + |
| 110 | + metadata.setdefault(key, []).append(value) |
| 111 | + |
| 112 | + return metadata |
| 113 | + |
| 114 | + |
| 115 | +def get_dependency_names(metadata: Dict[str, List[str]]) -> List[str]: |
| 116 | + """ |
| 117 | + Returns a list of strings, each being a kernel module name |
| 118 | + Normalized fields to a list of strings derived from the '.modinfo' metadata. The 'depends' field is a |
| 119 | + comma-separated list of required kernel module names |
| 120 | + """ |
| 121 | + dependency_names = [] |
| 122 | + |
| 123 | + for depends_entry in metadata.get('depends', []): |
| 124 | + dependency_names.extend( |
| 125 | + dependency.strip() |
| 126 | + for dependency in depends_entry.split(',') |
| 127 | + if dependency.strip() |
| 128 | + ) |
| 129 | + |
| 130 | + return dependency_names |
0 commit comments