[RFC PATCH 7/7] ima: Support measurement of kexec initramfs components

From: Jonathan McDowell
Date: Fri Jul 08 2022 - 06:13:06 EST


An initramfs can be made up of multiple components that are concatenated
together e.g. an early uncompressed cpio archive containing early
firmware followed by a gziped cpio archive containing the actual
userspace initramfs. Add a Kconfig option to allow the IMA subsystem to
measure these components separately rather than as a single blob,
allowing for easier reasoning about system state when checking TPM PCR
values or the IMA integrity log.

Signed-off-by: Jonathan McDowell <noodles@xxxxxx>
---
security/integrity/ima/Kconfig | 16 +++
security/integrity/ima/ima_main.c | 191 ++++++++++++++++++++++++++++--
2 files changed, 199 insertions(+), 8 deletions(-)

diff --git a/security/integrity/ima/Kconfig b/security/integrity/ima/Kconfig
index 7249f16257c7..b75da44a32f2 100644
--- a/security/integrity/ima/Kconfig
+++ b/security/integrity/ima/Kconfig
@@ -41,6 +41,22 @@ config IMA_KEXEC
Depending on the IMA policy, the measurement list can grow to
be very large.

+config IMA_MEASURE_INITRAMFS_COMPONENTS
+ bool "Enable measurement of individual kexec initramfs components"
+ depends on IMA
+ select CPIO
+ default n
+ help
+ initramfs images can be made up of multiple separate components,
+ e.g. an early uncompressed cpio archive containing early firmware
+ followed by a gziped cpio archive containing the actual userspace
+ initramfs. More complex systems might involve a firmware archive,
+ a userspace archive and then a kernel module archive, allowing for
+ only the piece that needs changed to vary between boots.
+
+ This option tells IMA to measure each individual component of the
+ initramfs separately, rather than as a single blob.
+
config IMA_MEASURE_PCR_IDX
int
depends on IMA
diff --git a/security/integrity/ima/ima_main.c b/security/integrity/ima/ima_main.c
index 040b03ddc1c7..be7f446df4f2 100644
--- a/security/integrity/ima/ima_main.c
+++ b/security/integrity/ima/ima_main.c
@@ -26,6 +26,8 @@
#include <linux/ima.h>
#include <linux/iversion.h>
#include <linux/fs.h>
+#include <linux/cpio.h>
+#include <linux/decompress/generic.h>

#include "ima.h"

@@ -198,6 +200,169 @@ void ima_file_free(struct file *file)
ima_check_last_writer(iint, inode, file);
}

