From 136757351f7de543366c21e788c4240548f384b4 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 15 Jul 2025 12:00:36 +0200 Subject: [PATCH 01/10] Initial --- uchecker.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/uchecker.py b/uchecker.py index b6a2ff0..0cddeed 100755 --- a/uchecker.py +++ b/uchecker.py @@ -122,7 +122,7 @@ def _linux_distribution(*args, **kwargs): elif k in ('distributor id', ): lsb_release['distributor_id'] = v elif k in ('description', ): - lsb_release['desciption_version_id'] = 'test' + lsb_release['description_version_id'] = v.split(' ')[-1] if v else '' for dist_file in sorted(check_output(['ls', '/etc']).split('\n')): if (dist_file.endswith('-release') or dist_file.endswith('_version')): @@ -246,6 +246,11 @@ class BuildIDParsingException(Exception): pass +ELF_MAGIC_BYTES = b'\x7fELF\x02\x01' +PROC_TIMEOUT = 30 +MAX_NOTE_SIZE = 4096 +BYTE_ALIGNMENT = 4 + def get_build_id(fileobj): try: @@ -260,7 +265,7 @@ def get_build_id(fileobj): e_shentsize, e_shnum, e_shstrndx) = hdr # Not an ELF file - if not e_ident.startswith(b'\x7fELF\x02\x01'): + if not e_ident.startswith(ELF_MAGIC_BYTES): raise NotAnELFException("Wrong header") # No program headers @@ -285,10 +290,10 @@ def get_build_id(fileobj): n_namesz, n_descsz, n_type = struct.unpack(ELF_NHDR, nhdr) # 4-byte align - if n_namesz % 4: - n_namesz = ((n_namesz // 4) + 1) * 4 - if n_descsz % 4: - n_descsz = ((n_descsz // 4) + 1) * 4 + if n_namesz % BYTE_ALIGNMENT: + n_namesz = ((n_namesz // BYTE_ALIGNMENT) + 1) * BYTE_ALIGNMENT + if n_descsz % BYTE_ALIGNMENT: + n_descsz = ((n_descsz // BYTE_ALIGNMENT) + 1) * BYTE_ALIGNMENT logging.debug("n_type: %d, n_namesz: %d, n_descsz: %d)", n_type, n_namesz, n_descsz) @@ -419,10 +424,10 @@ def iter_proc_lib(): with get_fileobj(pid, inode, pathname) as fileobj: cache[inode] = get_build_id(fileobj) except (NotAnELFException, BuildIDParsingException, IOError) as err: - logging.info("Can't read buildID from {0}: {1}".format(pathname, repr(err))) + logging.info("Can't read buildID from %s: %s", pathname, repr(err)) cache[inode] = None except Exception as err: - logging.error("Can't read buildID from {0}: {1}".format(pathname, repr(err))) + logging.error("Can't read buildID from %s: %s", pathname, repr(err)) cache[inode] = None build_id = cache[inode] yield pid, os.path.basename(pathname), build_id From c7d99a0898e0c8840bcfecf0477f67900aa5cb21 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 15 Jul 2025 12:08:12 +0200 Subject: [PATCH 02/10] Intrioduced check_output with timeout --- tests/test_kernelcare.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_kernelcare.py b/tests/test_kernelcare.py index 851cf5c..a719ea5 100644 --- a/tests/test_kernelcare.py +++ b/tests/test_kernelcare.py @@ -26,18 +26,18 @@ def tests_get_patched_data(mock_system, tmpdir): libcare_ctl.ensure(file=0) assert uchecker.get_patched_data() == set() libcare_ctl.ensure(file=1) - with mock.patch('uchecker.check_output', return_value='{}'): + with mock.patch('uchecker.check_output_with_timeout', return_value='{}'): assert uchecker.get_patched_data() == set() - with mock.patch('uchecker.check_output', return_value='{wrong-format}'): + with mock.patch('uchecker.check_output_with_timeout', return_value='{wrong-format}'): assert uchecker.get_patched_data() == set() - with mock.patch('uchecker.check_output', return_value=LIBCARE_INFO_OUT): + with mock.patch('uchecker.check_output_with_timeout', return_value=LIBCARE_INFO_OUT): assert uchecker.get_patched_data() == { (20025, '4cf1939f660008cfa869d8364651f31aacd2c1c4'), (20025, 'f9fafde281e0e0e2af45911ad0fa115b64c2cea8'), (20026, '4cf1939f660008cfa869d8364651f31aacd2c1c4'), (20026, 'f9fafde281e0e0e2af45911ad0fa115b64c2ce10') } - with mock.patch('uchecker.check_output', side_effect=IOError('test')): + with mock.patch('uchecker.check_output_with_timeout', side_effect=IOError('test')): assert uchecker.get_patched_data() == set() with mock.patch('uchecker.LIBCARE_CLIENT', '/file/that/not/exists/'): assert uchecker.get_patched_data() == set() @@ -114,6 +114,10 @@ def test_normalize_bytes(): assert uchecker.normalize(b"hello") == "hello" +def test_normalize_unicode(): + assert uchecker.normalize(u"hello") == "hello" + + def test_normalize_non_string_bytes(): with pytest.raises(AttributeError): uchecker.normalize(123) From 0833972884b16d79a0a29323e090da00584cdebe Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 15 Jul 2025 12:09:26 +0200 Subject: [PATCH 03/10] Forgotten change --- uchecker.py | 51 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/uchecker.py b/uchecker.py index 0cddeed..0a0bca2 100755 --- a/uchecker.py +++ b/uchecker.py @@ -84,6 +84,45 @@ def check_output(*args, **kwargs): return normalize(out) +def check_output_with_timeout(*args, **kwargs): + """Enhanced check_output with timeout support for Python 2/3.""" + timeout = kwargs.pop('timeout', 30) + + try: + import signal + + def timeout_handler(signum, frame): + raise OSError("Command timed out") + + # Set up timeout (Unix only) + if hasattr(signal, 'SIGALRM'): + old_handler = signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(timeout) + + try: + p = subprocess.Popen(stdout=subprocess.PIPE, stderr=subprocess.PIPE, + *args, **kwargs) + out, err = p.communicate() + + if hasattr(signal, 'SIGALRM'): + signal.alarm(0) + signal.signal(signal.SIGALRM, old_handler) + + if err or p.returncode != 0: + raise OSError("{0} ({1})".format(normalize(err), p.returncode)) + return normalize(out) + + except OSError: + if hasattr(signal, 'SIGALRM'): + signal.alarm(0) + signal.signal(signal.SIGALRM, old_handler) + raise + + except Exception as e: + logging.debug('Subprocess error: %s', str(e)) + return '' + + def _linux_distribution(*args, **kwargs): """ An alternative implementation became necessary because Python @@ -92,11 +131,11 @@ def _linux_distribution(*args, **kwargs): Additional parameters like `full_distribution_name` are not implemented. """ - uname_raw = check_output(['uname', '-rs']) + uname_raw = check_output_with_timeout(['uname', '-rs']) uname_name, _, uname_version = uname_raw.partition(' ') uname = {'id': uname_name.lower(), 'name': uname_name, 'release': uname_version} - os_release_raw = check_output(['cat', '/etc/os-release']) + os_release_raw = check_output_with_timeout(['cat', '/etc/os-release']) os_release = {} for line in os_release_raw.split('\n'): k, _, v = line.partition('=') @@ -110,7 +149,7 @@ def _linux_distribution(*args, **kwargs): elif k in ('pretty_name', ): os_release['pretty_name_version_id'] = v.split(' ')[-1] - lsb_release_raw = check_output(['lsb_release', '-a']) + lsb_release_raw = check_output_with_timeout(['lsb_release', '-a']) lsb_release = {} for line in lsb_release_raw.split('\n'): k, _, v = line.partition(':') @@ -124,9 +163,9 @@ def _linux_distribution(*args, **kwargs): elif k in ('description', ): lsb_release['description_version_id'] = v.split(' ')[-1] if v else '' - for dist_file in sorted(check_output(['ls', '/etc']).split('\n')): + for dist_file in sorted(check_output_with_timeout(['ls', '/etc']).split('\n')): if (dist_file.endswith('-release') or dist_file.endswith('_version')): - distro_release_raw = check_output(['cat', os.path.join('/etc', dist_file)]) + distro_release_raw = check_output_with_timeout(['cat', os.path.join('/etc', dist_file)]) if distro_release_raw: break @@ -194,7 +233,7 @@ def get_patched_data(): return result try: - std_out = check_output([LIBCARE_CLIENT, 'info', '-j']) + std_out = check_output_with_timeout([LIBCARE_CLIENT, 'info', '-j']) for line in std_out.splitlines(): try: item = json.loads(line) From 79753fdc9f4edbb9ff76cb1a8cff7ec197ae96aa Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 15 Jul 2025 12:17:01 +0200 Subject: [PATCH 04/10] Let's not try to read too long notes --- uchecker.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/uchecker.py b/uchecker.py index 0a0bca2..9fdde41 100755 --- a/uchecker.py +++ b/uchecker.py @@ -44,6 +44,11 @@ NT_GO_BUILD_ID = 4 IGNORED_PATHNAME = ["[heap]", "[stack]", "[vdso]", "[vsyscall]", "[vvar]"] +ELF_MAGIC_BYTES = b'\x7fELF\x02\x01' +PROC_TIMEOUT = 30 +MAX_NOTE_SIZE = 4096 +BYTE_ALIGNMENT = 4 + Vma = namedtuple('Vma', 'offset size start end') Map = namedtuple('Map', 'addr perm offset dev inode pathname flag') @@ -68,22 +73,6 @@ def normalize(data, encoding='utf-8'): return data.encode(encoding) -def check_output(*args, **kwargs): - """ Backported implementation for check_output. - """ - out = '' - try: - p = subprocess.Popen(stdout=subprocess.PIPE, stderr=subprocess.PIPE, - *args, **kwargs) - out, err = p.communicate() - if err or p.returncode != 0: - raise OSError("{0} ({1})".format(err, p.returncode)) - except OSError as e: - logging.debug('Subprocess `%s %s` error: %s', - args, kwargs, e) - return normalize(out) - - def check_output_with_timeout(*args, **kwargs): """Enhanced check_output with timeout support for Python 2/3.""" timeout = kwargs.pop('timeout', 30) @@ -94,7 +83,6 @@ def check_output_with_timeout(*args, **kwargs): def timeout_handler(signum, frame): raise OSError("Command timed out") - # Set up timeout (Unix only) if hasattr(signal, 'SIGALRM'): old_handler = signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(timeout) @@ -285,10 +273,6 @@ class BuildIDParsingException(Exception): pass -ELF_MAGIC_BYTES = b'\x7fELF\x02\x01' -PROC_TIMEOUT = 30 -MAX_NOTE_SIZE = 4096 -BYTE_ALIGNMENT = 4 def get_build_id(fileobj): @@ -336,6 +320,8 @@ def get_build_id(fileobj): logging.debug("n_type: %d, n_namesz: %d, n_descsz: %d)", n_type, n_namesz, n_descsz) + if n_namesz > MAX_NOTE_SIZE or n_descsz > MAX_NOTE_SIZE: + raise BuildIDParsingException("Note section too large") fileobj.read(n_namesz) desc = struct.unpack("<{0}B".format(n_descsz), fileobj.read(n_descsz)) if n_type is not None: From 6409740cd4f29657e6fe466ce259973df220c1f7 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 15 Jul 2025 12:20:43 +0200 Subject: [PATCH 05/10] Fixed flake8 complains --- uchecker.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/uchecker.py b/uchecker.py index 9fdde41..ef02bb1 100755 --- a/uchecker.py +++ b/uchecker.py @@ -76,36 +76,36 @@ def normalize(data, encoding='utf-8'): def check_output_with_timeout(*args, **kwargs): """Enhanced check_output with timeout support for Python 2/3.""" timeout = kwargs.pop('timeout', 30) - + try: import signal - + def timeout_handler(signum, frame): raise OSError("Command timed out") - + if hasattr(signal, 'SIGALRM'): old_handler = signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(timeout) - + try: p = subprocess.Popen(stdout=subprocess.PIPE, stderr=subprocess.PIPE, - *args, **kwargs) + *args, **kwargs) out, err = p.communicate() - + if hasattr(signal, 'SIGALRM'): signal.alarm(0) signal.signal(signal.SIGALRM, old_handler) - + if err or p.returncode != 0: raise OSError("{0} ({1})".format(normalize(err), p.returncode)) return normalize(out) - + except OSError: if hasattr(signal, 'SIGALRM'): signal.alarm(0) signal.signal(signal.SIGALRM, old_handler) raise - + except Exception as e: logging.debug('Subprocess error: %s', str(e)) return '' @@ -273,7 +273,6 @@ class BuildIDParsingException(Exception): pass - def get_build_id(fileobj): try: From ac7b6ce9127c601e263f3aa0ca44f6dfb9276ee1 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 15 Jul 2025 12:22:44 +0200 Subject: [PATCH 06/10] Fixed unused constant --- uchecker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uchecker.py b/uchecker.py index ef02bb1..ac27a28 100755 --- a/uchecker.py +++ b/uchecker.py @@ -75,7 +75,7 @@ def normalize(data, encoding='utf-8'): def check_output_with_timeout(*args, **kwargs): """Enhanced check_output with timeout support for Python 2/3.""" - timeout = kwargs.pop('timeout', 30) + timeout = kwargs.pop('timeout', PROC_TIMEOUT) try: import signal From 67d1dfce9802fb8b6b0f4c145f8ccbdb0cce39c6 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 15 Jul 2025 12:35:45 +0200 Subject: [PATCH 07/10] Tidy up imports --- uchecker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uchecker.py b/uchecker.py index ac27a28..265b34c 100755 --- a/uchecker.py +++ b/uchecker.py @@ -31,6 +31,7 @@ import re import json import struct +import signal import logging import subprocess @@ -78,7 +79,6 @@ def check_output_with_timeout(*args, **kwargs): timeout = kwargs.pop('timeout', PROC_TIMEOUT) try: - import signal def timeout_handler(signum, frame): raise OSError("Command timed out") From e7dfe884313fefe8d6606d77852c5e7037cc0e20 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 15 Jul 2025 12:40:06 +0200 Subject: [PATCH 08/10] Do not swallow too broad exceptions --- uchecker.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/uchecker.py b/uchecker.py index 265b34c..604e3bb 100755 --- a/uchecker.py +++ b/uchecker.py @@ -106,9 +106,12 @@ def timeout_handler(signum, frame): signal.signal(signal.SIGALRM, old_handler) raise - except Exception as e: - logging.debug('Subprocess error: %s', str(e)) + except (subprocess.SubprocessError, OSError) as e: + logging.error('Subprocess error: %s', str(e)) return '' + except Exception as e: + logging.critical('Unexpected error: %s', str(e)) + raise def _linux_distribution(*args, **kwargs): From 2b50de35a52f621f6a5ec0abb3f4f2022da09987 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 15 Jul 2025 12:42:20 +0200 Subject: [PATCH 09/10] More usefull exceptions --- uchecker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uchecker.py b/uchecker.py index 604e3bb..7302122 100755 --- a/uchecker.py +++ b/uchecker.py @@ -107,10 +107,10 @@ def timeout_handler(signum, frame): raise except (subprocess.SubprocessError, OSError) as e: - logging.error('Subprocess error: %s', str(e)) + logging.error('Subprocess error running %s: %s', args, str(e)) return '' except Exception as e: - logging.critical('Unexpected error: %s', str(e)) + logging.critical('Unexpected error running %s: %s', args, str(e)) raise From c87a2ba6ae9340b8eb31eb508cada65ea65ecfe7 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 15 Jul 2025 12:48:58 +0200 Subject: [PATCH 10/10] py27 compatible subprocess error --- uchecker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/uchecker.py b/uchecker.py index 7302122..a75743c 100755 --- a/uchecker.py +++ b/uchecker.py @@ -78,6 +78,9 @@ def check_output_with_timeout(*args, **kwargs): """Enhanced check_output with timeout support for Python 2/3.""" timeout = kwargs.pop('timeout', PROC_TIMEOUT) + # SubprocessError is not available in Python 2.7 + SubprocessError = (getattr(subprocess, 'SubprocessError', OSError), OSError) + try: def timeout_handler(signum, frame): @@ -106,7 +109,7 @@ def timeout_handler(signum, frame): signal.signal(signal.SIGALRM, old_handler) raise - except (subprocess.SubprocessError, OSError) as e: + except SubprocessError as e: logging.error('Subprocess error running %s: %s', args, str(e)) return '' except Exception as e: