Skip to content

Commit 9d4e54a

Browse files
authored
Some refactoting and compatibility fixes
Also, introduces `check_output_with_timeout` with a default timeout and enhanced exception handling
1 parent 34a98e0 commit 9d4e54a

File tree

2 files changed

+70
-31
lines changed

2 files changed

+70
-31
lines changed

tests/test_kernelcare.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,18 @@ def tests_get_patched_data(mock_system, tmpdir):
3131
libcare_ctl.ensure(file=0)
3232
assert uchecker.get_patched_data() == set()
3333
libcare_ctl.ensure(file=1)
34-
with mock.patch('uchecker.check_output', return_value='{}'):
34+
with mock.patch('uchecker.check_output_with_timeout', return_value='{}'):
3535
assert uchecker.get_patched_data() == set()
36-
with mock.patch('uchecker.check_output', return_value='{wrong-format}'):
36+
with mock.patch('uchecker.check_output_with_timeout', return_value='{wrong-format}'):
3737
assert uchecker.get_patched_data() == set()
38-
with mock.patch('uchecker.check_output', return_value=LIBCARE_INFO_OUT):
38+
with mock.patch('uchecker.check_output_with_timeout', return_value=LIBCARE_INFO_OUT):
3939
assert uchecker.get_patched_data() == {
4040
(20025, '4cf1939f660008cfa869d8364651f31aacd2c1c4'),
4141
(20025, 'f9fafde281e0e0e2af45911ad0fa115b64c2cea8'),
4242
(20026, '4cf1939f660008cfa869d8364651f31aacd2c1c4'),
4343
(20026, 'f9fafde281e0e0e2af45911ad0fa115b64c2ce10')
4444
}
45-
with mock.patch('uchecker.check_output', side_effect=IOError('test')):
45+
with mock.patch('uchecker.check_output_with_timeout', side_effect=IOError('test')):
4646
assert uchecker.get_patched_data() == set()
4747
with mock.patch('uchecker.LIBCARE_CLIENT', '/file/that/not/exists/'):
4848
assert uchecker.get_patched_data() == set()
@@ -119,6 +119,10 @@ def test_normalize_bytes():
119119
assert uchecker.normalize(b"hello") == "hello"
120120

121121

122+
def test_normalize_unicode():
123+
assert uchecker.normalize(u"hello") == "hello"
124+
125+
122126
def test_normalize_non_string_bytes():
123127
with pytest.raises(AttributeError):
124128
uchecker.normalize(123)

uchecker.py

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import re
3232
import json
3333
import struct
34+
import signal
3435
import logging
3536
import subprocess
3637

@@ -44,6 +45,11 @@
4445
NT_GO_BUILD_ID = 4
4546
IGNORED_PATHNAME = ["[heap]", "[stack]", "[vdso]", "[vsyscall]", "[vvar]"]
4647

48+
ELF_MAGIC_BYTES = b'\x7fELF\x02\x01'
49+
PROC_TIMEOUT = 30
50+
MAX_NOTE_SIZE = 4096
51+
BYTE_ALIGNMENT = 4
52+
4753
Vma = namedtuple('Vma', 'offset size start end')
4854
Map = namedtuple('Map', 'addr perm offset dev inode pathname flag')
4955

@@ -67,20 +73,47 @@ def normalize(data, encoding='utf-8'):
6773
return data.encode(encoding)
6874

6975

70-
def check_output(*args, **kwargs):
71-
""" Backported implementation for check_output.
72-
"""
73-
out = ''
76+
def check_output_with_timeout(*args, **kwargs):
77+
"""Enhanced check_output with timeout support for Python 2/3."""
78+
timeout = kwargs.pop('timeout', PROC_TIMEOUT)
79+
80+
# SubprocessError is not available in Python 2.7
81+
SubprocessError = (getattr(subprocess, 'SubprocessError', OSError), OSError)
82+
7483
try:
75-
p = subprocess.Popen(stdout=subprocess.PIPE, stderr=subprocess.PIPE,
76-
*args, **kwargs)
77-
out, err = p.communicate()
78-
if err or p.returncode != 0:
79-
raise OSError("{0} ({1})".format(err, p.returncode))
80-
except OSError as e:
81-
logging.debug('Subprocess `%s %s` error: %s',
82-
args, kwargs, e)
83-
return normalize(out)
84+
85+
def timeout_handler(signum, frame):
86+
raise OSError("Command timed out")
87+
88+
if hasattr(signal, 'SIGALRM'):
89+
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
90+
signal.alarm(timeout)
91+
92+
try:
93+
p = subprocess.Popen(stdout=subprocess.PIPE, stderr=subprocess.PIPE,
94+
*args, **kwargs)
95+
out, err = p.communicate()
96+
97+
if hasattr(signal, 'SIGALRM'):
98+
signal.alarm(0)
99+
signal.signal(signal.SIGALRM, old_handler)
100+
101+
if err or p.returncode != 0:
102+
raise OSError("{0} ({1})".format(normalize(err), p.returncode))
103+
return normalize(out)
104+
105+
except OSError:
106+
if hasattr(signal, 'SIGALRM'):
107+
signal.alarm(0)
108+
signal.signal(signal.SIGALRM, old_handler)
109+
raise
110+
111+
except SubprocessError as e:
112+
logging.error('Subprocess error running %s: %s', args, str(e))
113+
return ''
114+
except Exception as e:
115+
logging.critical('Unexpected error running %s: %s', args, str(e))
116+
raise
84117

