Skip to content

Commit

Permalink
add detection for custom libraries registered by ld.so.conf
Browse files Browse the repository at this point in the history
The in-place upgrade process does not support custom libraries
and also does not handle customized configuration of dynamic linked.
In such a case it can happen (and it happens) that the upgrade could
break in critical phases when linked libraries dissapear or are not
compatible with the new system.

We cannot decide whether or not such a custom configuration affects
the upgrade negatively, so let's detect any customisations
or unexpected configurations related to dynamic linker and in such
a case generate a high severity report, informing user about the
possible impact on the upgrade process.

Currently it's detectect:
  * modified default LD configuration: /etc/ld.so.conf
  * drop int configuration files under /etc/ld.so.conf.d/ that are
    not owned by any RHEL RPMs
  * envars: LD_LIBRARY_PATH, LD_PRELOAD

Jira ref.: OAMG-4460 / RHEL-11958
BZ ref.: BZ 1927700
  • Loading branch information
PeterMocary authored and pirat89 committed Nov 16, 2023
1 parent 5a3bded commit 043c6cb
Show file tree
Hide file tree
Showing 7 changed files with 528 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from leapp.actors import Actor
from leapp.libraries.actor.checkdynamiclinkerconfiguration import check_dynamic_linker_configuration
from leapp.models import DynamicLinkerConfiguration, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckDynamicLinkerConfiguration(Actor):
"""
Check for customization of dynamic linker configuration.
The in-place upgrade could potentionally be impacted in a negative way due
to the customization of dynamic linker configuration by user. This actor creates high
severity report upon detecting such customization.
"""

name = 'check_dynamic_linker_configuration'
consumes = (DynamicLinkerConfiguration,)
produces = (Report,)
tags = (ChecksPhaseTag, IPUWorkflowTag)

def process(self):
check_dynamic_linker_configuration()
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from leapp import reporting
from leapp.libraries.stdlib import api
from leapp.models import DynamicLinkerConfiguration

LD_SO_CONF_DIR = '/etc/ld.so.conf.d'
LD_SO_CONF_MAIN = '/etc/ld.so.conf'
LD_LIBRARY_PATH_VAR = 'LD_LIBRARY_PATH'
LD_PRELOAD_VAR = 'LD_PRELOAD'
FMT_LIST_SEPARATOR_1 = '\n- '
FMT_LIST_SEPARATOR_2 = '\n - '


def _report_custom_dynamic_linker_configuration(summary):
reporting.create_report([
reporting.Title(
'Detected customized configuration for dynamic linker.'
),
reporting.Summary(summary),
reporting.Remediation(hint=('Remove or revert the custom dynamic linker configurations and apply the changes '
'using the ldconfig command. In case of possible active software collections we '
'suggest disabling them persistently.')),
reporting.RelatedResource('file', '/etc/ld.so.conf'),
reporting.RelatedResource('directory', '/etc/ld.so.conf.d'),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups([reporting.Groups.OS_FACTS]),
])


def check_dynamic_linker_configuration():
configuration = next(api.consume(DynamicLinkerConfiguration), None)
if not configuration:
return

custom_configurations = ''
if configuration.main_config.modified:
custom_configurations += (
'{}The {} file has unexpected contents:{}{}'
.format(FMT_LIST_SEPARATOR_1, LD_SO_CONF_MAIN,
FMT_LIST_SEPARATOR_2, FMT_LIST_SEPARATOR_2.join(configuration.main_config.modified_lines))
)

custom_configs = []
for config in configuration.included_configs:
if config.modified:
custom_configs.append(config.path)

if custom_configs:
custom_configurations += (
'{}The following drop in config files were marked as custom:{}{}'
.format(FMT_LIST_SEPARATOR_1, FMT_LIST_SEPARATOR_2, FMT_LIST_SEPARATOR_2.join(custom_configs))
)

if configuration.used_variables:
custom_configurations += (
'{}The following variables contain unexpected dynamic linker configuration:{}{}'
.format(FMT_LIST_SEPARATOR_1, FMT_LIST_SEPARATOR_2,
FMT_LIST_SEPARATOR_2.join(configuration.used_variables))
)

