Repository: osbuild/osbuild
Pull request #2007 (2025-02-10 11:07:48)

Tools/osbuild-image-info: make read_selinux_ctx_mismatch more robust
Modify the function able to handle messages about skipped binary fcontext files and skip them. This started to happen on c10s. Extend the unit test to cover this new scenario.

This will fix PR #1998


Patch:
diff --git a/stages/org.osbuild.kernel-cmdline b/stages/org.osbuild.kernel-cmdline
index 159468ce..df72094f 100755
--- a/stages/org.osbuild.kernel-cmdline
+++ b/stages/org.osbuild.kernel-cmdline
@@ -11,7 +11,7 @@ MAX_SIZE_MAP = {
     "x86_64": 2048,
     # https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/setup.h
     "i386": 2048,
-    # https://github.com/torvalds/linux/blame/master/arch/arm/include/uapi/asm/setup.h
+    # https://github.com/torvalds/linux/blob/master/arch/arm/include/uapi/asm/setup.h
     "arm": 1024,
     # https://github.com/torvalds/linux/blob/master/arch/arm64/include/uapi/asm/setup.h
     "aarch64": 2048,
@@ -29,7 +29,8 @@ MAX_SIZE_MAP = {
 def main(tree, options):
     root_fs_uuid = options.get("root_fs_uuid", "")
     additional = options.get("kernel_opts", "")
-    max_cmdline_size = MAX_SIZE_MAP.get(platform.machine().lower(), 4096)
+    max_cmdline_size = options.get("kernel_cmdline_size",
+                                   MAX_SIZE_MAP.get(platform.machine().lower(), 4096))
 
     params = []
 
diff --git a/stages/org.osbuild.kernel-cmdline.meta.json b/stages/org.osbuild.kernel-cmdline.meta.json
index 4ba73d4e..e511078b 100644
--- a/stages/org.osbuild.kernel-cmdline.meta.json
+++ b/stages/org.osbuild.kernel-cmdline.meta.json
@@ -30,6 +30,12 @@
         "description": "Additional kernel boot options",
         "type": "string",
         "default": ""
+      },
+      "kernel_cmdline_size": {
+        "description": "Sets a custom maximum kernel cmdline size",
+        "type": "integer",
+        "minimum": 256,
+        "maximum": 4096
       }
     }
   }
diff --git a/tools/osbuild-image-info b/tools/osbuild-image-info
index 983b86d5..11a9c3bb 100755
--- a/tools/osbuild-image-info
+++ b/tools/osbuild-image-info
@@ -1118,9 +1118,13 @@ def read_selinux_ctx_mismatch(tree, is_ostree):
         setfiles_pattern = r"Would\s+relabel\s+(?P<filename>.+)\s+from\s+(?P<actual>.+)\s+to\s+(?P<expected>.+)"
         setfiles_re = re.compile(setfiles_pattern)
 
+        # skip messages about old compiled fcontext format
+        binary_fcontext_skip = rf"{tree}/etc/selinux/targeted/contexts/files/file_contexts.*\.bin:\s+Old compiled fcontext format, skipping"
+        binary_fcontext_skip_re = re.compile(binary_fcontext_skip)
+
         for line in output.splitlines():
             line = line.strip()
-            if not line:
+            if not line or binary_fcontext_skip_re.match(line):
                 continue
             match = setfiles_re.match(line)
             # do not silently ignore changes of 'setfiles' output


