Repository: osbuild/osbuild
Pull request #1911 (2024-10-23 10:43:27)

org.osbuild.selinux: support working on mounts
Required for: https://github.com/osbuild/osbuild/issues/1877

dustymabe: @achilleas-k @mvo5 mind a review?
nikita-dubrovskii: > This looks good to me, thank you! Tiny suggestion about details but feel free to ignore, they are really fine (but noticed them while reading)

Thx, tomorrow i'll update it.

Patch:
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ccadb86b..7dd514cc 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -52,6 +52,8 @@ RPM:
       - RUNNER:
           - aws/rhel-9.5-nightly-x86_64
           - aws/rhel-9.5-nightly-aarch64
+          - aws/rhel-9.6-nightly-x86_64
+          - aws/rhel-9.6-nightly-aarch64
           - aws/rhel-10.0-nightly-x86_64
           - aws/rhel-10.0-nightly-aarch64
         INTERNAL_NETWORK: "true"
diff --git a/Schutzfile b/Schutzfile
index 6c1e4f8c..1f118bbb 100644
--- a/Schutzfile
+++ b/Schutzfile
@@ -19,7 +19,7 @@
           {
             "title": "fedora-updates",
             "name": "fedora-updates",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f39/f39-x86_64-updates-released-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f39/f39-x86_64-updates-released-20241107"
           }
         ],
         "aarch64": [
@@ -31,7 +31,7 @@
           {
             "title": "fedora-updates",
             "name": "fedora-updates",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f39/f39-aarch64-updates-released-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f39/f39-aarch64-updates-released-20241107"
           }
         ]
       }
@@ -50,7 +50,7 @@
           {
             "title": "fedora-updates",
             "name": "fedora-updates",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f40/f40-x86_64-updates-released-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f40/f40-x86_64-updates-released-20241107"
           }
         ],
         "aarch64": [
@@ -62,7 +62,7 @@
           {
             "title": "fedora-updates",
             "name": "fedora-updates",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f40/f40-aarch64-updates-released-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f40/f40-aarch64-updates-released-20241107"
           }
         ]
       }
@@ -97,34 +97,34 @@
           {
             "title": "baseos",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-x86_64-baseos-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-x86_64-baseos-20241107"
           },
           {
             "title": "appstream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-x86_64-appstream-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-x86_64-appstream-20241107"
           },
           {
             "title": "crb",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-x86_64-crb-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-x86_64-crb-20241107"
           }
         ],
         "aarch64": [
           {
             "title": "baseos",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-aarch64-baseos-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-aarch64-baseos-20241107"
           },
           {
             "title": "appstream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-aarch64-appstream-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-aarch64-appstream-20241107"
           },
           {
             "title": "crb",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-aarch64-crb-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-aarch64-crb-20241107"
           }
         ]
       }
@@ -138,34 +138,34 @@
           {
             "title": "baseos",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-x86_64-baseos-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-x86_64-baseos-20241107"
           },
           {
             "title": "appstream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-x86_64-appstream-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-x86_64-appstream-20241107"
           },
           {
             "title": "crb",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-x86_64-crb-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-x86_64-crb-20241107"
           }
         ],
         "aarch64": [
           {
             "title": "baseos",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-aarch64-baseos-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-aarch64-baseos-20241107"
           },
           {
             "title": "appstream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-aarch64-appstream-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-aarch64-appstream-20241107"
           },
           {
             "title": "crb",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-aarch64-crb-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-aarch64-crb-20241107"
           }
         ]
       }
