Repository: osbuild/osbuild
Pull request #1675 (2024-03-20 16:04:14)

osbuild: make the entire /etc/selinux avaialble for the buildroot
When moving to `bootc install to-filesystem` we need more information for bootc from /etc/selinux than our current /etc/selinux/targeted/contexts policy.

This commit makes all of /etc/selinux available which unblocks the bootc install.


Patch:
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a2e537f4..8c3acb74 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -101,12 +101,12 @@ Manifests:
           - aws/centos-stream-9-aarch64
       - PARALLEL_EXEC: ["1/8", "2/8", "3/8", "4/8", "5/8", "6/8", "7/8", "8/8"]
         RUNNER:
-          - aws/rhel-8.8-ga-x86_64
-          - aws/rhel-8.8-ga-aarch64
+          - aws/rhel-8.9-ga-x86_64
+          - aws/rhel-8.9-ga-aarch64
           - aws/rhel-8.10-nightly-x86_64
           - aws/rhel-8.10-nightly-aarch64
-          - aws/rhel-9.2-ga-x86_64
-          - aws/rhel-9.2-ga-aarch64
+          - aws/rhel-9.3-ga-x86_64
+          - aws/rhel-9.3-ga-aarch64
           - aws/rhel-9.4-nightly-x86_64
           - aws/rhel-9.4-nightly-aarch64
         INTERNAL_NETWORK: "true"
diff --git a/Schutzfile b/Schutzfile
index 896106cf..c8736432 100644
--- a/Schutzfile
+++ b/Schutzfile
@@ -2,7 +2,7 @@
   "global": {
     "dependencies": {
       "manifest-db": {
-        "commit": "3bb2cb6ec1c037dfc6a6dd57f65e245c0b3e79ae"
+        "commit": "c15cf3e456877fe4b54ebbfb192b78f89a29b448"
       }
     }
   },
diff --git a/osbuild/buildroot.py b/osbuild/buildroot.py
index 1ceb06be..02b1b9f2 100644
--- a/osbuild/buildroot.py
+++ b/osbuild/buildroot.py
@@ -138,12 +138,6 @@ class BuildRoot(contextlib.AbstractContextManager):
 
             self.var = os.path.join(self.tmp, "var")
             os.makedirs(self.var, exist_ok=True)
-            # Ensure /var/tmp is available, see
-            #  https://github.com/osbuild/bootc-image-builder/issues/223
-            try:
-                os.symlink("/tmp", os.path.join(self.var, "tmp"))
-            except FileExistsError:
-                pass
 
             proc = os.path.join(self.tmp, "proc")
             os.makedirs(proc)
@@ -227,6 +221,10 @@ class BuildRoot(contextlib.AbstractContextManager):
         mounts += ["--tmpfs", "/tmp"]
         mounts += ["--bind", self.var, "/var"]
 
+        # Create a usable /var/tmp, see
+        #  https://github.com/osbuild/bootc-image-builder/issues/223
+        os.makedirs(os.path.join(self.var, "tmp"), 0o1777, exist_ok=True)
+
         # Setup API file-systems.
         mounts += ["--proc", "/proc"]
         mounts += ["--ro-bind", "/sys", "/sys"]
@@ -248,8 +246,8 @@ class BuildRoot(contextlib.AbstractContextManager):
                    os.path.join(self._rootdir, "ostree"),
                    "/ostree"]
         mounts += ["--ro-bind-try",
-                   os.path.join(self._rootdir, "etc/selinux/targeted/contexts"),
-                   "/etc/selinux/targeted/contexts"]
+                   os.path.join(self._rootdir, "etc/selinux/"),
+                   "/etc/selinux/"]
 
         # We execute our own modules by bind-mounting them from the host into
         # the build-root. We have minimal requirements on the build-root, so
diff --git a/osbuild/util/selinux.py b/osbuild/util/selinux.py
index a69a84ce..bf239e31 100644
--- a/osbuild/util/selinux.py
+++ b/osbuild/util/selinux.py
@@ -3,7 +3,7 @@
 import errno
 import os
 import subprocess
