Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add detection for custom libraries registered by ld.so.conf #1118

Merged
merged 1 commit into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading