Skip to content

Commit

Permalink
add inhibitor for custom libraries registered by ld.so.conf
Browse files Browse the repository at this point in the history
The in-place upgrade does not support custom libraries linked using the
ld.so configuration. The new actor introduced in this commit detects if
the configuration was tempered with and inhibits the upgrade in such
case.
  • Loading branch information
PeterMocary committed Aug 22, 2023
1 parent d6498b8 commit f873640
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 0 deletions.
23 changes: 23 additions & 0 deletions repos/system_upgrade/common/actors/checkldconf/actor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from leapp.actors import Actor
from leapp.libraries.actor.checkldsoconfiguration import check_ld_so_configuration
from leapp.models import InstalledRedHatSignedRPM, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckLdSoConfiguration(Actor):
"""
Check for customization of ld.so configuration
The ld.so configuration files are used to overwrite standard library links
in order to use different custom libraries. The in-place upgrade does not
support customization of this configuration by user. This actor inhibits the
upgrade upon detecting such customization.
"""

name = 'check_ld_so_configuration'
consumes = (InstalledRedHatSignedRPM,)
produces = (Report,)
tags = (ChecksPhaseTag, IPUWorkflowTag)

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

from leapp import reporting
from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import InstalledRedHatSignedRPM

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'
LIST_FORMAT_PREFIX = '\n - '


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


def _report_custom_ld_so_configuration(summary):
reporting.create_report([
reporting.Title(
'Third party libraries linked with ld.so.conf are not supported for the in-place upgrade.'
),
reporting.Summary(summary),
reporting.Remediation('Remove the custom ld.so configuration.'),
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, reporting.Groups.INHIBITOR]),
])


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

is_custom = False
try:
package_name = run(['rpm', '-qf', '--queryformat', '%{NAME}', config_path])['stdout']
is_custom = not has_package(InstalledRedHatSignedRPM, package_name)
except CalledProcessError:
is_custom = True
api.current_logger().debug('The following config file is{}considered a custom '
'ld.so configuration: {}'.format(' ' if is_custom else ' NOT ', config_path))
return is_custom


def _parse_main_ld_so_config():
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:
other_lines.append(line)

return included_configs, other_lines


def check_ld_so_configuration():
included_configs, other_lines = _parse_main_ld_so_config()
summary = ''

if other_lines:
# The main ld.so config file is expected to only include additional configs that are owned by a package
summary += ('The /etc/ld.so.conf file has unexpected contents:'
'{}{}'.format(LIST_FORMAT_PREFIX,
LIST_FORMAT_PREFIX.join(other_lines)))

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 "' + LD_SO_CONF_DEFAULT_INCLUDE +
'" is not present in the ' + LD_SO_CONF_MAIN + ' file.')

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 "' + LD_SO_CONF_DEFAULT_INCLUDE +
'" is not the only include in the ' + LD_SO_CONF_MAIN + ' file.')

# Detect custom ld configs from the includes in the main ld.so.conf
custom_ld_configs = []
for cfg_glob in included_configs:
custom_ld_configs += [cfg for cfg in glob.glob(cfg_glob) if _is_included_ld_so_config_custom(cfg)]

if custom_ld_configs:
summary += ('\nThe following config files were marked as unsupported:'
'{}{}'.format(LIST_FORMAT_PREFIX,
LIST_FORMAT_PREFIX.join(custom_ld_configs)))

if summary:
_report_custom_ld_so_configuration(summary)
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import glob
import os

import pytest

from leapp import reporting
from leapp.libraries.actor import checkldsoconfiguration
from leapp.libraries.common.testutils import create_report_mocked
from leapp.libraries.stdlib import CalledProcessError
from leapp.models import InstalledRedHatSignedRPM

INCLUDED_CONFIGS_GLOB_DICT_1 = {'/etc/ld.so.conf.d/*.conf': ['/etc/ld.so.conf.d/dyninst-x86_64.conf',
'/etc/ld.so.conf.d/mariadb-x86_64.conf',
'/etc/ld.so.conf.d/bind-export-x86_64.conf']}

INCLUDED_CONFIGS_GLOB_DICT_2 = {'/etc/ld.so.conf.d/*.conf': ['/etc/ld.so.conf.d/dyninst-x86_64.conf',
'/etc/ld.so.conf.d/mariadb-x86_64.conf',
'/etc/ld.so.conf.d/bind-export-x86_64.conf',
'/etc/ld.so.conf.d/custom1.conf',
'/etc/ld.so.conf.d/custom2.conf']}

