Skip to content

Commit 20e7b89

Browse files
Merge pull request #871 from jiridanek/jd_check_ldd_python
RHOAIENG-9707: chore(tests/containers): check shared objects with ldd
2 parents b757f86 + 6c707cb commit 20e7b89

File tree

1 file changed

+101
-1
lines changed

1 file changed

+101
-1
lines changed

tests/containers/base_image_test.py

+101-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
from __future__ import annotations
22

3+
import binascii
4+
import inspect
5+
import json
36
import logging
47
import pathlib
8+
import re
59
import tempfile
6-
from typing import TYPE_CHECKING
10+
import textwrap
11+
from typing import TYPE_CHECKING, Any, Callable
712

13+
import pytest
814
import testcontainers.core.container
915
import testcontainers.core.waiting_utils
1016

@@ -20,6 +26,84 @@
2026
class TestBaseImage:
2127
"""Tests that are applicable for all images we have in this repository."""
2228

29+
def test_elf_files_can_link_runtime_libs(self, subtests: pytest_subtests.SubTests, image):
30+
container = testcontainers.core.container.DockerContainer(image=image, user=0, group_add=[0])
31+
container.with_command("/bin/sh -c 'sleep infinity'")
32+
33+
def check_elf_file():
34+
"""This python function will be executed on the image itself.
35+
That's why it has to have here all imports it needs."""
36+
import glob
37+
import os
38+
import json
39+
import subprocess
40+
import stat
41+
42+
dirs = [
43+
"/bin",
44+
"/lib",
45+
"/lib64",
46+
"/opt/app-root"
47+
]
48+
for path in dirs:
49+
count_scanned = 0
50+
unsatisfied_deps: list[tuple[str, str]] = []
51+
for dlib in glob.glob(os.path.join(path, "**"), recursive=True):
52+
# we will visit all files eventually, no need to bother with symlinks
53+
s = os.stat(dlib, follow_symlinks=False)
54+
isdirectory = stat.S_ISDIR(s.st_mode)
55+
isfile = stat.S_ISREG(s.st_mode)
56+
executable = bool(s.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH))
57+
if isdirectory or not executable or not isfile:
58+
continue
59+
with open(dlib, mode='rb') as fp:
60+
magic = fp.read(4)
61+
if magic != b'\x7fELF':
62+
continue
63+
64+
count_scanned += 1
65+
ld_library_path = os.environ.get("LD_LIBRARY_PATH", "") + os.path.pathsep + os.path.dirname(dlib)
66+
output = subprocess.check_output(["ldd", dlib],
67+
# search the $ORIGIN, essentially; most python libs expect this
68+
env={**os.environ, "LD_LIBRARY_PATH": ld_library_path},
69+
text=True)
70+
for line in output.splitlines():
71+
if "not found" in line:
72+
unsatisfied_deps.append((dlib, line.strip()))
73+
assert output
74+
print("OUTPUT>", json.dumps({"dir": path, "count_scanned": count_scanned, "unsatisfied": unsatisfied_deps}))
75+
76+
try:
77+
container.start()
78+
ecode, output = container.exec(
79+
encode_python_function_execution_command_interpreter("/usr/bin/python3", check_elf_file))
80+
finally:
81+
docker_utils.NotebookContainer(container).stop(timeout=0)
82+
83+
for line in output.decode().splitlines():
84+
logging.debug(line)
85+
if not line.startswith("OUTPUT> "):
86+
continue
87+
data = json.loads(line[len("OUTPUT> "):])
88+
assert data['count_scanned'] > 0
89+
for dlib, deps in data["unsatisfied"]:
90+
# here goes the allowlist
91+
if re.search(r"^/lib64/python3.\d+/site-packages/hawkey/test/_hawkey_test.so", dlib) is not None:
92+
continue # this is some kind of self test or what
93+
if re.search(r"^/lib64/systemd/libsystemd-core-\d+.so", dlib) is not None:
94+
continue # this is expected and we don't use systemd anyway
95+
if deps.startswith("libodbc.so.2"):
96+
continue # todo(jdanek): known issue RHOAIENG-18904
97+
if deps.startswith("libcuda.so.1"):
98+
continue # cuda magic will mount this into /usr/lib64/libcuda.so.1 and it will be found
99+
if deps.startswith("libjvm.so"):
100+
continue # it's in ../server
101+
if deps.startswith("libtracker-extract.so"):
102+
continue # it's in ../
103+
104+
with subtests.test(f"{dlib=}"):
105+
pytest.fail(f"{dlib=} has unsatisfied dependencies {deps=}")
106+
23107
def test_oc_command_runs(self, image: str):
24108
container = testcontainers.core.container.DockerContainer(image=image, user=23456, group_add=[0])
25109
container.with_command("/bin/sh -c 'sleep infinity'")
@@ -78,3 +162,19 @@ def test_oc_command_runs_fake_fips(self, image: str, subtests: pytest_subtests.S
78162
assert ecode == 0, output.decode()
79163
finally:
80164
docker_utils.NotebookContainer(container).stop(timeout=0)
165+
166+
167+
def encode_python_function_execution_command_interpreter(python: str, function: Callable[..., Any], *args: list[Any]) -> list[str]:
168+
"""Returns a cli command that will run the given Python function encoded inline.
169+
All dependencies (imports, ...) must be part of function body."""
170+
code = textwrap.dedent(inspect.getsource(function))
171+
ccode = binascii.b2a_base64(code.encode())
172+
name = function.__name__
173+
parameters = ', '.join(repr(arg) for arg in args)
174+
program = textwrap.dedent(f"""
175+
import binascii;
176+
s=binascii.a2b_base64("{ccode.decode('ascii').strip()}");
177+
exec(s.decode());
178+
print({name}({parameters}));""")
179+
int_cmd = [python, "-c", program]
180+
return int_cmd

0 commit comments

Comments
 (0)