@@ -179,34 +179,75 @@
           {
             "title": "RHEL-9-RPMREPO-NIGHTLY-BaseOS",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-baseos-n9.5-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-baseos-n9.5-20241107"
           },
           {
             "title": "RHEL-9-RPMREPO-NIGHTLY-AppStream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-appstream-n9.5-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-appstream-n9.5-20241107"
           },
           {
             "title": "RHEL-9-RPMREPO-NIGHTLY-CRB",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-crb-n9.5-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-crb-n9.5-20241107"
           }
         ],
         "aarch64": [
           {
             "title": "RHEL-9-RPMREPO-NIGHTLY-BaseOS",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-baseos-n9.5-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-baseos-n9.5-20241107"
           },
           {
             "title": "RHEL-9-RPMREPO-NIGHTLY-AppStream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-appstream-n9.5-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-appstream-n9.5-20241107"
           },
           {
             "title": "RHEL-9-RPMREPO-NIGHTLY-CRB",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-crb-n9.5-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-crb-n9.5-20241107"
+          }
+        ]
+      }
+    ]
+  },
+  "rhel-9.6": {
+    "repos": [
+      {
+        "file": "/etc/yum.repos.d/rhel9internal.repo",
+        "x86_64": [
+          {
+            "title": "RHEL-9-RPMREPO-NIGHTLY-BaseOS",
+            "name": "baseos",
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-baseos-n9.6-20241101"
+          },
+          {
+            "title": "RHEL-9-RPMREPO-NIGHTLY-AppStream",
+            "name": "appstream",
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-appstream-n9.6-20241101"
+          },
+          {
+            "title": "RHEL-9-RPMREPO-NIGHTLY-CRB",
+            "name": "crb",
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-crb-n9.6-20241101"
+          }
+        ],
+        "aarch64": [
+          {
+            "title": "RHEL-9-RPMREPO-NIGHTLY-BaseOS",
+            "name": "baseos",
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-baseos-n9.6-20241101"
+          },
+          {
+            "title": "RHEL-9-RPMREPO-NIGHTLY-AppStream",
+            "name": "appstream",
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-appstream-n9.6-20241101"
+          },
+          {
+            "title": "RHEL-9-RPMREPO-NIGHTLY-CRB",
+            "name": "crb",
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-crb-n9.6-20241101"
           }
         ]
       }
@@ -220,37 +261,37 @@
           {
             "title": "RHEL-10-RPMREPO-NIGHTLY-BaseOS",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-baseos-n10.0-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-baseos-n10.0-20241107"
           },
           {
             "title": "RHEL-10-RPMREPO-NIGHTLY-AppStream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-appstream-n10.0-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-appstream-n10.0-20241107"
           },
           {
             "title": "RHEL-10-RPMREPO-NIGHTLY-CRB",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-crb-n10.0-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-crb-n10.0-20241107"
           }
         ],
         "aarch64": [
           {
             "title": "RHEL-10-RPMREPO-NIGHTLY-BaseOS",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-baseos-n10.0-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-baseos-n10.0-20241107"
           },
           {
             "title": "RHEL-10-RPMREPO-NIGHTLY-AppStream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-appstream-n10.0-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-appstream-n10.0-20241107"
           },
           {
             "title": "RHEL-10-RPMREPO-NIGHTLY-CRB",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-crb-n10.0-20240918"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-crb-n10.0-20241107"
           }
         ]
       }
     ]
   }
-}
\ No newline at end of file
+}
diff --git a/osbuild/util/parsing.py b/osbuild/util/parsing.py
index f8fb2768..d6d16f22 100644
--- a/osbuild/util/parsing.py
+++ b/osbuild/util/parsing.py
@@ -2,7 +2,7 @@
 
 import os
 import re
-from typing import Dict, Union
+from typing import Dict, Tuple, Union
 from urllib.parse import ParseResult, urlparse
 
 
@@ -72,16 +72,20 @@ def parse_input(url: ParseResult, args: Dict) -> os.PathLike:
     return root
 
 
-def parse_location(location: str, args: Dict) -> str:
+def parse_location_into_parts(location: str, args: Dict) -> Tuple[str, str]:
     """
-    Parses the location URL to derive the corresponding file path.
+    Parses the location URL to derive the corresponding root and url path.
 
     Parameters:
-    - location (str): The location URL to be parsed.
+    - location (str): The location URL to be parsed. If the URL has no scheme,
+    then 'tree://' is implied
     - args (Dict): A dictionary containing arguments including mounts and
     path information as passed by osbuild.api.arguments()
     """
 
+    if "://" not in location:
+        location = f"tree://{location}"
+
     url = urlparse(location)
 
     scheme = url.scheme
@@ -97,11 +101,24 @@ def parse_location(location: str, args: Dict) -> str:
     if not url.path.startswith("/"):
         raise ValueError(f"url.path from location must start with '/', got: {url.path}")
 