-from typing import Dict, TextIO
+from typing import Dict, List, Optional, TextIO
 
 # Extended attribute name for SELinux labels
 XATTR_NAME_SELINUX = b"security.selinux"
@@ -35,7 +35,7 @@ def config_get_policy(config: Dict[str, str]):
     return config.get('SELINUXTYPE', None)
 
 
-def setfiles(spec_file: str, root: str, *paths):
+def setfiles(spec_file: str, root: str, *paths, exclude_paths: Optional[List[str]] = None) -> None:
     """Initialize the security context fields for `paths`
 
     Initialize the security context fields (extended attributes)
@@ -43,10 +43,18 @@ def setfiles(spec_file: str, root: str, *paths):
     `root` argument determines the root path of the file system
     and the entries in `path` are interpreted as relative to it.
     Uses the setfiles(8) tool to actually set the contexts.
+    Paths can be excluded via the exclude_paths argument.
     """
+    if exclude_paths is None:
+        exclude_paths = []
+    exclude_paths_args = []
+    for p in exclude_paths:
+        exclude_paths_args.extend(["-e", p])
+
     for path in paths:
         subprocess.run(["setfiles", "-F",
                         "-r", root,
+                        *exclude_paths_args,
                         spec_file,
                         f"{root}{path}"],
                        check=True)
diff --git a/stages/org.osbuild.selinux b/stages/org.osbuild.selinux
index d9fe2c5a..cf5e850a 100755
--- a/stages/org.osbuild.selinux
+++ b/stages/org.osbuild.selinux
@@ -9,7 +9,10 @@ from osbuild.util import selinux
 
 def main(tree, options):
     file_contexts = os.path.join(f"{tree}", options["file_contexts"])
-    selinux.setfiles(file_contexts, os.fspath(tree), "")
+    exclude_paths = options.get("exclude_paths")
+    if exclude_paths:
+        exclude_paths = [os.path.join(tree, p.lstrip("/")) for p in exclude_paths]
+    selinux.setfiles(file_contexts, os.fspath(tree), "", exclude_paths=exclude_paths)
 
     labels = options.get("labels", {})
     for path, label in labels.items():
diff --git a/stages/org.osbuild.selinux.meta.json b/stages/org.osbuild.selinux.meta.json
index 57d31b62..02cf991c 100644
--- a/stages/org.osbuild.selinux.meta.json
+++ b/stages/org.osbuild.selinux.meta.json
@@ -27,6 +27,13 @@
                 "type": "string",
                 "description": "Path to the active SELinux policy's `file_contexts`"
             },