if custom_configurations:
summary = (
'Custom configurations to the dynamic linker could potentially impact '
'the upgrade in a negative way. The custom configuration includes '
'modifications to {main_conf}, custom or modified drop in config '
'files in the {conf_dir} directory and additional entries in the '
'{ldlib_envar} or {ldpre_envar} variables. These modifications '
'configure the dynamic linker to use different libraries that might '
'not be provided by Red Hat products or might not be present during '
'the whole upgrade process. The following custom configurations '
'were detected by leapp:{cust_configs}'
.format(
main_conf=LD_SO_CONF_MAIN,
conf_dir=LD_SO_CONF_DIR,
ldlib_envar=LD_LIBRARY_PATH_VAR,
ldpre_envar=LD_PRELOAD_VAR,
cust_configs=custom_configurations
)
)
_report_custom_dynamic_linker_configuration(summary)
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import pytest

from leapp import reporting
from leapp.libraries.actor.checkdynamiclinkerconfiguration import (
check_dynamic_linker_configuration,
LD_LIBRARY_PATH_VAR,
LD_PRELOAD_VAR
)
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models import DynamicLinkerConfiguration, LDConfigFile, MainLDConfigFile

INCLUDED_CONFIG_PATHS = ['/etc/ld.so.conf.d/dyninst-x86_64.conf',
'/etc/ld.so.conf.d/mariadb-x86_64.conf',
'/custom/path/custom1.conf']


@pytest.mark.parametrize(('included_configs_modifications', 'used_variables', 'modified_lines'),
[
([False, False, False], [], []),
([True, True, True], [], []),
([False, False, False], [LD_LIBRARY_PATH_VAR], []),
([False, False, False], [], ['modified line 1', 'midified line 2']),
([True, False, True], [LD_LIBRARY_PATH_VAR, LD_PRELOAD_VAR], ['modified line']),
])
def test_check_ld_so_configuration(monkeypatch, included_configs_modifications, used_variables, modified_lines):
assert len(INCLUDED_CONFIG_PATHS) == len(included_configs_modifications)

main_config = MainLDConfigFile(path="/etc/ld.so.conf", modified=any(modified_lines), modified_lines=modified_lines)
included_configs = []
for path, modified in zip(INCLUDED_CONFIG_PATHS, included_configs_modifications):
included_configs.append(LDConfigFile(path=path, modified=modified))

configuration = DynamicLinkerConfiguration(main_config=main_config,
included_configs=included_configs,
used_variables=used_variables)

monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[configuration]))
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())

check_dynamic_linker_configuration()

report_expected = any(included_configs_modifications) or modified_lines or used_variables
if not report_expected:
assert reporting.create_report.called == 0
return

assert reporting.create_report.called == 1
assert 'configuration for dynamic linker' in reporting.create_report.reports[0]['title']
summary = reporting.create_report.reports[0]['summary']

if any(included_configs_modifications):
assert 'The following drop in config files were marked as custom:' in summary
for config, modified in zip(INCLUDED_CONFIG_PATHS, included_configs_modifications):
assert modified == (config in summary)

if modified_lines:
assert 'The /etc/ld.so.conf file has unexpected contents' in summary
for line in modified_lines:
assert line in summary

if used_variables:
assert 'The following variables contain unexpected dynamic linker configuration:' in summary
for var in used_variables:
assert '- {}'.format(var) in summary
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from leapp.actors import Actor
from leapp.libraries.actor.scandynamiclinkerconfiguration import scan_dynamic_linker_configuration
from leapp.models import DynamicLinkerConfiguration, InstalledRedHatSignedRPM
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanDynamicLinkerConfiguration(Actor):
"""
Scan the dynamic linker configuration and find modifications.
The dynamic linker configuration files can be used to replace standard libraries
with different custom libraries. The in-place upgrade does not support customization
of this configuration by user. This actor produces information about detected
modifications.
"""

name = 'scan_dynamic_linker_configuration'
consumes = (InstalledRedHatSignedRPM,)
produces = (DynamicLinkerConfiguration,)
tags = (FactsPhaseTag, IPUWorkflowTag)

def process(self):
scan_dynamic_linker_configuration()
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import glob
import os

from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import DynamicLinkerConfiguration, InstalledRedHatSignedRPM, LDConfigFile, MainLDConfigFile

