Skip to content

Commit 6461034

Browse files
PeterMocarypirat89
authored andcommitted
add detection for custom libraries registered by ld.so.conf
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
1 parent 5a3bded commit 6461034

File tree

7 files changed

+519
-0
lines changed

7 files changed

+519
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from leapp.actors import Actor
2+
from leapp.libraries.actor.checkdynamiclinkerconfiguration import check_dynamic_linker_configuration
3+
from leapp.models import DynamicLinkerConfiguration, Report
4+
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
5+
6+
7+
class CheckDynamicLinkerConfiguration(Actor):
8+
"""
9+
Check for customization of dynamic linker configuration.
10+
11+
The in-place upgrade could potentionally be impacted in a negative way due
12+
to the customization of dynamic linker configuration by user. This actor creates high
13+
severity report upon detecting such customization.
14+
"""
15+
16+
name = 'check_dynamic_linker_configuration'
17+
consumes = (DynamicLinkerConfiguration,)
18+
produces = (Report,)
19+
tags = (ChecksPhaseTag, IPUWorkflowTag)
20+
21+
def process(self):
22+
check_dynamic_linker_configuration()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from leapp import reporting
2+
from leapp.libraries.stdlib import api
3+
from leapp.models import DynamicLinkerConfiguration
4+
5+
LD_SO_CONF_DIR = '/etc/ld.so.conf.d'
6+
LD_SO_CONF_MAIN = '/etc/ld.so.conf'
7+
LD_LIBRARY_PATH_VAR = 'LD_LIBRARY_PATH'
8+
LD_PRELOAD_VAR = 'LD_PRELOAD'
9+
FMT_LIST_SEPARATOR_1 = '\n- '
10+
FMT_LIST_SEPARATOR_2 = '\n - '
11+
12+
13+
def _report_custom_dynamic_linker_configuration(summary):
14+
reporting.create_report([
15+
reporting.Title(
16+
'Detected customized configuration for dynamic linker.'
17+
),
18+
reporting.Summary(summary),
19+
reporting.Remediation(hint=('Remove or revert the custom dynamic linker configurations and apply the changes '
20+
'using the ldconfig command. In case of possible active software collections we '
21+
'suggest disabling them persistently.')),
22+
reporting.RelatedResource('file', '/etc/ld.so.conf'),
23+
reporting.RelatedResource('directory', '/etc/ld.so.conf.d'),
24+
reporting.Severity(reporting.Severity.HIGH),
25+
reporting.Groups([reporting.Groups.OS_FACTS]),
26+
])
27+
28+
29+
def check_dynamic_linker_configuration():
30+
configuration = next(api.consume(DynamicLinkerConfiguration), None)
31+
if not configuration:
32+
return
33+
34+
custom_configurations = ''
35+
if configuration.main_config.modified:
36+
custom_configurations += (
37+
'{}The {} file has unexpected contents:{}{}'
38+
.format(FMT_LIST_SEPARATOR_1, LD_SO_CONF_MAIN,
39+
FMT_LIST_SEPARATOR_2, FMT_LIST_SEPARATOR_2.join(configuration.main_config.modified_lines))
40+
)
41+
42+
custom_configs = []
43+
for config in configuration.included_configs:
44+
if config.modified:
45+
custom_configs.append(config.path)
46+
47+
if custom_configs:
48+
custom_configurations += (
49+
'{}The following drop in config files were marked as custom:{}{}'
50+
.format(FMT_LIST_SEPARATOR_1, FMT_LIST_SEPARATOR_2, FMT_LIST_SEPARATOR_2.join(custom_configs))
51+
)
52+
53+
if configuration.used_variables:
54+
custom_configurations += (
55+
'{}The following variables contain unexpected dynamic linker configuration:{}{}'
56+
.format(FMT_LIST_SEPARATOR_1, FMT_LIST_SEPARATOR_2,
57+
FMT_LIST_SEPARATOR_2.join(configuration.used_variables))
58+
)
59+
60+
if custom_configurations:
61+
summary = (
62+
'Custom configurations to the dynamic linker could potentially impact the upgrade in a negative '
63+
'way. The custom configuration includes modifications to {}, custom or modified drop in config '
64+
'files in the {} directory and additional entries in the {} or {} variables. These modifications '
65+
'configure the dynamic linker to use different libraries that might not be provided by Red Hat '
66+
'products or might not be present during the whole upgrade process. The following custom '
67+
'configurations were detected by leapp:{}'
68+
.format(LD_SO_CONF_MAIN, LD_SO_CONF_DIR, LD_LIBRARY_PATH_VAR, LD_PRELOAD_VAR, custom_configurations)
69+
)
70+
_report_custom_dynamic_linker_configuration(summary)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import pytest
2+
3+
from leapp import reporting
4+
from leapp.libraries.actor.checkdynamiclinkerconfiguration import (
5+
check_dynamic_linker_configuration,
6+
LD_LIBRARY_PATH_VAR,
7+
LD_PRELOAD_VAR
8+
)
9+
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
10+
from leapp.libraries.stdlib import api
11+
from leapp.models import DynamicLinkerConfiguration, LDConfigFile, MainLDConfigFile
12+
13+
INCLUDED_CONFIG_PATHS = ['/etc/ld.so.conf.d/dyninst-x86_64.conf',
14+
'/etc/ld.so.conf.d/mariadb-x86_64.conf',
15+
'/custom/path/custom1.conf']
16+
17+
18+
@pytest.mark.parametrize(('included_configs_modifications', 'used_variables', 'modified_lines'),
19+
[
20+
([False, False, False], [], []),
21+
([True, True, True], [], []),
22+
([False, False, False], [LD_LIBRARY_PATH_VAR], []),
23+
([False, False, False], [], ['modified line 1', 'midified line 2']),
24+
([True, False, True], [LD_LIBRARY_PATH_VAR, LD_PRELOAD_VAR], ['modified line']),
25+
])
26+
def test_check_ld_so_configuration(monkeypatch, included_configs_modifications, used_variables, modified_lines):
27+
assert len(INCLUDED_CONFIG_PATHS) == len(included_configs_modifications)
28+
29+
main_config = MainLDConfigFile(path="/etc/ld.so.conf", modified=any(modified_lines), modified_lines=modified_lines)
30+
included_configs = []
31+
for path, modified in zip(INCLUDED_CONFIG_PATHS, included_configs_modifications):
32+
included_configs.append(LDConfigFile(path=path, modified=modified))
33+
34+
configuration = DynamicLinkerConfiguration(main_config=main_config,
35+
included_configs=included_configs,
36+
used_variables=used_variables)
37+
38+
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[configuration]))
39+
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
40+
41+
check_dynamic_linker_configuration()
42+
43+
report_expected = any(included_configs_modifications) or modified_lines or used_variables
44+
if not report_expected:
45+
assert reporting.create_report.called == 0
46+
return
47+
48+
assert reporting.create_report.called == 1
49+
assert 'configuration for dynamic linker' in reporting.create_report.reports[0]['title']
50+
summary = reporting.create_report.reports[0]['summary']
51+
52+
if any(included_configs_modifications):
53+
assert 'The following drop in config files were marked as custom:' in summary
54+
for config, modified in zip(INCLUDED_CONFIG_PATHS, included_configs_modifications):
55+
assert modified == (config in summary)
56+
57+
if modified_lines:
58+
assert 'The /etc/ld.so.conf file has unexpected contents' in summary
59+
for line in modified_lines:
60+
assert line in summary
61+
62+
if used_variables:
63+
assert 'The following variables contain unexpected dynamic linker configuration:' in summary
64+
for var in used_variables:
65+
assert '- {}'.format(var) in summary
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from leapp.actors import Actor
2+
from leapp.libraries.actor.scandynamiclinkerconfiguration import scan_dynamic_linker_configuration
3+
from leapp.models import DynamicLinkerConfiguration, InstalledRedHatSignedRPM
4+
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
5+
6+
7+
class ScanDynamicLinkerConfiguration(Actor):
8+
"""
9+
Scan the dynamoc linker configuration and find modifications.
10+
11+
The dynamic linker configuration files can be used to replace standard libraries
12+
with different custom libraries. The in-place upgrade does not support customization
13+
of this configuration by user. This actor produces information about detected
14+
modifications.
15+
"""
16+
17+
name = 'scan_dynamic_linker_configuration'
18+
consumes = (InstalledRedHatSignedRPM,)
19+
produces = (DynamicLinkerConfiguration,)
20+
tags = (FactsPhaseTag, IPUWorkflowTag)
21+
22+
def process(self):
23+
scan_dynamic_linker_configuration()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import glob
2+
import os
3+
4+
from leapp.libraries.common.rpms import has_package
5+
from leapp.libraries.stdlib import api, CalledProcessError, run
6+
from leapp.models import DynamicLinkerConfiguration, InstalledRedHatSignedRPM, LDConfigFile, MainLDConfigFile
7+
8+
LD_SO_CONF_DIR = '/etc/ld.so.conf.d'
9+
LD_SO_CONF_MAIN = '/etc/ld.so.conf'
10+
LD_SO_CONF_DEFAULT_INCLUDE = 'ld.so.conf.d/*.conf'
11+
LD_SO_CONF_COMMENT_PREFIX = '#'
12+
LD_LIBRARY_PATH_VAR = 'LD_LIBRARY_PATH'
13+
LD_PRELOAD_VAR = 'LD_PRELOAD'
14+
15+
16+
def _read_file(file_path):
17+
with open(file_path, 'r') as fd:
18+
return fd.readlines()
19+
20+
21+
def _is_modified(config_path):
22+
""" Decide if the configuration file was modified based on the package it belongs to. """
23+
result = run(['rpm', '-Vf', config_path], checked=False)
24+
if not result['exit_code']:
25+
return False
26+
modification_flags = result['stdout'].split(' ', 1)[0]
27+
# The file is considered modified only when the checksum does not match
28+
return '5' in modification_flags
29+
30+
31+
def _is_included_config_custom(config_path):
32+
if not os.path.isfile(config_path):
33+
return False
34+
35+
# Check if the config file has any lines that have an effect on dynamic linker configuration
36+
has_effective_line = False
37+
for line in _read_file(config_path):
38+
line = line.strip()
39+
if line and not line.startswith(LD_SO_CONF_COMMENT_PREFIX):
40+
has_effective_line = True
41+
break
42+
43+
if not has_effective_line:
44+
return False
45+
46+
is_custom = False
47+
try:
48+
package_name = run(['rpm', '-qf', '--queryformat', '%{NAME}', config_path])['stdout']
49+
is_custom = not has_package(InstalledRedHatSignedRPM, package_name) or _is_modified(config_path)
50+
except CalledProcessError:
51+
is_custom = True
52+
53+
return is_custom
54+
55+
56+
def _parse_main_config():
57+
"""
58+
Extracts included configs from the main dynamic linker configuration file (/etc/ld.so.conf)
59+
along with lines that are likely custom. The lines considered custom are simply those that are
60+
not includes.
61+
62+
:returns: tuple containing all the included files and lines considered custom
63+
:rtype: tuple(list, list)
64+
"""
65+
config = _read_file(LD_SO_CONF_MAIN)
66+
67+
included_configs = []
68+
other_lines = []
69+
for line in config:
70+
line = line.strip()
71+
if line.startswith('include'):
72+
cfg_glob = line.split(' ', 1)[1].strip()
73+
cfg_glob = os.path.join('/etc', cfg_glob) if not os.path.isabs(cfg_glob) else cfg_glob
74+
included_configs.append(cfg_glob)
75+
elif line and not line.startswith(LD_SO_CONF_COMMENT_PREFIX):
76+
other_lines.append(line)
77+
78+
return included_configs, other_lines
79+
80+
81+
def scan_dynamic_linker_configuration():
82+
included_configs, other_lines = _parse_main_config()
83+
84+
is_default_include_present = '/etc/' + LD_SO_CONF_DEFAULT_INCLUDE in included_configs
85+
if not is_default_include_present:
86+
api.current_logger().debug('The default include "{}" is not present in '
87+
'the {} file.'.format(LD_SO_CONF_DEFAULT_INCLUDE, LD_SO_CONF_MAIN))
88+
89+
if is_default_include_present and len(included_configs) != 1:
90+
# The additional included configs will most likely be created manually by the user
91+
# and therefore will get flagged as custom in the next part of this function
92+
api.current_logger().debug('The default include "{}" is not the only include in '
93+
'the {} file.'.format(LD_SO_CONF_DEFAULT_INCLUDE, LD_SO_CONF_MAIN))
94+
95+
main_config_file = MainLDConfigFile(path=LD_SO_CONF_MAIN, modified=any(other_lines), modified_lines=other_lines)
96+
97+
# Expand the config paths from globs and ensure uniqueness of resulting paths
98+
config_paths = set()
99+
for cfg_glob in included_configs:
100+
for cfg in glob.glob(cfg_glob):
101+
config_paths.add(cfg)
102+
103+
included_config_files = []
104+
for config_path in config_paths:
105+
config_file = LDConfigFile(path=config_path, modified=_is_included_config_custom(config_path))
106+
included_config_files.append(config_file)
107+
108+
# Check if dynamic linker variables used for specifying custom libraries are set
109+
variables = [LD_LIBRARY_PATH_VAR, LD_PRELOAD_VAR]
110+
used_variables = [var for var in variables if os.getenv(var, None)]
111+
112+
configuration = DynamicLinkerConfiguration(main_config=main_config_file,
113+
included_configs=included_config_files,
114+
used_variables=used_variables)
115+
116+
if other_lines or any([config.modified for config in included_config_files]) or used_variables:
117+
api.produce(configuration)

0 commit comments

Comments
 (0)