-    path = os.path.relpath(url.path, "/")
+    return root, url.path
+
+
+def parse_location(location: str, args: Dict) -> str:
+    """
+    Parses the location URL to derive the corresponding file path.
+
+    Parameters:
+    - location (str): The location URL to be parsed.
+    - args (Dict): A dictionary containing arguments including mounts and
+    path information as passed by osbuild.api.arguments()
+    """
+
+    root, urlpath = parse_location_into_parts(location, args)
+    path = os.path.relpath(urlpath, "/")
     path = os.path.join(root, path)
     path = os.path.normpath(path)
-
-    if url.path.endswith("/"):
+    if urlpath.endswith("/"):
         path = os.path.join(path, ".")
 
     return path
diff --git a/schutzbot/terraform b/schutzbot/terraform
index a1ac8695..2ae0b1ea 100644
--- a/schutzbot/terraform
+++ b/schutzbot/terraform
@@ -1,1 +1,1 @@
-961f6cb4e780da441964297df7e0e6b7e7ca4cf1
+9c4a3c9573208bf66d8c91a95ee759a39ed0a556
diff --git a/stages/org.osbuild.mkdir b/stages/org.osbuild.mkdir
index d2d11a7a..01f5f431 100755
--- a/stages/org.osbuild.mkdir
+++ b/stages/org.osbuild.mkdir
@@ -15,12 +15,9 @@ def main(args):
         parents = item.get("parents", False)
         exist_ok = item.get("exist_ok", False)
 
-        if "://" not in path:
-            if not path.startswith("/"):
-                print("WARNING: relative path used, this is discouraged!")
-                path = f"tree:///{path}"
-            else:
-                path = f"tree://{path}"
+        if "://" not in path and not path.startswith("/"):
+            print("WARNING: relative path used, this is discouraged!")
+            path = f"tree:///{path}"
 
         target = parsing.parse_location(path, args)
         if parents:
diff --git a/stages/org.osbuild.selinux b/stages/org.osbuild.selinux
index 563d827b..8e25a281 100755
--- a/stages/org.osbuild.selinux
+++ b/stages/org.osbuild.selinux
@@ -4,26 +4,34 @@ import pathlib
 import sys
 
 import osbuild.api
-from osbuild.util import selinux
+from osbuild.util import parsing, selinux
 
 
-def main(tree, options):
+def main(args):
+    # Get the path where the tree is
+    tree = args["tree"]
+    options = args["options"]
     file_contexts = options.get("file_contexts")
     exclude_paths = options.get("exclude_paths")
+    target = options.get("target", "tree:///")
+    root, target = parsing.parse_location_into_parts(target, args)
 
     if file_contexts:
-        file_contexts = os.path.join(f"{tree}", options["file_contexts"])
+        if "://" not in file_contexts:
+            file_contexts = os.path.normpath(f"{tree}/{file_contexts}")
+        else:
+            file_contexts = parsing.parse_location(file_contexts, args)
         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)
+            exclude_paths = [os.path.normpath(f"{root}/{target}/{p}") for p in exclude_paths]
+        selinux.setfiles(file_contexts, os.path.normpath(root), target, exclude_paths=exclude_paths)
 
     labels = options.get("labels", {})
     for path, label in labels.items():
-        fullpath = os.path.join(tree, path.lstrip("/"))
+        fullpath = parsing.parse_location(path, args)
         selinux.setfilecon(fullpath, label)
 
     if options.get("force_autorelabel", False):
-        stamp = pathlib.Path(tree, ".autorelabel")
+        stamp = pathlib.Path(root, ".autorelabel")
         # Creating just empty /.autorelabel resets only the type of files.
         # To ensure that the full context is reset, we write "-F" into the file.
         # This mimics the behavior of `fixfiles -F boot`. The "-F" option is
@@ -34,6 +42,5 @@ def main(tree, options):
 
 
 if __name__ == '__main__':
-    args = osbuild.api.arguments()
-    r = main(args["tree"], args["options"])
+    r = main(osbuild.api.arguments())
     sys.exit(r)