Test patch:
diff --git a/stages/test/test_kernel_cmdline.py b/stages/test/test_kernel_cmdline.py
new file mode 100644
index 00000000..9b0c87fd
--- /dev/null
+++ b/stages/test/test_kernel_cmdline.py
@@ -0,0 +1,63 @@
+#!/usr/bin/python3
+
+from unittest import mock
+
+import pytest  # type: ignore
+
+from osbuild import testutil
+
+STAGE_NAME = "org.osbuild.kernel-cmdline"
+
+
+@pytest.mark.parametrize("options, expected", [
+    ({"kernel_opts": "--custom-opt=some"}, "--custom-opt=some"),
+    ({"root_fs_uuid": "some-uuid"}, "root=UUID=some-uuid"),
+    ({"root_fs_uuid": "other-uuid", "kernel_opts": "--custom-opt=other"},
+     "root=UUID=other-uuid --custom-opt=other"),
+])
+def test_kernel_cmdline(tmp_path, stage_module, options, expected):
+    tree = tmp_path / "tree"
+    tree.mkdir()
+
+    stage_module.main(tree, options)
+    cmdline_file = tree / "etc" / "kernel" / "cmdline"
+    assert cmdline_file.exists()
+    assert cmdline_file.read_text() == expected
+
+
+@pytest.mark.parametrize("options, arch", [
+    ({"kernel_opts": "a" * 2049}, "x86_64"),
+    ({"kernel_opts": "a" * 1025}, "arm"),
+    ({"kernel_opts": "a" * 2049}, "aarch64"),
+    ({"kernel_opts": "a" * 257, "kernel_cmdline_size": 256}, "any"),
+])
+@mock.patch("platform.machine")
+def test_kernel_opts_size_check(mock_machine, tmp_path, stage_module, options, arch):
+    tree = tmp_path / "tree"
+    tree.mkdir()
+    mock_machine.return_value = arch
+
+    with pytest.raises(ValueError) as e:
+        stage_module.main(tree, options)
+    assert str(e.value).startswith("The size of the kernel cmdline options cannot be larger than")
+
+
+@pytest.mark.parametrize("options, expected_error", [
+    ({"kernel_cmdline_size": 256}, ""),
+    ({"kernel_cmdline_size": 4096}, ""),
+    ({"kernel_cmdline_size": "not integer"}, "is not of type 'integer'"),
+    ({"kernel_cmdline_size": 0}, "is less than the minimum of 256"),
+    ({"kernel_cmdline_size": 4097}, "is greater than the maximum of 4096"),
+])
+def test_schema_validation_kernel_cmdline(stage_schema, options, expected_error):
+    test_input = {
+        "type": STAGE_NAME,
+        "options": options
+    }
+    res = stage_schema.validate(test_input)
+
+    if not expected_error:
+        assert res.valid
+    else:
+        assert not res.valid
+        testutil.assert_jsonschema_error_contains(res, expected_error, expected_num_errs=1)
diff --git a/tools/test/test_osbuild_image_info.py b/tools/test/test_osbuild_image_info.py
index 61b0de56..07b7b3c8 100644
--- a/tools/test/test_osbuild_image_info.py
+++ b/tools/test/test_osbuild_image_info.py
@@ -224,3 +224,116 @@ def test_analyse_iso_fail_no_tarball(tmp_path):
             subprocess.CalledProcessError,
             match=r"^Command '\['tar', '--selinux', '--xattrs', '--acls', '-x', '--auto-compress', '-f', '/tmp/\w+/liveimg.tar.gz"):
         osbuild_image_info.analyse_iso(image_path)
