/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
+#include "dirent-util.h"
#include "env-file.h"
#include "env-util.h"
#include "fd-util.h"
#include "fs-util.h"
#include "macro.h"
#include "os-util.h"
+#include "parse-util.h"
#include "path-util.h"
+#include "stat-util.h"
#include "string-util.h"
#include "strv.h"
#include "utf8.h"
+#include "xattr-util.h"
bool image_name_is_valid(const char *s) {
if (!filename_is_valid(s))
if (laccess(path, F_OK) < 0)
return -errno;
- /* We use /usr/lib/extension-release.d/extension-release.NAME as flag file if something is a system extension,
- * and {/etc|/usr/lib}/os-release as flag file if something is an OS (in case extension == NULL) */
+ /* We use /usr/lib/extension-release.d/extension-release[.NAME] as flag for something being a system extension,
+ * and {/etc|/usr/lib}/os-release as a flag for something being an OS (when not an extension). */
r = open_extension_release(path, extension, NULL, NULL);
if (r == -ENOENT) /* We got nothing */
return 0;
r = chase_symlinks(extension_full_path, root, CHASE_PREFIX_ROOT,
ret_path ? &q : NULL,
ret_fd ? &fd : NULL);
+ /* Cannot find the expected extension-release file? The image filename might have been
+ * mangled on deployment, so fallback to checking for any file in the extension-release.d
+ * directory, and return the first one with a user.extension-release xattr instead.
+ * The user.extension-release.strict xattr is checked to ensure the author of the image
+ * considers it OK if names do not match. */
+ if (r == -ENOENT) {
+ _cleanup_free_ char *extension_release_dir_path = NULL;
+ _cleanup_closedir_ DIR *extension_release_dir = NULL;
+
+ r = chase_symlinks_and_opendir("/usr/lib/extension-release.d/", root, CHASE_PREFIX_ROOT,
+ &extension_release_dir_path, &extension_release_dir);
+ if (r < 0)
+ return r;
+
+ r = -ENOENT;
+ struct dirent *de;
+ FOREACH_DIRENT(de, extension_release_dir, return -errno) {
+ int k;
+
+ if (!IN_SET(de->d_type, DT_REG, DT_UNKNOWN))
+ continue;
+
+ const char *image_name = startswith(de->d_name, "extension-release.");
+ if (!image_name)
+ continue;
+
+ if (!image_name_is_valid(image_name))
+ continue;
+
+ /* We already chased the directory, and checked that
+ * this is a real file, so we shouldn't fail to open it. */
+ _cleanup_close_ int extension_release_fd = openat(dirfd(extension_release_dir),
+ de->d_name,
+ O_PATH|O_CLOEXEC|O_NOFOLLOW);
+ if (extension_release_fd < 0)
+ return log_debug_errno(errno,
+ "Failed to open extension-release file %s/%s: %m",
+ extension_release_dir_path,
+ de->d_name);
+
+ /* Really ensure it is a regular file after we open it. */
+ if (fd_verify_regular(extension_release_fd) < 0)
+ continue;
+
+ /* No xattr or cannot parse it? Then skip this. */
+ _cleanup_free_ char *extension_release_xattr = NULL;
+ k = fgetxattrat_fake_malloc(extension_release_fd, NULL, "user.extension-release.strict", AT_EMPTY_PATH, &extension_release_xattr);
+ if (k < 0 && !ERRNO_IS_NOT_SUPPORTED(k) && k != -ENODATA)
+ log_debug_errno(k,
+ "Failed to read 'user.extension-release.strict' extended attribute from extension-release file %s/%s: %m",
+ extension_release_dir_path,
+ de->d_name);
+ if (k < 0)
+ continue;
+
+ /* Explicitly set to request strict matching? Skip it. */
+ k = parse_boolean(extension_release_xattr);
+ if (k < 0)
+ log_debug_errno(k,
+ "Failed to parse 'user.extension-release.strict' extended attribute value from extension-release file %s/%s: %m",
+ extension_release_dir_path,
+ de->d_name);
+ if (k < 0 || k > 0)
+ continue;
+
+ /* We already found what we were looking for, but there's another candidate?
+ * We treat this as an error, as we want to enforce that there are no ambiguities
+ * in case we are in the fallback path.*/
+ if (r == 0) {
+ r = -ENOTUNIQ;
+ break;
+ }
+
+ r = 0; /* Found it! */
+
+ if (ret_fd)
+ fd = TAKE_FD(extension_release_fd);
+
+ if (ret_path) {
+ q = path_join(extension_release_dir_path, de->d_name);
+ if (!q)
+ return -ENOMEM;
+ }
+ }
+ }
} else {
const char *p;
_META_MAX,
};
- static const char *paths[_META_MAX] = {
+ static const char *const paths[_META_MAX] = {
[META_HOSTNAME] = "/etc/hostname\0",
[META_MACHINE_ID] = "/etc/machine-id\0",
[META_MACHINE_INFO] = "/etc/machine-info\0",
- [META_OS_RELEASE] = "/etc/os-release\0"
- "/usr/lib/os-release\0",
- [META_EXTENSION_RELEASE] = NULL,
+ [META_OS_RELEASE] = ("/etc/os-release\0"
+ "/usr/lib/os-release\0"),
+ [META_EXTENSION_RELEASE] = "extension-release\0", /* Used only for logging. */
};
_cleanup_strv_free_ char **machine_info = NULL, **os_release = NULL, **extension_release = NULL;
assert(m);
- /* As per the os-release spec, if the image is an extension it will have a file
- * named after the image name in extension-release.d/ */
- if (m->image_name) {
- char *ext;
-
- ext = strjoina("/usr/lib/extension-release.d/extension-release.", m->image_name, "0");
- ext[strlen(ext) - 1] = '\0'; /* Extra \0 for NULSTR_FOREACH using placeholder from above */
- paths[META_EXTENSION_RELEASE] = ext;
- } else
- log_debug("No image name available, will skip extension-release metadata");
-
for (; n_meta_initialized < _META_MAX; n_meta_initialized ++) {
if (!paths[n_meta_initialized]) {
fds[2*n_meta_initialized] = fds[2*n_meta_initialized+1] = -1;
fds[2*k] = safe_close(fds[2*k]);
- NULSTR_FOREACH(p, paths[k]) {
- fd = chase_symlinks_and_open(p, t, CHASE_PREFIX_ROOT, O_RDONLY|O_CLOEXEC|O_NOCTTY, NULL);
- if (fd >= 0)
- break;
- }
+ if (k == META_EXTENSION_RELEASE) {
+ /* As per the os-release spec, if the image is an extension it will have a file
+ * named after the image name in extension-release.d/ - we use the image name
+ * and try to resolve it with the extension-release helpers, as sometimes
+ * the image names are mangled on deployment and do not match anymore.
+ * Unlike other paths this is not fixed, and the image name
+ * can be mangled on deployment, so by calling into the helper
+ * we allow a fallback that matches on the first extension-release
+ * file found in the directory, if one named after the image cannot
+ * be found first. */
+ r = open_extension_release(t, m->image_name, NULL, &fd);
+ if (r < 0)
+ fd = r; /* Propagate the error. */
+ } else
+ NULSTR_FOREACH(p, paths[k]) {
+ fd = chase_symlinks_and_open(p, t, CHASE_PREFIX_ROOT, O_RDONLY|O_CLOEXEC|O_NOCTTY, NULL);
+ if (fd >= 0)
+ break;
+ }
if (fd < 0) {
log_debug_errno(fd, "Failed to read %s file of image, ignoring: %m", paths[k]);
fds[2*k+1] = safe_close(fds[2*k+1]);
export initdir="$TESTDIR/app1"
mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/usr/lib/systemd/system" "$initdir/opt"
- grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app1"
- echo "${version_id}" >>"$initdir/usr/lib/extension-release.d/extension-release.app1"
+ grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app2"
+ echo "${version_id}" >>"$initdir/usr/lib/extension-release.d/extension-release.app2"
+ setfattr -n user.extension-release.strict -v false "$initdir/usr/lib/extension-release.d/extension-release.app2"
cat >"$initdir/usr/lib/systemd/system/app1.service" <<EOF
[Service]
Type=oneshot
#!/bin/bash
set -e
test -e /usr/lib/os-release
-cat /usr/lib/extension-release.d/extension-release.app1
+cat /usr/lib/extension-release.d/extension-release.app2
EOF
chmod +x "$initdir/opt/script1.sh"
echo MARKER=1 >"$initdir/usr/lib/systemd/system/other_file"
set -o pipefail
export SYSTEMD_LOG_LEVEL=debug
+mkdir -p /run/systemd/system/systemd-portabled.service.d/
+cat <<EOF >/run/systemd/system/systemd-portabled.service.d/override.conf
+[Service]
+Environment=SYSTEMD_LOG_LEVEL=debug
+EOF
portablectl attach --now --runtime /usr/share/minimal_0.raw app0
portablectl list | grep -q -F "No images."
-root="/usr/share/minimal_0.raw"
-app1="/usr/share/app1.raw"
+portablectl attach --now --runtime --extension /usr/share/app0.raw /usr/share/minimal_0.raw app0
-portablectl attach --now --runtime --extension ${app1} ${root} app1
+systemctl is-active app0.service
+
+portablectl reattach --now --runtime --extension /usr/share/app0.raw /usr/share/minimal_1.raw app0
+
+systemctl is-active app0.service
+
+portablectl detach --now --runtime --extension /usr/share/app0.raw /usr/share/minimal_1.raw app0
+
+portablectl attach --now --runtime --extension /usr/share/app1.raw /usr/share/minimal_0.raw app1
systemctl is-active app1.service
-portablectl reattach --now --runtime --extension ${app1} ${root} app1
+portablectl reattach --now --runtime --extension /usr/share/app1.raw /usr/share/minimal_1.raw app1
systemctl is-active app1.service
-portablectl detach --now --runtime --extension ${app1} ${root} app1
+portablectl detach --now --runtime --extension /usr/share/app1.raw /usr/share/minimal_1.raw app1
# portablectl also works with directory paths rather than images
mkdir /tmp/rootdir /tmp/app1 /tmp/overlay
-mount ${app1} /tmp/app1
-mount ${root} /tmp/rootdir
+mount /usr/share/app1.raw /tmp/app1
+mount /usr/share/minimal_0.raw /tmp/rootdir
mount -t overlay overlay -o lowerdir=/tmp/app1:/tmp/rootdir /tmp/overlay
portablectl attach --copy=symlink --now --runtime /tmp/overlay app1
systemd-run -P --property ExtensionImages=/usr/share/app0.raw --property RootImage="${image}.raw" cat /usr/lib/systemd/system/some_file | grep -q -F "MARKER=1"
systemd-run -P --property ExtensionImages="/usr/share/app0.raw /usr/share/app1.raw" --property RootImage="${image}.raw" cat /opt/script0.sh | grep -q -F "extension-release.app0"
systemd-run -P --property ExtensionImages="/usr/share/app0.raw /usr/share/app1.raw" --property RootImage="${image}.raw" cat /usr/lib/systemd/system/some_file | grep -q -F "MARKER=1"
-systemd-run -P --property ExtensionImages="/usr/share/app0.raw /usr/share/app1.raw" --property RootImage="${image}.raw" cat /opt/script1.sh | grep -q -F "extension-release.app1"
+systemd-run -P --property ExtensionImages="/usr/share/app0.raw /usr/share/app1.raw" --property RootImage="${image}.raw" cat /opt/script1.sh | grep -q -F "extension-release.app2"
systemd-run -P --property ExtensionImages="/usr/share/app0.raw /usr/share/app1.raw" --property RootImage="${image}.raw" cat /usr/lib/systemd/system/other_file | grep -q -F "MARKER=1"
cat >/run/systemd/system/testservice-50e.service <<EOF
[Service]