LD_SO_CONF_DIR = '/etc/ld.so.conf.d'
LD_SO_CONF_MAIN = '/etc/ld.so.conf'
LD_SO_CONF_DEFAULT_INCLUDE = 'ld.so.conf.d/*.conf'
LD_SO_CONF_COMMENT_PREFIX = '#'
LD_LIBRARY_PATH_VAR = 'LD_LIBRARY_PATH'
LD_PRELOAD_VAR = 'LD_PRELOAD'


def _read_file(file_path):
with open(file_path, 'r') as fd:
return fd.readlines()


def _is_modified(config_path):
""" Decide if the configuration file was modified based on the package it belongs to. """
result = run(['rpm', '-Vf', config_path], checked=False)
if not result['exit_code']:
return False
modification_flags = result['stdout'].split(' ', 1)[0]
# The file is considered modified only when the checksum does not match
return '5' in modification_flags


def _is_included_config_custom(config_path):
if not os.path.isfile(config_path):
return False

# Check if the config file has any lines that have an effect on dynamic linker configuration
has_effective_line = False
for line in _read_file(config_path):
line = line.strip()
if line and not line.startswith(LD_SO_CONF_COMMENT_PREFIX):
has_effective_line = True
break

if not has_effective_line:
return False

is_custom = False
try:
package_name = run(['rpm', '-qf', '--queryformat', '%{NAME}', config_path])['stdout']
is_custom = not has_package(InstalledRedHatSignedRPM, package_name) or _is_modified(config_path)
except CalledProcessError:
is_custom = True

return is_custom


def _parse_main_config():
"""
Extracts included configs from the main dynamic linker configuration file (/etc/ld.so.conf)
along with lines that are likely custom. The lines considered custom are simply those that are
not includes.
:returns: tuple containing all the included files and lines considered custom
:rtype: tuple(list, list)
"""
config = _read_file(LD_SO_CONF_MAIN)

included_configs = []
other_lines = []
for line in config:
line = line.strip()
if line.startswith('include'):
cfg_glob = line.split(' ', 1)[1].strip()
cfg_glob = os.path.join('/etc', cfg_glob) if not os.path.isabs(cfg_glob) else cfg_glob
included_configs.append(cfg_glob)
elif line and not line.startswith(LD_SO_CONF_COMMENT_PREFIX):
other_lines.append(line)

return included_configs, other_lines


def scan_dynamic_linker_configuration():
included_configs, other_lines = _parse_main_config()

is_default_include_present = '/etc/' + LD_SO_CONF_DEFAULT_INCLUDE in included_configs
if not is_default_include_present:
api.current_logger().debug('The default include "{}" is not present in '
'the {} file.'.format(LD_SO_CONF_DEFAULT_INCLUDE, LD_SO_CONF_MAIN))

if is_default_include_present and len(included_configs) != 1:
# The additional included configs will most likely be created manually by the user
# and therefore will get flagged as custom in the next part of this function
api.current_logger().debug('The default include "{}" is not the only include in '
'the {} file.'.format(LD_SO_CONF_DEFAULT_INCLUDE, LD_SO_CONF_MAIN))

main_config_file = MainLDConfigFile(path=LD_SO_CONF_MAIN, modified=any(other_lines), modified_lines=other_lines)

# Expand the config paths from globs and ensure uniqueness of resulting paths
config_paths = set()
for cfg_glob in included_configs:
for cfg in glob.glob(cfg_glob):
config_paths.add(cfg)

included_config_files = []
for config_path in config_paths:
config_file = LDConfigFile(path=config_path, modified=_is_included_config_custom(config_path))
included_config_files.append(config_file)

# Check if dynamic linker variables used for specifying custom libraries are set
variables = [LD_LIBRARY_PATH_VAR, LD_PRELOAD_VAR]
used_variables = [var for var in variables if os.getenv(var, None)]

configuration = DynamicLinkerConfiguration(main_config=main_config_file,
included_configs=included_config_files,
used_variables=used_variables)

if other_lines or any([config.modified for config in included_config_files]) or used_variables:
api.produce(configuration)
Loading

0 comments on commit 043c6cb

Please sign in to comment.