85118

86119
def _linux_distribution(*args, **kwargs):
@@ -91,11 +124,11 @@ def _linux_distribution(*args, **kwargs):
91124
Additional parameters like `full_distribution_name` are not implemented.
92125
"""
93126

94-
uname_raw = check_output(['uname', '-rs'])
127+
uname_raw = check_output_with_timeout(['uname', '-rs'])
95128
uname_name, _, uname_version = uname_raw.partition(' ')
96129
uname = {'id': uname_name.lower(), 'name': uname_name, 'release': uname_version}
97130

98-
os_release_raw = check_output(['cat', '/etc/os-release'])
131+
os_release_raw = check_output_with_timeout(['cat', '/etc/os-release'])
99132
os_release = {}
100133
for line in os_release_raw.split('\n'):
101134
k, _, v = line.partition('=')
@@ -109,7 +142,7 @@ def _linux_distribution(*args, **kwargs):
109142
elif k in ('pretty_name', ):
110143
os_release['pretty_name_version_id'] = v.split(' ')[-1]
111144

112-
lsb_release_raw = check_output(['lsb_release', '-a'])
145+
lsb_release_raw = check_output_with_timeout(['lsb_release', '-a'])
113146
lsb_release = {}
114147
for line in lsb_release_raw.split('\n'):
115148
k, _, v = line.partition(':')
@@ -121,11 +154,11 @@ def _linux_distribution(*args, **kwargs):
121154
elif k in ('distributor id', ):
122155
lsb_release['distributor_id'] = v
123156
elif k in ('description', ):
124-
lsb_release['desciption_version_id'] = 'test'
157+
lsb_release['description_version_id'] = v.split(' ')[-1] if v else ''
125158

126-
for dist_file in sorted(check_output(['ls', '/etc']).split('\n')):
159+
for dist_file in sorted(check_output_with_timeout(['ls', '/etc']).split('\n')):
127160
if (dist_file.endswith('-release') or dist_file.endswith('_version')):
128-
distro_release_raw = check_output(['cat', os.path.join('/etc', dist_file)])
161+
distro_release_raw = check_output_with_timeout(['cat', os.path.join('/etc', dist_file)])
129162
if distro_release_raw:
130163
break
131164

@@ -193,7 +226,7 @@ def get_patched_data():
193226
return result
194227

195228
try:
196-
std_out = check_output([LIBCARE_CLIENT, 'info', '-j'])
229+
std_out = check_output_with_timeout([LIBCARE_CLIENT, 'info', '-j'])
197230
for line in std_out.splitlines():
198231
try:
199232
item = json.loads(line)
@@ -259,7 +292,7 @@ def get_build_id(fileobj):
259292
e_shentsize, e_shnum, e_shstrndx) = hdr
260293

261294
# Not an ELF file
262-
if not e_ident.startswith(b'\x7fELF\x02\x01'):
295+
if not e_ident.startswith(ELF_MAGIC_BYTES):
263296
raise NotAnELFException("Wrong header")
264297

265298
# No program headers
@@ -284,13 +317,15 @@ def get_build_id(fileobj):
284317
n_namesz, n_descsz, n_type = struct.unpack(ELF_NHDR, nhdr)
285318

286319
# 4-byte align
287-
if n_namesz % 4:
288-
n_namesz = ((n_namesz // 4) + 1) * 4
289-
if n_descsz % 4:
290-
n_descsz = ((n_descsz // 4) + 1) * 4
320+
if n_namesz % BYTE_ALIGNMENT:
321+
n_namesz = ((n_namesz // BYTE_ALIGNMENT) + 1) * BYTE_ALIGNMENT
322+
if n_descsz % BYTE_ALIGNMENT:
323+
n_descsz = ((n_descsz // BYTE_ALIGNMENT) + 1) * BYTE_ALIGNMENT
291324

292325
logging.debug("n_type: %d, n_namesz: %d, n_descsz: %d)",
293326
n_type, n_namesz, n_descsz)
327+
if n_namesz > MAX_NOTE_SIZE or n_descsz > MAX_NOTE_SIZE:
328+
raise BuildIDParsingException("Note section too large")
294329
fileobj.read(n_namesz)
295330
desc = struct.unpack("<{0}B".format(n_descsz), fileobj.read(n_descsz))
296331
if n_type is not None:
@@ -418,10 +453,10 @@ def iter_proc_lib():
418453
with get_fileobj(pid, inode, pathname) as fileobj:
419454
cache[inode] = get_build_id(fileobj)
420455
except (NotAnELFException, BuildIDParsingException, IOError) as err:
421-
logging.info("Can't read buildID from {0}: {1}".format(pathname, repr(err)))
456+
logging.info("Can't read buildID from %s: %s", pathname, repr(err))
422457
cache[inode] = None
423458
except Exception as err:
424-
logging.error("Can't read buildID from {0}: {1}".format(pathname, repr(err)))
459+
logging.error("Can't read buildID from %s: %s", pathname, repr(err))
425460
cache[inode] = None
426461
build_id = cache[inode]
427462
yield pid, os.path.basename(pathname), build_id

0 commit comments

Comments
 (0)