Skip to content

Commit 309a2ba

Browse files
committed
Inital
0 parents  commit 309a2ba

1 file changed

Lines changed: 269 additions & 0 deletions

File tree

uchecker.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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

Comments
 (0)