+
+
+@pytest.mark.parametrize("subprocess_output,expected_report", [
+    pytest.param(
+        """Would relabel {tmp_path}/etc/shells from unconfined_u:object_r:etc_t:s0 to system_u:object_r:etc_t:s0
+Would relabel {tmp_path}/etc/ld.so.cache from unconfined_u:object_r:ld_so_cache_t:s0 to system_u:object_r:ld_so_cache_t:s0
+Would relabel {tmp_path}/etc/alternatives/roff.7.gz from unconfined_u:object_r:etc_t:s0 to system_u:object_r:etc_t:s0
+Would relabel {tmp_path}/var/lib/selinux/targeted/active from unconfined_u:object_r:semanage_store_t:s0 to system_u:object_r:semanage_store_t:s0
+Would relabel {tmp_path}/var/lib/alternatives/roff.7.gz from unconfined_u:object_r:rpm_var_lib_t:s0 to system_u:object_r:rpm_var_lib_t:s0
+""",
+        [
+            {
+                "filename": "/etc/alternatives/roff.7.gz",
+                "actual": "unconfined_u:object_r:etc_t:s0",
+                "expected": "system_u:object_r:etc_t:s0",
+            },
+            {
+                "filename": "/etc/ld.so.cache",
+                "actual": "unconfined_u:object_r:ld_so_cache_t:s0",
+                "expected": "system_u:object_r:ld_so_cache_t:s0",
+            },
+            {
+                "filename": "/etc/shells",
+                "actual": "unconfined_u:object_r:etc_t:s0",
+                "expected": "system_u:object_r:etc_t:s0",
+            },
+            {
+                "filename": "/var/lib/alternatives/roff.7.gz",
+                "actual": "unconfined_u:object_r:rpm_var_lib_t:s0",
+                "expected": "system_u:object_r:rpm_var_lib_t:s0",
+            },
+            {
+                "filename": "/var/lib/selinux/targeted/active",
+                "actual": "unconfined_u:object_r:semanage_store_t:s0",
+                "expected": "system_u:object_r:semanage_store_t:s0",
+            },
+        ],
+        id="happy case",
+    ),
+    pytest.param(
+        "",
+        [],
+        id="empty",
+    ),
+    pytest.param(
+        """{tmp_path}/etc/selinux/targeted/contexts/files/file_contexts.bin:  Old compiled fcontext format, skipping
+{tmp_path}/etc/selinux/targeted/contexts/files/file_contexts.homedirs.bin:  Old compiled fcontext format, skipping
+""",
+        [],
+        id="only lines to skip",
+    ),
+    pytest.param(
+        """{tmp_path}/etc/selinux/targeted/contexts/files/file_contexts.bin:  Old compiled fcontext format, skipping
+{tmp_path}/etc/selinux/targeted/contexts/files/file_contexts.homedirs.bin:  Old compiled fcontext format, skipping
+
+Would relabel {tmp_path}/etc/shells from unconfined_u:object_r:etc_t:s0 to system_u:object_r:etc_t:s0
+Would relabel {tmp_path}/etc/ld.so.cache from unconfined_u:object_r:ld_so_cache_t:s0 to system_u:object_r:ld_so_cache_t:s0
+Would relabel {tmp_path}/etc/alternatives/roff.7.gz from unconfined_u:object_r:etc_t:s0 to system_u:object_r:etc_t:s0
+Would relabel {tmp_path}/var/lib/selinux/targeted/active from unconfined_u:object_r:semanage_store_t:s0 to system_u:object_r:semanage_store_t:s0
+Would relabel {tmp_path}/var/lib/alternatives/roff.7.gz from unconfined_u:object_r:rpm_var_lib_t:s0 to system_u:object_r:rpm_var_lib_t:s0
+""",
+        [
+            {
+                "filename": "/etc/alternatives/roff.7.gz",
+                "actual": "unconfined_u:object_r:etc_t:s0",
+                "expected": "system_u:object_r:etc_t:s0",
+            },
+            {
+                "filename": "/etc/ld.so.cache",
+                "actual": "unconfined_u:object_r:ld_so_cache_t:s0",
+                "expected": "system_u:object_r:ld_so_cache_t:s0",
+            },
+            {
+                "filename": "/etc/shells",
+                "actual": "unconfined_u:object_r:etc_t:s0",
+                "expected": "system_u:object_r:etc_t:s0",
+            },
+            {
+                "filename": "/var/lib/alternatives/roff.7.gz",
+                "actual": "unconfined_u:object_r:rpm_var_lib_t:s0",
+                "expected": "system_u:object_r:rpm_var_lib_t:s0",
+            },
+            {
+                "filename": "/var/lib/selinux/targeted/active",
+                "actual": "unconfined_u:object_r:semanage_store_t:s0",
+                "expected": "system_u:object_r:semanage_store_t:s0",
+            },
+        ],
+        id="valid lines mixed with lines to skip",
+    )
+])
+def test_read_selinux_ctx_mismatch(tmp_path, subprocess_output, expected_report):
+    """
+    Test the read_selinux_ctx_mismatch function
+    """
+    policy_dir = tmp_path / "etc/selinux/targeted/policy"
+    policy_dir.mkdir(parents=True)
+    policy_file = policy_dir / "policy.33"
+    policy_file.touch()
+
+    with patch("subprocess.check_output") as subprocess_check_output:
+        subprocess_check_output.return_value = subprocess_output.format(tmp_path=tmp_path)
+        report = osbuild_image_info.read_selinux_ctx_mismatch(tmp_path.as_posix(), False)
+
+        assert subprocess_check_output.call_count == 1
+        assert subprocess_check_output.call_args[0][0] == [
+            "setfiles", "-r", tmp_path.as_posix(),
+            "-nvF",
+            "-c", os.fspath(tmp_path / "etc/selinux/targeted/policy/policy.33"),
+            os.fspath(tmp_path / "etc/selinux/targeted/contexts/files/file_contexts"),
+            tmp_path.as_posix(),
+        ]
+        assert report == expected_report