INCLUDED_CONFIGS_GLOB_DICT_3 = {'/etc/ld.so.conf.d/*.conf': ['/etc/ld.so.conf.d/dyninst-x86_64.conf',
'/etc/ld.so.conf.d/custom1.conf',
'/etc/ld.so.conf.d/mariadb-x86_64.conf',
'/etc/ld.so.conf.d/bind-export-x86_64.conf',
'/etc/ld.so.conf.d/custom2.conf'],
'/custom/path/*.conf': ['/custom/path/custom1.conf',
'/custom/path/custom2.conf']}


@pytest.mark.parametrize(('included_configs_glob_dict', 'other_lines', 'custom_configs'),
[
(INCLUDED_CONFIGS_GLOB_DICT_1, [], []),
(INCLUDED_CONFIGS_GLOB_DICT_1, ['/custom/path.lib'], []),
(INCLUDED_CONFIGS_GLOB_DICT_2, [], ['/etc/ld.so.conf.d/custom1.conf',
'/etc/ld.so.conf.d/custom2.conf']),
(INCLUDED_CONFIGS_GLOB_DICT_3, ['/custom/path.lib'], ['/etc/ld.so.conf.d/custom1.conf',
'/etc/ld.so.conf.d/custom2.conf'
'/custom/path/custom1.conf',
'/custom/path/custom2.conf']),
])
def test_check_ld_so_configuration(monkeypatch, included_configs_glob_dict, other_lines, custom_configs):
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
monkeypatch.setattr(glob, 'glob', lambda glob: included_configs_glob_dict[glob])
monkeypatch.setattr(checkldsoconfiguration, '_is_included_ld_so_config_custom',
lambda config: config in custom_configs)
monkeypatch.setattr(checkldsoconfiguration, '_parse_main_ld_so_config',
lambda: (included_configs_glob_dict.keys(), other_lines))

checkldsoconfiguration.check_ld_so_configuration()

report_expected = custom_configs or other_lines
if not report_expected:
assert reporting.create_report.called == 0
return

assert reporting.create_report.called == 1
assert 'ld.so.conf' in reporting.create_report.reports[0]['title']
summary = reporting.create_report.reports[0]['summary']

all_configs = []
for configs in included_configs_glob_dict.values():
all_configs += configs

if custom_configs:
assert 'The following config files were marked as unsupported:' in summary

for config in all_configs:
assert (config in custom_configs) == (config in summary)

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

for other_line in other_lines:
assert other_line in summary


@pytest.mark.parametrize(('config_contents', 'included_config_paths', 'other_lines'),
[
(['include ld.so.conf.d/*.conf\n'],
['/etc/ld.so.conf.d/*.conf'], []),
(['include ld.so.conf.d/*.conf\n', '\n', '/custom/path.lib\n'],
['/etc/ld.so.conf.d/*.conf'], ['/custom/path.lib']),
(['include ld.so.conf.d/*.conf\n', 'include /custom/path.conf\n'],
['/etc/ld.so.conf.d/*.conf', '/custom/path.conf'], []),
([' \n'],
[], [])
])
def test_parse_main_ld_so_config(monkeypatch, config_contents, included_config_paths, other_lines):
def mocked_read_file(path):
assert path == checkldsoconfiguration.LD_SO_CONF_MAIN
return config_contents

monkeypatch.setattr(checkldsoconfiguration, '_read_file', mocked_read_file)

_included_config_paths, _other_lines = checkldsoconfiguration._parse_main_ld_so_config()

assert _included_config_paths == included_config_paths
assert _other_lines == other_lines


@pytest.mark.parametrize(('config_path', 'run_result', 'package_exists', 'is_custom'),
[
('/etc/ld.so.conf.d/dyninst-x86_64.conf', 'dyninst', True, False),
('/etc/ld.so.conf.d/somelib.conf', CalledProcessError, False, True),
('/etc/custom/custom.conf', 'custom', False, True)
])
def test_is_included_ld_so_config_custom(monkeypatch, config_path, run_result, package_exists, is_custom):
def mocked_run(command):
assert config_path in command
if run_result and not isinstance(run_result, str):
raise CalledProcessError("message", ["command"], "result")
return {'stdout': run_result}

def mocked_has_package(model, package_name):
assert model is InstalledRedHatSignedRPM
assert package_name == run_result
return package_exists

monkeypatch.setattr(checkldsoconfiguration, 'run', mocked_run)
monkeypatch.setattr(checkldsoconfiguration, 'has_package', mocked_has_package)
monkeypatch.setattr(os.path, 'isfile', lambda _: True)

result = checkldsoconfiguration._is_included_ld_so_config_custom(config_path)
assert result == is_custom

0 comments on commit f873640

Please sign in to comment.