+            "exclude_paths": {
+                "type": "array",
+                "description": "Paths to exclude when setting labels via file_contexts",
+                "items": {
+                    "type": "string"
+                }
+            },
             "labels": {
                 "type": "object",
                 "description": "Labels to set of the specified files or folders",


Test patch:
diff --git a/schutzbot/manifest_tests.sh b/schutzbot/manifest_tests.sh
index aad8b49a..4700875c 100755
--- a/schutzbot/manifest_tests.sh
+++ b/schutzbot/manifest_tests.sh
@@ -3,8 +3,7 @@ set -euxo pipefail
 # Get OS details.
 source /etc/os-release
 ARCH=$(uname -m)
-DISTRO_CODE="${DISTRO_CODE:-${ID}-${VERSION_ID//./}}"
-
+DISTRO_CODE="${DISTRO_CODE:-${ID}-${VERSION_ID}}"
 # get manifest-db at the version specified for this arch+distro
 MANIFEST_DB_COMMIT=$(cat Schutzfile | jq -r '.global.dependencies."manifest-db".commit')
 MANIFEST_DB_REPO="https://github.com/osbuild/manifest-db"
diff --git a/stages/test/test_selinux.py b/stages/test/test_selinux.py
index 469d46d4..7e0987eb 100644
--- a/stages/test/test_selinux.py
+++ b/stages/test/test_selinux.py
@@ -26,6 +26,7 @@ def get_test_input(test_data, implicit_file_contexts=True):
     # good
     ({"labels": {"/usr/bin/cp": "system_u:object_r:install_exec_t:s0"}}, ""),
     ({"force_autorelabel": True}, ""),
+    ({"exclude_paths": ["/sysroot"]}, ""),
     # bad
     ({"file_contexts": 1234}, "1234 is not of type 'string'"),
     ({"labels": "xxx"}, "'xxx' is not of type 'object'"),
@@ -57,9 +58,23 @@ def test_selinux_file_contexts(mocked_setfiles, tmp_path, stage_module):
     stage_module.main(tmp_path, options)
 
     assert len(mocked_setfiles.call_args_list) == 1
-    assert mocked_setfiles.call_args_list == [
-        call(f"{tmp_path}/etc/selinux/thing", os.fspath(tmp_path), "")
-    ]
+    args, kwargs = mocked_setfiles.call_args_list[0]
+    assert args == (f"{tmp_path}/etc/selinux/thing", os.fspath(tmp_path), "")
+    assert kwargs == {"exclude_paths": None}
+
+
+@patch("osbuild.util.selinux.setfiles")
+def test_selinux_file_contexts_exclude(mocked_setfiles, tmp_path, stage_module):
+    options = {
+        "file_contexts": "etc/selinux/thing",
+        "exclude_paths": ["/sysroot"],
+    }
+    stage_module.main(tmp_path, options)
+
+    assert len(mocked_setfiles.call_args_list) == 1
+    args, kwargs = mocked_setfiles.call_args_list[0]
+    assert args == (f"{tmp_path}/etc/selinux/thing", os.fspath(tmp_path), "")
+    assert kwargs == {"exclude_paths": [f"{tmp_path}/sysroot"]}
 
 
 @patch("osbuild.util.selinux.setfilecon")
diff --git a/test/mod/test_buildroot.py b/test/mod/test_buildroot.py
index d7146338..ba3ed1cf 100644
--- a/test/mod/test_buildroot.py
+++ b/test/mod/test_buildroot.py
@@ -50,10 +50,9 @@ def test_basic(tempdir, runner):
         assert r.returncode != 0
 
         # Test that fs setup looks correct
-        r = root.run(["readlink", "-f", "/var/tmp"], monitor)
+        r = root.run(["test", "-d", "/var/tmp"], monitor)
         assert r.returncode == 0
-        assert r.stdout.strip().split("\n")[-1] == "/tmp"
-        r = root.run(["stat", "-L", "--format=%a", "/var/tmp"], monitor)
+        r = root.run(["stat", "--format=%a", "/var/tmp"], monitor)
         assert r.returncode == 0
         assert r.stdout.strip().split("\n")[-1] == "1777"
 
diff --git a/test/mod/test_util_selinux.py b/test/mod/test_util_selinux.py
index e147bcf8..4b1015ce 100644
--- a/test/mod/test_util_selinux.py
+++ b/test/mod/test_util_selinux.py
@@ -74,3 +74,16 @@ def test_selinux_setfiles(mocked_run, tmp_path):
             ["setfiles", "-F", "-r", os.fspath(tmp_path),
              "/etc/selinux/thing", os.fspath(tmp_path) + "/boot"], check=True),
     ]
+
+
+@mock.patch("subprocess.run")
+def test_selinux_setfiles_exclude(mocked_run, tmp_path):
+    selinux.setfiles("/etc/selinux/thing", os.fspath(tmp_path), "/", exclude_paths=["/sysroot", "/other/dir"])
+
+    assert len(mocked_run.call_args_list) == 1
+    assert mocked_run.call_args_list == [
+        mock.call(
+            ["setfiles", "-F", "-r", os.fspath(tmp_path),
+             "-e", "/sysroot", "-e", "/other/dir",
+             "/etc/selinux/thing", os.fspath(tmp_path) + "/"], check=True),
+    ]