diff --git a/stages/org.osbuild.selinux.meta.json b/stages/org.osbuild.selinux.meta.json
index 30dbddae..9a9d7bb1 100644
--- a/stages/org.osbuild.selinux.meta.json
+++ b/stages/org.osbuild.selinux.meta.json
@@ -1,8 +1,8 @@
 {
   "summary": "Set SELinux file contexts",
   "description": [
-    "Sets correct SELinux labels for every file in the tree, according to the",
-    "SELinux policy installed inside the tree.",
+    "Sets correct SELinux labels for every file in the tree or on mount, according to",
+    "the SELinux policy.",
     "Uses the host's `setfiles` program and the tree's `file_contexts`, usually",
     "    /etc/selinux/<SELINUXTYPE>/contexts/files/file_contexts",
     "where <SELINUXTYPE> is the value set in /etc/selinux/config (usually \"targeted\"",
@@ -33,9 +33,15 @@
         }
       ],
       "properties": {
-        "file_contexts": {
+        "target": {
           "type": "string",
-          "description": "Path to the active SELinux policy's `file_contexts`"
+          "description": "Target path in the tree or on a mount",
+          "pattern": "^mount://[^/]+/|^tree:///",
+          "default": "tree:///"
+        },
+        "file_contexts": {
+          "description": "Path to the active SELinux policy's `file_contexts`. Supports `tree://`, `mount://`, and `input://` schemes. Plain paths imply `tree://`.",
+          "type": "string"
         },
         "exclude_paths": {
           "type": "array",
@@ -53,7 +59,7 @@
         },
         "force_autorelabel": {
           "type": "boolean",
-          "description": "Do not use. Forces auto-relabelling on first boot.",
+          "description": "Do not use. Forces auto-relabelling on first boot. Affects target's root or tree:/// by default",
           "default": false
         }
       }
@@ -64,6 +70,10 @@
     },
     "mounts": {
       "type": "array"
+    },
+    "inputs": {
+      "type": "object",
+      "additionalProperties": true
     }
   }
 }
diff --git a/tools/osbuild-mpp b/tools/osbuild-mpp
index eb610be2..f861ba94 100755
--- a/tools/osbuild-mpp
+++ b/tools/osbuild-mpp
@@ -1673,6 +1673,8 @@ class ManifestFileV2(ManifestFile):
         if not mpp:
             return
 
+        self._process_format(mpp)
+
         refs = element_enter(inputs_images, "references", {})
         manifest_lists = []
 


Test patch:
diff --git a/stages/test/test_selinux.py b/stages/test/test_selinux.py
index 7cb51bcd..9a1e01f2 100644
--- a/stages/test/test_selinux.py
+++ b/stages/test/test_selinux.py
@@ -3,7 +3,7 @@
 import os.path
 from unittest.mock import call, patch
 
-import pytest
+import pytest  # type: ignore
 
 from osbuild import testutil
 
@@ -29,7 +29,11 @@ def get_test_input(test_data, file_contexts=False, labels=False):
     ({"labels": {"/usr/bin/cp": "system_u:object_r:install_exec_t:s0"}}, ""),
     ({"force_autorelabel": True}, ""),
     ({"exclude_paths": ["/sysroot"]}, ""),
+    ({"target": "mount://disk/boot/efi"}, ""),
+    ({"target": "tree:///boot/efi"}, ""),
+    ({"file_contexts": "path/to/file_contexts"}, ""),
     # bad
+    ({"target": "/boot/efi"}, "'/boot/efi' does not match '^mount://[^/]+/|^tree:///'"),
     ({"file_contexts": 1234}, "1234 is not of type 'string'"),
     ({"labels": "xxx"}, "'xxx' is not of type 'object'"),
     ({"force_autorelabel": "foo"}, "'foo' is not of type 'boolean'"),
@@ -55,51 +59,144 @@ def test_schema_validation_selinux_required_options(stage_schema):
 
 
 @patch("osbuild.util.selinux.setfiles")
