From d69741f756538d6cdd3f0c84d2ad275324e7245a Mon Sep 17 00:00:00 2001 From: Daniel Garcia Moreno Date: Tue, 10 Oct 2023 12:29:17 +0200 Subject: [PATCH] Add new check to detect links to scripts This new check looks for links in /usr/bin that points to some script in a non bin path that contains a shebang, and checks if the needed interpreter is included as a requirement, because in that case rpm won't be able to inject the requirement by itself. Fix https://github.com/rpm-software-management/rpmlint/issues/1120 --- rpmlint/checks/FilesCheck.py | 30 ++++++++++++++ rpmlint/descriptions/FilesCheck.toml | 6 +++ test/test_files.py | 60 +++++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/rpmlint/checks/FilesCheck.py b/rpmlint/checks/FilesCheck.py index 3c357e912..dec75615f 100644 --- a/rpmlint/checks/FilesCheck.py +++ b/rpmlint/checks/FilesCheck.py @@ -862,6 +862,35 @@ def _check_file_link_relative(self, pkg, fname, pkgfile): 'symlink-contains-up-and-down-segments', fname, link) + def _check_file_link_bindir_shebang(self, pkg, fname, pkgfile): + basedir = Path(fname).parent + linkto = str((basedir / Path(pkgfile.linkto)).resolve()) + # Link to a file not in the package, so ignore + if linkto not in pkg.files: + return + + realbin = pkg.files[linkto] + # Link to something in bindir is okay + if bin_regex.search(realbin.name): + return + if not stat.S_ISREG(realbin.mode): + return + + file_chunk, file_istext = self.peek(realbin.path, pkg) + file_interpreter, _file_interpreter_args = script_interpreter(file_chunk) + # Not a script with shebang, so ignore + if not file_interpreter: + return + + # If the shebang interpreter is a dependency, it's okay + deps = [x[0] for x in pkg.requires] + if file_interpreter in deps: + return + + self.output.add_info('W', pkg, 'symlink-to-binary-with-shebang', fname, + f'is a link to a script ({realbin.name}) but missing' + f' requires for {file_interpreter}') + def _check_file_link(self, pkg, fname, pkgfile): if not stat.S_ISLNK(pkgfile.mode): return @@ -871,6 +900,7 @@ def _check_file_link(self, pkg, fname, pkgfile): self._check_file_link_bindir_exes(pkg, fname) self._check_file_link_absolute(pkg, fname, pkgfile) self._check_file_link_relative(pkg, fname, pkgfile) + self._check_file_link_bindir_shebang(pkg, fname, pkgfile) def _check_file_dir(self, pkg, fname, pkgfile): if not stat.S_ISDIR(pkgfile.mode): diff --git a/rpmlint/descriptions/FilesCheck.toml b/rpmlint/descriptions/FilesCheck.toml index 7598c35d5..30a59a3e6 100644 --- a/rpmlint/descriptions/FilesCheck.toml +++ b/rpmlint/descriptions/FilesCheck.toml @@ -403,3 +403,9 @@ manual-page-in-subfolder=""" Manual page should not be placed in a subfolder of a manual section directory. """ + +symlink-to-binary-with-shebang=""" +A file in /usr/bin is a link to a script in a different place with a shebang. +rpm won't be able to inject the needed interpreter as dependency, so it should +be done manually. +""" diff --git a/test/test_files.py b/test/test_files.py index 052abeb9d..de0f1fce6 100644 --- a/test/test_files.py +++ b/test/test_files.py @@ -1,3 +1,5 @@ +import stat + import pytest from rpmlint.checks.FilesCheck import FilesCheck from rpmlint.checks.FilesCheck import pyc_magic_from_chunk, pyc_mtime_from_chunk @@ -5,7 +7,7 @@ from rpmlint.checks.FilesCheck import script_interpreter as se from rpmlint.filter import Filter -from Testing import CONFIG, get_tested_package, get_tested_path +from Testing import CONFIG, get_tested_mock_package, get_tested_package, get_tested_path @pytest.fixture(scope='function', autouse=True) @@ -13,7 +15,19 @@ def filescheck(): CONFIG.info = True output = Filter(CONFIG) test = FilesCheck(CONFIG, output) - return output, test + yield output, test + + +@pytest.fixture +def output(filescheck): + output, _test = filescheck + yield output + + +@pytest.fixture +def test(filescheck): + _output, test = filescheck + yield test def test_pep3147(): @@ -244,3 +258,45 @@ def test_manual_pages(tmp_path, package, filescheck): assert 'W: manpage-not-compressed bz2 /usr/share/man/man1/test.1.zst' in out assert 'E: bad-manual-page-folder /usr/share/man/man0p/foo.3.gz expected folder: man3' in out assert 'bad-manual-page-folder /usr/share/man/man3/some.3pm.gz' not in out + + +@pytest.mark.parametrize('package', [ + get_tested_mock_package( + files={ + '/usr/share/package/bin.py': { + 'content': '#!/usr/bin/python3\nprint("python required")', + 'metadata': {'mode': 0o755 | stat.S_IFREG}, + }, + '/usr/bin/testlink': { + 'linkto': '../share/package/bin.py', + }, + }, + header={}, + ), +]) +def test_shebang(package, output, test): + test.check(package) + out = output.print_results(output.results) + assert 'W: symlink-to-binary-with-shebang /usr/bin/testlink' in out + + +@pytest.mark.parametrize('package', [ + get_tested_mock_package( + files={ + '/usr/share/package/bin.py': { + 'content': '#!/usr/bin/python3\nprint("python required")', + 'metadata': {'mode': 0o755 | stat.S_IFREG}, + }, + '/usr/bin/testlink': { + 'linkto': '../share/package/bin.py', + }, + }, + header={ + 'requires': ['/usr/bin/python3'], + }, + ), +]) +def test_shebang_ok(package, output, test): + test.check(package) + out = output.print_results(output.results) + assert 'W: symlink-to-binary-with-shebang /usr/bin/testlink' not in out