|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +""" Detect outdated shared libraries. |
| 4 | +
|
| 5 | +Detect and report not up-to-date shared libraries that used by running |
| 6 | +processes. Detection based on BuildID comparison and aware of deleted or |
| 7 | +replaced files. |
| 8 | +
|
| 9 | +This program is free software: you can redistribute it and/or modify it under |
| 10 | +the terms of the GNU General Public License as published by the Free Software |
| 11 | +Foundation, either version 2 of the License, or (at your option) any later |
| 12 | +version. |
| 13 | +
|
| 14 | +This program is distributed in the hope that it will be useful, but WITHOUT |
| 15 | +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 16 | +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. |
| 17 | +
|
| 18 | +You should have received a copy of the GNU General Public License along with |
| 19 | +this program. If not, see <http://www.gnu.org/licenses/>. |
| 20 | +""" |
| 21 | + |
| 22 | +__author__ = 'Rinat Sabitov' |
| 23 | +__copyright__ = "Copyright (c) Cloud Linux GmbH & Cloud Linux Software, Inc" |
| 24 | +__license__ = "GPLv2" |
| 25 | +__maintainer__ = 'Rinat Sabitov' |
| 26 | +__email__ = 'rsabitov@cloudlinux.com' |
| 27 | +__status__ = 'Beta' |
| 28 | +__version__ = '0.1' |
| 29 | + |
| 30 | +import os |
| 31 | +import json |
| 32 | +import struct |
| 33 | +import logging |
| 34 | + |
| 35 | +from collections import namedtuple |
| 36 | + |
| 37 | +ELF64_HEADER = "<16sHHIQQQIHHHHHH" |
| 38 | +ELF_PH_HEADER = "<IIQQQQQQ" |
| 39 | +ELF_NHDR = "<3I" |
| 40 | +PT_NOTE = 4 |
| 41 | +NT_GNU_BUILD_ID = 3 |
| 42 | +IGNORED_PATHNAME = ["[heap]", "[stack]", "[vdso]", "[vsyscall]", "[vvar]"] |
| 43 | + |
| 44 | +Vma = namedtuple('Vma', 'offset size start end') |
| 45 | +Map = namedtuple('Map', 'addr perm offset dev inode pathname flag') |
| 46 | + |
| 47 | +try: |
| 48 | + from urllib.request import urlopen |
| 49 | +except ImportError: |
| 50 | + from urllib2 import urlopen |
| 51 | + |
| 52 | +USERSPACE_JSON = 'http://patches04.kernelcare.com/userspace.json' |
| 53 | +LOGLEVEL = os.environ.get('LOGLEVEL', 'ERROR').upper() |
| 54 | +logging.basicConfig(level=LOGLEVEL, format='%(message)s') |
| 55 | + |
| 56 | + |
| 57 | +class NotAnELFException(Exception): |
| 58 | + pass |
| 59 | + |
| 60 | + |
| 61 | +class BuildIDParsingException(Exception): |
| 62 | + pass |
| 63 | + |
| 64 | + |
| 65 | +def get_build_id(fileobj): |
| 66 | + |
| 67 | + try: |
| 68 | + header = fileobj.read(struct.calcsize(ELF64_HEADER)) |
| 69 | + hdr = struct.unpack(ELF64_HEADER, header) |
| 70 | + except struct.error as err: |
| 71 | + # Cant't read ELF header |
| 72 | + raise NotAnELFException("Can't read header: {0}".format(err)) |
| 73 | + |
| 74 | + (e_ident, e_type, e_machine, e_version, e_entry, e_phoff, |
| 75 | + e_shoff, e_flags, e_ehsize, e_phentsize, e_phnum, |
| 76 | + e_shentsize, e_shnum, e_shstrndx) = hdr |
| 77 | + |
| 78 | + # Not an ELF file |
| 79 | + if not e_ident.startswith(b'\x7fELF\x02\x01'): |
| 80 | + raise NotAnELFException("Wrong header") |
| 81 | + |
| 82 | + # No program headers |
| 83 | + if not e_phoff: |
| 84 | + raise BuildIDParsingException("Program headers not found.") |
| 85 | + |
| 86 | + logging.debug("e_phoff: %d, e_phnum: %d, e_phentsize: %d", e_phoff, e_phnum, e_phentsize) |
| 87 | + |
| 88 | + fileobj.seek(e_phoff) |
| 89 | + for idx in range(e_phnum): |
| 90 | + ph = fileobj.read(e_phentsize) |
| 91 | + (p_type, p_flags, p_offset, p_vaddr, p_paddr, |
| 92 | + p_filesz, p_memsz, p_align) = struct.unpack(ELF_PH_HEADER, ph) |
| 93 | + logging.debug("p_idx: %d, p_type: %d", idx, p_type) |
| 94 | + if p_type == PT_NOTE: |
| 95 | + logging.debug("p_offset: %d, p_filesz: %d", p_offset, p_filesz) |
| 96 | + p_end = p_offset + p_filesz |
| 97 | + fileobj.seek(p_offset) |
| 98 | + n_type = None |
| 99 | + while n_type != NT_GNU_BUILD_ID and fileobj.tell() <= p_end: |
| 100 | + nhdr = fileobj.read(struct.calcsize(ELF_NHDR)) |
| 101 | + n_namesz, n_descsz, n_type = struct.unpack(ELF_NHDR, nhdr) |
| 102 | + |
| 103 | + # 4-byte align |
| 104 | + if n_namesz % 4: |
| 105 | + n_namesz = ((n_namesz // 4) + 1) * 4 |
| 106 | + if n_descsz % 4: |
| 107 | + n_descsz = ((n_descsz // 4) + 1) * 4 |
| 108 | + |
| 109 | + logging.debug("n_type: %d, n_namesz: %d, n_descsz: %d)", n_type, n_namesz, n_descsz) |
| 110 | + fileobj.read(n_namesz) |
| 111 | + desc = struct.unpack("<{0}B".format(n_descsz), fileobj.read(n_descsz)) |
| 112 | + if n_type is not None: |
| 113 | + return ''.join('{:02x}'.format(x) for x in desc) |
| 114 | + # Nothing was found |
| 115 | + raise BuildIDParsingException("Program header PT_NOTE with NT_GNU_BUILD_ID was not found.") |
| 116 | + |
| 117 | + |
| 118 | +def iter_maps(pid): |
| 119 | + with open('/proc/{:d}/maps'.format(pid), 'r') as mapfd: |
| 120 | + for line in mapfd: |
| 121 | + data = (line.split() + [None, None])[:7] |
| 122 | + yield Map(*data) |
| 123 | + |
| 124 | + |
| 125 | +def get_vmas(pid, inode): |
| 126 | + result = [] |
| 127 | + for mmap in iter_maps(pid): |
| 128 | + if mmap.inode == inode: |
| 129 | + start, _, end = mmap.addr.partition('-') |
| 130 | + offset, start, end = map(lambda x: int(x, 16), [mmap.offset, start, end]) |
| 131 | + rng = Vma(offset, end - start, start, end) |
| 132 | + result.append(rng) |
| 133 | + return result |
| 134 | + |
| 135 | + |
| 136 | +def is_valid_file_mmap(mmap): |
| 137 | + return mmap.pathname and mmap.flag not in ['(deleted)'] \ |
| 138 | + and mmap.pathname not in IGNORED_PATHNAME \ |
| 139 | + and not mmap.pathname.startswith('anon_inode:') \ |
| 140 | + and not mmap.pathname.startswith('/dev/') |
| 141 | + |
| 142 | + |
| 143 | +def get_process_files(pid): |
| 144 | + result = set() |
| 145 | + for mmap in iter_maps(pid): |
| 146 | + if is_valid_file_mmap(mmap): |
| 147 | + result.add((mmap.pathname, mmap.inode)) |
| 148 | + return result |
| 149 | + |
| 150 | + |
| 151 | +class FileMMapped(object): |
| 152 | + |
| 153 | + def __init__(self, pid, inode): |
| 154 | + self.fileobj = open('/proc/{:d}/mem'.format(pid), 'rb') |
| 155 | + self.vmas = get_vmas(pid, inode) |
| 156 | + self.pos = 0 |
| 157 | + self.fileobj.seek(self._get_vma(0).start) |
| 158 | + |
| 159 | + def _get_vma(self, offset): |
| 160 | + for rng in self.vmas: |
| 161 | + if rng.offset <= offset < rng.offset + rng.size: |
| 162 | + return rng |
| 163 | + raise ValueError("Offset {0} is not in ranges {1}".format(offset, self.vmas)) |
| 164 | + |
| 165 | + def tell(self): |
| 166 | + return self.pos |
| 167 | + |
| 168 | + def __enter__(self): |
| 169 | + return self |
| 170 | + |
| 171 | + def __exit__(self, type, value, traceback): |
| 172 | + self.fileobj.close() |
| 173 | + |
| 174 | + def close(self): |
| 175 | + self.fileobj.close() |
| 176 | + |
| 177 | + def seek(self, offset, whence=0): |
| 178 | + rng = self._get_vma(offset) |
| 179 | + addr = rng.start + (offset - rng.offset) |
| 180 | + self.fileobj.seek(addr, whence) |
| 181 | + self.pos = offset |
| 182 | + |
| 183 | + def read(self, size): |
| 184 | + result = self.fileobj.read(size) |
| 185 | + self.pos += len(result) |
| 186 | + return result |
| 187 | + |
| 188 | + |
| 189 | +open_mmapped = FileMMapped |
| 190 | + |
| 191 | + |
| 192 | +def get_comm(pid): |
| 193 | + comm_filename = '/proc/{:d}/comm'.format(pid) |
| 194 | + with open(comm_filename, 'r') as fd: |
| 195 | + return fd.read().strip() |
| 196 | + |
| 197 | + |
| 198 | +def iter_pids(): |
| 199 | + for pid in os.listdir('/proc/'): |
| 200 | + try: |
| 201 | + yield int(pid) |
| 202 | + except ValueError: |
| 203 | + pass |
| 204 | + |
| 205 | + |
| 206 | +def iter_proc_map(): |
| 207 | + for pid in iter_pids(): |
| 208 | + for pathname, inode in get_process_files(pid): |
| 209 | + yield pid, inode, pathname |
| 210 | + |
| 211 | + |
| 212 | +def iter_proc_lib(): |
| 213 | + cache = {} |
| 214 | + for pid, inode, pathname in iter_proc_map(): |
| 215 | + if inode not in cache: |
| 216 | + logging.debug("path: %s", pathname) |
| 217 | + # If mapped file exists and has the same inode |
| 218 | + if os.path.isfile(pathname) and os.stat(pathname).st_ino == int(inode): |
| 219 | + fileobj = open(pathname, 'rb') |
| 220 | + # If file exists only as a mapped to the memory |
| 221 | + else: |
| 222 | + fileobj = open_mmapped(pid, inode) |
| 223 | + logging.warning("Library `%s` was gathered from memory.", pathname) |
| 224 | + |
| 225 | + try: |
| 226 | + cache[inode] = get_build_id(fileobj) |
| 227 | + except NotAnELFException as err: |
| 228 | + logging.debug("Cat't read buildID from {0}: {1}".format(pathname, err)) |
| 229 | + cache[inode] = None |
| 230 | + except Exception as err: |
| 231 | + logging.error("Cat't read buildID from {0}: {1}".format(pathname, err)) |
| 232 | + cache[inode] = None |
| 233 | + finally: |
| 234 | + fileobj.close() |
| 235 | + build_id = cache[inode] |
| 236 | + yield pid, os.path.basename(pathname), build_id |
| 237 | + |
| 238 | + |
| 239 | +def is_kcplus_handled(build_id): |
| 240 | + return True |
| 241 | + |
| 242 | + |
| 243 | +def main(): |
| 244 | + data = json.load(urlopen(USERSPACE_JSON)) |
| 245 | + failed = False |
| 246 | + for pid, libname, build_id in iter_proc_lib(): |
| 247 | + comm = get_comm(pid) |
| 248 | + logging.info("For %s[%s] `%s` was found with buid id = %s", |
| 249 | + comm, pid, libname, build_id) |
| 250 | + if libname in data and build_id and build_id not in data[libname]: |
| 251 | + failed = True |
| 252 | + logging.error( |
| 253 | + "[%s] Process %s[%d] linked to the `%s` that is not up to date.", |
| 254 | + "*" if is_kcplus_handled(build_id) else " ", |
| 255 | + comm, |
| 256 | + pid, |
| 257 | + libname |
| 258 | + ) |
| 259 | + |
| 260 | + if not failed: |
| 261 | + print("Everything is OK.") |
| 262 | + else: |
| 263 | + print("\nYou may want to update libraries above and restart corresponding processes.\n\n" |
| 264 | + "KernelCare+ allows to resolve such issues with no process downtime. " |
| 265 | + "To find out more, please, visit https://lp.kernelcare.com/kernelcare-early-access?") |
| 266 | + |
| 267 | + |
| 268 | +if __name__ == '__main__': |
| 269 | + exit(main()) |
0 commit comments