-def test_selinux_file_contexts(mocked_setfiles, tmp_path, stage_module):
-    options = {
-        "file_contexts": "etc/selinux/thing",
+def test_selinux_file_contexts_schemeless_relpath(mocked_setfiles, tmp_path, stage_module):
+    tree = tmp_path / "tree"
+
+    args = {
+        "tree": tree,
+        "options": {
+            "file_contexts": "etc/selinux/thing",
+        }
+    }
+    stage_module.main(args)
+
+    assert len(mocked_setfiles.call_args_list) == 1
+    args, kwargs = mocked_setfiles.call_args_list[0]
+    assert args == (f"{tree}/etc/selinux/thing", os.fspath(tree), "/")
+    assert kwargs == {"exclude_paths": None}
+
+
+@patch("osbuild.util.selinux.setfiles")
+def test_selinux_file_contexts_schemeless_abspath(mocked_setfiles, tmp_path, stage_module):
+    tree = tmp_path / "tree"
+
+    args = {
+        "tree": tree,
+        "options": {
+            "file_contexts": "/etc/selinux/thing",
+        }
     }
-    stage_module.main(tmp_path, options)
+    stage_module.main(args)
 
     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 args == (f"{tree}/etc/selinux/thing", os.fspath(tree), "/")
+    assert kwargs == {"exclude_paths": None}
+
+
+@patch("osbuild.util.selinux.setfiles")
+def test_selinux_file_contexts_mounts(mocked_setfiles, tmp_path, stage_module):
+    tree = tmp_path / "tree"
+    mounts = tmp_path / "mounts"
+    root = mounts / "."
+
+    args = {
+        "tree": tree,
+        "options": {
+            "file_contexts": "mount://root/etc/selinux/file_contexts",
+            "target": "mount://root/"
+        },
+        "paths": {
+            "mounts": mounts,
+        },
+        "mounts": {
+            "root": {
+                "path": root
+            }
+        }
+    }
+    stage_module.main(args)
+
+    assert len(mocked_setfiles.call_args_list) == 1
+    args, kwargs = mocked_setfiles.call_args_list[0]
+    assert args == (f"{mounts}/etc/selinux/file_contexts", f"{mounts}", "/")
     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"],
+    tree = tmp_path / "tree"
+
+    args = {
+        "tree": tree,
+        "options": {
+            "file_contexts": "etc/selinux/thing",
+            "exclude_paths": ["/sysroot"],
+        }
     }
-    stage_module.main(tmp_path, options)
+    stage_module.main(args)
 
     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"]}
+    assert args == (f"{tree}/etc/selinux/thing", os.fspath(tree), "/")
+    assert kwargs == {"exclude_paths": [f"{tree}/sysroot"]}
 
 
 @patch("osbuild.util.selinux.setfilecon")
 @patch("osbuild.util.selinux.setfiles")
 def test_selinux_labels(mocked_setfiles, mocked_setfilecon, tmp_path, stage_module):
+    tree = tmp_path / "tree"
     testutil.make_fake_input_tree(tmp_path, {
-        "/usr/bin/bootc": "I'm only an imposter",
+        "/usr/bin/echo": "I'm only an imposter",
+        "/sbin/sulogin": "I'm only an imposter",
     })
 
     options = {
         "file_contexts": "etc/selinux/thing",
         "labels": {
-            "/tree/usr/bin/bootc": "system_u:object_r:install_exec_t:s0",
+            "tree:///usr/bin/echo": "system_u:object_r:bin_t:s0",
+            "/sbin/sulogin": "system_u:object_r:sulogin_exec_t:s0",
         }
     }
-    stage_module.main(tmp_path, options)
+    args = {
+        "tree": tree,
+        "options": options
+    }
+    stage_module.main(args)
 
     assert len(mocked_setfiles.call_args_list) == 1
+    assert len(mocked_setfilecon.call_args_list) == 2
+    assert mocked_setfilecon.call_args_list == [
+        call(f"{tree}/usr/bin/echo", "system_u:object_r:bin_t:s0"),
+        call(f"{tree}/sbin/sulogin", "system_u:object_r:sulogin_exec_t:s0"),
+    ]
+
+
+@patch("osbuild.util.selinux.setfilecon")
+def test_selinux_labels_mount(mocked_setfilecon, tmp_path, stage_module):
+    tree = tmp_path / "tree"
+    mounts = tmp_path / "mounts"
+
+    testutil.make_fake_tree(mounts, {"/sbin/su": "I'm only an imposter"})
+    args = {
+        "tree": tree,
+        "options": {
+            "labels": {
+                "mount://root/sbin/su": "system_u:object_r:su_exec_t:s0",
+            }
+        },
+        "paths": {
+            "mounts": mounts,
+        },
+        "mounts": {
+            "root": {"path": mounts}
+        }
+    }
+    stage_module.main(args)
+
     assert len(mocked_setfilecon.call_args_list) == 1
     assert mocked_setfilecon.call_args_list == [
-        call(f"{tmp_path}/tree/usr/bin/bootc", "system_u:object_r:install_exec_t:s0"),
+        call(f"{mounts}/sbin/su", "system_u:object_r:su_exec_t:s0"),
     ]
 
 
@@ -110,7 +207,11 @@ def test_selinux_force_autorelabel(mocked_setfiles, tmp_path, stage_module):  #
             "file_contexts": "etc/selinux/thing",
             "force_autorelabel": enable_autorelabel,
         }