+#ifdef CONFIG_IMA_MEASURE_INITRAMFS_COMPONENTS
+static void initrd_error(char *x)
+{
+ pr_err("measure initrd: error from decompressor: %s\n", x);
+}
+
+static long initrd_flush(void *buf, unsigned long size)
+{
+ return size;
+}
+
+static int process_initrd_measurement(struct integrity_iint_cache *iint,
+ struct file *file, char *buf,
+ loff_t size, const char *pathname,
+ struct modsig *modsig, int pcr,
+ struct evm_ima_xattr_data *xattr_value,
+ int xattr_len,
+ struct ima_template_desc *template_desc)
+{
+ struct cpio_context cpio_ctx;
+ const char *compress_name;
+ enum hash_algo hash_algo;
+ decompress_fn decompress;
+ long consumed, written;
+ char *start, *cur;
+ char *component;
+ int buf_len;
+ bool in_cpio;
+ int rc = 0;
+ int part;
+
+ /*
+ * We collect this once, over the whole buffer.
+ */
+ if (modsig)
+ ima_collect_modsig(modsig, buf, size);
+
+ hash_algo = ima_get_hash_algo(xattr_value, xattr_len);
+
+ /*
+ * Pathname, compression name, 2 : separators, single digit part
+ * and a trailing NUL.
+ */
+ buf_len = strlen(pathname) + 5 + 2 + 2;
+ component = kmalloc(buf_len, GFP_KERNEL);
+ if (!component)
+ return -ENOMEM;
+
+ memset(&cpio_ctx, 0, sizeof(cpio_ctx));
+ cpio_ctx.parse_only = true;
+ rc = cpio_start(&cpio_ctx);
+ if (rc)
+ goto out;
+ in_cpio = false;
+ start = buf;
+ cur = buf;
+ part = 0;
+
+ while (rc == 0 && size) {
+ loff_t saved_offset = cpio_ctx.this_header;
+
+ /* It's a CPIO archive, process it */
+ if (*buf == '0' && !(cpio_ctx.this_header & 3)) {
+ in_cpio = true;
+ cpio_ctx.state = CPIO_START;
+ written = cpio_write_buffer(&cpio_ctx, buf, size);
+
+ if (written < 0) {
+ pr_err("Failed to process archive: %ld\n",
+ written);
+ break;
+ }
+
+ buf += written;
+ size -= written;
+ continue;
+ }
+ if (!*buf) {
+ buf++;
+ size--;
+ cpio_ctx.this_header++;
+ continue;
+ }
+
+ if (in_cpio) {
+ iint->flags &= ~(IMA_COLLECTED);
+ iint->measured_pcrs &= ~(0x1 << pcr);
+ rc = ima_collect_measurement(iint, file, cur,
+ buf - cur, hash_algo,
+ NULL);
+ if (rc == -ENOMEM)
+ return rc;
+
+ snprintf(component, buf_len, "%s:%s:%d",
+ pathname, "cpio", part);
+
+ ima_store_measurement(iint, file, component,
+ xattr_value, xattr_len, NULL, pcr,
+ template_desc);
+ part++;
+
+ in_cpio = false;
+ }
+
+ decompress = decompress_method(buf, size, &compress_name);
+ if (decompress) {
+ rc = decompress(buf, size, NULL, initrd_flush, NULL,
+ &consumed, initrd_error);
+ if (rc) {
+ pr_err("Failed to decompress archive\n");
+ break;
+ }
+ } else if (compress_name) {
+ pr_info("Compression method %s not configured.\n", compress_name);
+ break;
+ }
+
+ iint->flags &= ~(IMA_COLLECTED);
+ iint->measured_pcrs &= ~(0x1 << pcr);
+ rc = ima_collect_measurement(iint, file, buf,
+ consumed, hash_algo, NULL);
+ if (rc == -ENOMEM)
+ goto out;
+
+ snprintf(component, buf_len, "%s:%s:%d", pathname,
+ compress_name, part);
+
+ ima_store_measurement(iint, file, component,
+ xattr_value, xattr_len, NULL, pcr,
+ template_desc);
+ part++;
+
+ cpio_ctx.this_header = saved_offset + consumed;
+ buf += consumed;
+ size -= consumed;
+ cur = buf;
+ }
+ cpio_finish(&cpio_ctx);
+
+ /* Measure anything that remains */
+ if (size != 0) {
+ iint->flags &= ~(IMA_COLLECTED);
+ iint->measured_pcrs &= ~(0x1 << pcr);
+ rc = ima_collect_measurement(iint, file, buf, size, hash_algo,
+ NULL);
+ if (rc == -ENOMEM)
+ goto out;
+
+ snprintf(component, buf_len, "%s:left:%d",
+ pathname,
+ part);
+
+ ima_store_measurement(iint, file, component,
+ xattr_value, xattr_len, NULL, pcr,
+ template_desc);
+ }
+
+out:
+ kfree(component);
+ return rc;
+}
+#endif
+
static int process_measurement(struct file *file, const struct cred *cred,
u32 secid, char *buf, loff_t size, int mask,
enum ima_hooks func)
@@ -334,17 +499,27 @@ static int process_measurement(struct file *file, const struct cred *cred,

hash_algo = ima_get_hash_algo(xattr_value, xattr_len);

- rc = ima_collect_measurement(iint, file, buf, size, hash_algo, modsig);
- if (rc == -ENOMEM)
- goto out_locked;
-
if (!pathbuf) /* ima_rdwr_violation possibly pre-fetched */
pathname = ima_d_path(&file->f_path, &pathbuf, filename);

- if (action & IMA_MEASURE)
- ima_store_measurement(iint, file, pathname,
- xattr_value, xattr_len, modsig, pcr,
- template_desc);
+ if (IS_ENABLED(CONFIG_IMA_MEASURE_INITRAMFS_COMPONENTS) &&
+ (action & IMA_MEASURE) && func == KEXEC_INITRAMFS_CHECK) {
+ rc = process_initrd_measurement(iint, file, buf, size,
+ pathname, modsig, pcr,
+ xattr_value, xattr_len,
+ template_desc);
+ } else {
+ rc = ima_collect_measurement(iint, file, buf, size, hash_algo,
+ modsig);
+ if (rc == -ENOMEM)
+ goto out_locked;
+
+ if (action & IMA_MEASURE)
+ ima_store_measurement(iint, file, pathname,
+ xattr_value, xattr_len, modsig,
+ pcr, template_desc);
+ }
+
if (rc == 0 && (action & IMA_APPRAISE_SUBMASK)) {
rc = ima_check_blacklist(iint, modsig, pcr);
if (rc != -EPERM) {
--
2.36.1