]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
shared: add userspace cpio writer for credentials
authorPaul Meyer <katexochen0@gmail.com>
Sat, 23 May 2026 14:25:55 +0000 (16:25 +0200)
committerPaul Meyer <katexochen0@gmail.com>
Wed, 24 Jun 2026 10:47:42 +0000 (12:47 +0200)
Add a small newc-format cpio encoder that builds an archive with each
credential as a file under .extra/system_credentials/<id>.cred and
writes it to a temp file. This mirrors what systemd-stub produces from
ESP credentials, so PID1's import_credentials_boot() picks them up
unchanged via the new /.extra/system_credentials/ initrd path.

Motivated by vmspawn under SEV-SNP, where SMBIOS credentials aren't
covered by the launch measurement and are discarded by PID1 in
confidential guests, so they must be delivered via the measured initrd
instead. The writer lives in src/shared/ so other host-side tooling
can reuse it.

Signed-off-by: Paul Meyer <katexochen0@gmail.com>
src/shared/initrd-cpio.c [new file with mode: 0644]
src/shared/initrd-cpio.h [new file with mode: 0644]
src/shared/meson.build
src/test/meson.build
src/test/test-initrd-cpio.c [new file with mode: 0644]

diff --git a/src/shared/initrd-cpio.c b/src/shared/initrd-cpio.c
new file mode 100644 (file)
index 0000000..e19bbcd
--- /dev/null
@@ -0,0 +1,185 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+#include <sys/stat.h>
+
+#include "alloc-util.h"
+#include "creds-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "initrd-cpio.h"
+#include "io-util.h"
+#include "log.h"
+#include "machine-credential.h"
+#include "memory-util.h"
+#include "memstream-util.h"
+#include "string-util.h"
+#include "tmpfile-util.h"
+
+static void write_cpio_word(FILE *f, uint32_t v) {
+        assert(f);
+
+        /* Writes a CPIO header 8 character hex value */
+
+        fprintf(f, "%08" PRIx32, v);
+}
+
+static int append_pad4(FILE *f) {
+        off_t p;
+
+        assert(f);
+
+        /* Appends NUL bytes until the stream position is a multiple of 4 */
+
+        p = ftello(f);
+        if (p < 0)
+                return -errno;
+
+        for (size_t pad = (4 - ((size_t) p & 3)) & 3; pad > 0; pad--)
+                fputc(0, f);
+
+        return 0;
+}
+
+static int append_cpio_entry(
+                FILE *f,
+                uint32_t mode,                  /* full mode incl. S_IFDIR or S_IFREG */
+                const char *path,
+                const void *data,               /* NULL for directories */
+                size_t data_size,               /* 0 for directories */
+                uint32_t *inode_counter) {
+
+        int r;
+
+        assert(f);
+        assert(path);
+        assert(data || data_size == 0);
+        assert(inode_counter);
+
+        if (data_size > UINT32_MAX) /* cpio cannot deal with > 32-bit file sizes */
+                return -EFBIG;
+
+        if (*inode_counter == UINT32_MAX) /* more than 2^32-1 inodes? cpio cannot represent that either */
+                return -EOVERFLOW;
+
+        size_t namesize = strlen(path) + 1;
+        if (namesize > UINT32_MAX) /* cpio also cannot deal with names > 32-bit */
+                return -ENAMETOOLONG;
+
+        fputs("070701", f);             /* magic ID */
+        write_cpio_word(f, (*inode_counter)++); /* inode */
+        write_cpio_word(f, mode);               /* mode */
+        write_cpio_word(f, 0);                  /* uid */
+        write_cpio_word(f, 0);                  /* gid */
+        write_cpio_word(f, 1);                  /* nlink */
+        write_cpio_word(f, 0);                  /* mtime */
+        write_cpio_word(f, data_size);          /* size */
+        write_cpio_word(f, 0);                  /* major(dev) */
+        write_cpio_word(f, 0);                  /* minor(dev) */
+        write_cpio_word(f, 0);                  /* major(rdev) */
+        write_cpio_word(f, 0);                  /* minor(rdev) */
+        write_cpio_word(f, namesize);           /* fname size */
+        write_cpio_word(f, 0);                  /* crc */
+
+        fwrite(path, 1, namesize, f);
+
+        r = append_pad4(f);
+        if (r < 0)
+                return r;
+
+        if (data_size > 0)
+                fwrite(data, 1, data_size, f);
+
+        return append_pad4(f);
+}
+
+static void append_cpio_trailer(FILE *f) {
+        static const char trailer[] =
+                "070701"
+                "00000000"
+                "00000000"
+                "00000000"
+                "00000000"
+                "00000001"
+                "00000000"
+                "00000000"
+                "00000000"
+                "00000000"
+                "00000000"
+                "00000000"
+                "0000000b"
+                "00000000"
+                "TRAILER!!!\0\0\0"; /* There's a fourth NUL byte appended here, because this is a string */
+
+        assert_cc(sizeof(trailer) % 4 == 0);
+        assert(f);
+
+        fwrite(trailer, 1, sizeof trailer, f);
+}
+
+int initrd_cpio_credentials_to_tempfile(
+                const MachineCredentialContext *creds,
+                char **ret_path) {
+
+        _cleanup_(memstream_done) MemStream m = {};
+        _cleanup_(unlink_and_freep) char *path = NULL;
+        _cleanup_close_ int fd = -EBADF;
+        _cleanup_(erase_and_freep) char *buf = NULL; /* holds plaintext credential bytes; scrub on free */
+        size_t buf_size = 0;
+        uint32_t inode = 1;
+        FILE *f;
+        int r;
+
+        assert(creds);
+        assert(ret_path);
+
+        if (creds->n_credentials == 0) {
+                *ret_path = NULL;
+                return 0;
+        }
+
+        f = memstream_init(&m);
+        if (!f)
+                return log_oom();
+
+        r = append_cpio_entry(f, S_IFDIR | 0555, ".extra", NULL, 0, &inode);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write '.extra' directory entry to credentials cpio: %m");
+        r = append_cpio_entry(f, S_IFDIR | 0500, ".extra/system_credentials", NULL, 0, &inode);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write '.extra/system_credentials' directory entry to credentials cpio: %m");
+
+        FOREACH_ARRAY(c, creds->credentials, creds->n_credentials) {
+                if (!credential_name_valid(c->id))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid credential name '%s'.", strna(c->id));
+
+                _cleanup_free_ char *cpath = strjoin(".extra/system_credentials/", c->id, ".cred");
+                if (!cpath)
+                        return log_oom();
+
+                r = append_cpio_entry(f, S_IFREG | 0400, cpath, c->data, c->size, &inode);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to write credential '%s' to credentials cpio: %m", c->id);
+        }
+
+        append_cpio_trailer(f);
+
+        r = memstream_finalize(&m, &buf, &buf_size);
+        if (r < 0)
+                return log_error_errno(r, "Failed to finalize credentials cpio: %m");
+
+        r = tempfn_random_child(NULL, "credentials-cpio", &path);
+        if (r < 0)
+                return log_error_errno(r, "Failed to generate temp file name: %m");
+
+        fd = open(path, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0600);
+        if (fd < 0)
+                return log_error_errno(errno, "Failed to create temp file %s: %m", path);
+
+        r = loop_write(fd, buf, buf_size);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write credentials cpio: %m");
+
+        *ret_path = TAKE_PTR(path);
+        return 0;
+}
diff --git a/src/shared/initrd-cpio.h b/src/shared/initrd-cpio.h
new file mode 100644 (file)
index 0000000..dd6a737
--- /dev/null
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "shared-forward.h"
+
+/* Builds a new CPIO archive containing each credential as a
+ * file under .extra/system_credentials/<id>.cred, writes it to a
+ * freshly-created temp file, and returns the path in *ret_path.
+ * Caller takes ownership and is responsible for unlink()+free().
+ * If creds contains no credentials, no file will be created and
+ * *ret_path is set to NULL. */
+int initrd_cpio_credentials_to_tempfile(
+                const MachineCredentialContext *creds,
+                char **ret_path);
index 8e874cb99d1d673361af4b52cec9feed3c8b7bbc..2297f8a1afa29b848e9ac43556b059933969ee89 100644 (file)
@@ -103,6 +103,7 @@ shared_sources = files(
         'image-policy.c',
         'import-util.c',
         'in-addr-prefix-util.c',
+        'initrd-cpio.c',
         'install-file.c',
         'install-printf.c',
         'install.c',
index 0501ac0b0c558f1a5d3d19eeb4f5cf82a9226874..cd54f4d8dc2f39a287beedcb9874664b0365f9e9 100644 (file)
@@ -126,6 +126,7 @@ simple_tests += files(
         'test-import-util.c',
         'test-in-addr-prefix-util.c',
         'test-in-addr-util.c',
+        'test-initrd-cpio.c',
         'test-install-file.c',
         'test-install-root.c',
         'test-io-util.c',
diff --git a/src/test/test-initrd-cpio.c b/src/test/test-initrd-cpio.c
new file mode 100644 (file)
index 0000000..cea23a6
--- /dev/null
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#include "alloc-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "initrd-cpio.h"
+#include "machine-credential.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST_RET(initrd_cpio_credentials_basic) {
+        struct stat st;
+        size_t foo_size, bar_size;
+        _cleanup_free_ char *cmd = NULL, *extra_path = NULL, *creds_path = NULL, *foo_path = NULL, *bar_path = NULL, *foo_content = NULL, *bar_content = NULL;
+        _cleanup_(machine_credential_context_done) MachineCredentialContext creds = {};
+        _cleanup_(unlink_and_freep) char *cpio_path = NULL;
+        _cleanup_(rm_rf_physical_and_freep) char *extract_dir = NULL;
+        int r;
+
+        r = find_executable("cpio", NULL);
+        if (r < 0)
+                return log_tests_skipped_errno(r, "Could not find cpio binary: %m");
+
+        ASSERT_OK(machine_credential_add(&creds, "foo", "hello", 5));
+        ASSERT_OK(machine_credential_add(&creds, "bar", "abc\0def", 7));
+
+        ASSERT_OK(initrd_cpio_credentials_to_tempfile(&creds, &cpio_path));
+        ASSERT_NOT_NULL(cpio_path);
+
+        ASSERT_OK(mkdtemp_malloc(NULL, &extract_dir));
+        ASSERT_OK(asprintf(&cmd, "cd %s && cpio -idm < %s", extract_dir, cpio_path));
+        ASSERT_OK_ZERO_ERRNO(system(cmd));
+
+        ASSERT_NOT_NULL(extra_path = path_join(extract_dir, ".extra"));
+        ASSERT_OK_ERRNO(stat(extra_path, &st));
+        ASSERT_TRUE(S_ISDIR(st.st_mode));
+        ASSERT_EQ((mode_t) (st.st_mode & 07777), (mode_t) 0555);
+
+        ASSERT_NOT_NULL(creds_path = path_join(extract_dir, ".extra/system_credentials"));
+        ASSERT_OK_ERRNO(stat(creds_path, &st));
+        ASSERT_TRUE(S_ISDIR(st.st_mode));
+        ASSERT_EQ((mode_t) (st.st_mode & 07777), (mode_t) 0500);
+
+        ASSERT_NOT_NULL(foo_path = path_join(extract_dir, ".extra/system_credentials/foo.cred"));
+        ASSERT_OK_ERRNO(stat(foo_path, &st));
+        ASSERT_TRUE(S_ISREG(st.st_mode));
+        ASSERT_EQ((mode_t) (st.st_mode & 07777), (mode_t) 0400);
+        ASSERT_OK(read_full_file(foo_path, &foo_content, &foo_size));
+        ASSERT_EQ(foo_size, 5U);
+        ASSERT_EQ(memcmp(foo_content, "hello", 5), 0);
+
+        ASSERT_NOT_NULL(bar_path = path_join(extract_dir, ".extra/system_credentials/bar.cred"));
+        ASSERT_OK_ERRNO(stat(bar_path, &st));
+        ASSERT_TRUE(S_ISREG(st.st_mode));
+        ASSERT_EQ((mode_t) (st.st_mode & 07777), (mode_t) 0400);
+        ASSERT_OK(read_full_file(bar_path, &bar_content, &bar_size));
+        ASSERT_EQ(bar_size, 7U);
+        ASSERT_EQ(memcmp(bar_content, "abc\0def", 7), 0);
+
+        return 0;
+}
+
+TEST(initrd_cpio_credentials_rejects_invalid_name) {
+        _cleanup_(machine_credential_context_done) MachineCredentialContext creds = {};
+        _cleanup_(unlink_and_freep) char *cpio_path = NULL;
+
+        /* Bypass the validating machine_credential_add()/_set()/_load() helpers and inject an id that would
+         * escape the .extra/system_credentials/ directory, to exercise the writer's defense-in-depth check. */
+        ASSERT_NOT_NULL(creds.credentials = new0(MachineCredential, 1));
+        creds.n_credentials = 1;
+        ASSERT_NOT_NULL(creds.credentials[0].id = strdup("../../etc/evil"));
+        ASSERT_NOT_NULL(creds.credentials[0].data = memdup("x", 1));
+        creds.credentials[0].size = 1;
+
+        ASSERT_ERROR(initrd_cpio_credentials_to_tempfile(&creds, &cpio_path), EINVAL);
+        ASSERT_NULL(cpio_path);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);