overlay coreos/user-patches: Regenerate patches for sys-apps/systemd

Signed-off-by: Krzesimir Nowak <knowak@microsoft.com>
This commit is contained in:
Krzesimir Nowak 2026-04-01 15:55:59 +02:00
parent 281c8f3bb1
commit d97ab840cd
23 changed files with 630 additions and 1549 deletions

View File

@ -1,7 +1,7 @@
From 6055d8b50c4a39d3e5f4fa0cf017a3b04786c5ba Mon Sep 17 00:00:00 2001
From 3e713e019ab2e13e0d48bf30bab0ddaf3573458d Mon Sep 17 00:00:00 2001
From: David Michael <dm0@redhat.com>
Date: Tue, 16 Apr 2019 02:44:51 +0000
Subject: [PATCH 01/20] wait-online: set --any by default
Subject: [PATCH 01/14] wait-online: set --any by default
The systemd-networkd-wait-online command would normally continue
waiting after a network interface is usable if other interfaces are

View File

@ -1,7 +1,7 @@
From 5bff53a23228b10d93d342510f0ffd41185e3011 Mon Sep 17 00:00:00 2001
From d34fa493e6d69b97633e329d55413a549da8239d Mon Sep 17 00:00:00 2001
From: Alex Crawford <alex.crawford@coreos.com>
Date: Wed, 2 Mar 2016 10:46:33 -0800
Subject: [PATCH 02/20] needs-update: don't require strictly newer usr
Subject: [PATCH 02/14] needs-update: don't require strictly newer usr
Updates should be triggered whenever usr changes, not only when it is newer.
---
@ -23,7 +23,7 @@ index d9d78262a1..761bbdecca 100644
This requires that updates to <filename>/usr/</filename> are always
followed by an update of the modification time of
diff --git a/src/shared/condition.c b/src/shared/condition.c
index b09eff1bfb..3a170b1820 100644
index 15e3ee9840..381378e77a 100644
--- a/src/shared/condition.c
+++ b/src/shared/condition.c
@@ -817,7 +817,7 @@ static int condition_test_needs_update(Condition *c, char **env) {

View File

@ -1,7 +1,7 @@
From df56cf2ad0c6c84a22e9fca8893c610b82b78377 Mon Sep 17 00:00:00 2001
From 2cc519ebec4f01f76bcdcde61259ba23a810ea30 Mon Sep 17 00:00:00 2001
From: Adrian Vladu <avladu@cloudbasesolutions.com>
Date: Fri, 16 Feb 2024 11:22:08 +0000
Subject: [PATCH 03/20] core: use max for DefaultTasksMax
Subject: [PATCH 03/14] core: use max for DefaultTasksMax
Since systemd v228, systemd has a DefaultTasksMax which defaulted
to 512, later 15% of the system's maximum number of PIDs. This
@ -21,7 +21,7 @@ Signed-off-by: Adrian Vladu <avladu@cloudbasesolutions.com>
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/man/systemd-system.conf.xml b/man/systemd-system.conf.xml
index cf5a3612f6..a0f9f8ba57 100644
index b7fe53dc9c..175fe67139 100644
--- a/man/systemd-system.conf.xml
+++ b/man/systemd-system.conf.xml
@@ -227,7 +227,7 @@
@ -34,10 +34,10 @@ index cf5a3612f6..a0f9f8ba57 100644
Kernel has a default value for <varname>kernel.pid_max=</varname> and an algorithm of counting in case of more than 32 cores.
For example, with the default <varname>kernel.pid_max=</varname>, <varname>DefaultTasksMax=</varname> defaults to 4915,
diff --git a/src/core/manager.c b/src/core/manager.c
index 20a535f2f4..be1c352045 100644
index a5a51023c5..ef0ce9e31d 100644
--- a/src/core/manager.c
+++ b/src/core/manager.c
@@ -112,7 +112,7 @@
@@ -113,7 +113,7 @@
/* How many units and jobs to process of the bus queue before returning to the event loop. */
#define MANAGER_BUS_MESSAGE_BUDGET 100U

View File

@ -1,7 +1,7 @@
From 38ef166d85928d1f806bc48f3d29f45563d1abde Mon Sep 17 00:00:00 2001
From a8c18ecc95e15af2d669649115826430698dcc5d Mon Sep 17 00:00:00 2001
From: Matthew Garrett <mjg59@coreos.com>
Date: Tue, 20 Dec 2016 16:43:22 +0000
Subject: [PATCH 04/20] systemd: Disable SELinux permissions checks
Subject: [PATCH 04/14] systemd: Disable SELinux permissions checks
We don't care about the interaction between systemd and SELinux policy, so
let's just disable these checks rather than having to incorporate policy
@ -12,7 +12,7 @@ to limit containers and not anything running directly on the host.
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/core/selinux-access.c b/src/core/selinux-access.c
index 8ccc31630d..34e9cebee8 100644
index 7457b3d456..82afe343dd 100644
--- a/src/core/selinux-access.c
+++ b/src/core/selinux-access.c
@@ -2,7 +2,7 @@
@ -22,8 +22,8 @@ index 8ccc31630d..34e9cebee8 100644
-#if HAVE_SELINUX
+#if 0
#include <selinux/avc.h>
#include <selinux/selinux.h>
#include <unistd.h>
--
2.52.0

View File

@ -1,7 +1,7 @@
From 4e071bef0713099cfe2540a5576744c0e5c41723 Mon Sep 17 00:00:00 2001
From 33a603bb00fce6e4c3b4faf80157e8532932fb00 Mon Sep 17 00:00:00 2001
From: Sayan Chowdhury <schowdhury@microsoft.com>
Date: Fri, 16 Dec 2022 16:28:26 +0530
Subject: [PATCH 05/20] Revert "getty: Pass tty to use by agetty via stdin"
Subject: [PATCH 05/14] Revert "getty: Pass tty to use by agetty via stdin"
This reverts commit b4bf9007cbee7dc0b1356897344ae2a7890df84c.
@ -17,17 +17,17 @@ Signed-off-by: Sayan Chowdhury <schowdhury@microsoft.com>
4 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/units/console-getty.service.in b/units/console-getty.service.in
index 967d8337ab..1f2d8b910f 100644
index 278048724f..5731e68d8f 100644
--- a/units/console-getty.service.in
+++ b/units/console-getty.service.in
@@ -20,12 +20,12 @@ Before=getty.target
ConditionPathExists=/dev/console
[Service]
-ExecStart=-/sbin/agetty --noreset --noclear --issue-file=/etc/issue:/etc/issue.d:/run/issue.d:/usr/lib/issue.d --keep-baud 115200,57600,38400,9600 - ${TERM}
-ExecStart=-{{AGETTY}} --noreset --noclear --issue-file=/etc/issue:/etc/issue.d:/run/issue.d:/usr/lib/issue.d --keep-baud 115200,57600,38400,9600 - ${TERM}
+# The '-o' option value tells agetty to replace 'login' arguments with '--' for
+# safety, and then the entered username.
+ExecStart=-/sbin/agetty -o '-- \\u' --noreset --noclear --keep-baud 115200,57600,38400,9600 console ${TERM}
+ExecStart=-{{AGETTY}} -o '-- \\u' --noreset --noclear --keep-baud 115200,57600,38400,9600 console ${TERM}
Type=idle
Restart=always
UtmpIdentifier=cons
@ -37,17 +37,17 @@ index 967d8337ab..1f2d8b910f 100644
TTYReset=yes
TTYVHangup=yes
diff --git a/units/container-getty@.service.in b/units/container-getty@.service.in
index e0b27613df..5f27653d1f 100644
index 18e5a98a7f..568fcd1e53 100644
--- a/units/container-getty@.service.in
+++ b/units/container-getty@.service.in
@@ -25,13 +25,13 @@ Conflicts=rescue.service
Before=rescue.service
[Service]
-ExecStart=-/sbin/agetty --noreset --noclear --issue-file=/etc/issue:/etc/issue.d:/run/issue.d:/usr/lib/issue.d - ${TERM}
-ExecStart=-{{AGETTY}} --noreset --noclear --issue-file=/etc/issue:/etc/issue.d:/run/issue.d:/usr/lib/issue.d - ${TERM}
+# The '-o' option value tells agetty to replace 'login' arguments with '--' for
+# safety, and then the entered username.
+ExecStart=-/sbin/agetty -o '-- \\u' --noreset --noclear pts/%I ${TERM}
+ExecStart=-{{AGETTY}} -o '-- \\u' --noreset --noclear pts/%I ${TERM}
Type=idle
Restart=always
RestartSec=0
@ -58,17 +58,17 @@ index e0b27613df..5f27653d1f 100644
TTYReset=yes
TTYVHangup=yes
diff --git a/units/getty@.service.in b/units/getty@.service.in
index 104c4acc96..1819627d1c 100644
index 15f1a572fd..a3285d956e 100644
--- a/units/getty@.service.in
+++ b/units/getty@.service.in
@@ -34,13 +34,13 @@ Before=rescue.service
ConditionPathExists=/dev/tty0
[Service]
-ExecStart=-/sbin/agetty --noreset --noclear --issue-file=/etc/issue:/etc/issue.d:/run/issue.d:/usr/lib/issue.d - ${TERM}
-ExecStart=-{{AGETTY}} --noreset --noclear --issue-file=/etc/issue:/etc/issue.d:/run/issue.d:/usr/lib/issue.d - ${TERM}
+# The '-o' option value tells agetty to replace 'login' arguments with '--' for
+# safety, and then the entered username.
+ExecStart=-/sbin/agetty -o '-- \\u' --noreset --noclear %I ${TERM}
+ExecStart=-{{AGETTY}} -o '-- \\u' --noreset --noclear %I ${TERM}
Type=idle
Restart=always
RestartSec=0
@ -79,17 +79,17 @@ index 104c4acc96..1819627d1c 100644
TTYReset=yes
TTYVHangup=yes
diff --git a/units/serial-getty@.service.in b/units/serial-getty@.service.in
index 0134c83d48..ba4cbc0edb 100644
index 8b5a63d681..29ab8a0533 100644
--- a/units/serial-getty@.service.in
+++ b/units/serial-getty@.service.in
@@ -30,12 +30,12 @@ Conflicts=rescue.service
Before=rescue.service
[Service]
-ExecStart=-/sbin/agetty --noreset --noclear --issue-file=/etc/issue:/etc/issue.d:/run/issue.d:/usr/lib/issue.d --keep-baud 115200,57600,38400,9600 - ${TERM}
-ExecStart=-{{AGETTY}} --noreset --noclear --issue-file=/etc/issue:/etc/issue.d:/run/issue.d:/usr/lib/issue.d --keep-baud 115200,57600,38400,9600 - ${TERM}
+# The '-o' option value tells agetty to replace 'login' arguments with '--' for
+# safety, and then the entered username.
+ExecStart=-/sbin/agetty -o '-- \\u' --noreset --noclear --keep-baud 115200,57600,38400,9600 %I ${TERM}
+ExecStart=-{{AGETTY}} -o '-- \\u' --noreset --noclear --keep-baud 115200,57600,38400,9600 %I ${TERM}
Type=idle
Restart=always
UtmpIdentifier=%I

View File

@ -1,7 +1,7 @@
From b097e139801009d722c33a9580bcda23a4a7a1e1 Mon Sep 17 00:00:00 2001
From 6c83b73ac087aaa1f08551c064cbac119ad92490 Mon Sep 17 00:00:00 2001
From: Adrian Vladu <avladu@cloudbasesolutions.com>
Date: Fri, 16 Feb 2024 11:29:04 +0000
Subject: [PATCH 06/20] units: Keep using old journal file format
Subject: [PATCH 06/14] units: Keep using old journal file format
Systemd 252 made an incompatible change in journal file format. Temporarily
force journald to use the old journal format to give logging containers more

View File

@ -1,7 +1,7 @@
From 0ba9b9356861f8012c0e7794d9c61ebf21a9c6d7 Mon Sep 17 00:00:00 2001
From 9d6db023c34d96b582e763da77c464629266f8e8 Mon Sep 17 00:00:00 2001
From: Krzesimir Nowak <knowak@microsoft.com>
Date: Wed, 22 Oct 2025 10:39:42 +0200
Subject: [PATCH 07/20] tmpfiles.d: Fix DNS issues with default k8s
Subject: [PATCH 07/14] tmpfiles.d: Fix DNS issues with default k8s
configuration
The Kubelet takes /etc/resolv.conf for, e.g., CoreDNS which has dnsPolicy

View File

@ -1,7 +1,7 @@
From b3430348f5ae93251076fb4e3b4aecbfa02513b5 Mon Sep 17 00:00:00 2001
From bbc8ff28a7f0208b6b1020d0d2ca900cdfb5a42f Mon Sep 17 00:00:00 2001
From: Krzesimir Nowak <knowak@microsoft.com>
Date: Fri, 24 Oct 2025 11:06:57 +0200
Subject: [PATCH 08/20] units: Make multi-user.target the default target
Subject: [PATCH 08/14] units: Make multi-user.target the default target
Signed-off-by: Krzesimir Nowak <knowak@microsoft.com>
---
@ -9,7 +9,7 @@ Signed-off-by: Krzesimir Nowak <knowak@microsoft.com>
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/units/meson.build b/units/meson.build
index 4f47a3b2bd..63940a72be 100644
index 12cf47d797..58de76dca9 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -47,10 +47,7 @@ units = [
@ -33,9 +33,9 @@ index 4f47a3b2bd..63940a72be 100644
+ 'file' : 'multi-user.target',
+ 'symlinks' : ['default.target'],
+ },
{ 'file' : 'network-online.target' },
{ 'file' : 'network-pre.target' },
{ 'file' : 'network.target' },
{
'file' : 'systemd-mute-console.socket',
'symlinks' : ['sockets.target.wants/']
--
2.52.0

View File

@ -1,7 +1,7 @@
From 530ffcd9e3212e0c93002e752b682dd41a8889b1 Mon Sep 17 00:00:00 2001
From b6e987ebcc0efd6ea5a68582fe194650ebc89636 Mon Sep 17 00:00:00 2001
From: Kai Lueke <kailuke@microsoft.com>
Date: Thu, 20 Nov 2025 23:43:55 +0900
Subject: [PATCH 10/20] discover-image: Follow symlinks in a given root
Subject: [PATCH 09/14] discover-image: Follow symlinks in a given root
So far systemd-sysext with --root= specified didn't follow extension
symlinks (such as the "current" symlinks managed by systemd-sysupdate).
@ -21,23 +21,23 @@ Without a strict --image-policy= set we would anyway mount filesystems
right away which is another attack vector but, again, the main use case
is to do this for the final system which is trusted at this stage.
---
src/shared/discover-image.c | 162 +++++++++++++++++++++++++++---------
1 file changed, 122 insertions(+), 40 deletions(-)
src/shared/discover-image.c | 169 +++++++++++++++++++-----------------
1 file changed, 90 insertions(+), 79 deletions(-)
diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c
index 888f11f206..53ee30c3f8 100644
index 41fcaa9f44..6366316088 100644
--- a/src/shared/discover-image.c
+++ b/src/shared/discover-image.c
@@ -356,6 +356,8 @@ static int image_make(
@@ -400,6 +400,8 @@ static int image_make(
/* We explicitly *do* follow symlinks here, since we want to allow symlinking trees, raw files and block
* devices into /var/lib/machines/, and treat them normally.
+ * Note that if the caller does not want to follow symlinks (and does not care about symlink races)
+ * then the caller should pass in a resolved dir_path and filename and an fd.
+ * then the caller should pass in a resolved path and an fd.
*
* This function returns -ENOENT if we can't find the image after all, and -EMEDIUMTYPE if it's not a file we
* recognize. */
@@ -700,10 +702,7 @@ int image_find(RuntimeScope scope,
@@ -736,10 +738,7 @@ int image_find(RuntimeScope scope,
const char *root,
Image **ret) {
@ -49,17 +49,15 @@ index 888f11f206..53ee30c3f8 100644
assert(scope < _RUNTIME_SCOPE_MAX && scope != RUNTIME_SCOPE_GLOBAL);
assert(class >= 0);
@@ -718,16 +717,24 @@ int image_find(RuntimeScope scope,
@@ -754,32 +753,47 @@ int image_find(RuntimeScope scope,
if (!names)
return -ENOMEM;
+ _cleanup_close_ int root_fd = AT_FDCWD;
+ _cleanup_close_ int rfd = AT_FDCWD;
+ if (root) {
+ root_fd = open(root, O_CLOEXEC|O_DIRECTORY|O_PATH);
+ if (root_fd < 0) {
+ log_debug_errno(errno, "Failed to open root directory '%s': %m", root);
+ return -errno;
+ }
+ rfd = open(root, O_CLOEXEC|O_DIRECTORY|O_PATH);
+ if (rfd < 0)
+ return log_debug_errno(errno, "Failed to open root directory '%s': %m", root);
+ }
+
_cleanup_strv_free_ char **search = NULL;
@ -67,40 +65,47 @@ index 888f11f206..53ee30c3f8 100644
if (r < 0)
return r;
STRV_FOREACH(path, search) {
STRV_FOREACH(s, search) {
- _cleanup_free_ char *resolved = NULL;
_cleanup_closedir_ DIR *d = NULL;
+ _cleanup_free_ char *search_path = NULL;
- r = chase_and_opendir(*path, root, CHASE_PREFIX_ROOT, &resolved, &d);
+ r = chase_and_opendir(*path, root, CHASE_PREFIX_ROOT, NULL, &d);
- r = chase_and_opendir(*s, root, CHASE_PREFIX_ROOT, &resolved, &d);
+ r = chase_and_opendirat(rfd, *s, CHASE_AT_RESOLVE_IN_ROOT, &search_path, &d);
if (r == -ENOENT)
continue;
if (r < 0)
@@ -736,11 +743,20 @@ int image_find(RuntimeScope scope,
return r;
STRV_FOREACH(n, names) {
_cleanup_free_ char *fname_buf = NULL;
- _cleanup_free_ char *fname_buf = NULL;
const char *fname = *n;
+ _cleanup_free_ char *fname_path = NULL;
+ _cleanup_free_ char *resolved_file = NULL;
+ _cleanup_free_ char *fname_path = NULL, *chased_path = NULL, *resolved_file = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_close_ int subdirfd = -EBADF;
- _cleanup_close_ int fd = openat(dirfd(d), fname, O_PATH|O_CLOEXEC|open_flags);
+ fname_path = path_join(*path, fname);
+ if (!fname_path)
+ return -ENOMEM;
+
+ /* Follow symlinks only inside given root */
+ fd = chase_and_open(fname_path, root, CHASE_PREFIX_ROOT, O_PATH|O_CLOEXEC, &resolved_file);
if (fd < 0) {
- if (fd < 0) {
- if (errno != ENOENT)
- return -errno;
+ if (fd != -ENOENT)
+ return fd;
+ fname_path = path_join(search_path, fname);
+ if (!fname_path)
+ return -ENOMEM;
+ /* Follow symlinks only inside given root */
+ r = chaseat(rfd, fname_path, CHASE_AT_RESOLVE_IN_ROOT, &chased_path, &fd);
+ if (r == -ENOENT)
continue;
}
@@ -769,10 +785,6 @@ int image_find(RuntimeScope scope,
- }
+ if (r < 0)
+ return r;
+
+ r = chaseat_prefix_root(chased_path, root, &resolved_file);
+ if (r < 0)
+ return r;
struct stat st;
if (fstat(fd, &st) < 0)
@@ -805,10 +819,6 @@ int image_find(RuntimeScope scope,
*ASSERT_PTR(endswith(suffix, ".v")) = 0;
@ -111,81 +116,75 @@ index 888f11f206..53ee30c3f8 100644
PickFilter filter = {
.type_mask = endswith(suffix, ".raw") ? (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) : (UINT32_C(1) << DT_DIR),
.basename = name,
@@ -782,23 +794,27 @@ int image_find(RuntimeScope scope,
@@ -818,48 +828,44 @@ int image_find(RuntimeScope scope,
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
r = path_pick(root,
- /* toplevel_fd= */ AT_FDCWD,
- vp,
+ root_fd,
+ fname_path,
+ rfd,
+ fname_path, /* This has to be the unresolved entry with the .v suffix */
&filter,
/* n_filters= */ 1,
- PICK_ARCHITECTURE|PICK_TRIES,
+ PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
&result);
if (r < 0) {
- log_debug_errno(r, "Failed to pick versioned image on '%s', skipping: %m", vp);
+ log_debug_errno(r, "Failed to pick versioned image on '%s/%s', skipping: %m", strempty(root), skip_leading_slash(fname_path));
+ log_debug_errno(r, "Failed to pick versioned image on '%s%s', skipping: %m", empty_to_root(root), skip_leading_slash(fname_path));
continue;
}
if (!result.path) {
- log_debug("Found versioned directory '%s', without matching entry, skipping: %m", vp);
+ log_debug("Found versioned directory '%s/%s', without matching entry, skipping.", strempty(root), skip_leading_slash(fname_path));
- log_debug("Found versioned directory '%s', without matching entry, skipping.", vp);
+ log_debug("Found versioned directory '%s%s', without matching entry, skipping.", empty_to_root(root), skip_leading_slash(fname_path));
continue;
}
/* Refresh the stat data for the discovered target */
st = result.st;
- fd = safe_close(fd);
+ /* Set subdirfd to indicate it should be used instead of dirfd(d).
+ * We reuse the O_PATH fd because it's only used in openat style in image_make().
+ * This subdirfd can be deleted with https://github.com/systemd/systemd/pull/39970 */
+ subdirfd = TAKE_FD(fd);
+ close_and_replace(fd, result.fd);
_cleanup_free_ char *bn = NULL;
r = path_extract_filename(result.path, &bn);
@@ -812,13 +828,38 @@ int image_find(RuntimeScope scope,
return log_oom();
fname = fname_buf;
+ fname_path = mfree(fname_path);
+ fname_path = path_join(*path, fname);
+ if (!fname_path)
+ return log_oom();
+ resolved_file = mfree(resolved_file);
close_and_replace(fd, result.fd);
+ free(resolved_file);
+ resolved_file = path_join(root, result.path);
+ if (!resolved_file)
+ return log_oom();
+ return -ENOMEM;
- _cleanup_free_ char *bn = NULL;
- r = path_extract_filename(result.path, &bn);
- if (r < 0) {
- log_debug_errno(r, "Failed to extract basename of image path '%s', skipping: %m", result.path);
- continue;
- }
-
- fname_buf = path_join(fname, bn);
- if (!fname_buf)
- return log_oom();
-
- fname = fname_buf;
-
+ /* fname and fname_path are invalid now because they would need to be set
+ * from result.path by extracting the filename to set
+ * fname = path_join(fname, filename) and then
+ * fname_path = path_join(*s, fname) but since they are unused we don't do it */
+ fname = NULL;
+ fname_path = mfree(fname_path);
} else if (!S_ISDIR(st.st_mode) && !S_ISBLK(st.st_mode)) {
log_debug("Ignoring non-directory and non-block device file '%s' without suffix.", fname);
continue;
}
- r = image_make(class, name, dirfd(d), resolved, fname, fd, &st, ret);
+ /* Only put resolved paths into the image entry.
+ * Defending against symlink races is out of scope
+ * and we trust a given root in that regard. */
+ _cleanup_free_ char *resolved_dir = NULL;
+ _cleanup_free_ char *resolved_fname = NULL;
+
+ r = path_extract_directory(resolved_file, &resolved_dir);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to extract directory name of image path '%s', skipping: %m", resolved_file);
+ continue;
+ }
+ r = path_extract_filename(resolved_file, &resolved_fname);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to extract basename of image path '%s', skipping: %m", resolved_file);
+ continue;
+ }
+
+ r = image_make(class, name, subdirfd == -EBADF ? dirfd(d) : subdirfd, resolved_dir, resolved_fname, fd, &st, ret);
- _cleanup_free_ char *path = path_join(resolved, fname);
- if (!path)
- return -ENOMEM;
-
- r = image_make(class, name, fd, path, &st, ret);
+ /* Only put resolved paths into the image entry (incl. --root=).
+ * Defending against symlink races is not done
+ * and would be a TODO. */
+ r = image_make(class, name, fd, resolved_file, &st, ret);
if (IN_SET(r, -ENOENT, -EMEDIUMTYPE))
continue;
if (r < 0)
@@ -899,26 +940,31 @@ int image_discover(
@@ -940,46 +946,58 @@ int image_discover(
const char *root,
Hashmap **images) {
@ -200,13 +199,11 @@ index 888f11f206..53ee30c3f8 100644
assert(class < _IMAGE_CLASS_MAX);
assert(images);
+ _cleanup_close_ int root_fd = AT_FDCWD;
+ _cleanup_close_ int rfd = AT_FDCWD;
+ if (root) {
+ root_fd = open(root, O_CLOEXEC|O_DIRECTORY|O_PATH);
+ if (root_fd < 0) {
+ log_debug_errno(errno, "Failed to open root directory '%s': %m", root);
+ return -errno;
+ }
+ rfd = open(root, O_CLOEXEC|O_DIRECTORY|O_PATH);
+ if (rfd < 0)
+ return log_debug_errno(errno, "Failed to open root directory '%s': %m", root);
+ }
+
_cleanup_strv_free_ char **search = NULL;
@ -214,43 +211,52 @@ index 888f11f206..53ee30c3f8 100644
if (r < 0)
return r;
STRV_FOREACH(path, search) {
STRV_FOREACH(s, search) {
- _cleanup_free_ char *resolved = NULL;
_cleanup_closedir_ DIR *d = NULL;
+ _cleanup_free_ char *search_path = NULL;
- r = chase_and_opendir(*path, root, CHASE_PREFIX_ROOT, &resolved, &d);
+ r = chase_and_opendir(*path, root, CHASE_PREFIX_ROOT, NULL, &d);
- r = chase_and_opendir(*s, root, CHASE_PREFIX_ROOT, &resolved, &d);
+ r = chase_and_opendirat(rfd, *s, CHASE_AT_RESOLVE_IN_ROOT, &search_path, &d);
if (r == -ENOENT)
continue;
if (r < 0)
@@ -928,14 +974,23 @@ int image_discover(
_cleanup_free_ char *pretty = NULL, *fname_buf = NULL;
return r;
FOREACH_DIRENT_ALL(de, d, return -errno) {
- _cleanup_free_ char *pretty = NULL, *fname_buf = NULL;
+ _cleanup_free_ char *pretty = NULL, *fname_path = NULL, *chased_path = NULL, *resolved_file = NULL;
_cleanup_(image_unrefp) Image *image = NULL;
const char *fname = de->d_name;
+ _cleanup_free_ char *fname_path = NULL;
+ _cleanup_free_ char *resolved_file = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_close_ int subdirfd = -EBADF;
if (dot_or_dot_dot(fname))
continue;
- _cleanup_close_ int fd = openat(dirfd(d), fname, O_PATH|O_CLOEXEC|open_flags);
+ fname_path = path_join(*path, fname);
+ if (!fname_path)
+ return -ENOMEM;
+
+ /* Follow symlinks only inside given root */
+ fd = chase_and_open(fname_path, root, CHASE_PREFIX_ROOT, O_PATH|O_CLOEXEC, &resolved_file);
if (fd < 0) {
- if (fd < 0) {
- if (errno != ENOENT)
- return -errno;
+ if (fd != -ENOENT)
+ return fd;
+ fname_path = path_join(search_path, fname);
+ if (!fname_path)
+ return -ENOMEM;
continue; /* Vanished while we were looking at it */
}
@@ -977,10 +1032,6 @@ int image_discover(
- continue; /* Vanished while we were looking at it */
- }
+ /* Follow symlinks only inside given root */
+ r = chaseat(rfd, fname_path, CHASE_AT_RESOLVE_IN_ROOT, &chased_path, &fd);
+ if (r == -ENOENT)
+ continue;
+ if (r < 0)
+ return r;
+
+ r = chaseat_prefix_root(chased_path, root, &resolved_file);
+ if (r < 0)
+ return r;
struct stat st;
if (fstat(fd, &st) < 0)
@@ -1018,10 +1036,6 @@ int image_discover(
continue;
}
@ -261,80 +267,74 @@ index 888f11f206..53ee30c3f8 100644
PickFilter filter = {
.type_mask = endswith(suffix, ".raw") ? (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) : (UINT32_C(1) << DT_DIR),
.basename = pretty,
@@ -990,23 +1041,27 @@ int image_discover(
@@ -1031,38 +1045,36 @@ int image_discover(
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
r = path_pick(root,
- /* toplevel_fd= */ AT_FDCWD,
- vp,
+ root_fd,
+ fname_path,
+ rfd,
+ fname_path, /* This has to be the unresolved entry with the .v suffix */
&filter,
/* n_filters= */ 1,
- PICK_ARCHITECTURE|PICK_TRIES,
+ PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
&result);
if (r < 0) {
- log_debug_errno(r, "Failed to pick versioned image on '%s', skipping: %m", vp);
+ log_debug_errno(r, "Failed to pick versioned image on '%s/%s', skipping: %m", strempty(root), skip_leading_slash(fname_path));
+ log_debug_errno(r, "Failed to pick versioned image on '%s%s', skipping: %m", empty_to_root(root), skip_leading_slash(fname_path));
continue;
}
if (!result.path) {
- log_debug("Found versioned directory '%s', without matching entry, skipping: %m", vp);
+ log_debug("Found versioned directory '%s/%s', without matching entry, skipping.", strempty(root), skip_leading_slash(fname_path));
- log_debug("Found versioned directory '%s', without matching entry, skipping.", vp);
+ log_debug("Found versioned directory '%s%s', without matching entry, skipping.", empty_to_root(root), skip_leading_slash(fname_path));
continue;
}
/* Refresh the stat data for the discovered target */
st = result.st;
- fd = safe_close(fd);
+ /* Set subdirfd to indicate it should be used instead of dirfd(d).
+ * We reuse the O_PATH fd because it's only used in openat style in image_make().
+ * This subdirfd can be deleted with https://github.com/systemd/systemd/pull/39970 */
+ subdirfd = TAKE_FD(fd);
+ close_and_replace(fd, result.fd);
_cleanup_free_ char *bn = NULL;
r = path_extract_filename(result.path, &bn);
@@ -1020,6 +1075,15 @@ int image_discover(
return log_oom();
fname = fname_buf;
+ fname_path = mfree(fname_path);
+ fname_path = path_join(*path, fname);
+ if (!fname_path)
+ return log_oom();
+
+ resolved_file = mfree(resolved_file);
close_and_replace(fd, result.fd);
+ free(resolved_file);
+ resolved_file = path_join(root, result.path);
+ if (!resolved_file)
+ return log_oom();
+ return -ENOMEM;
- _cleanup_free_ char *bn = NULL;
- r = path_extract_filename(result.path, &bn);
- if (r < 0) {
- log_debug_errno(r, "Failed to extract basename of image path '%s', skipping: %m", result.path);
- continue;
- }
-
- fname_buf = path_join(fname, bn);
- if (!fname_buf)
- return log_oom();
-
- fname = fname_buf;
-
+ /* fname and fname_path are invalid now because they would need to
+ * be set from result.path by extracting the filename to set
+ * fname = path_join(fname, filename) and then
+ * fname_path = path_join(*s, fname) but since they are unused we
+ * don't do it */
+ fname = NULL;
+ fname_path = mfree(fname_path);
} else {
r = extract_image_basename(
fname,
@@ -1052,7 +1116,25 @@ int image_discover(
@@ -1095,11 +1107,10 @@ int image_discover(
if (hashmap_contains(*images, pretty))
continue;
- r = image_make(class, pretty, dirfd(d), resolved, fname, fd, &st, &image);
- _cleanup_free_ char *path = path_join(resolved, fname);
- if (!path)
- return -ENOMEM;
-
- r = image_make(class, pretty, fd, path, &st, &image);
+ /* Only put resolved paths into the image entry.
+ * Defending against symlink races is out of scope
+ * and we trust a given root in that regard. */
+ _cleanup_free_ char *resolved_dir = NULL;
+ _cleanup_free_ char *resolved_fname = NULL;
+
+ r = path_extract_directory(resolved_file, &resolved_dir);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to extract directory name of image path '%s', skipping: %m", resolved_file);
+ continue;
+ }
+ r = path_extract_filename(resolved_file, &resolved_fname);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to extract basename of image path '%s', skipping: %m", resolved_file);
+ continue;
+ }
+
+ r = image_make(class, pretty, subdirfd == -EBADF ? dirfd(d) : subdirfd, resolved_dir, resolved_fname, fd, &st, &image);
+
+ * Defending against symlink races is not done
+ * and would be a TODO. */
+ r = image_make(class, pretty, fd, resolved_file, &st, &image);
if (IN_SET(r, -ENOENT, -EMEDIUMTYPE))
continue;
if (r < 0)

View File

@ -1,33 +0,0 @@
From 42b6a55f8d2bdf68ff93764219b3bedffb11f4e0 Mon Sep 17 00:00:00 2001
From: Kai Lueke <kailuke@microsoft.com>
Date: Thu, 20 Nov 2025 23:43:55 +0900
Subject: [PATCH 09/20] vpick: Don't use openat directly but resolve symlinks
in given root
With systemd-sysext --root= all symlinks should be followed relative to
the given root and direct openat usage doesn't work.
Change the openat call to use the chase helper function to resolve the
symlink in the given root.
---
src/shared/vpick.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/shared/vpick.c b/src/shared/vpick.c
index 07d9d9ffd8..b203609cc9 100644
--- a/src/shared/vpick.c
+++ b/src/shared/vpick.c
@@ -471,9 +471,9 @@ static int make_choice(
if (!p)
return log_oom_debug();
- object_fd = openat(dir_fd, best_filename, O_CLOEXEC|O_PATH);
+ object_fd = chase_and_openat(toplevel_fd, p, CHASE_AT_RESOLVE_IN_ROOT, O_PATH|O_CLOEXEC, NULL);
if (object_fd < 0)
- return log_debug_errno(errno, "Failed to open '%s/%s': %m",
+ return log_debug_errno(object_fd, "Failed to open '%s/%s': %m",
empty_to_root(toplevel_path), skip_leading_slash(inode_path));
return pin_choice(
--
2.52.0

View File

@ -1,7 +1,7 @@
From 6a95919888a99d92636e0aa28c68d0f95f16e48e Mon Sep 17 00:00:00 2001
From 3a1d8cb52278fb7926d7666385d9b7ef120508d6 Mon Sep 17 00:00:00 2001
From: Kai Lueke <kailuke@microsoft.com>
Date: Thu, 20 Nov 2025 23:43:55 +0900
Subject: [PATCH 11/20] sysext: Use correct image name for extension release
Subject: [PATCH 10/14] sysext: Use correct image name for extension release
checks
For the extension release check the image name is needed and was derived
@ -21,10 +21,10 @@ device but directly the extension name we have at hand.
2 files changed, 10 insertions(+)
diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c
index 53ee30c3f8..2801793d6d 100644
index 6366316088..34512ea6da 100644
--- a/src/shared/discover-image.c
+++ b/src/shared/discover-image.c
@@ -1844,6 +1844,11 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy) {
@@ -2172,6 +2172,11 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy, RuntimeScope
if (r < 0)
return log_debug_errno(r, "Failed to decrypt image '%s': %m", i->path);
@ -37,10 +37,10 @@ index 53ee30c3f8..2801793d6d 100644
m,
/* userns_fd= */ -EBADF,
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index 5d432b42da..72da02cd89 100644
index 2f6bee58c4..cb0a3046e1 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -1819,6 +1819,11 @@ static int merge_subprocess(
@@ -1885,6 +1885,11 @@ static int merge_subprocess(
if (r < 0)
return r;

View File

@ -0,0 +1,67 @@
From 0d5830bf407c218b898c7fec6153fc6ae4da645f Mon Sep 17 00:00:00 2001
From: Kai Lueke <kailuke@microsoft.com>
Date: Wed, 3 Dec 2025 00:02:32 +0900
Subject: [PATCH 11/14] sysext: Create mutable directory with the right mode
When the mutable directory didn't exist but gets created with
--mutable=yes then it used to get mode 700 and later it got patched by
a chmod because it is the top layer and must match the target hierarchy.
This meant one could not call the function to resolve the mutable
directory twice before the mount because it has a check for a proper
mode when the directory exists which is the case for the second call.
Also, this resulted in /var/lib/extensions.mutable getting created with
mode 700 which is not really required.
Don't rely on the chmod for the upper dir but directly create the
directory with the right mode by first creating all missing directories
with 755 as a sane default and then changing the mode as needed for the
mutable directory.
---
src/sysext/sysext.c | 26 ++++++++++++++------------
1 file changed, 14 insertions(+), 12 deletions(-)
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index cb0a3046e1..b926ce4c83 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -1011,23 +1011,25 @@ static int resolve_mutable_directory(
}
if (IN_SET(arg_mutable, MUTABLE_YES, MUTABLE_EPHEMERAL, MUTABLE_EPHEMERAL_IMPORT)) {
- _cleanup_free_ char *path_in_root = NULL;
+ _cleanup_close_ int path_fd = -EBADF, chmod_fd = -EBADF;
- path_in_root = path_join(root, path);
- if (!path_in_root)
- return log_oom();
-
- r = mkdir_p(path_in_root, 0700);
+ /* This also creates, e.g., /var/lib/extensions.mutable/usr if needed and all parent
+ * directories plus it also works when the last part is a symlink to the real /usr but we
+ * can't use chase_and_open here because it does not behave the same. */
+ r = chase(path, root, CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY|CHASE_PREFIX_ROOT, /* ret_path */ NULL, &path_fd);
if (r < 0)
- return log_error_errno(r, "Failed to create a directory '%s': %m", path_in_root);
+ return log_error_errno(r, "Failed to chase/create base directory '%s/%s': %m", strempty(root), skip_leading_slash(path));
+
+ chmod_fd = fd_reopen(path_fd, O_CLOEXEC|O_DIRECTORY);
+ if (chmod_fd < 0)
+ return log_error_errno(chmod_fd, "Failed to reopen '%s/%s': %m", strempty(root), skip_leading_slash(path));
- _cleanup_close_ int atfd = open(path_in_root, O_DIRECTORY|O_CLOEXEC);
- if (atfd < 0)
- return log_error_errno(errno, "Failed to open directory '%s': %m", path_in_root);
+ if (fchmod(chmod_fd, hierarchy_mode) < 0)
+ return log_error_errno(errno, "Failed to chmod directory '%s/%s': %m", strempty(root), skip_leading_slash(path));
- r = mac_selinux_fix_full(atfd, /* inode_path= */ NULL, hierarchy, /* flags= */ 0);
+ r = mac_selinux_fix_full(chmod_fd, /* inode_path= */ NULL, hierarchy, /* flags= */ 0);
if (r < 0)
- return log_error_errno(r, "Failed to fix SELinux label for '%s': %m", path_in_root);
+ return log_error_errno(r, "Failed to fix SELinux label for '%s/%s': %m", strempty(root), skip_leading_slash(path));
}
r = chase(path, root, CHASE_PREFIX_ROOT, &resolved_path, NULL);
--
2.52.0

View File

@ -1,7 +1,7 @@
From d8ccdfe333a2eda7770371112cf5dea0ae67598c Mon Sep 17 00:00:00 2001
From 404b72c801f4b95e74b655f0771f08f63473d28a Mon Sep 17 00:00:00 2001
From: Kai Lueke <kailuke@microsoft.com>
Date: Wed, 26 Nov 2025 00:04:43 +0900
Subject: [PATCH 14/20] sysext: Skip refresh if no changes are found
Subject: [PATCH 12/14] sysext: Skip refresh if no changes are found
When the extensions for the final system are already set up from the
initrd we should avoid disrupting the boot process with the remount
@ -34,42 +34,38 @@ Luckily, we can rule out online modification of directories or image
files because this is anyway not well supported with overlay mounts, so
we don't do a file checksum nor do we recurse into a directory to look
for the most recently touched files. But, as said, with the
always-refresh flag one can force a reload. Another case that is not
supported is changed values of SYSTEMD_SYSEXT_OVERLAYFS_MOUNT_OPTIONS=
and these also need an explicit refresh to be applied.
always-refresh flag one can force a reload.
---
man/systemd-sysext.xml | 14 ++
man/systemd-sysext.xml | 13 ++
shell-completion/bash/systemd-sysext | 1 +
src/basic/mountpoint-util.c | 98 ++++++++-
src/basic/mountpoint-util.h | 4 +-
src/basic/mountpoint-util.c | 110 ++++++++-
src/basic/mountpoint-util.h | 5 +-
src/include/override/fcntl.h | 5 +
src/shared/discover-image.c | 54 +++++
src/shared/discover-image.c | 48 ++++
src/shared/discover-image.h | 4 +
src/shared/varlink-io.systemd.sysext.c | 1 +
src/sysext/sysext.c | 271 ++++++++++++++++++++++---
test/units/TEST-50-DISSECT.sysext.sh | 41 ++++
10 files changed, 458 insertions(+), 35 deletions(-)
src/sysext/sysext.c | 301 ++++++++++++++++++++++---
9 files changed, 450 insertions(+), 38 deletions(-)
diff --git a/man/systemd-sysext.xml b/man/systemd-sysext.xml
index 07e97071a5..3f60c85dba 100644
index b5ab6826a2..d6bbc0141d 100644
--- a/man/systemd-sysext.xml
+++ b/man/systemd-sysext.xml
@@ -366,6 +366,20 @@
@@ -371,6 +371,19 @@
<xi:include href="version-info.xml" xpointer="v248"/></listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>--always-refresh=yes|no</option></term>
+
+ <listitem><para>When refreshing system extensions on <filename>/usr/</filename> and
+ <filename>/opt/</filename> for sysext and <filename>/etc/</filename> for confext,
+ ignore when the existing merged extensions already match what would be merged.
+ By default the refresh is skipped when no changes are found. Note that changes
+ done to an extension directory while it's merged are ignored without this flag
+ (unless an other extension got changed). Note that changing the contents while
+ merged is also undefined behavior in overlayfs.</para>
+ <listitem><para>When refreshing system extensions on <filename>/usr/</filename> and <filename>/opt/</filename>
+ for sysext and <filename>/etc/</filename> for confext, ignore when the existing merged extensions
+ already match what would be merged.
+ By default the refresh is skipped when no changes are found. Note that changes done to an extension
+ directory while it's merged are ignored without this flag (unless an other extension got changed).
+ Note that changing the contents while merged is also undefined behavior in overlayfs.</para>
+
+ <xi:include href="version-info.xml" xpointer="v250"/></listitem>
+ <xi:include href="version-info.xml" xpointer="v260"/></listitem>
+ </varlistentry>
+
<varlistentry>
@ -88,10 +84,10 @@ index c605237ed6..69d786c33e 100644
--mutable'
)
diff --git a/src/basic/mountpoint-util.c b/src/basic/mountpoint-util.c
index b7c4870931..0d52d4cfea 100644
index b7c4870931..aefbed6346 100644
--- a/src/basic/mountpoint-util.c
+++ b/src/basic/mountpoint-util.c
@@ -51,12 +51,16 @@ int name_to_handle_at_loop(
@@ -51,12 +51,13 @@ int name_to_handle_at_loop(
const char *path,
struct file_handle **ret_handle,
int *ret_mnt_id,
@ -103,13 +99,10 @@ index b7c4870931..0d52d4cfea 100644
assert(fd >= 0 || fd == AT_FDCWD);
- assert((flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH|AT_HANDLE_FID)) == 0);
+ assert((flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH|AT_HANDLE_FID|AT_HANDLE_MNT_ID_UNIQUE)) == 0);
+
+ if (isempty(path))
+ flags |= AT_EMPTY_PATH;
/* We need to invoke name_to_handle_at() in a loop, given that it might return EOVERFLOW when the specified
* buffer is too small. Note that in contrast to what the docs might suggest, MAX_HANDLE_SZ is only good as a
@@ -67,7 +71,8 @@ int name_to_handle_at_loop(
@@ -67,7 +68,8 @@ int name_to_handle_at_loop(
for (;;) {
_cleanup_free_ struct file_handle *h = NULL;
@ -119,30 +112,27 @@ index b7c4870931..0d52d4cfea 100644
h = malloc0(offsetof(struct file_handle, f_handle) + n);
if (!h)
@@ -75,12 +80,20 @@ int name_to_handle_at_loop(
@@ -75,11 +77,18 @@ int name_to_handle_at_loop(
h->handle_bytes = n;
- if (name_to_handle_at(fd, strempty(path), h, &mnt_id, flags) >= 0) {
+ if (flags & AT_HANDLE_MNT_ID_UNIQUE)
+ if (FLAGS_SET(flags, AT_HANDLE_MNT_ID_UNIQUE))
+ /* The kernel will still use this as uint64_t pointer */
+ r = name_to_handle_at(fd, strempty(path), h, (int *) &unique_mnt_id, flags);
+ else
+ r = name_to_handle_at(fd, strempty(path), h, &mnt_id, flags);
+
+ if (r >= 0) {
if (ret_handle)
*ret_handle = TAKE_PTR(h);
- if (ret_mnt_id)
+ if (ret_unique_mnt_id && flags & AT_HANDLE_MNT_ID_UNIQUE)
+ if (ret_unique_mnt_id)
+ *ret_unique_mnt_id = unique_mnt_id;
+ if (ret_mnt_id && (flags & AT_HANDLE_MNT_ID_UNIQUE) == 0)
if (ret_mnt_id)
*ret_mnt_id = mnt_id;
return 0;
@@ -88,13 +101,16 @@ int name_to_handle_at_loop(
@@ -88,13 +97,16 @@ int name_to_handle_at_loop(
if (errno != EOVERFLOW)
return -errno;
@ -154,24 +144,24 @@ index b7c4870931..0d52d4cfea 100644
* be filled in, and the caller was interested in only the mount ID an nothing else. */
- *ret_mnt_id = mnt_id;
+ if (ret_unique_mnt_id && flags & AT_HANDLE_MNT_ID_UNIQUE)
+ if (ret_unique_mnt_id)
+ *ret_unique_mnt_id = unique_mnt_id;
+ else if (ret_mnt_id)
+ if (ret_mnt_id)
+ *ret_mnt_id = mnt_id;
return 0;
}
@@ -128,11 +144,52 @@ int name_to_handle_at_try_fid(
@@ -128,11 +140,55 @@ int name_to_handle_at_try_fid(
* we'll try without the flag, in order to support older kernels that didn't have AT_HANDLE_FID
* (i.e. older than Linux 6.5). */
- r = name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, flags | AT_HANDLE_FID);
+ r = name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, NULL, flags | AT_HANDLE_FID);
+ r = name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, /* ret_unique_mnt_id= */ NULL, flags | AT_HANDLE_FID);
if (r >= 0 || is_name_to_handle_at_fatal_error(r))
return r;
- return name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, flags & ~AT_HANDLE_FID);
+ return name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, NULL, flags & ~AT_HANDLE_FID);
+ return name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, /* ret_unique_mnt_id= */ NULL, flags & ~AT_HANDLE_FID);
+}
+
+int name_to_handle_at_try_unique_mntid_fid(
@ -188,7 +178,7 @@ index b7c4870931..0d52d4cfea 100644
+ /* First issues name_to_handle_at() with AT_HANDLE_MNT_ID_UNIQUE and AT_HANDLE_FID.
+ * If this fails and this is not a fatal error we'll try without the
+ * AT_HANDLE_MNT_ID_UNIQUE flag because it's only available from Linux 6.12 onwards. */
+ r = name_to_handle_at_loop(fd, path, ret_handle, NULL, ret_mnt_id, flags | AT_HANDLE_MNT_ID_UNIQUE | AT_HANDLE_FID);
+ r = name_to_handle_at_loop(fd, path, ret_handle, /* ret_mnt_id= */ NULL, ret_mnt_id, flags | AT_HANDLE_MNT_ID_UNIQUE | AT_HANDLE_FID);
+ if (r >= 0 || is_name_to_handle_at_fatal_error(r))
+ return r;
+
@ -198,25 +188,51 @@ index b7c4870931..0d52d4cfea 100644
+ * we'll try without the flag, in order to support older kernels that didn't have AT_HANDLE_FID
+ * (i.e. older than Linux 6.5). */
+
+ r = name_to_handle_at_loop(fd, path, ret_handle, &mnt_id, NULL, flags | AT_HANDLE_FID);
+ if (ret_mnt_id && mnt_id >= 0) {
+ /* See if we can do better because statx can do unique mount IDs since Linux 6.8
+ * and only if this doesn't work we use the non-unique mnt_id as returned.
+ * The function only sets mnt_id after checking the error code, so omitted above. */
+ if (path_get_unique_mnt_id_at(fd, path, ret_mnt_id) < 0)
+ *ret_mnt_id = mnt_id;
+ }
+ if (r >= 0 || is_name_to_handle_at_fatal_error(r))
+ r = name_to_handle_at_loop(fd, path, ret_handle, &mnt_id, /* ret_unique_mnt_id= */ NULL, flags | AT_HANDLE_FID);
+ if (r < 0 && is_name_to_handle_at_fatal_error(r))
+ return r;
+ if (r >= 0) {
+ if (ret_mnt_id && mnt_id >= 0) {
+ /* See if we can do better because statx can do unique mount IDs since Linux 6.8
+ * and only if this doesn't work we use the non-unique mnt_id as returned. */
+ if (path_get_unique_mnt_id_at(fd, path, ret_mnt_id) < 0)
+ *ret_mnt_id = mnt_id;
+ }
+
+ r = name_to_handle_at_loop(fd, path, ret_handle, &mnt_id, NULL, flags & ~AT_HANDLE_FID);
+ return r;
+ }
+
+ r = name_to_handle_at_loop(fd, path, ret_handle, &mnt_id, /* ret_unique_mnt_id= */ NULL, flags & ~AT_HANDLE_FID);
+ if (ret_mnt_id && mnt_id >= 0)
+ *ret_mnt_id = mnt_id;
+ return r;
}
static int fd_fdinfo_mnt_id(int fd, const char *filename, int flags, int *ret_mnt_id) {
@@ -373,7 +430,7 @@ int path_get_mnt_id_at_fallback(int dir_fd, const char *path, int *ret) {
@@ -197,6 +253,22 @@ bool file_handle_equal(const struct file_handle *a, const struct file_handle *b)
return memcmp_nn(a->f_handle, a->handle_bytes, b->f_handle, b->handle_bytes) == 0;
}
+struct file_handle* file_handle_dup(const struct file_handle *fh) {
+ _cleanup_free_ struct file_handle *fh_copy = NULL;
+
+ assert(fh);
+
+ fh_copy = malloc0(offsetof(struct file_handle, f_handle) + fh->handle_bytes);
+ if (!fh_copy)
+ return NULL;
+
+ fh_copy->handle_bytes = fh->handle_bytes;
+ fh_copy->handle_type = fh->handle_type;
+ memcpy(fh_copy->f_handle, fh->f_handle, fh->handle_bytes);
+
+ return TAKE_PTR(fh_copy);
+}
+
int is_mount_point_at(int fd, const char *filename, int flags) {
bool fd_is_self;
int r;
@@ -373,7 +445,7 @@ int path_get_mnt_id_at_fallback(int dir_fd, const char *path, int *ret) {
assert(dir_fd >= 0 || dir_fd == AT_FDCWD);
assert(ret);
@ -225,7 +241,7 @@ index b7c4870931..0d52d4cfea 100644
if (r >= 0 || is_name_to_handle_at_fatal_error(r))
return r;
@@ -403,6 +460,29 @@ int path_get_mnt_id_at(int dir_fd, const char *path, int *ret) {
@@ -403,6 +475,28 @@ int path_get_mnt_id_at(int dir_fd, const char *path, int *ret) {
return path_get_mnt_id_at_fallback(dir_fd, path, ret);
}
@ -244,22 +260,21 @@ index b7c4870931..0d52d4cfea 100644
+ &sx) < 0)
+ return -errno;
+
+ if (FLAGS_SET(sx.stx_mask, STATX_MNT_ID_UNIQUE)) {
+ *ret = sx.stx_mnt_id;
+ return 0;
+ }
+ if (!FLAGS_SET(sx.stx_mask, STATX_MNT_ID_UNIQUE))
+ return -EOPNOTSUPP;
+
+ return -EOPNOTSUPP;
+ *ret = sx.stx_mnt_id;
+ return 0;
+}
+
bool fstype_is_network(const char *fstype) {
const char *x;
diff --git a/src/basic/mountpoint-util.h b/src/basic/mountpoint-util.h
index 004d2b2af5..2d3e8390ba 100644
index 180d75343c..e70d2c40d0 100644
--- a/src/basic/mountpoint-util.h
+++ b/src/basic/mountpoint-util.h
@@ -34,8 +34,9 @@
@@ -34,16 +34,19 @@
bool is_name_to_handle_at_fatal_error(int err);
@ -269,8 +284,10 @@ index 004d2b2af5..2d3e8390ba 100644
+int name_to_handle_at_try_unique_mntid_fid(int fd, const char *path, struct file_handle **ret_handle, uint64_t *ret_mnt_id, int flags);
bool file_handle_equal(const struct file_handle *a, const struct file_handle *b);
+struct file_handle* file_handle_dup(const struct file_handle *fh);
@@ -44,6 +45,7 @@ int path_get_mnt_id_at(int dir_fd, const char *path, int *ret);
int path_get_mnt_id_at_fallback(int dir_fd, const char *path, int *ret);
int path_get_mnt_id_at(int dir_fd, const char *path, int *ret);
static inline int path_get_mnt_id(const char *path, int *ret) {
return path_get_mnt_id_at(AT_FDCWD, path, ret);
}
@ -279,7 +296,7 @@ index 004d2b2af5..2d3e8390ba 100644
int is_mount_point_at(int fd, const char *filename, int flags);
int path_is_mount_point_full(const char *path, const char *root, int flags);
diff --git a/src/include/override/fcntl.h b/src/include/override/fcntl.h
index 5f1d90ad79..f244ffa9f1 100644
index f2b40a6a17..b41f364534 100644
--- a/src/include/override/fcntl.h
+++ b/src/include/override/fcntl.h
@@ -17,3 +17,8 @@
@ -289,23 +306,21 @@ index 5f1d90ad79..f244ffa9f1 100644
+
+/* This is defined since glibc-2.42. */
+#ifndef AT_HANDLE_MNT_ID_UNIQUE
+#define AT_HANDLE_MNT_ID_UNIQUE 0x001 /* Return the u64 unique mount ID. */
+#define AT_HANDLE_MNT_ID_UNIQUE 0x001 /* Return the u64 unique mount ID. */
+#endif
diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c
index 2801793d6d..192ed18687 100644
index 34512ea6da..d480848473 100644
--- a/src/shared/discover-image.c
+++ b/src/shared/discover-image.c
@@ -35,6 +35,9 @@
@@ -35,6 +35,7 @@
#include "lock-util.h"
#include "log.h"
#include "loop-util.h"
#include "mkdir.h"
+#include "mountpoint-util.h"
+#include "namespace-util.h"
+#include "nsresource.h"
#include "namespace-util.h"
#include "nsresource.h"
#include "nulstr-util.h"
#include "os-util.h"
#include "path-util.h"
@@ -125,6 +128,8 @@ static Image* image_free(Image *i) {
@@ -139,6 +140,8 @@ static Image* image_free(Image *i) {
free(i->name);
free(i->path);
@ -314,7 +329,7 @@ index 2801793d6d..192ed18687 100644
free(i->hostname);
strv_free(i->machine_info);
strv_free(i->os_release);
@@ -194,6 +199,9 @@ static int image_new(
@@ -241,6 +244,9 @@ static int image_new(
bool read_only,
usec_t crtime,
usec_t mtime,
@ -324,7 +339,7 @@ index 2801793d6d..192ed18687 100644
Image **ret) {
_cleanup_(image_unrefp) Image *i = NULL;
@@ -215,12 +223,24 @@ static int image_new(
@@ -262,12 +268,20 @@ static int image_new(
.read_only = read_only,
.crtime = crtime,
.mtime = mtime,
@ -337,37 +352,33 @@ index 2801793d6d..192ed18687 100644
};
+ if (fh) {
+ i->fh = malloc0(offsetof(struct file_handle, f_handle) + fh->handle_bytes);
+ i->fh = file_handle_dup(fh);
+ if (!i->fh)
+ return -ENOMEM;
+
+ i->fh->handle_bytes = fh->handle_bytes;
+ i->fh->handle_type = fh->handle_type;
+ memcpy(i->fh->f_handle, fh->f_handle, fh->handle_bytes);
+ }
+
i->name = strdup(pretty);
if (!i->name)
return -ENOMEM;
@@ -391,6 +411,28 @@ static int image_make(
(dir_path && path_startswith(dir_path, "/usr")) ||
@@ -429,6 +443,28 @@ static int image_make(
path_startswith(path, "/usr") ||
(faccessat(fd, "", W_OK, AT_EACCESS|AT_EMPTY_PATH) < 0 && errno == EROFS);
+ uint64_t on_mount_id = 0;
+ _cleanup_free_ struct file_handle *fh = NULL;
+
+ r = name_to_handle_at_try_unique_mntid_fid(fd, NULL, &fh, &on_mount_id, 0);
+ r = name_to_handle_at_try_unique_mntid_fid(fd, /* path= */ NULL, &fh, &on_mount_id, /* flags= */ 0);
+ if (r < 0) {
+ if (is_name_to_handle_at_fatal_error(r))
+ return r;
+
+ r = path_get_unique_mnt_id_at(fd, NULL, &on_mount_id);
+ r = path_get_unique_mnt_id_at(fd, /* path= */ NULL, &on_mount_id);
+ if (r < 0) {
+ if (!ERRNO_IS_NEG_NOT_SUPPORTED(r))
+ return r;
+
+ int on_mount_id_fallback = -1;
+ r = path_get_mnt_id_at(fd, NULL, &on_mount_id_fallback);
+ r = path_get_mnt_id_at(fd, /* path= */ NULL, &on_mount_id_fallback);
+ if (r < 0)
+ return r;
+
@ -378,7 +389,7 @@ index 2801793d6d..192ed18687 100644
if (S_ISDIR(st->st_mode)) {
unsigned file_attr = 0;
usec_t crtime = 0;
@@ -433,6 +475,9 @@ static int image_make(
@@ -470,6 +506,9 @@ static int image_make(
info.read_only || read_only,
info.otime,
info.ctime,
@ -388,7 +399,7 @@ index 2801793d6d..192ed18687 100644
ret);
if (r < 0)
return r;
@@ -458,6 +503,9 @@ static int image_make(
@@ -495,6 +534,9 @@ static int image_make(
read_only || (file_attr & FS_IMMUTABLE_FL),
crtime,
0, /* we don't use mtime of stat() here, since it's not the time of last change of the tree, but only of the top-level dir */
@ -398,7 +409,7 @@ index 2801793d6d..192ed18687 100644
ret);
if (r < 0)
return r;
@@ -495,6 +543,9 @@ static int image_make(
@@ -532,6 +574,9 @@ static int image_make(
!(st->st_mode & 0222) || read_only,
crtime,
timespec_load(&st->st_mtim),
@ -408,7 +419,7 @@ index 2801793d6d..192ed18687 100644
ret);
if (r < 0)
return r;
@@ -553,6 +604,9 @@ static int image_make(
@@ -589,6 +634,9 @@ static int image_make(
!(st->st_mode & 0222) || read_only,
0,
0,
@ -419,7 +430,7 @@ index 2801793d6d..192ed18687 100644
if (r < 0)
return r;
diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h
index 60f5a4dce1..7b5593f08d 100644
index 881a59fdc2..b28d961fb0 100644
--- a/src/shared/discover-image.h
+++ b/src/shared/discover-image.h
@@ -27,6 +27,10 @@ typedef struct Image {
@ -446,7 +457,7 @@ index 90eb8177d1..e48804c148 100644
static SD_VARLINK_DEFINE_METHOD_FULL(
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index d63cf39fbb..bfe71f2267 100644
index b926ce4c83..30e33a01e5 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -8,6 +8,7 @@
@ -457,15 +468,33 @@ index d63cf39fbb..bfe71f2267 100644
#include "sd-varlink.h"
#include "argv-util.h"
@@ -87,6 +88,7 @@ static PagerFlags arg_pager_flags = 0;
@@ -84,6 +85,17 @@ static const char* const mutable_mode_table[_MUTABLE_MAX] = {
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(mutable_mode, MutableMode, MUTABLE_YES);
+enum {
+ MERGE_NOTHING_FOUND,
+ MERGE_MOUNTED,
+ MERGE_SKIP_REFRESH,
+};
+
+enum {
+ MERGE_EXIT_NOTHING_FOUND = 123,
+ MERGE_EXIT_SKIP_REFRESH = 124,
+};
+
static char **arg_hierarchies = NULL; /* "/usr" + "/opt" by default for sysext and /etc by default for confext */
static char *arg_root = NULL;
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
@@ -91,6 +103,7 @@ static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static bool arg_force = false;
static bool arg_no_reload = false;
+static bool arg_always_refresh = false;
static int arg_noexec = -1;
static ImagePolicy *arg_image_policy = NULL;
static bool arg_varlink = false;
@@ -1401,6 +1403,28 @@ static int write_extensions_file(ImageClass image_class, char **extensions, cons
static bool arg_image_policy_set = false; /* Tracks initialization */
@@ -1465,6 +1478,30 @@ static int write_extensions_file(ImageClass image_class, char **extensions, cons
return 0;
}
@ -484,7 +513,9 @@ index d63cf39fbb..bfe71f2267 100644
+ if (!hierarchy_path)
+ return log_oom();
+
+ r = write_string_file_full(AT_FDCWD, f, strempty(origin_content), WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755|WRITE_STRING_FILE_LABEL|WRITE_STRING_FILE_AVOID_NEWLINE, /* ts= */ NULL, hierarchy_path);
+ r = write_string_file_full(AT_FDCWD, f, strempty(origin_content),
+ WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755|WRITE_STRING_FILE_LABEL|WRITE_STRING_FILE_AVOID_NEWLINE,
+ /* ts= */ NULL, hierarchy_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write origin meta file '%s': %m", f);
+
@ -494,7 +525,7 @@ index d63cf39fbb..bfe71f2267 100644
static int write_dev_file(ImageClass image_class, const char *meta_path, const char *overlay_path, const char *hierarchy) {
_cleanup_free_ char *f = NULL;
struct stat st;
@@ -1505,6 +1529,7 @@ static int write_work_dir_file(ImageClass image_class, const char *meta_path, co
@@ -1569,6 +1606,7 @@ static int write_work_dir_file(ImageClass image_class, const char *meta_path, co
static int store_info_in_meta(
ImageClass image_class,
char **extensions,
@ -502,7 +533,7 @@ index d63cf39fbb..bfe71f2267 100644
const char *meta_path,
const char *overlay_path,
const char *work_dir,
@@ -1537,6 +1562,10 @@ static int store_info_in_meta(
@@ -1601,6 +1639,10 @@ static int store_info_in_meta(
if (r < 0)
return r;
@ -513,7 +544,7 @@ index d63cf39fbb..bfe71f2267 100644
r = write_dev_file(image_class, meta_path, overlay_path, hierarchy);
if (r < 0)
return r;
@@ -1595,6 +1624,7 @@ static int merge_hierarchy(
@@ -1659,6 +1701,7 @@ static int merge_hierarchy(
int noexec,
char **extensions,
char **paths,
@ -521,7 +552,7 @@ index d63cf39fbb..bfe71f2267 100644
const char *meta_path,
const char *overlay_path,
const char *workspace_path) {
@@ -1640,7 +1670,7 @@ static int merge_hierarchy(
@@ -1704,7 +1747,7 @@ static int merge_hierarchy(
if (r < 0)
return r;
@ -530,7 +561,7 @@ index d63cf39fbb..bfe71f2267 100644
if (r < 0)
return r;
@@ -1692,19 +1722,29 @@ static int merge_subprocess(
@@ -1756,19 +1799,29 @@ static int merge_subprocess(
ImageClass image_class,
char **hierarchies,
bool force,
@ -543,7 +574,7 @@ index d63cf39fbb..bfe71f2267 100644
*host_os_release_version_id = NULL, *host_os_release_api_level = NULL,
- *filename = NULL;
+ *filename = NULL, *old_origin_content = NULL,
+ *extensions_origin_content = NULL, *arg_root_resolved = NULL;
+ *extensions_origin_content = NULL, *root_resolved = NULL;
_cleanup_strv_free_ char **extensions = NULL, **extensions_v = NULL, **paths = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *extensions_origin_entries = NULL,
+ *extensions_origin_json = NULL, *mutable_dir_entries = NULL;
@ -553,7 +584,7 @@ index d63cf39fbb..bfe71f2267 100644
int r;
+ if (!isempty(arg_root)) {
+ r = chase(arg_root, NULL, CHASE_MUST_BE_DIRECTORY, &arg_root_resolved, NULL);
+ r = chase(arg_root, /* root= */ NULL, CHASE_MUST_BE_DIRECTORY, &root_resolved, /* ret_fd= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to resolve --root='%s': %m", strempty(arg_root));
+ }
@ -561,7 +592,7 @@ index d63cf39fbb..bfe71f2267 100644
assert(path_startswith(workspace, "/run/"));
/* Mark the whole of /run as MS_SLAVE, so that we can mount stuff below it that doesn't show up on
@@ -1742,7 +1782,8 @@ static int merge_subprocess(
@@ -1806,7 +1859,8 @@ static int merge_subprocess(
/* Let's now mount all images */
HASHMAP_FOREACH(img, images) {
@ -571,20 +602,20 @@ index d63cf39fbb..bfe71f2267 100644
p = path_join(workspace, image_class_info[image_class].short_identifier_plural, img->name);
if (!p)
@@ -1841,6 +1882,12 @@ static int merge_subprocess(
@@ -1905,6 +1959,12 @@ static int merge_subprocess(
if (r < 0)
return r;
+ if (verity_settings.root_hash) {
+ r = sd_json_variant_new_hex(&verity_hash, verity_settings.root_hash, verity_settings.root_hash_size);
+ if (iovec_is_set(&verity_settings.root_hash)) {
+ r = sd_json_variant_new_hex(&verity_hash, verity_settings.root_hash.iov_base, verity_settings.root_hash.iov_len);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create origin verity entry for '%s': %m", img->name);
+ }
+
r = dissected_image_decrypt(m, /* passphrase= */ NULL, &verity_settings, flags);
r = dissected_image_decrypt(m, /* passphrase= */ NULL, &verity_settings, pick_image_policy(img), flags);
if (r < 0)
return r;
@@ -1910,6 +1957,59 @@ static int merge_subprocess(
@@ -1974,6 +2034,60 @@ static int merge_subprocess(
if (r < 0)
return log_oom();
@ -593,36 +624,37 @@ index d63cf39fbb..bfe71f2267 100644
+
+ if (!isempty(arg_root)) {
+ const char *without_root = NULL;
+ without_root = path_startswith(img->path, arg_root_resolved);
+ if (!isempty(without_root))
+ without_root = path_startswith(img->path, root_resolved);
+ if (!isempty(without_root)) {
+ path_without_root = strjoin("/", without_root);
+ if (!path_without_root)
+ return log_oom();
+ }
+ }
+ if (!path_without_root)
+ if (!path_without_root) {
+ path_without_root = strdup(img->path);
+ if (!path_without_root)
+ return log_oom();
+ }
+
+ /* The verity hash is not available for all extension types,
+ * thus, but only as fallback, also include data to check for
+ * file/directory replacements through a file handle and unique
+ * mount ID (or inode and mount ID as fallback).
+ * A unique mount ID is best because st_dev gets reused too easily,
+ * e.g., by a loop dev mount. For the mount ID to be valid it
+ * has to be resolved before we enter the new mount namespace.
+ * Thus, here it wouldn't work and so instead it gets provided
+ * by the image dissect logic and handed over to this subprocess
+ * we are in.
+ * Online modification is not well supported with overlay
+ * mounts, so we don't do a file checksum nor do we recurse
+ * into a directory to look for touched files. If users want
+ * modifications to be picked up, they need to set the
+ * --always-refresh=yes flag (as will be printed out). */
+ /* The verity hash is not available for all extension types, thus, but only as fallback,
+ * also include data to check for file/directory replacements through a file handle and
+ * unique mount ID (or inode and mount ID as fallback).
+ * A unique mount ID is best because st_dev gets reused too easily, e.g., by a loop dev
+ * mount. For the mount ID to be valid it has to be resolved before we enter the new mount
+ * namespace. Thus, here it wouldn't work and so instead it gets provided by the image
+ * dissect logic and handed over to this subprocess we are in.
+ * Online modification is not well supported with overlay mounts, so we don't do a file
+ * checksum nor do we recurse into a directory to look for touched files. If users want
+ * modifications to be picked up, they need to set the --always-refresh=yes flag (as will be
+ * printed out). */
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *origin_entry = NULL;
+
+ /* We suppress inclusion of weak identifiers when a strong one is there
+ * so that, e.g., a confext image stored on /usr gets identified only
+ * by the verity hash instead of also the mount ID because that changes
+ * when a sysext overlay mount appears but since the verity hash is the
+ * same for the confext it can actually be reused. */
+ /* We suppress inclusion of weak identifiers when a strong one is there so that, e.g.,
+ * a confext image stored on /usr gets identified only by the verity hash instead of also
+ * the mount ID because that changes when a sysext overlay mount appears but since the
+ * verity hash is the same for the confext it can actually be reused. */
+ r = sd_json_buildo(&origin_entry,
+ SD_JSON_BUILD_PAIR_STRING("path", path_without_root),
+ SD_JSON_BUILD_PAIR_CONDITION(!!verity_hash, "verityHash", SD_JSON_BUILD_VARIANT(verity_hash)),
@ -644,7 +676,15 @@ index d63cf39fbb..bfe71f2267 100644
n_extensions++;
}
@@ -1926,6 +2026,96 @@ static int merge_subprocess(
@@ -1983,13 +2097,112 @@ static int merge_subprocess(
log_info("No suitable extensions found (%u ignored due to incompatible image(s)).", n_ignored);
else
log_info("No extensions found.");
- return 0;
+ return MERGE_NOTHING_FOUND;
}
/* Order by version sort with strverscmp_improved() */
typesafe_qsort(extensions, n_extensions, strverscmp_improvedp);
typesafe_qsort(extensions_v, n_extensions, strverscmp_improvedp);
@ -659,12 +699,18 @@ index d63cf39fbb..bfe71f2267 100644
+
+ if (op->resolved_mutable_directory && !isempty(arg_root)) {
+ const char *without_root = NULL;
+ without_root = path_startswith(op->resolved_mutable_directory, arg_root_resolved);
+ if (!isempty(without_root))
+ without_root = path_startswith(op->resolved_mutable_directory, root_resolved);
+ if (!isempty(without_root)) {
+ mutable_directory_without_root = strjoin("/", without_root);
+ if (!mutable_directory_without_root)
+ return log_oom();
+ }
+ }
+ if (!mutable_directory_without_root && op->resolved_mutable_directory)
+ if (!mutable_directory_without_root && op->resolved_mutable_directory) {
+ mutable_directory_without_root = strdup(op->resolved_mutable_directory);
+ if (!mutable_directory_without_root)
+ return log_oom();
+ }
+
+ if (mutable_directory_without_root) {
+ r = sd_json_variant_set_field_string(&mutable_dir_entries, *h, mutable_directory_without_root);
@ -673,7 +719,7 @@ index d63cf39fbb..bfe71f2267 100644
+ }
+
+ /* Find existing origin file for comparison. */
+ r = chase(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL);
+ r = chase(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, /* ret_fd= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root), *h);
+
@ -690,7 +736,7 @@ index d63cf39fbb..bfe71f2267 100644
+ if (old_origin_content)
+ continue;
+
+ r = read_full_file(f, &buf, NULL);
+ r = read_full_file(f, &buf, /* ret_size */ NULL);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to open '%s', continuing search: %m", f);
+ continue;
@ -701,10 +747,13 @@ index d63cf39fbb..bfe71f2267 100644
+
+ r = sd_json_buildo(&extensions_origin_json,
+ SD_JSON_BUILD_PAIR_OBJECT("mutable",
+ SD_JSON_BUILD_PAIR_INTEGER("mode", arg_mutable),
+ SD_JSON_BUILD_PAIR_STRING("mode", mutable_mode_to_string(arg_mutable)),
+ SD_JSON_BUILD_PAIR_CONDITION(!!mutable_dir_entries,
+ "mutableDirs",
+ SD_JSON_BUILD_VARIANT(mutable_dir_entries))),
+ SD_JSON_BUILD_PAIR_CONDITION(!isempty(arg_overlayfs_mount_options),
+ "mountOptions",
+ SD_JSON_BUILD_STRING(arg_overlayfs_mount_options)),
+ SD_JSON_BUILD_PAIR_CONDITION(!!extensions_origin_entries,
+ "extensions",
+ SD_JSON_BUILD_VARIANT(extensions_origin_entries)));
@ -721,7 +770,7 @@ index d63cf39fbb..bfe71f2267 100644
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *old_origin_json = NULL;
+
+ log_debug("Old extension origin entry (unordered):\n%s\n", old_origin_content);
+ r = sd_json_parse(old_origin_content, 0, &old_origin_json, NULL, NULL);
+ r = sd_json_parse(old_origin_content, /* flags= */ 0, &old_origin_json, /* reterr_line= */ NULL, /* reterr_column= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse existing extension origin content: %m");
+
@ -730,7 +779,7 @@ index d63cf39fbb..bfe71f2267 100644
+ if (!always_refresh) {
+ /* This only happens during refresh, not merge, thus talk about refresh here. */
+ log_info("Skipping extension refresh because no change was found, use --always-refresh=yes to always do a refresh.");
+ return 2;
+ return MERGE_SKIP_REFRESH;
+ }
+
+ log_debug("No change found based on origin entry but continuing as requested by --always-refresh=yes.");
@ -741,7 +790,7 @@ index d63cf39fbb..bfe71f2267 100644
if (n_extensions == 0) {
assert(arg_mutable != MUTABLE_NO);
log_info("No extensions found, proceeding in mutable mode.");
@@ -2008,6 +2198,7 @@ static int merge_subprocess(
@@ -2072,6 +2285,7 @@ static int merge_subprocess(
noexec,
extensions,
paths,
@ -749,7 +798,15 @@ index d63cf39fbb..bfe71f2267 100644
meta_path,
overlay_path,
merge_hierarchy_workspace);
@@ -2059,6 +2250,7 @@ static int merge(ImageClass image_class,
@@ -2116,13 +2330,14 @@ static int merge_subprocess(
log_info("Merged extensions into '%s'.", resolved);
}
- return 1;
+ return MERGE_MOUNTED;
}
static int merge(ImageClass image_class,
char **hierarchies,
bool force,
bool no_reload,
@ -757,7 +814,7 @@ index d63cf39fbb..bfe71f2267 100644
int noexec,
Hashmap *images) {
pid_t pid;
@@ -2070,14 +2262,20 @@ static int merge(ImageClass image_class,
@@ -2138,21 +2353,28 @@ static int merge(ImageClass image_class,
if (r == 0) {
/* Child with its own mount namespace */
@ -770,28 +827,29 @@ index d63cf39fbb..bfe71f2267 100644
* created below /run. Nice! */
- _exit(r > 0 ? EXIT_SUCCESS : 123); /* 123 means: didn't find any extensions */
+ /* 0/123 means: didn't find any extensions, 2/124 means: skipped refresh */
+ if (r < 0)
+ _exit(EXIT_FAILURE);
+ if (r == 0)
+ _exit(123);
+ if (r == 2)
+ _exit(124);
+ if (r == MERGE_NOTHING_FOUND)
+ _exit(MERGE_EXIT_NOTHING_FOUND);
+ if (r == MERGE_SKIP_REFRESH)
+ _exit(MERGE_EXIT_SKIP_REFRESH);
+
+ _exit(EXIT_SUCCESS);
}
r = wait_for_terminate_and_check("(sd-merge)", pid, WAIT_LOG_ABNORMAL);
@@ -2085,6 +2283,8 @@ static int merge(ImageClass image_class,
if (r < 0)
return r;
if (r == 123) /* exit code 123 means: didn't do anything */
return 0;
+ if (r == 124) /* exit code 124 means: skipped refresh */
+ return 1;
- if (r == 123) /* exit code 123 means: didn't do anything */
- return 0;
+ if (r == MERGE_EXIT_NOTHING_FOUND)
+ return 0; /* Tell refresh to unmount */
+ if (r == MERGE_EXIT_SKIP_REFRESH)
+ return 1; /* Same return code as below when we have merged new */
if (r > 0)
return log_error_errno(SYNTHETIC_ERRNO(EPROTO), "Failed to merge hierarchies");
@@ -2182,6 +2382,7 @@ static int verb_merge(int argc, char **argv, void *userdata) {
@@ -2250,6 +2472,7 @@ static int verb_merge(int argc, char **argv, void *userdata) {
arg_hierarchies,
arg_force,
arg_no_reload,
@ -799,7 +857,7 @@ index d63cf39fbb..bfe71f2267 100644
arg_noexec,
images);
}
@@ -2190,16 +2391,18 @@ typedef struct MethodMergeParameters {
@@ -2258,16 +2481,18 @@ typedef struct MethodMergeParameters {
const char *class;
int force;
int no_reload;
@ -822,7 +880,7 @@ index d63cf39fbb..bfe71f2267 100644
{}
};
@@ -2215,6 +2418,7 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var
@@ -2283,6 +2508,7 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var
MethodMergeParameters p = {
.force = -1,
.no_reload = -1,
@ -830,7 +888,7 @@ index d63cf39fbb..bfe71f2267 100644
.noexec = -1,
};
_cleanup_strv_free_ char **hierarchies = NULL;
@@ -2249,6 +2453,7 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var
@@ -2317,6 +2543,7 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var
hierarchies ?: arg_hierarchies,
p.force >= 0 ? p.force : arg_force,
p.no_reload >= 0 ? p.no_reload : arg_no_reload,
@ -838,7 +896,7 @@ index d63cf39fbb..bfe71f2267 100644
p.noexec >= 0 ? p.noexec : arg_noexec,
images);
if (r < 0)
@@ -2262,6 +2467,7 @@ static int refresh(
@@ -2330,6 +2557,7 @@ static int refresh(
char **hierarchies,
bool force,
bool no_reload,
@ -846,7 +904,7 @@ index d63cf39fbb..bfe71f2267 100644
int noexec) {
_cleanup_hashmap_free_ Hashmap *images = NULL;
@@ -2272,9 +2478,10 @@ static int refresh(
@@ -2340,9 +2568,10 @@ static int refresh(
return r;
/* Returns > 0 if it did something, i.e. a new overlayfs is mounted now. When it does so it
@ -859,7 +917,7 @@ index d63cf39fbb..bfe71f2267 100644
if (r < 0)
return r;
if (r == 0) /* No images found? Then unmerge. The goal of --refresh is after all that after having
@@ -2286,7 +2493,8 @@ static int refresh(
@@ -2354,7 +2583,8 @@ static int refresh(
* 1. If an overlayfs was mounted before and no extensions exist anymore, we'll have unmerged things.
*
* 2. If an overlayfs was mounted before, and there are still extensions installed' we'll have
@ -869,7 +927,7 @@ index d63cf39fbb..bfe71f2267 100644
*
* 3. If an overlayfs so far wasn't mounted, and there are extensions installed, we'll have it
* mounted now.
@@ -2310,6 +2518,7 @@ static int verb_refresh(int argc, char **argv, void *userdata) {
@@ -2378,6 +2608,7 @@ static int verb_refresh(int argc, char **argv, void *userdata) {
arg_hierarchies,
arg_force,
arg_no_reload,
@ -877,7 +935,7 @@ index d63cf39fbb..bfe71f2267 100644
arg_noexec);
}
@@ -2318,6 +2527,7 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v
@@ -2386,6 +2617,7 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v
MethodMergeParameters p = {
.force = -1,
.no_reload = -1,
@ -885,7 +943,7 @@ index d63cf39fbb..bfe71f2267 100644
.noexec = -1,
};
_cleanup_strv_free_ char **hierarchies = NULL;
@@ -2338,6 +2548,7 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v
@@ -2406,6 +2638,7 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v
hierarchies ?: arg_hierarchies,
p.force >= 0 ? p.force : arg_force,
p.no_reload >= 0 ? p.no_reload : arg_no_reload,
@ -893,7 +951,7 @@ index d63cf39fbb..bfe71f2267 100644
p.noexec >= 0 ? p.noexec : arg_noexec);
if (r < 0)
return r;
@@ -2456,6 +2667,8 @@ static int verb_help(int argc, char **argv, void *userdata) {
@@ -2524,6 +2757,8 @@ static int verb_help(int argc, char **argv, void *userdata) {
" Generate JSON output\n"
" --force Ignore version incompatibilities\n"
" --no-reload Do not reload the service manager\n"
@ -902,7 +960,7 @@ index d63cf39fbb..bfe71f2267 100644
" --image-policy=POLICY\n"
" Specify disk image dissection policy\n"
" --noexec=BOOL Whether to mount extension overlay with noexec\n"
@@ -2483,21 +2696,23 @@ static int parse_argv(int argc, char *argv[]) {
@@ -2551,21 +2786,23 @@ static int parse_argv(int argc, char *argv[]) {
ARG_IMAGE_POLICY,
ARG_NOEXEC,
ARG_NO_RELOAD,
@ -937,7 +995,7 @@ index d63cf39fbb..bfe71f2267 100644
{}
};
@@ -2561,6 +2776,12 @@ static int parse_argv(int argc, char *argv[]) {
@@ -2632,6 +2869,12 @@ static int parse_argv(int argc, char *argv[]) {
arg_no_reload = true;
break;
@ -948,60 +1006,8 @@ index d63cf39fbb..bfe71f2267 100644
+ break;
+
case ARG_MUTABLE:
r = parse_mutable_mode(optarg);
if (r < 0)
diff --git a/test/units/TEST-50-DISSECT.sysext.sh b/test/units/TEST-50-DISSECT.sysext.sh
index 3eec224eb6..05f691b457 100755
--- a/test/units/TEST-50-DISSECT.sysext.sh
+++ b/test/units/TEST-50-DISSECT.sysext.sh
@@ -1402,6 +1402,47 @@ rm -rf "$fake_root/var/lib/extensions/test-extension.raw.v" "$fake_root/var/othe
# Done with the above vpick symlink tests for --root= and without
+( init_trap
+: "Check if refresh skips correctly"
+fake_root=${roots_dir:+"$roots_dir/refresh-skip"}
+hierarchy=/opt
+
+findmnt --kernel=listmount >/dev/null || {
+ echo >&2 "Can't run test on old kernel, skipping test."
+ exit 0
+}
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image "$fake_root" "$hierarchy"
+prepare_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+# The mountinfo ID gets reused and is useless here, we require a unique ID from listmount
+MOUNTID1=$(findmnt --kernel=listmount -o UNIQ-ID --raw --noheadings --target "$fake_root$hierarchy")
+run_systemd_sysext "$fake_root" refresh
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+MOUNTID2=$(findmnt --kernel=listmount -o UNIQ-ID --raw --noheadings --target "$fake_root$hierarchy")
+if [ "$MOUNTID1" != "$MOUNTID2" ]; then
+ echo >&2 "Unexpected remount with 'refresh'"
+ exit 1
+fi
+rm -rf "$fake_root/var/lib/extensions/test-extension2"
+cp -ar "$fake_root/var/lib/extensions/test-extension" "$fake_root/var/lib/extensions/test-extension2"
+rm -rf "$fake_root/var/lib/extensions/test-extension"
+mv "$fake_root/var/lib/extensions/test-extension2" "$fake_root/var/lib/extensions/test-extension"
+run_systemd_sysext "$fake_root" refresh
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+MOUNTID3=$(findmnt --kernel=listmount -o UNIQ-ID --raw --noheadings --target "$fake_root$hierarchy")
+if [ "$MOUNTID2" = "$MOUNTID3" ]; then
+ echo >&2 "Unexpected skip with 'refresh'"
+ exit 1
+fi
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+)
+
} # End of run_sysext_tests
if (streq(optarg, "help")) {
if (arg_legend)
--
2.52.0

View File

@ -1,335 +0,0 @@
From 187e60032a26fb58b8944aac5c48a495f9de2644 Mon Sep 17 00:00:00 2001
From: Kai Lueke <kailuke@microsoft.com>
Date: Thu, 20 Nov 2025 23:43:55 +0900
Subject: [PATCH 12/20] test: Add tests for handling symlinks with
systemd-sysext
When we now allow following symlinks inside a --root= we should also
test that it works in various cases from simple relative and absolute
symlinks to .v being a symlink itself or its contents, both for
directory and for .raw image extensions. While at it, also add a simple
test for .v without symlinks which wasn't there for direct usage of
systemd-sysext.
---
test/units/TEST-50-DISSECT.sysext.sh | 298 +++++++++++++++++++++++++++
1 file changed, 298 insertions(+)
diff --git a/test/units/TEST-50-DISSECT.sysext.sh b/test/units/TEST-50-DISSECT.sysext.sh
index ecf0b83b1d..3eec224eb6 100755
--- a/test/units/TEST-50-DISSECT.sysext.sh
+++ b/test/units/TEST-50-DISSECT.sysext.sh
@@ -163,6 +163,24 @@ prepare_extension_image_with_matching_id_like() {
prepend_trap "rm -rf ${ext_dir@Q}"
}
+prepare_extension_image_raw() {
+ local root=${1:-}
+ local hierarchy=${2:?}
+ local ext_dir ext_release name
+
+ name="test-extension"
+ ext_dir="$root/var/lib/extensions/$name"
+ ext_release="$ext_dir/usr/lib/extension-release.d/extension-release.$name"
+ mkdir -p "${ext_release%/*}"
+ echo "ID=_any" >"$ext_release"
+ mkdir -p "$ext_dir/$hierarchy"
+ touch "$ext_dir$hierarchy/preexisting-file-in-extension-image"
+ mksquashfs "$ext_dir" "$ext_dir.raw" -all-root -noappend -quiet
+ rm -rf "$ext_dir"
+
+ prepend_trap "rm -rf ${ext_dir@Q}.raw"
+}
+
prepare_extension_mutable_dir() {
local dir=${1:?}
@@ -1104,6 +1122,286 @@ fi
rm "${fake_root}/etc/initrd-release"
)
+# A couple of symlink tests follow below
+
+( init_trap
+: "Check if following a relative extension directory symlink works with and without --root="
+fake_root=${roots_dir:+"$roots_dir/follow-relative-dir-symlink"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image "$fake_root" "$hierarchy"
+mv -T "$fake_root/var/lib/extensions/test-extension" "$fake_root/var/othername-extension"
+ln -s "../../othername-extension" "$fake_root/var/lib/extensions/test-extension"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/othername-extension"
+)
+
+( init_trap
+: "Check if following an absolute extension directory symlink works with and without --root="
+fake_root=${roots_dir:+"$roots_dir/follow-absolute-dir-symlink"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image "$fake_root" "$hierarchy"
+mv -T "$fake_root/var/lib/extensions/test-extension" "$fake_root/var/othername-extension"
+ln -s "/var/othername-extension" "$fake_root/var/lib/extensions/test-extension"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/othername-extension"
+)
+
+( init_trap
+: "Check if following a relative extension image symlink works with and without --root="
+fake_root=${roots_dir:+"$roots_dir/follow-relative-image-symlink"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image_raw "$fake_root" "$hierarchy"
+mv "$fake_root/var/lib/extensions/test-extension.raw" "$fake_root/var/othername-extension.raw"
+ln -s "../../othername-extension.raw" "$fake_root/var/lib/extensions/test-extension.raw"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/othername-extension.raw"
+)
+
+( init_trap
+: "Check if following an absolute extension image symlink works with and without --root="
+fake_root=${roots_dir:+"$roots_dir/follow-absolute-image-symlink"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image_raw "$fake_root" "$hierarchy"
+mv "$fake_root/var/lib/extensions/test-extension.raw" "$fake_root/var/othername-extension.raw"
+ln -s "/var/othername-extension.raw" "$fake_root/var/lib/extensions/test-extension.raw"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/othername-extension.raw"
+)
+
+# And now a couple of vpick tests, including following symlinks
+
+( init_trap
+: "Check if vpick works for directory extensions"
+fake_root=${roots_dir:+"$roots_dir/vpick-dir"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image "$fake_root" "$hierarchy"
+mkdir -p "$fake_root/var/lib/extensions/test-extension.v"
+mv -T "$fake_root/var/lib/extensions/test-extension" "$fake_root/var/lib/extensions/test-extension.v/test-extension_1.0"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/lib/extensions/test-extension.v"
+)
+
+( init_trap
+: "Check if vpick works for image extensions"
+fake_root=${roots_dir:+"$roots_dir/vpick-image"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image_raw "$fake_root" "$hierarchy"
+mkdir -p "$fake_root/var/lib/extensions/test-extension.raw.v"
+mv "$fake_root/var/lib/extensions/test-extension.raw" "$fake_root/var/lib/extensions/test-extension.raw.v/test-extension_1.0.raw"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/lib/extensions/test-extension.raw.v"
+)
+
+( init_trap
+: "Check if vpick works for directory extensions if .v is a relative symlink"
+fake_root=${roots_dir:+"$roots_dir/vpick-dir-relative-symlink"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image "$fake_root" "$hierarchy"
+mkdir -p "$fake_root/var/test-extension-vpick"
+mv -T "$fake_root/var/lib/extensions/test-extension" "$fake_root/var/test-extension-vpick/test-extension_1.0"
+ln -s "../../test-extension-vpick" "$fake_root/var/lib/extensions/test-extension.v"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/lib/extensions/test-extension.v" "$fake_root/var/test-extension-vpick"
+)
+
+( init_trap
+: "Check if vpick works for directory extensions if .v is an absolute symlink"
+fake_root=${roots_dir:+"$roots_dir/vpick-dir-absolute-symlink"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image "$fake_root" "$hierarchy"
+mkdir -p "$fake_root/var/test-extension-vpick"
+mv -T "$fake_root/var/lib/extensions/test-extension" "$fake_root/var/test-extension-vpick/test-extension_1.0"
+ln -s "/var/test-extension-vpick" "$fake_root/var/lib/extensions/test-extension.v"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/lib/extensions/test-extension.v" "$fake_root/var/test-extension-vpick"
+)
+
+( init_trap
+: "Check if vpick works for image extensions if .v is a relative symlink"
+fake_root=${roots_dir:+"$roots_dir/vpick-image-relative-symlink"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image_raw "$fake_root" "$hierarchy"
+mkdir -p "$fake_root/var/test-extension-vpick"
+mv "$fake_root/var/lib/extensions/test-extension.raw" "$fake_root/var/test-extension-vpick/test-extension_1.0.raw"
+ln -s "../../test-extension-vpick" "$fake_root/var/lib/extensions/test-extension.raw.v"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/lib/extensions/test-extension.raw.v" "$fake_root/var/test-extension-vpick"
+)
+
+( init_trap
+: "Check if vpick works for image extensions if .v is an absolute symlink"
+fake_root=${roots_dir:+"$roots_dir/vpick-image-absolute-symlink"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image_raw "$fake_root" "$hierarchy"
+mkdir -p "$fake_root/var/test-extension-vpick"
+mv "$fake_root/var/lib/extensions/test-extension.raw" "$fake_root/var/test-extension-vpick/test-extension_1.0.raw"
+ln -s "/var/test-extension-vpick" "$fake_root/var/lib/extensions/test-extension.raw.v"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/lib/extensions/test-extension.raw.v" "$fake_root/var/test-extension-vpick"
+)
+
+( init_trap
+: "Check if vpick works for directory extensions if inside a .v there is a relative symlink"
+fake_root=${roots_dir:+"$roots_dir/vpick-dir-relative-symlink-inside"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image "$fake_root" "$hierarchy"
+mv -T "$fake_root/var/lib/extensions/test-extension" "$fake_root/var/othername-extension"
+mkdir -p "$fake_root/var/lib/extensions/test-extension.v"
+ln -s "../../../othername-extension" "$fake_root/var/lib/extensions/test-extension.v/test-extension_1.0"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/lib/extensions/test-extension.v" "$fake_root/var/othername-extension"
+)
+
+( init_trap
+: "Check if vpick works for directory extensions if inside a .v there is an absolute symlink"
+fake_root=${roots_dir:+"$roots_dir/vpick-dir-absolute-symlink-inside"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image "$fake_root" "$hierarchy"
+mv -T "$fake_root/var/lib/extensions/test-extension" "$fake_root/var/othername-extension"
+mkdir -p "$fake_root/var/lib/extensions/test-extension.v"
+ln -s "/var/othername-extension" "$fake_root/var/lib/extensions/test-extension.v/test-extension_1.0"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/lib/extensions/test-extension.v" "$fake_root/var/othername-extension"
+)
+
+( init_trap
+: "Check if vpick works for image extensions if inside a .v there is a relative symlink"
+fake_root=${roots_dir:+"$roots_dir/vpick-image-relative-symlink-inside"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image_raw "$fake_root" "$hierarchy"
+mv "$fake_root/var/lib/extensions/test-extension.raw" "$fake_root/var/othername-extension.raw"
+mkdir -p "$fake_root/var/lib/extensions/test-extension.raw.v"
+ln -s "../../../othername-extension.raw" "$fake_root/var/lib/extensions/test-extension.raw.v/test-extension_1.0.raw"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/lib/extensions/test-extension.raw.v" "$fake_root/var/othername-extension.raw"
+)
+
+( init_trap
+: "Check if vpick works for image extensions if inside a .v there is an absolute symlink"
+fake_root=${roots_dir:+"$roots_dir/vpick-image-absolute-symlink-inside"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image_raw "$fake_root" "$hierarchy"
+mv "$fake_root/var/lib/extensions/test-extension.raw" "$fake_root/var/othername-extension.raw"
+mkdir -p "$fake_root/var/lib/extensions/test-extension.raw.v"
+ln -s "/var/othername-extension.raw" "$fake_root/var/lib/extensions/test-extension.raw.v/test-extension_1.0.raw"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
+run_systemd_sysext "$fake_root" unmerge
+extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
+rm -rf "$fake_root/var/lib/extensions/test-extension.raw.v" "$fake_root/var/othername-extension.raw"
+)
+
+# Done with the above vpick symlink tests for --root= and without
+
} # End of run_sysext_tests
--
2.52.0

View File

@ -1,45 +0,0 @@
From 773073faa6582a0bbb6f3c4d3b35a1a81fbffd81 Mon Sep 17 00:00:00 2001
From: Kai Lueke <kailuke@microsoft.com>
Date: Wed, 3 Dec 2025 00:02:32 +0900
Subject: [PATCH 13/20] sysext: Create mutable directory with the right mode
When the mutable directory didn't exist but gets created with
--mutable=yes then it used to get mode 700 and later it got patched by
a chmod because it is the top layer and must match the target hierarchy.
This meant one could not call the function to resolve the mutable
directory twice before the mount because it has a check for a proper
mode when the directory exists which is the case for the second call.
Also, this resulted in /var/lib/extensions.mutable getting created with
mode 700 which is not really required.
Don't rely on the chmod for the upper dir but directly create the
directory with the right mode by first creating all missing directories
with 755 as a sane default and then changing the mode as needed for the
mutable directory.
---
src/sysext/sysext.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index 72da02cd89..d63cf39fbb 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -952,10 +952,14 @@ static int resolve_mutable_directory(
if (!path_in_root)
return log_oom();
- r = mkdir_p(path_in_root, 0700);
+ /* This also creates /var/lib/extensions.mutable if needed */
+ r = mkdir_p(path_in_root, 0755);
if (r < 0)
return log_error_errno(r, "Failed to create a directory '%s': %m", path_in_root);
+ if (chmod(path_in_root, hierarchy_mode) < 0)
+ return log_error_errno(errno, "Failed to chmod directory '%s': %m", path_in_root);
+
_cleanup_close_ int atfd = open(path_in_root, O_DIRECTORY|O_CLOEXEC);
if (atfd < 0)
return log_error_errno(errno, "Failed to open directory '%s': %m", path_in_root);
--
2.52.0

View File

@ -1,7 +1,7 @@
From a228e6433b6febd4d252a3cb71bb0c2e63156b93 Mon Sep 17 00:00:00 2001
From c910dda50a611fbada3f1166b9c1cf3b91091cc6 Mon Sep 17 00:00:00 2001
From: Kai Lueke <kailuke@microsoft.com>
Date: Thu, 27 Nov 2025 17:49:15 +0900
Subject: [PATCH 15/20] sysext: Get verity user certs from given --root=
Subject: [PATCH 13/14] sysext: Get verity user certs from given --root=
The verity user certs weren't looked up in the given --root= for
systemd-sysext which made it fail to set up extensions with a strict
@ -10,151 +10,150 @@ Look up verity user certs from inside the --root= when we operate on
images in it. The main use case where this matters is when the initrd
sets up the extensions for the final system and thus systemd-sysext
should do the same thing as it would do in the final system.
Signed-off-by: Kai Lueke <kailuke@microsoft.com>
---
src/core/namespace.c | 1 +
src/machine/image-dbus.c | 8 ++--
src/machine/machined-varlink.c | 2 +-
src/mountfsd/mountwork.c | 1 +
src/portable/portabled-image-bus.c | 2 +-
src/shared/discover-image.c | 3 +-
src/shared/discover-image.h | 2 +-
src/shared/dissect-image.c | 22 ++++++-----
src/shared/dissect-image.h | 2 +-
src/sysext/sysext.c | 4 +-
test/units/TEST-50-DISSECT.sysext.sh | 58 ++++++++++++++++++++++++++++
11 files changed, 85 insertions(+), 20 deletions(-)
src/core/namespace.c | 3 ++-
src/machine/image-dbus.c | 8 ++++----
src/machine/machined-varlink.c | 2 +-
src/mountfsd/mountwork.c | 1 +
src/portable/portabled-image-bus.c | 2 +-
src/shared/discover-image.c | 3 ++-
src/shared/discover-image.h | 2 +-
src/shared/dissect-image.c | 20 ++++++++++++--------
src/shared/dissect-image.h | 2 +-
src/sysext/sysext.c | 4 ++--
10 files changed, 27 insertions(+), 20 deletions(-)
diff --git a/src/core/namespace.c b/src/core/namespace.c
index 2e3b2a4177..95f8714ea6 100644
index 0a78c4922a..ead9caafe6 100644
--- a/src/core/namespace.c
+++ b/src/core/namespace.c
@@ -2593,6 +2593,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) {
@@ -2596,7 +2596,8 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) {
r = dissected_image_decrypt(
dissected_image,
NULL,
+ NULL,
- NULL,
+ /* root= */ NULL,
+ /* passphrase= */ NULL,
p->verity,
p->root_image_policy,
dissect_image_flags);
if (r < 0)
diff --git a/src/machine/image-dbus.c b/src/machine/image-dbus.c
index 8bc6565079..2857cd18be 100644
index f7e3f7f93c..966948be44 100644
--- a/src/machine/image-dbus.c
+++ b/src/machine/image-dbus.c
@@ -284,7 +284,7 @@ int bus_image_method_get_hostname(
@@ -295,7 +295,7 @@ int bus_image_method_get_hostname(
int r;
if (!image->metadata_valid) {
- r = image_read_metadata(image, &image_policy_container);
+ r = image_read_metadata(image, NULL, &image_policy_container);
- r = image_read_metadata(image, &image_policy_container, m->runtime_scope);
+ r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
@@ -302,7 +302,7 @@ int bus_image_method_get_machine_id(
@@ -314,7 +314,7 @@ int bus_image_method_get_machine_id(
int r;
if (!image->metadata_valid) {
- r = image_read_metadata(image, &image_policy_container);
+ r = image_read_metadata(image, NULL, &image_policy_container);
- r = image_read_metadata(image, &image_policy_container, m->runtime_scope);
+ r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
@@ -330,7 +330,7 @@ int bus_image_method_get_machine_info(
@@ -343,7 +343,7 @@ int bus_image_method_get_machine_info(
int r;
if (!image->metadata_valid) {
- r = image_read_metadata(image, &image_policy_container);
+ r = image_read_metadata(image, NULL, &image_policy_container);
- r = image_read_metadata(image, &image_policy_container, m->runtime_scope);
+ r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
@@ -347,7 +347,7 @@ int bus_image_method_get_os_release(
@@ -361,7 +361,7 @@ int bus_image_method_get_os_release(
int r;
if (!image->metadata_valid) {
- r = image_read_metadata(image, &image_policy_container);
+ r = image_read_metadata(image, NULL, &image_policy_container);
- r = image_read_metadata(image, &image_policy_container, m->runtime_scope);
+ r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
diff --git a/src/machine/machined-varlink.c b/src/machine/machined-varlink.c
index 064ffab137..f3676e625c 100644
index 56abcb9438..2697a87b6d 100644
--- a/src/machine/machined-varlink.c
+++ b/src/machine/machined-varlink.c
@@ -621,7 +621,7 @@ static int list_image_one_and_maybe_read_metadata(sd_varlink *link, Image *image
@@ -627,7 +627,7 @@ static int list_image_one_and_maybe_read_metadata(Manager *m, sd_varlink *link,
assert(image);
if (should_acquire_metadata(am) && !image->metadata_valid) {
- r = image_read_metadata(image, &image_policy_container);
+ r = image_read_metadata(image, NULL, &image_policy_container);
- r = image_read_metadata(image, &image_policy_container, m->runtime_scope);
+ r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
if (r < 0 && am != ACQUIRE_METADATA_GRACEFUL)
return log_debug_errno(r, "Failed to read image metadata: %m");
if (r < 0)
diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c
index bfb8c05c22..260a668525 100644
index b078dc17a3..5ed4552d52 100644
--- a/src/mountfsd/mountwork.c
+++ b/src/mountfsd/mountwork.c
@@ -495,6 +495,7 @@ static int vl_method_mount_image(
@@ -538,6 +538,7 @@ static int vl_method_mount_image(
r = dissected_image_decrypt(
di,
+ NULL,
+ /* root= */ NULL,
p.password,
&verity,
dissect_flags);
use_policy,
diff --git a/src/portable/portabled-image-bus.c b/src/portable/portabled-image-bus.c
index e8bcb900ef..380a6d5d45 100644
index 21d7bab262..b5dc0f5955 100644
--- a/src/portable/portabled-image-bus.c
+++ b/src/portable/portabled-image-bus.c
@@ -61,7 +61,7 @@ int bus_image_common_get_os_release(
return 1;
if (!image->metadata_valid) {
- r = image_read_metadata(image, &image_policy_service);
+ r = image_read_metadata(image, NULL, &image_policy_service);
- r = image_read_metadata(image, &image_policy_service, m->runtime_scope);
+ r = image_read_metadata(image, /* root= */ NULL, &image_policy_service, m->runtime_scope);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c
index 192ed18687..925bc6010b 100644
index d480848473..03fffad37e 100644
--- a/src/shared/discover-image.c
+++ b/src/shared/discover-image.c
@@ -1766,7 +1766,7 @@ int image_set_pool_limit(ImageClass class, uint64_t referenced_max) {
@@ -2087,7 +2087,7 @@ int image_setup_pool(RuntimeScope scope, ImageClass class, bool use_btrfs_subvol
return 0;
}
-int image_read_metadata(Image *i, const ImagePolicy *image_policy) {
+int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_policy) {
-int image_read_metadata(Image *i, const ImagePolicy *image_policy, RuntimeScope scope) {
+int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_policy, RuntimeScope scope) {
_cleanup_(release_lock_file) LockFile global_lock = LOCK_FILE_INIT, local_lock = LOCK_FILE_INIT;
int r;
@@ -1892,6 +1892,7 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy) {
@@ -2213,6 +2213,7 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy, RuntimeScope
r = dissected_image_decrypt(
m,
+ root,
/* passphrase= */ NULL,
&verity,
flags);
image_policy,
diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h
index 7b5593f08d..4d64a306c8 100644
index b28d961fb0..c1838f9959 100644
--- a/src/shared/discover-image.h
+++ b/src/shared/discover-image.h
@@ -73,7 +73,7 @@ int image_name_lock(const char *name, int operation, LockFile *ret);
int image_set_limit(Image *i, uint64_t referenced_max);
int image_set_pool_limit(ImageClass class, uint64_t referenced_max);
@@ -78,7 +78,7 @@ int image_get_pool_usage(RuntimeScope scope, ImageClass class, uint64_t *ret);
int image_get_pool_limit(RuntimeScope scope, ImageClass class, uint64_t *ret);
int image_setup_pool(RuntimeScope scope, ImageClass class, bool use_btrfs_subvol, bool use_btrfs_quota);
-int image_read_metadata(Image *i, const ImagePolicy *image_policy);
+int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_policy);
-int image_read_metadata(Image *i, const ImagePolicy *image_policy, RuntimeScope scope);
+int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_policy, RuntimeScope scope);
bool image_in_search_path(RuntimeScope scope, ImageClass class, const char *root, const char *image);
diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c
index 64639000b1..cec4225e92 100644
index 51725e6cfb..b2be12e842 100644
--- a/src/shared/dissect-image.c
+++ b/src/shared/dissect-image.c
@@ -2740,7 +2740,7 @@ static char* dm_deferred_remove_clean(char *name) {
@@ -2810,7 +2810,7 @@ static char* dm_deferred_remove_clean(char *name) {
}
DEFINE_TRIVIAL_CLEANUP_FUNC(char *, dm_deferred_remove_clean);
@ -162,8 +161,8 @@ index 64639000b1..cec4225e92 100644
+static int validate_signature_userspace(const VeritySettings *verity, const char *root, DissectImageFlags flags) {
int r;
if (!FLAGS_SET(flags, DISSECT_IMAGE_ALLOW_USERSPACE_VERITY)) {
@@ -2785,7 +2785,7 @@ static int validate_signature_userspace(const VeritySettings *verity, DissectIma
/* Returns > 0 if signature checks out, == 0 if not, < 0 on unexpected errors */
@@ -2855,7 +2855,7 @@ static int validate_signature_userspace(const VeritySettings *verity, DissectIma
/* Because installing a signature certificate into the kernel chain is so messy, let's optionally do
* userspace validation. */
@ -172,198 +171,120 @@ index 64639000b1..cec4225e92 100644
if (r < 0)
return log_debug_errno(r, "Failed to enumerate certificates: %m");
if (strv_isempty(certs)) {
@@ -2847,6 +2847,7 @@ static int validate_signature_userspace(const VeritySettings *verity, DissectIma
@@ -2917,6 +2917,7 @@ static int validate_signature_userspace(const VeritySettings *verity, DissectIma
static int do_crypt_activate_verity(
struct crypt_device *cd,
+ const char *root,
const char *name,
const VeritySettings *verity,
DissectImageFlags flags) {
@@ -2894,7 +2895,7 @@ static int do_crypt_activate_verity(
DissectImageFlags flags,
@@ -2966,7 +2967,7 @@ static int do_crypt_activate_verity(
/* Preferably propagate the original kernel error, so that the fallback logic can work,
* as the device-mapper is finicky around concurrent activations of the same volume */
- k = validate_signature_userspace(verity, flags);
+ k = validate_signature_userspace(verity, root, flags);
if (k < 0)
return r < 0 ? r : k;
if (k == 0)
@@ -2934,8 +2935,9 @@ static usec_t verity_timeout(void) {
static int verity_partition(
return k;
if (k == 0) {
@@ -3026,6 +3027,7 @@ static int verity_partition(
PartitionDesignator designator,
- DissectedPartition *m,
- DissectedPartition *v,
+ DissectedPartition *m, /* data partition */
+ DissectedPartition *v, /* verity partition */
DissectedPartition *m, /* data partition */
DissectedPartition *v, /* verity partition */
+ const char *root, /* The root to get user verity certs from (for a sysext) */
const VeritySettings *verity,
DissectImageFlags flags,
DecryptedImage *d) {
@@ -3015,7 +3017,7 @@ static int verity_partition(
PartitionPolicyFlags policy_flags,
@@ -3111,7 +3113,7 @@ static int verity_partition(
goto check; /* The device already exists. Let's check it. */
/* The symlink to the device node does not exist yet. Assume not activated, and let's activate it. */
- r = do_crypt_activate_verity(cd, name, verity, flags);
+ r = do_crypt_activate_verity(cd, root, name, verity, flags);
- r = do_crypt_activate_verity(cd, name, verity, flags, policy_flags);
+ r = do_crypt_activate_verity(cd, root, name, verity, flags, policy_flags);
if (r >= 0)
goto try_open; /* The device is activated. Let's open it. */
/* libdevmapper can return EINVAL when the device is already in the activation stage.
@@ -3109,7 +3111,7 @@ static int verity_partition(
@@ -3205,7 +3207,7 @@ static int verity_partition(
*/
sym_crypt_free(cd);
cd = NULL;
- return verity_partition(designator, m, v, verity, flags & ~DISSECT_IMAGE_VERITY_SHARE, d);
+ return verity_partition(designator, m, v, root, verity, flags & ~DISSECT_IMAGE_VERITY_SHARE, d);
- return verity_partition(designator, m, v, verity, flags & ~DISSECT_IMAGE_VERITY_SHARE, policy_flags, d);
+ return verity_partition(designator, m, v, root, verity, flags & ~DISSECT_IMAGE_VERITY_SHARE, policy_flags, d);
}
return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "All attempts to activate verity device %s failed.", name);
@@ -3129,6 +3131,7 @@ success:
@@ -3225,6 +3227,7 @@ success:
int dissected_image_decrypt(
DissectedImage *m,
+ const char *root, /* The root to get user verity certs from (for a sysext) */
const char *passphrase,
const VeritySettings *verity,
DissectImageFlags flags) {
@@ -3176,7 +3179,7 @@ int dissected_image_decrypt(
if (k >= 0) {
flags |= getenv_bool("SYSTEMD_VERITY_SHARING") != 0 ? DISSECT_IMAGE_VERITY_SHARE : 0;
const ImagePolicy *policy,
@@ -3281,7 +3284,7 @@ int dissected_image_decrypt(
- r = verity_partition(i, p, m->partitions + k, verity, flags, d);
+ r = verity_partition(i, p, m->partitions + k, root, verity, flags, d);
k = partition_verity_hash_of(i);
if (k >= 0) {
- r = verity_partition(i, p, m->partitions + k, verity, flags, fl, d);
+ r = verity_partition(i, p, m->partitions + k, root, verity, flags, fl, d);
if (r < 0)
return r;
}
@@ -3209,7 +3212,7 @@ int dissected_image_decrypt_interactively(
@@ -3314,7 +3317,7 @@ int dissected_image_decrypt_interactively(
n--;
for (;;) {
- r = dissected_image_decrypt(m, passphrase, verity, flags);
+ r = dissected_image_decrypt(m, NULL, passphrase, verity, flags);
- r = dissected_image_decrypt(m, passphrase, verity, image_policy, flags);
+ r = dissected_image_decrypt(m, /* root= */ NULL, passphrase, verity, image_policy, flags);
if (r >= 0)
return r;
if (r == -EKEYREJECTED)
@@ -4455,6 +4458,7 @@ int verity_dissect_and_mount(
r = dissected_image_decrypt(
dissected_image,
NULL,
+ NULL,
verity,
dissect_image_flags);
if (r < 0)
@@ -4564,7 +4567,8 @@ int verity_dissect_and_mount(
r = dissected_image_decrypt(
dissected_image,
- NULL,
+ /* root= */ NULL,
+ /* passphrase= */ NULL,
verity,
image_policy,
dissect_image_flags);
diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h
index 97431bca67..004dc46dc3 100644
index 951ae66055..674a87f684 100644
--- a/src/shared/dissect-image.h
+++ b/src/shared/dissect-image.h
@@ -171,7 +171,7 @@ void dissected_image_close(DissectedImage *m);
@@ -170,7 +170,7 @@ void dissected_image_close(DissectedImage *m);
DissectedImage* dissected_image_unref(DissectedImage *m);
DEFINE_TRIVIAL_CLEANUP_FUNC(DissectedImage*, dissected_image_unref);
-int dissected_image_decrypt(DissectedImage *m, const char *passphrase, const VeritySettings *verity, DissectImageFlags flags);
+int dissected_image_decrypt(DissectedImage *m, const char *root, const char *passphrase, const VeritySettings *verity, DissectImageFlags flags);
int dissected_image_decrypt_interactively(DissectedImage *m, const char *passphrase, const VeritySettings *verity, DissectImageFlags flags);
-int dissected_image_decrypt(DissectedImage *m, const char *passphrase, const VeritySettings *verity, const ImagePolicy *image_policy, DissectImageFlags flags);
+int dissected_image_decrypt(DissectedImage *m, const char *root, const char *passphrase, const VeritySettings *verity, const ImagePolicy *image_policy, DissectImageFlags flags);
int dissected_image_decrypt_interactively(DissectedImage *m, const char *passphrase, const VeritySettings *verity, const ImagePolicy *image_policy, DissectImageFlags flags);
int dissected_image_mount(DissectedImage *m, const char *where, uid_t uid_shift, uid_t uid_range, int userns_fd, DissectImageFlags flags);
int dissected_image_mount_and_warn(DissectedImage *m, const char *where, uid_t uid_shift, uid_t uid_range, int userns_fd, DissectImageFlags flags);
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index bfe71f2267..20acc60724 100644
index 30e33a01e5..76d84f108a 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -1888,7 +1888,7 @@ static int merge_subprocess(
@@ -1965,7 +1965,7 @@ static int merge_subprocess(
return log_error_errno(r, "Failed to create origin verity entry for '%s': %m", img->name);
}
- r = dissected_image_decrypt(m, /* passphrase= */ NULL, &verity_settings, flags);
+ r = dissected_image_decrypt(m, arg_root, /* passphrase= */ NULL, &verity_settings, flags);
- r = dissected_image_decrypt(m, /* passphrase= */ NULL, &verity_settings, pick_image_policy(img), flags);
+ r = dissected_image_decrypt(m, arg_root, /* passphrase= */ NULL, &verity_settings, pick_image_policy(img), flags);
if (r < 0)
return r;
@@ -2312,7 +2312,7 @@ static int image_discover_and_read_metadata(ImageClass image_class, Hashmap **re
@@ -2402,7 +2402,7 @@ static int image_discover_and_read_metadata(ImageClass image_class, Hashmap **re
return log_error_errno(r, "Failed to discover images: %m");
HASHMAP_FOREACH(img, images) {
- r = image_read_metadata(img, image_class_info[image_class].default_image_policy);
+ r = image_read_metadata(img, arg_root, image_class_info[image_class].default_image_policy);
- r = image_read_metadata(img, image_class_info[image_class].default_image_policy, RUNTIME_SCOPE_SYSTEM);
+ r = image_read_metadata(img, arg_root, image_class_info[image_class].default_image_policy, RUNTIME_SCOPE_SYSTEM);
if (r < 0)
return log_error_errno(r, "Failed to read metadata for image %s: %m", img->name);
}
diff --git a/test/units/TEST-50-DISSECT.sysext.sh b/test/units/TEST-50-DISSECT.sysext.sh
index 05f691b457..6e64eea492 100755
--- a/test/units/TEST-50-DISSECT.sysext.sh
+++ b/test/units/TEST-50-DISSECT.sysext.sh
@@ -181,6 +181,52 @@ prepare_extension_image_raw() {
prepend_trap "rm -rf ${ext_dir@Q}.raw"
}
+prepare_extension_image_raw_verity() {
+ local root=${1:-}
+ local hierarchy=${2:?}
+ local ext_dir ext_release name tmpcrt
+
+ name="test-extension"
+ ext_dir="$root/var/lib/extensions/$name"
+ ext_release="$ext_dir/usr/lib/extension-release.d/extension-release.$name"
+ tmpcrt=$(mktemp --directory "/tmp/test-sysext.crt.XXXXXXXXXX")
+
+ prepend_trap "rm -rf ${ext_dir@Q} ${ext_dir@Q}.raw '$root/etc/verity.d/test-ext.crt' '$tmpcrt'"
+
+ mkdir -p "${ext_release%/*}"
+ echo "ID=_any" >"$ext_release"
+ mkdir -p "$ext_dir/$hierarchy"
+ touch "$ext_dir$hierarchy/preexisting-file-in-extension-image"
+ tee >"$tmpcrt/verity.openssl.cnf" <<EOF
+[ req ]
+prompt = no
+distinguished_name = req_distinguished_name
+[ req_distinguished_name ]
+C = DE
+ST = Test State
+L = Test Locality
+O = Org Name
+OU = Org Unit Name
+CN = Common Name
+emailAddress = test@email.com
+EOF
+ openssl req \
+ -config "$tmpcrt/verity.openssl.cnf" \
+ -new -x509 \
+ -newkey rsa:1024 \
+ -keyout "$tmpcrt/test-ext.key" \
+ -out "$tmpcrt/test-ext.crt" \
+ -days 365 \
+ -nodes
+ systemd-repart --make-ddi=sysext \
+ --private-key="$tmpcrt/test-ext.key" --certificate="$tmpcrt/test-ext.crt" \
+ --copy-source="$ext_dir" "$ext_dir.raw"
+ rm -rf "$ext_dir"
+ mkdir -p "$root/etc/verity.d"
+ mv "$tmpcrt/test-ext.crt" "$root/etc/verity.d/"
+ rm -rf "$tmpcrt"
+}
+
prepare_extension_mutable_dir() {
local dir=${1:?}
@@ -1439,6 +1485,18 @@ if [ "$MOUNTID2" = "$MOUNTID3" ]; then
exit 1
fi
+( init_trap
+: "Check if verity user certs get loaded from --root="
+fake_root=${roots_dir:+"$roots_dir/verity-user-cert-from-root"}
+hierarchy=/opt
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image_raw_verity "$fake_root" "$hierarchy"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+run_systemd_sysext "$fake_root" merge --image-policy=root=signed+absent:usr=signed+absent
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+
run_systemd_sysext "$fake_root" unmerge
extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
)
--
2.52.0

View File

@ -0,0 +1,29 @@
From bc53b894296eb8650aa20f047335888dac75d258 Mon Sep 17 00:00:00 2001
From: Krzesimir Nowak <knowak@microsoft.com>
Date: Mon, 20 Apr 2026 17:47:14 +0200
Subject: [PATCH 14/14] Fix name_to_handle_at_try_unique_mntid_fid for empty
paths
Signed-off-by: Krzesimir Nowak <knowak@microsoft.com>
---
src/basic/mountpoint-util.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/basic/mountpoint-util.c b/src/basic/mountpoint-util.c
index aefbed6346..ce5698b772 100644
--- a/src/basic/mountpoint-util.c
+++ b/src/basic/mountpoint-util.c
@@ -156,6 +156,10 @@ int name_to_handle_at_try_unique_mntid_fid(
int mnt_id = -1, r;
+ if (isempty(path)) {
+ flags |= AT_EMPTY_PATH;
+ }
+
assert(fd >= 0 || fd == AT_FDCWD);
/* First issues name_to_handle_at() with AT_HANDLE_MNT_ID_UNIQUE and AT_HANDLE_FID.
--
2.52.0

View File

@ -1,89 +0,0 @@
From aeacbbca05e0479c0768c4b368a2ea68668d20bc Mon Sep 17 00:00:00 2001
From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
Date: Thu, 17 Jul 2025 05:03:54 -0400
Subject: [PATCH 16/20] sysext: introduce global config file
Introduce systemd/{sysext/confext}.conf and systemd/{sysext/confext}.conf.d to provide an
alternative way of setting the cmdline options in systemd-sysext.
The config file has to have a [Sysext] or [Confext] option respectively,
which will be overridden by the cmdline.
As an example of supported config, add Mutable= option.
---
src/sysext/sysext.c | 38 ++++++++++++++++++++++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index 20acc60724..332fc55bb3 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -18,6 +18,7 @@
#include "bus-util.h"
#include "capability-util.h"
#include "chase.h"
+#include "conf-parser.h"
#include "devnum-util.h"
#include "discover-image.h"
#include "dissect-image.h"
@@ -150,6 +151,36 @@ static int parse_mutable_mode(const char *p) {
return mutable_mode_from_string(p);
}
+static DEFINE_CONFIG_PARSE_ENUM(config_parse_mutable_mode, mutable_mode, MutableMode);
+
+static int parse_config_file(ImageClass image_class) {
+ const char *section = image_class == IMAGE_SYSEXT ? "SysExt" : "ConfExt";
+ const ConfigTableItem items[] = {
+ { section, "Mutable", config_parse_mutable_mode, 0, &arg_mutable },
+ {}
+ };
+ _cleanup_free_ char *config_file = NULL;
+ int r;
+
+ config_file = strjoin("systemd/", image_class_info[image_class].short_identifier, ".conf");
+ if (!config_file)
+ return log_oom();
+
+ r = config_parse_standard_file_with_dropins_full(
+ arg_root,
+ config_file,
+ image_class == IMAGE_SYSEXT ? "SysExt\0" : "ConfExt\0",
+ config_item_table_lookup, items,
+ CONFIG_PARSE_WARN,
+ /* userdata = */ NULL,
+ /* ret_stats_by_path = */ NULL,
+ /* ret_dropin_files = */ NULL);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
static int is_our_mount_point(
ImageClass image_class,
const char *p) {
@@ -2828,6 +2859,7 @@ static int run(int argc, char *argv[]) {
arg_image_class = invoked_as(argv, "systemd-confext") ? IMAGE_CONFEXT : IMAGE_SYSEXT;
+ /* Parse environment variable first */
env_var = getenv(image_class_info[arg_image_class].mode_env);
if (env_var) {
r = parse_mutable_mode(env_var);
@@ -2838,6 +2870,12 @@ static int run(int argc, char *argv[]) {
arg_mutable = r;
}
+ /* Parse configuration file */
+ r = parse_config_file(arg_image_class);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse global config file, ignoring: %m");
+
+ /* Parse command line */
r = parse_argv(argc, argv);
if (r <= 0)
return r;
--
2.52.0

View File

@ -1,156 +0,0 @@
From d8eabd012273376febada7ad6c9481a360c2e113 Mon Sep 17 00:00:00 2001
From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
Date: Thu, 17 Jul 2025 05:28:21 -0400
Subject: [PATCH 17/20] man/sysext.conf: add systemd-sysext config files
Add sysext.conf, which similar to other configs like coredump, will be
searched in:
/{etc run usr/lib}/systemd/{sysext/confext}.conf
but also
/{etc run usr/lib}/systemd/{sysext/confext}.conf.d/*
This config is an alternative to command line options, especially useful
if we want to extend the service units without modifying them.
---
man/rules/meson.build | 1 +
man/sysext.conf.xml | 89 ++++++++++++++++++++++++++++++++++++++++++
man/systemd-sysext.xml | 8 +++-
3 files changed, 97 insertions(+), 1 deletion(-)
create mode 100644 man/sysext.conf.xml
diff --git a/man/rules/meson.build b/man/rules/meson.build
index 33f44b0659..6284183756 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -1138,6 +1138,7 @@ manpages = [
'systemd-sysext-initrd.service',
'systemd-sysext.service'],
'ENABLE_SYSEXT'],
+ ['sysext.conf', '5', ['confext.conf'], 'ENABLE_SYSEXT'],
['systemd-system-update-generator', '8', [], ''],
['systemd-system.conf',
'5',
diff --git a/man/sysext.conf.xml b/man/sysext.conf.xml
new file mode 100644
index 0000000000..cdd88f2447
--- /dev/null
+++ b/man/sysext.conf.xml
@@ -0,0 +1,89 @@
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="sysext.conf" conditional='ENABLE_SYSEXT'
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+ <refentryinfo>
+ <title>sysext.conf</title>
+ <productname>systemd</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>sysext.conf</refentrytitle>
+ <manvolnum>5</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>sysext.conf</refname>
+ <refname>confext.conf</refname>
+ <refname>sysext.conf.d</refname>
+ <refname>confext.conf.d</refname>
+ <refpurpose>Configuration files for systemd-sysext</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <para><filename>/etc/systemd/sysext.conf</filename></para>
+ <para><filename>/etc/systemd/sysext.conf.d/*.conf</filename></para>
+ <para><filename>/run/systemd/sysext.conf</filename></para>
+ <para><filename>/run/systemd/sysext.conf.d/*.conf</filename></para>
+ <para><filename>/usr/lib/systemd/sysext.conf</filename></para>
+ <para><filename>/usr/lib/systemd/sysext.conf.d/*.conf</filename></para>
+ <para><filename>/etc/systemd/confext.conf</filename></para>
+ <para><filename>/etc/systemd/confext.conf.d/*.conf</filename></para>
+ <para><filename>/run/systemd/confext.conf</filename></para>
+ <para><filename>/run/systemd/confext.conf.d/*.conf</filename></para>
+ <para><filename>/usr/lib/systemd/confext.conf</filename></para>
+ <para><filename>/usr/lib/systemd/confext.conf.d/*.conf</filename></para>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para>These configuration files control the behavior of
+ <citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry> and
+ <citerefentry><refentrytitle>systemd-confext</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
+ They are especially useful when needing to customize the behavior of the
+ respective extension service units.</para>
+ </refsect1>
+
+ <xi:include href="standard-conf.xml" xpointer="main-conf" />
+
+ <refsect1>
+ <title>Options</title>
+
+ <para>The following options are understood in both the <literal>[Sysext]</literal> and
+ <literal>[Confext]</literal> sections:</para>
+
+ <refsect2>
+ <title>Section Options</title>
+
+ <variablelist class='config-directives'>
+ <varlistentry>
+ <term><varname>Mutable=</varname></term>
+ <listitem><para>Set the mutable mode for system extensions. Takes one of <literal>no</literal>,
+ <literal>yes</literal>, <literal>auto</literal>, <literal>import</literal>,
+ <literal>ephemeral</literal>, or <literal>ephemeral-import</literal>. For details about the modes,
+ see the <option>--mutable=</option> option in
+ <citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
+ Defaults to <literal>no</literal>.</para>
+
+ <xi:include href="version-info.xml" xpointer="v259"/>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect2>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+ <para><simplelist type="inline">
+ <member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>systemd.syntax</refentrytitle><manvolnum>7</manvolnum></citerefentry></member>
+ </simplelist></para>
+ </refsect1>
+
+</refentry>
diff --git a/man/systemd-sysext.xml b/man/systemd-sysext.xml
index 3f60c85dba..6df2d94e9f 100644
--- a/man/systemd-sysext.xml
+++ b/man/systemd-sysext.xml
@@ -74,7 +74,12 @@
<para>System extension images are strictly read-only by default. On mutable host file systems,
<filename>/usr/</filename> and <filename>/opt/</filename> hierarchies become read-only while extensions
are merged, unless mutability is enabled. Mutability may be enabled via the <option>--mutable=</option>
- option; see "Mutability" below for more information.</para>
+ option and the <varname>Mutable=</varname> option in the configuration file;
+ see "Mutability" below for more information.</para>
+
+ <para>Various command options can be configured globally via configuration files. See
+ <citerefentry><refentrytitle>sysext.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+ for details.</para>
<para>System extensions are supposed to be purely additive, i.e. they are supposed to include only files
that do not exist in the underlying basic OS image. However, the underlying mechanism (overlayfs) also
@@ -491,6 +496,7 @@
<title>See Also</title>
<para><simplelist type="inline">
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>sysext.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>importctl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
--
2.52.0

View File

@ -1,50 +0,0 @@
From dccee58738d9602dd62f482ed11152f51b4da896 Mon Sep 17 00:00:00 2001
From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
Date: Thu, 17 Jul 2025 10:16:24 -0400
Subject: [PATCH 18/20] sysext: support ImagePolicy global config option
Just as Mutable=, support ImagePolicy in systemd/{sysext/confext}.conf and
dropins in systemd/{sysext.confext}.conf.d/* configs.
---
man/sysext.conf.xml | 12 ++++++++++++
src/sysext/sysext.c | 1 +
2 files changed, 13 insertions(+)
diff --git a/man/sysext.conf.xml b/man/sysext.conf.xml
index cdd88f2447..f717b74426 100644
--- a/man/sysext.conf.xml
+++ b/man/sysext.conf.xml
@@ -73,6 +73,18 @@
<xi:include href="version-info.xml" xpointer="v259"/>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><varname>ImagePolicy=</varname></term>
+ <listitem><para>Set the image policy. Takes an image policy string as argument, as per
+ <citerefentry><refentrytitle>systemd.image-policy</refentrytitle><manvolnum>7</manvolnum></citerefentry>.
+ For details, see the <option>--image-policy=</option> option in
+ <citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
+ </para>
+
+ <xi:include href="version-info.xml" xpointer="v259"/>
+ </listitem>
+ </varlistentry>
</variablelist>
</refsect2>
</refsect1>
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index 332fc55bb3..9656e975c4 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -157,6 +157,7 @@ static int parse_config_file(ImageClass image_class) {
const char *section = image_class == IMAGE_SYSEXT ? "SysExt" : "ConfExt";
const ConfigTableItem items[] = {
{ section, "Mutable", config_parse_mutable_mode, 0, &arg_mutable },
+ { section, "ImagePolicy", config_parse_image_policy, 0, &arg_image_policy },
{}
};
_cleanup_free_ char *config_file = NULL;
--
2.52.0

View File

@ -1,183 +0,0 @@
From 5d8c8737ea0b44c50e4e60a9c93c7321051f7955 Mon Sep 17 00:00:00 2001
From: Kai Lueke <kailuke@microsoft.com>
Date: Thu, 11 Dec 2025 19:49:20 +0900
Subject: [PATCH 19/20] sysext: Fix config file support with --root=
Config files for --root= weren't picked up as expected because the
--root= flag got parsed after the config file.
Switch the order of config file and CLI flag parsing while letting the
CLI flags overwrite things set by the config files by tracking state
during parsing.
---
src/sysext/sysext.c | 38 +++++++++++++++-----
test/units/TEST-50-DISSECT.sysext.sh | 52 ++++++++++++++++++++++++++++
2 files changed, 82 insertions(+), 8 deletions(-)
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index 9656e975c4..1fb0c72e48 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -92,8 +92,10 @@ static bool arg_no_reload = false;
static bool arg_always_refresh = false;
static int arg_noexec = -1;
static ImagePolicy *arg_image_policy = NULL;
+static bool arg_image_policy_set = false; /* Tracks initialization */
static bool arg_varlink = false;
static MutableMode arg_mutable = MUTABLE_NO;
+static bool arg_mutable_set = false; /* Tracks initialization */
/* Is set to IMAGE_CONFEXT when systemd is called with the confext functionality instead of the default */
static ImageClass arg_image_class = IMAGE_SYSEXT;
@@ -154,10 +156,13 @@ static int parse_mutable_mode(const char *p) {
static DEFINE_CONFIG_PARSE_ENUM(config_parse_mutable_mode, mutable_mode, MutableMode);
static int parse_config_file(ImageClass image_class) {
+ _cleanup_(image_policy_freep) ImagePolicy *config_image_policy = NULL;
+ MutableMode config_mutable = MUTABLE_NO;
const char *section = image_class == IMAGE_SYSEXT ? "SysExt" : "ConfExt";
+ const char *sections = image_class == IMAGE_SYSEXT ? "SysExt\0" : "ConfExt\0";
const ConfigTableItem items[] = {
- { section, "Mutable", config_parse_mutable_mode, 0, &arg_mutable },
- { section, "ImagePolicy", config_parse_image_policy, 0, &arg_image_policy },
+ { section, "Mutable", config_parse_mutable_mode, 0, &config_mutable },
+ { section, "ImagePolicy", config_parse_image_policy, 0, &config_image_policy },
{}
};
_cleanup_free_ char *config_file = NULL;
@@ -170,7 +175,7 @@ static int parse_config_file(ImageClass image_class) {
r = config_parse_standard_file_with_dropins_full(
arg_root,
config_file,
- image_class == IMAGE_SYSEXT ? "SysExt\0" : "ConfExt\0",
+ sections,
config_item_table_lookup, items,
CONFIG_PARSE_WARN,
/* userdata = */ NULL,
@@ -179,6 +184,17 @@ static int parse_config_file(ImageClass image_class) {
if (r < 0)
return r;
+ /* Because this runs after parse_argv we only overwrite when things aren't set yet. */
+ if (!arg_mutable_set) {
+ arg_mutable = config_mutable;
+ arg_mutable_set = true;
+ }
+
+ if (!arg_image_policy_set) {
+ arg_image_policy = TAKE_PTR(config_image_policy);
+ arg_image_policy_set = true;
+ }
+
return 0;
}
@@ -2794,6 +2810,9 @@ static int parse_argv(int argc, char *argv[]) {
r = parse_image_policy_argument(optarg, &arg_image_policy);
if (r < 0)
return r;
+ /* When the CLI flag is given we initialize even if NULL
+ * so that the config file entry won't overwrite it */
+ arg_image_policy_set = true;
break;
case ARG_NOEXEC:
@@ -2819,6 +2838,7 @@ static int parse_argv(int argc, char *argv[]) {
if (r < 0)
return log_error_errno(r, "Failed to parse argument to --mutable=: %s", optarg);
arg_mutable = r;
+ arg_mutable_set = true;
break;
case '?':
@@ -2871,11 +2891,6 @@ static int run(int argc, char *argv[]) {
arg_mutable = r;
}
- /* Parse configuration file */
- r = parse_config_file(arg_image_class);
- if (r < 0)
- log_warning_errno(r, "Failed to parse global config file, ignoring: %m");
-
/* Parse command line */
r = parse_argv(argc, argv);
if (r <= 0)
@@ -2888,6 +2903,13 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return log_error_errno(r, "Failed to parse environment variable: %m");
+ /* Parse configuration file after argv because it needs --root=.
+ * The config entries will not overwrite values set already by
+ * env/argv because we track initialization. */
+ r = parse_config_file(arg_image_class);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse global config file, ignoring: %m");
+
if (arg_varlink) {
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL;
diff --git a/test/units/TEST-50-DISSECT.sysext.sh b/test/units/TEST-50-DISSECT.sysext.sh
index 6e64eea492..7a3146a0e7 100755
--- a/test/units/TEST-50-DISSECT.sysext.sh
+++ b/test/units/TEST-50-DISSECT.sysext.sh
@@ -1501,6 +1501,58 @@ run_systemd_sysext "$fake_root" unmerge
extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
)
+
+( init_trap
+: "Check config file support for --root="
+fake_root=${roots_dir:+"$roots_dir/config-file"}
+hierarchy=/opt
+extension_data_dir="$fake_root/var/lib/extensions.mutable$hierarchy"
+
+[[ "$FSTYPE" == "fuseblk" ]] && exit 0
+if [ "$roots_dir" = "" ]; then
+ echo >&2 "Skipping test when --root= is not used"
+ exit 0
+fi
+
+prepare_root "$fake_root" "$hierarchy"
+prepare_extension_image_raw "$fake_root" "$hierarchy"
+prepare_extension_mutable_dir "$extension_data_dir"
+prepare_read_only_hierarchy "$fake_root" "$hierarchy"
+
+mkdir -p "$fake_root/etc/systemd/"
+{ echo "[SysExt]" ; echo "Mutable=auto" ; } > "$fake_root/etc/systemd/sysext.conf"
+# Config file should be picked up with --root= set
+run_systemd_sysext "$fake_root" merge
+MNTOPT=$(findmnt "$fake_root$hierarchy" --first-only --direction backward --raw --noheadings -o VFS-OPTIONS | grep -o rw || true)
+if [ "$MNTOPT" != "rw" ]; then
+ echo >&2 "Merge did not pick up mutable setting from config file"
+ exit 1
+fi
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h -u
+run_systemd_sysext "$fake_root" unmerge
+
+# CLI arg should be able to overwrite config file
+run_systemd_sysext "$fake_root" merge --mutable=no
+MNTOPT=$(findmnt "$fake_root$hierarchy" --first-only --direction backward --raw --noheadings -o VFS-OPTIONS | grep -o ro || true)
+if [ "$MNTOPT" != "ro" ]; then
+ echo >&2 "Merge did not pick up CLI arg to overwrite mutable setting from config file"
+ exit 1
+fi
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+run_systemd_sysext "$fake_root" unmerge
+
+{ echo "[SysExt]" ; echo "ImagePolicy=root=signed+absent:usr=signed+absent" ; } > "$fake_root/etc/systemd/sysext.conf"
+# Config file should be picked up with --root= set
+if run_systemd_sysext "$fake_root" merge; then
+ echo >&2 "Merge did not fail with strict image policy in config file"
+ exit 1
+fi
+# CLI arg should be able to overwrite config file
+run_systemd_sysext "$fake_root" merge --image-policy="*"
+extension_verify_after_merge "$fake_root" "$hierarchy" -e -h
+run_systemd_sysext "$fake_root" unmerge
+)
+
} # End of run_sysext_tests
--
2.52.0

View File

@ -1,41 +0,0 @@
From 4bf1282faa430669eba4169837657f00f2cba019 Mon Sep 17 00:00:00 2001
From: Justin Kromlinger <hashworks@archlinux.org>
Date: Wed, 8 Oct 2025 16:55:09 +0200
Subject: [PATCH 20/20] Drop `machine-id` OSC event field if /etc/machine-id
doesn't exist
While we can safely assume that `/proc/sys/kernel/random/boot_id`
exists, the same can't be said for `/etc/machine-id` in environments
where systemd is installed, but not running. An example would be OCI
containers like with the official Arch Linux image, see [0].
Without this check the prompt would constantly output `/etc/machine-id:
no such file or directory` with the OSC events introduced in dadbb34
(v258).
[0] https://gitlab.archlinux.org/archlinux/archlinux-docker/-/issues/107
(cherry picked from commit 0fe45b98dd737da86fcbb703809ebf2163c397f3)
---
profile.d/80-systemd-osc-context.sh | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/profile.d/80-systemd-osc-context.sh b/profile.d/80-systemd-osc-context.sh
index a0ac858828..ead61b6753 100644
--- a/profile.d/80-systemd-osc-context.sh
+++ b/profile.d/80-systemd-osc-context.sh
@@ -28,7 +28,10 @@ __systemd_osc_context_escape() {
}
__systemd_osc_context_common() {
- printf ";user=%s;hostname=%s;machineid=%s;bootid=%s;pid=%s" "$USER" "$HOSTNAME" "$(</etc/machine-id)" "$(</proc/sys/kernel/random/boot_id)" "$$"
+ if [ -f /etc/machine-id ]; then
+ printf ";machineid=%s" "$(</etc/machine-id)"
+ fi
+ printf ";user=%s;hostname=%s;bootid=%s;pid=%s" "$USER" "$HOSTNAME" "$(</proc/sys/kernel/random/boot_id)" "$$"
}
__systemd_osc_context_precmdline() {
--
2.52.0

View File

@ -21,18 +21,8 @@ Most of these patches are not really upstreamable:
These patches can be dropped after we update to systemd 260:
- `0009-vpick-Don-t-use-openat-directly-but-resolve-symlinks.patch`
- `0010-discover-image-Follow-symlinks-in-a-given-root.patch`
- `0011-sysext-Use-correct-image-name-for-extension-release-.patch`
- `0012-test-Add-tests-for-handling-symlinks-with-systemd-sy.patch`
- `0013-sysext-Create-mutable-directory-with-the-right-mode.patch`
- `0014-sysext-Skip-refresh-if-no-changes-are-found.patch`
- `0015-sysext-Get-verity-user-certs-from-given-root.patch`
- `0016-sysext-introduce-global-config-file.patch`
- `0017-man-sysext.conf-add-systemd-sysext-config-files.patch`
- `0018-sysext-support-ImagePolicy-global-config-option.patch`
- `0019-sysext-Fix-config-file-support-with-root.patch`
This patch can be dropped after updating to systemd 258.5:
- `0020-Drop-machine-id-OSC-event-field-if-etc-machine-id-do.patch`
- `0009-discover-image-Follow-symlinks-in-a-given-root.patch`
- `0010-sysext-Use-correct-image-name-for-extension-release-.patch`
- `0011-sysext-Create-mutable-directory-with-the-right-mode.patch`
- `0012-sysext-Skip-refresh-if-no-changes-are-found.patch`
- `0013-sysext-Get-verity-user-certs-from-given-root.patch`