-        stage_module.main(tmp_path, options)
+        args = {
+            "tree": f"{tmp_path}",
+            "options": options
+        }
+        stage_module.main(args)
 
         assert (tmp_path / ".autorelabel").exists() == enable_autorelabel
         if enable_autorelabel:
diff --git a/test/mod/test_util_parsing.py b/test/mod/test_util_parsing.py
index e501310c..8284b4c1 100644
--- a/test/mod/test_util_parsing.py
+++ b/test/mod/test_util_parsing.py
@@ -48,18 +48,31 @@ def test_parse_location_mounts():
             "root": {
                 "path": "/run/osbuild/mounts/.",
             },
+            "boot": {
+                "path": "/run/osbuild/mounts/boot"
+            }
         },
     }
     location = "mount://root/"
+    root, path = parsing.parse_location_into_parts(location, args)
+    assert [root, path] == ["/run/osbuild/mounts/.", "/"]
     path = parsing.parse_location(location, args)
     assert path == "/run/osbuild/mounts/."
 
+    location = "mount://boot/efi/EFI/Linux"
+    root, path = parsing.parse_location_into_parts(location, args)
+    assert [root, path] == ["/run/osbuild/mounts/boot", "/efi/EFI/Linux"]
+    path = parsing.parse_location(location, args)
+    assert path == "/run/osbuild/mounts/boot/efi/EFI/Linux"
+
 
 def test_parse_location_tree():
     args = {
         "tree": "/run/osbuild/tree",
     }
     location = "tree:///disk.img"
+    root, path = parsing.parse_location_into_parts(location, args)
+    assert [root, path] == ["/run/osbuild/tree", "/disk.img"]
     path = parsing.parse_location(location, args)
     assert path == "/run/osbuild/tree/disk.img"
 
@@ -73,5 +86,41 @@ def test_parse_location_inputs():
         },
     }
     location = "input://tree/"
+    root, path = parsing.parse_location_into_parts(location, args)
+    assert [root, path] == ["/run/osbuild/inputs/tree", "/"]
     path = parsing.parse_location(location, args)
     assert path == "/run/osbuild/inputs/tree/."
+
+
+def test_parse_location_abspath():
+    args = {
+        "tree": "/run/osbuild/tree",
+    }
+    location = "/path/to/file"
+    root, path = parsing.parse_location_into_parts(location, args)
+    assert [root, path] == ["/run/osbuild/tree", "/path/to/file"]
+    path = parsing.parse_location(location, args)
+    assert path == "/run/osbuild/tree/path/to/file"
+
+
+def test_parse_location_badpath():
+    args = {
+        "tree": "/run/osbuild/tree",
+        "paths": {
+            "mounts": "/run/osbuild/mounts",
+        },
+        "mounts": {
+            "root": {
+                "path": "/run/osbuild/mounts/."
+            }
+        },
+    }
+    location = "file"
+    with pytest.raises(ValueError) as ex:
+        parsing.parse_location_into_parts(location, args)
+    assert "url.path from location must start with '/'" in str(ex.value)
+
+    location = "mount://disk/file"
+    with pytest.raises(ValueError) as ex:
+        parsing.parse_location_into_parts(location, args)
+    assert "Unknown mount" in str(ex.value)