diff --git a/security/ipe/Kconfig b/security/ipe/Kconfig
index 619bf179af43..8746022f147b 100644
--- a/security/ipe/Kconfig
+++ b/security/ipe/Kconfig
@@ -81,4 +81,21 @@ config IPE_AUDIT_HASH_ALG
 
 source "security/ipe/modules/Kconfig"
 
+config SECURITY_IPE_KUNIT_TEST
+	bool "Build KUnit tests for IPE" if !KUNIT_ALL_TESTS
+	depends on KUNIT=y
+	default KUNIT_ALL_TESTS
+	help
+	  This builds the IPE KUnit tests.
+
+	  KUnit tests run during boot and output the results to the debug log
+	  in TAP format (https://testanything.org/). Only useful for kernel devs
+	  running KUnit test harness and are not for inclusion into a
+	  production build.
+
+	  For more information on KUnit and unit tests in general please refer
+	  to the KUnit documentation in Documentation/dev-tools/kunit/.
+
+	  If unsure, say N.
+
 endif
diff --git a/security/ipe/Makefile b/security/ipe/Makefile
index 1a1f7484caee..c91672a05d5f 100644
--- a/security/ipe/Makefile
+++ b/security/ipe/Makefile
@@ -31,4 +31,7 @@ obj-$(CONFIG_SECURITY_IPE) += \
 
 obj-$(CONFIG_IPE_AUDIT) += audit.o
 
+obj-$(CONFIG_SECURITY_IPE_KUNIT_TEST) += \
+	policy_parser_tests.o \
+
 clean-files := boot-policy.c \
diff --git a/security/ipe/ctx_test.c b/security/ipe/ctx_test.c
new file mode 100644
index 000000000000..1bbf5d5320e0
--- /dev/null
+++ b/security/ipe/ctx_test.c
@@ -0,0 +1,718 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) Microsoft Corporation. All rights reserved.
+ */
+#include <kunit/test.h>
+#include <linux/workqueue.h>
+#include <linux/types.h>
+#include <linux/list.h>
+#include "ctx.h"
+#include "policy.h"
+#include "hooks.h"
+
+struct eval_case {
+	const char *const desc;
+	const char *const policy;
+	int		  errno;
+
+	const struct file *fake_file;
+	const struct ipe_bdev *bdev_sec;
+	const struct ipe_inode *inode_sec;
+	bool initsb;
+};
+
+static const u8 fake_digest[] = { 0xDE, 0xAD, 0xBE, 0xEF };
+
+static const struct ipe_bdev fake_bdev_no_data = {};
+static const struct ipe_bdev fake_bdev_no_sig = {
+	.digest = fake_digest,
+	.digest_len = ARRAY_SIZE(fake_digest),
+	.digest_algo = "test",
+};
+
+static const struct ipe_bdev fake_bdev_signed = {
+	.dm_verity_signed = true,
+	.digest = fake_digest,
+	.digest_len = ARRAY_SIZE(fake_digest),
+	.digest_algo = "test",
+};
+
+static const struct ipe_inode fake_ino_no_data = {};
+static const struct ipe_inode fake_ino_signed = {
+	.fs_verity_signed = true,
+};
+
+static struct inode fake_inode = {
+	.i_flags = S_VERITY
+};
+
+static const struct file fake_verity = {
+	.f_inode = &fake_inode,
+};
+
+static const struct eval_case cases[] = {
+	{
+		"boot_verified_trust_no_source",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE boot_verified=TRUE action=ALLOW\n"
+		"op=KERNEL_READ boot_verified=TRUE action=ALLOW\n",
+		-EACCES, NULL, NULL, NULL, false
+	},
+	{
+		"boot_verified_distrust",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE boot_verified=FALSE action=ALLOW\n"
+		"op=KERNEL_READ boot_verified=FALSE action=ALLOW\n",
+		0, NULL, NULL, NULL, false
+	},
+	{
+		"boot_verified_trust",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE boot_verified=TRUE action=ALLOW\n"
+		"op=KERNEL_READ boot_verified=TRUE action=ALLOW\n",
+		0, NULL, NULL, NULL, true
+	},
+	{
+		"boot_verified_trust",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE boot_verified=FALSE action=ALLOW\n"
+		"op=KERNEL_READ boot_verified=FALSE action=ALLOW\n",
+		-EACCES, NULL, NULL, NULL, true
+	},
+	{
+		"dmverity_signature_trust_no_bdev",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE dmverity_signature=FALSE action=ALLOW\n"
+		"op=KERNEL_READ dmverity_signature=FALSE action=ALLOW\n",
+		0, NULL, NULL, NULL, true
+	},
+	{
+		"dmverity_signature_distrust_no_bdev",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE dmverity_signature=TRUE action=ALLOW\n"
+		"op=KERNEL_READ dmverity_signature=TRUE action=ALLOW\n",
+		-EACCES, NULL, NULL, NULL, false
+	},
+	{
+		"dmverity_signature_distrust_sigdata",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE dmverity_signature=FALSE action=ALLOW\n"
+		"op=KERNEL_READ dmverity_signature=FALSE action=ALLOW\n",
+		-EACCES, NULL, &fake_bdev_signed, &fake_ino_no_data, false
+	},
+	{
+		"dmverity_signature_trust_sigdata",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE dmverity_signature=TRUE action=ALLOW\n"
+		"op=KERNEL_READ dmverity_signature=TRUE action=ALLOW\n",
+		0, NULL, &fake_bdev_signed, &fake_ino_no_data, true
+	},
+	{
+		"dmverity_roothash_trust_no_bdev",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE dmverity_roothash=test:DEADBEEF action=ALLOW\n"
+		"op=KERNEL_READ dmverity_roothash=test:DEADBEEF action=ALLOW\n",
+		-EACCES, NULL, NULL, NULL, true
+	},
+	{
+		"dmverity_roothash_distrust_no_bdev",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"op=EXECUTE dmverity_roothash=test:deadbeef action=DENY\n"
+		"op=KERNEL_READ dmverity_roothash=test:deadbeef action=DENY\n",
+		0, NULL, NULL, NULL, false
+	},
+	{
+		"dmverity_roothash_trust_hash",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE dmverity_roothash=test:DEADBEEF action=ALLOW\n"
+		"op=KERNEL_READ dmverity_roothash=test:DEADBEEF action=ALLOW\n",
+		0, NULL, &fake_bdev_no_sig, &fake_ino_no_data, false
+	},
+	{
+		"dmverity_roothash_distrust_hash",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"op=EXECUTE dmverity_roothash=test:DEADBEEF action=DENY\n"
+		"op=KERNEL_READ dmverity_roothash=test:DEADBEEF action=DENY\n",
+		-EACCES, NULL, &fake_bdev_no_sig, &fake_ino_no_data, false
+	},
+	{
+		"dmverity_signature_revoke_hash",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE dmverity_roothash=test:DEADBEEF action=DENY\n"
+		"op=EXECUTE dmverity_signature=TRUE action=ALLOW\n"
+		"op=KERNEL_READ dmverity_roothash=test:DEADBEEF action=DENY\n"
+		"op=KERNEL_READ dmverity_signature=TRUE action=ALLOW\n",
+		-EACCES, NULL, &fake_bdev_signed, &fake_ino_no_data, false
+	},
+	{
+		"fsverity_signature_trust_sigdata",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE fsverity_signature=TRUE action=ALLOW\n"
+		"op=KERNEL_READ fsverity_signature=TRUE action=ALLOW\n",
+		0, &fake_verity, &fake_bdev_no_data, &fake_ino_signed, false
+	},
+	{
+		"fsverity_signature_distrust_sigdata",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"op=EXECUTE fsverity_signature=TRUE action=DENY\n"
+		"op=KERNEL_READ fsverity_signature=TRUE action=DENY\n",
+		-EACCES, &fake_verity, &fake_bdev_no_data, &fake_ino_signed, false
+	},
+	{
+		"fsverity_signature_trust_no_sigdata",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"op=EXECUTE fsverity_signature=FALSE action=ALLOW\n"
+		"op=KERNEL_READ fsverity_signature=FALSE action=ALLOW\n",
+		0, &fake_verity, &fake_bdev_signed, &fake_ino_no_data, true
+	},
+	{
+		"fsverity_signature_distrust_no_sigdata",
+		"policy_name='Test' policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"op=EXECUTE fsverity_signature=FALSE action=DENY\n"
+		"op=KERNEL_READ fsverity_signature=FALSE action=DENY\n",
+		-EACCES, &fake_verity, &fake_bdev_signed, &fake_ino_no_data, true
+	}
+};
+
+static void case_to_desc(const struct eval_case *c, char *desc)
+{
+	strncpy(desc, c->desc, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(ipe_eval, cases, case_to_desc);
+
+/**
+ * fake_free_ctx: Fake function to deallocate a context structure.
+ */
+static void fake_free_ctx(struct ipe_context *ctx)
+{
+	struct ipe_policy *p = NULL;
+
+	if (!IS_ERR_OR_NULL(ctx) || !refcount_dec_and_test(&ctx->refcount))
+		return;
+
+	list_for_each_entry(p, &ctx->policies, next)
+		ipe_put_policy(p);
+
+	kfree(ctx);
+}
+
+/**
+ * create_fake_ctx: Build a fake ipe_context for use
+ *		    in a test.
+ * Return:
+ * !IS_ERR - OK
+ */
+static struct ipe_context *create_fake_ctx(void)
+{
+	struct ipe_context *ctx = NULL;
+
+	ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
+	if (!ctx)
+		return ERR_PTR(-ENOMEM);
+
+	INIT_LIST_HEAD(&ctx->policies);
+	refcount_set(&ctx->refcount, 1);
+	spin_lock_init(&ctx->lock);
+	WRITE_ONCE(ctx->enforce, true);
+
+	return ctx;
+}
+
+/**
+ * ipe_ctx_eval_test: Parse a policy, and run a mock through the
+ *		      evaluation loop to check the functional result.
+ * @test: Supplies a pointer to a kunit structure.
+ *
+ * This is called by the kunit harness.
+ */
+static void ipe_ctx_eval_test(struct kunit *test)
+{
+	int rc = 0;
+	enum ipe_operation i = ipe_operation_exec;
+	struct ipe_policy *pol = NULL;
+	struct ipe_context *ctx = NULL;
+	struct ipe_eval_ctx eval = { 0 };
+	const struct eval_case *t = test->param_value;
+
+	ctx = create_fake_ctx();
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+	pol = ipe_new_policy(t->policy, strlen(t->policy), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pol);
+
+	ipe_add_policy(ctx, pol);
+	KUNIT_ASSERT_EQ(test, 0, ipe_set_active_pol(pol));
+	KUNIT_EXPECT_EQ(test, refcount_read(&pol->refcount), 2);
+	KUNIT_EXPECT_PTR_EQ(test, pol->policyfs, NULL);
+	KUNIT_EXPECT_PTR_EQ(test, pol->pkcs7, NULL);
+
+	eval.ipe_bdev = t->bdev_sec;
+	eval.ipe_inode = t->inode_sec;
+	eval.from_init_sb = t->initsb;
+	eval.ci_ctx = ctx;
+	eval.file = t->fake_file;
+
+	for (i = ipe_operation_exec; i < ipe_operation_max; ++i) {
+		eval.op = i;
+		rc = evaluate(&eval);
+		KUNIT_EXPECT_EQ(test, rc, t->errno);
+	}
+
+	fake_free_ctx(ctx);
+	ipe_put_policy(pol);
+}
+
+/**
+ * ipe_ctx_eval_permissive_test: Parse a policy, and run a mock through the
+ *				 evaluation loop to with permissive on,
+ *				 checking the functional result.
+ * @test: Supplies a pointer to a kunit structure.
+ *
+ * This is called by the kunit harness.
+ */
+static void ipe_ctx_eval_permissive_test(struct kunit *test)
+{
+	int rc = 0;
+	enum ipe_operation i = ipe_operation_exec;
+	struct ipe_policy *pol = NULL;
+	struct ipe_context *ctx = NULL;
+	struct ipe_eval_ctx eval = { 0 };
+	const struct eval_case *t = test->param_value;
+
+	ctx = create_fake_ctx();
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+	WRITE_ONCE(ctx->enforce, false);
+
+	pol = ipe_new_policy(t->policy, strlen(t->policy), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pol);
+
+	ipe_add_policy(ctx, pol);
+	KUNIT_ASSERT_EQ(test, 0, ipe_set_active_pol(pol));
+	KUNIT_EXPECT_EQ(test, refcount_read(&pol->refcount), 2);
+	KUNIT_EXPECT_PTR_EQ(test, pol->policyfs, NULL);
+	KUNIT_EXPECT_PTR_EQ(test, pol->pkcs7, NULL);
+
+	eval.ipe_bdev = t->bdev_sec;
+	eval.ipe_inode = t->inode_sec;
+	eval.from_init_sb = t->initsb;
+	eval.ci_ctx = ctx;
+	eval.file = t->fake_file;
+
+	for (i = ipe_operation_exec; i < ipe_operation_max; ++i) {
+		eval.op = i;
+		rc = evaluate(&eval);
+		KUNIT_EXPECT_EQ(test, rc, 0);
+	}
+
+	fake_free_ctx(ctx);
+	ipe_put_policy(pol);
+}
+
+/**
+ * ipe_ctx_default_eval_test: Ensure an operation-level default
+ *			      is taken over a global-level default.
+ * @test: Supplies a pointer to a kunit structure.
+ *
+ * This is called by the kunit harness.
+ */
+static void ipe_ctx_default_eval_test(struct kunit *test)
+{
+	int rc = 0;
+	struct ipe_policy *pol = NULL;
+	struct ipe_context *ctx = NULL;
+	struct ipe_eval_ctx eval = { 0 };
+	const char *const policy =
+		"policy_name=Test policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n"
+		"DEFAULT op=EXECUTE action=ALLOW";
+
+	ctx = create_fake_ctx();
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+	pol = ipe_new_policy(policy, strlen(policy), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pol);
+	KUNIT_EXPECT_EQ(test, pol->parsed->global_default, ipe_action_deny);
+	KUNIT_EXPECT_EQ(test, pol->parsed->rules[ipe_operation_exec].default_action,
+			ipe_action_allow);
+
+	ipe_add_policy(ctx, pol);
+	KUNIT_ASSERT_EQ(test, 0, ipe_set_active_pol(pol));
+	KUNIT_EXPECT_EQ(test, refcount_read(&pol->refcount), 2);
+	KUNIT_EXPECT_PTR_EQ(test, pol->policyfs, NULL);
+	KUNIT_EXPECT_PTR_EQ(test, pol->pkcs7, NULL);
+
+	eval.ipe_bdev = NULL;
+	eval.ipe_inode = NULL;
+	eval.from_init_sb = NULL;
+	eval.ci_ctx = ctx;
+	eval.file = NULL;
+	eval.op = ipe_operation_exec;
+
+	rc = evaluate(&eval);
+	KUNIT_EXPECT_EQ(test, rc, 0);
+
+	eval.op = ipe_operation_kexec_image;
+	rc = evaluate(&eval);
+	KUNIT_EXPECT_EQ(test, rc, -EACCES);
+
+	fake_free_ctx(ctx);
+	ipe_put_policy(pol);
+}
+
+/**
+ * ipe_ctx_replace_policy - Associate a policy with a context, then replace it.
+ * @test: Supplies a pointer to a kunit structure.
+ *
+ * This is called by the kunit harness.
+ */
+static void ipe_ctx_replace_policy(struct kunit *test)
+{
+	struct ipe_policy *p1 = NULL;
+	struct ipe_policy *p2 = NULL;
+	struct ipe_policy *pp = NULL;
+	struct ipe_context *ctx = NULL;
+	const char *const policy1 = "policy_name=t policy_version=0.0.0\n"
+				    "DEFAULT action=ALLOW";
+	const char *const policy2 = "policy_name=t policy_version=0.0.1\n"
+				    "DEFAULT action=DENY\n";
+
+	ctx = create_fake_ctx();
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+	p1 = ipe_new_policy(policy1, strlen(policy1), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p1);
+	p2 = ipe_new_policy(policy2, strlen(policy2), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p2);
+
+	ipe_add_policy(ctx, p1);
+	KUNIT_EXPECT_TRUE(test, list_is_singular(&ctx->policies));
+
+	pp = list_first_entry(&ctx->policies, struct ipe_policy, next);
+	KUNIT_EXPECT_PTR_EQ(test, pp, p1);
+
+	ipe_replace_policy(p1, p2);
+	KUNIT_EXPECT_TRUE(test, list_is_singular(&ctx->policies));
+	pp = list_first_entry(&ctx->policies, struct ipe_policy, next);
+	KUNIT_EXPECT_PTR_EQ(test, pp, p2);
+
+	fake_free_ctx(ctx);
+	ipe_put_policy(p1);
+	ipe_put_policy(p2);
+}
+
+/**
+ * ipe_ctx_replace_policy - Associate a policy with a context, mark the policy active,
+ *			    then replace it.
+ * @test: Supplies a pointer to a kunit structure.
+ *
+ * This is called by the kunit harness.
+ */
+static void ipe_ctx_replace_active_policy(struct kunit *test)
+{
+	struct ipe_policy *p1 = NULL;
+	struct ipe_policy *p2 = NULL;
+	struct ipe_policy *pp = NULL;
+	struct ipe_context *ctx = NULL;
+	const char *const policy1 = "policy_name=t policy_version=0.0.0\n"
+				    "DEFAULT action=ALLOW";
+	const char *const policy2 = "policy_name=t policy_version=0.0.1\n"
+				    "DEFAULT action=DENY\n";
+
+	ctx = create_fake_ctx();
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+	p1 = ipe_new_policy(policy1, strlen(policy1), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p1);
+	p2 = ipe_new_policy(policy2, strlen(policy2), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p2);
+
+	ipe_add_policy(ctx, p1);
+	KUNIT_ASSERT_EQ(test, 0, ipe_set_active_pol(p1));
+
+	rcu_read_lock();
+	pp = ipe_get_policy_rcu(ctx->active_policy);
+	rcu_read_unlock();
+	KUNIT_EXPECT_PTR_EQ(test, pp, p1);
+	ipe_put_policy(pp);
+
+	ipe_replace_policy(p1, p2);
+
+	rcu_read_lock();
+	pp = ipe_get_policy_rcu(ctx->active_policy);
+	rcu_read_unlock();
+	KUNIT_EXPECT_PTR_EQ(test, pp, p2);
+	ipe_put_policy(pp);
+
+	fake_free_ctx(ctx);
+	ipe_put_policy(p1);
+	ipe_put_policy(p2);
+}
+
+/**
+ * ipe_ctx_update_policy - Associate a policy with a context, then update it.
+ * @test: Supplies a pointer to a kunit structure.
+ *
+ * This is called by the kunit harness. This function differs from replace above,
+ * as it performs additional error checking.
+ */
+static void ipe_ctx_update_policy(struct kunit *test)
+{
+	struct ipe_policy *p1 = NULL;
+	struct ipe_policy *p2 = NULL;
+	struct ipe_policy *pp = NULL;
+	struct ipe_context *ctx = NULL;
+	const char *const policy1 = "policy_name=t policy_version=0.0.0\n"
+				    "DEFAULT action=ALLOW";
+	const char *const policy2 = "policy_name=t policy_version=0.0.1\n"
+				    "DEFAULT action=DENY\n";
+
+	ctx = create_fake_ctx();
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+	p1 = ipe_new_policy(policy1, strlen(policy1), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p1);
+
+	ipe_add_policy(ctx, p1);
+	ipe_set_active_pol(p1);
+
+	rcu_read_lock();
+	pp = ipe_get_policy_rcu(ctx->active_policy);
+	rcu_read_unlock();
+	KUNIT_EXPECT_PTR_EQ(test, pp, p1);
+	ipe_put_policy(pp);
+
+	p2 = ipe_update_policy(p1, policy2, strlen(policy2), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p2);
+
+	rcu_read_lock();
+	pp = ipe_get_policy_rcu(ctx->active_policy);
+	rcu_read_unlock();
+	KUNIT_EXPECT_PTR_EQ(test, pp, p2);
+	ipe_put_policy(pp);
+
+	fake_free_ctx(ctx);
+	ipe_put_policy(p1);
+	ipe_put_policy(p2);
+}
+
+/**
+ * ipe_ctx_update_wrong_policy - Associate a policy with a context, then
+ *				 attempt update it with the wrong policy.
+ * @test: Supplies a pointer to a kunit structure.
+ *
+ * This is called by the kunit harness.
+ */
+static void ipe_ctx_update_wrong_policy(struct kunit *test)
+{
+	struct ipe_policy *p1 = NULL;
+	struct ipe_policy *p2 = NULL;
+	struct ipe_policy *pp = NULL;
+	struct ipe_context *ctx = NULL;
+	const char *const policy1 = "policy_name=t policy_version=0.0.0\n"
+				    "DEFAULT action=ALLOW";
+	const char *const policy2 = "policy_name=t2 policy_version=0.0.0\n"
+				    "DEFAULT action=DENY\n";
+
+	ctx = create_fake_ctx();
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+	p1 = ipe_new_policy(policy1, strlen(policy1), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p1);
+
+	ipe_add_policy(ctx, p1);
+	ipe_set_active_pol(p1);
+
+	rcu_read_lock();
+	pp = ipe_get_policy_rcu(ctx->active_policy);
+	rcu_read_unlock();
+	KUNIT_EXPECT_PTR_EQ(test, pp, p1);
+	ipe_put_policy(pp);
+
+	p2 = ipe_update_policy(p1, policy2, strlen(policy2), NULL, 0);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(p2), -EINVAL);
+
+	rcu_read_lock();
+	pp = ipe_get_policy_rcu(ctx->active_policy);
+	rcu_read_unlock();
+	KUNIT_EXPECT_PTR_EQ(test, pp, p1);
+	ipe_put_policy(pp);
+
+	fake_free_ctx(ctx);
+	ipe_put_policy(p1);
+	ipe_put_policy(p2);
+}
+
+/**
+ * ipe_ctx_update_wrong_policy - Associate a policy with a context, mark it active,
+ *				 then attempt update it with a stale policy.
+ * @test: Supplies a pointer to a kunit structure.
+ *
+ * This is called by the kunit harness.
+ */
+static void ipe_ctx_update_rollback_policy(struct kunit *test)
+{
+	struct ipe_policy *p1 = NULL;
+	struct ipe_policy *p2 = NULL;
+	struct ipe_policy *pp = NULL;
+	struct ipe_context *ctx = NULL;
+	const char *const policy1 = "policy_name=t policy_version=0.0.1\n"
+				    "DEFAULT action=ALLOW";
+	const char *const policy2 = "policy_name=t policy_version=0.0.0\n"
+				    "DEFAULT action=DENY\n";
+
+	ctx = create_fake_ctx();
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+	p1 = ipe_new_policy(policy1, strlen(policy1), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p1);
+
+	ipe_add_policy(ctx, p1);
+	KUNIT_ASSERT_EQ(test, 0, ipe_set_active_pol(p1));
+
+	rcu_read_lock();
+	pp = ipe_get_policy_rcu(ctx->active_policy);
+	rcu_read_unlock();
+	KUNIT_EXPECT_PTR_EQ(test, pp, p1);
+	ipe_put_policy(pp);
+
+	p2 = ipe_update_policy(p1, policy2, strlen(policy2), NULL, 0);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(p2), -EINVAL);
+
+	rcu_read_lock();
+	pp = ipe_get_policy_rcu(ctx->active_policy);
+	rcu_read_unlock();
+	KUNIT_EXPECT_PTR_EQ(test, pp, p1);
+	ipe_put_policy(pp);
+
+	fake_free_ctx(ctx);
+	ipe_put_policy(p1);
+	ipe_put_policy(p2);
+}
+
+/**
+ * ipe_ctx_rollback - Associate two policies with a context, then
+ *		      attempt rollback the active policy.
+ * @test: Supplies a pointer to a kunit structure.
+ *
+ * This is called by the kunit harness.
+ */
+static void ipe_ctx_rollback(struct kunit *test)
+{
+	struct ipe_policy *p1 = NULL;
+	struct ipe_policy *p2 = NULL;
+	struct ipe_context *ctx = NULL;
+	const char *const policy1 = "policy_name=t policy_version=0.0.1\n"
+				    "DEFAULT action=ALLOW";
+	const char *const policy2 = "policy_name=t2 policy_version=0.0.0\n"
+				    "DEFAULT action=DENY\n";
+
+	ctx = create_fake_ctx();
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+	p1 = ipe_new_policy(policy1, strlen(policy1), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p1);
+	ipe_add_policy(ctx, p1);
+	KUNIT_ASSERT_EQ(test, 0, ipe_set_active_pol(p1));
+
+	p2 = ipe_new_policy(policy2, strlen(policy2), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p2);
+	ipe_add_policy(ctx, p2);
+	KUNIT_ASSERT_EQ(test, -EINVAL, ipe_set_active_pol(p2));
+
+	fake_free_ctx(ctx);
+	ipe_put_policy(p1);
+	ipe_put_policy(p2);
+}
+
+/**
+ * ipe_ctx_update_rollback_inactive - Associate a policy with a context, then
+ *				      attempt update it with a stale policy.
+ * @test: Supplies a pointer to a kunit structure.
+ *
+ * This is called by the kunit harness.
+ */
+static void ipe_ctx_update_rollback_inactive(struct kunit *test)
+{
+	struct ipe_policy *p1 = NULL;
+	struct ipe_policy *p2 = NULL;
+	struct ipe_context *ctx = NULL;
+	const char *const policy1 = "policy_name=t policy_version=0.0.1\n"
+				    "DEFAULT action=ALLOW";
+	const char *const policy2 = "policy_name=t policy_version=0.0.0\n"
+				    "DEFAULT action=DENY\n";
+
+	ctx = create_fake_ctx();
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+	p1 = ipe_new_policy(policy1, strlen(policy1), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p1);
+
+	ipe_add_policy(ctx, p1);
+
+	p2 = ipe_update_policy(p1, policy2, strlen(policy2), NULL, 0);
+	KUNIT_EXPECT_NOT_ERR_OR_NULL(test, p2);
+
+	fake_free_ctx(ctx);
+	ipe_put_policy(p1);
+	ipe_put_policy(p2);
+}
+
+static void ipe_ctx_oob_enum(struct kunit *test)
+{
+	struct ipe_policy *p = NULL;
+	struct ipe_context *ctx = NULL;
+	const char *const policy = "policy_name=t policy_version=0.0.0\n"
+				   "DEFAULT action=ALLOW\n"
+				   "DEFAULT op=KERNEL_READ action=DENY\n";
+
+	ctx = create_fake_ctx();
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+	p = ipe_new_policy(policy, strlen(policy), NULL, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p);
+
+	ipe_add_policy(ctx, p);
+	ipe_set_active_pol(p);
+
+	KUNIT_EXPECT_EQ(test, 0, ipe_kernel_read_file(NULL, READING_MAX_ID + 1, false));
+	KUNIT_EXPECT_EQ(test, 0, ipe_kernel_load_data(LOADING_MAX_ID + 1, false));
+}
+
+static struct kunit_case ipe_ctx_test_cases[] = {
+	KUNIT_CASE_PARAM(ipe_ctx_eval_test, ipe_eval_gen_params),
+	KUNIT_CASE_PARAM(ipe_ctx_eval_permissive_test, ipe_eval_gen_params),
+	KUNIT_CASE(ipe_ctx_default_eval_test),
+	KUNIT_CASE(ipe_ctx_replace_active_policy),
+	KUNIT_CASE(ipe_ctx_replace_policy),
+	KUNIT_CASE(ipe_ctx_update_policy),
+	KUNIT_CASE(ipe_ctx_update_wrong_policy),
+	KUNIT_CASE(ipe_ctx_update_rollback_policy),
+	KUNIT_CASE(ipe_ctx_update_rollback_inactive),
+	KUNIT_CASE(ipe_ctx_rollback),
+	KUNIT_CASE(ipe_ctx_oob_enum),
+};
+
+static struct kunit_suite ipe_ctx_test_suite = {
+	.name = "ipe-context",
+	.test_cases = ipe_ctx_test_cases,
+};
+
+kunit_test_suite(ipe_ctx_test_suite);
diff --git a/security/ipe/eval.c b/security/ipe/eval.c
index e8205a6fce44..ce67b5e1afd7 100644
--- a/security/ipe/eval.c
+++ b/security/ipe/eval.c
@@ -234,3 +234,7 @@ void ipe_invalidate_pinned_sb(const struct super_block *mnt_sb)
 
 	spin_unlock(&pin_lock);
 }
+
+#ifdef CONFIG_SECURITY_IPE_KUNIT_TEST
+#include "ctx_test.c"
+#endif /* CONFIG_SECURITY_IPE_KUNIT_TEST */
diff --git a/security/ipe/policy_parser_tests.c b/security/ipe/policy_parser_tests.c
new file mode 100644
index 000000000000..38d5fb7d785a
--- /dev/null
+++ b/security/ipe/policy_parser_tests.c
@@ -0,0 +1,313 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) Microsoft Corporation. All rights reserved.
+ */
+
+#include <linux/slab.h>
+#include <linux/types.h>
+#include <linux/list.h>
+#include <kunit/test.h>
+#include "policy.h"
+
+struct policy_case {
+	const char *const policy;
+	int	   errno;
+	const char *const desc;
+};
+
+static const struct policy_case policy_cases[] = {
+	{
+		"policy_name=\"allowall\" policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW",
+		0,
+		"basic",
+	},
+	{
+		"policy_name='trailing comment' policy_version=152.0.0 #This is comment\n"
+		"DEFAULT action=ALLOW",
+		0,
+		"trailing comment",
+	},
+	{
+		"policy_name=allowallnewline policy_version=0.2.0\n"
+		"DEFAULT action=ALLOW\n"
+		"\n",
+		0,
+		"trailing newline",
+	},
+	{
+		"policy_name=\"carriage return\tline feed\" policy_version=0.0.1\n"
+		"DEFAULT action=ALLOW\n"
+		"\r\n",
+		0,
+		"clrf newline",
+	},
+	{
+		"policy_name='whitespace' policy_version=0.0.0\n"
+		"DEFAULT\taction=ALLOW\n"
+		"     \t     DEFAULT \t    op=EXECUTE      action=DENY\n"
+		"op=EXECUTE boot_verified=TRUE action=ALLOW\n"
+		"# this is a\tcomment\t\t\t\t\n"
+		"DEFAULT \t op=KERNEL_READ\t\t\t  action=DENY\r\n"
+		"op=KERNEL_READ boot_verified=TRUE action=ALLOW\n",
+		0,
+		"various whitespaces and nested default",
+	},
+	{
+		"policy_name='boot verified' policy_version=-1236.0.0\n"
+		"DEFAULT\taction=ALLOW\n",
+		-EINVAL,
+		"negative version",
+	},
+	{
+		"policy_name=\"# this is not a comment\" policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW",
+		0,
+		"quoted comment character",
+	},
+	{
+		"policy_name=$@!*&^%%\\:;{}() policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW",
+		0,
+		"special characters",
+	},
+	{
+		"policy_name=test policy_version=999999.0.0\n"
+		"DEFAULT action=ALLOW",
+		-ERANGE,
+		"overflow version",
+	},
+	{
+		"policy_name=test policy_version=255.0\n"
+		"DEFAULT action=ALLOW",
+		-EBADMSG,
+		"incomplete version",
+	},
+	{
+		"policy_name=test policy_version=111.0.0.0\n"
+		"DEFAULT action=ALLOW",
+		-EBADMSG,
+		"extra version",
+	},
+	{
+		"",
+		-EBADMSG,
+		"0-length policy",
+	},
+	{
+		"policy_name=\"test\"\0policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW",
+		-EBADMSG,
+		"random null in header",
+	},
+	{
+		"policy_name=\"test\" policy_version=0.0.0\n"
+		"\0DEFAULT action=ALLOW",
+		-EBADMSG,
+		"incomplete policy from NULL",
+	},
+	{
+		"policy_name=\"test\" policy_version=0.0.0\n"
+		"DEFAULT action=DENY\n\0"
+		"op=EXECUTE dmverity_signature=TRUE action=ALLOW\n",
+		0,
+		"NULL truncates policy",
+	},
+	{
+		"policy_name=\"test\" policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"op=EXECUTE dmverity_signature=abc action=ALLOW",
+		-EBADMSG,
+		"invalid property type",
+	},
+	{
+		"DEFAULT action=ALLOW",
+		-EBADMSG,
+		"missing policy header",
+	},
+	{
+		"policy_name=\"test\" policy_version=0.0.0\n",
+		-EBADMSG,
+		"missing default definition",
+	},
+	{
+		"policy_name=\"test\" policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"op=KERNEL_READ dmverity_signature=TRUE action=ALLOW",
+		0,
+		"aliased operation",
+	},
+	{
+		"policy_name=\"test\" policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"dmverity_signature=TRUE op=EXECUTE action=ALLOW",
+		-EINVAL,
+		"invalid rule ordering"
+	},
+	{
+		"policy_name=\"test\" policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"action=ALLOW op=EXECUTE dmverity_signature=TRUE",
+		-EINVAL,
+		"invalid rule ordering (2)",
+	},
+	{
+		"policy_name=\"test\" policy_version=0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"op=EXECUTE dmverity_signature=TRUE action=ALLOW",
+		-EBADMSG,
+		"invalid version",
+	},
+	{
+		"policy_name=\"test\" policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"op=UNKNOWN dmverity_signature=TRUE action=ALLOW",
+		-ENOENT,
+		"unknown operation",
+	},
+	{
+		"policy_name=\"asdv\"policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n",
+		-EBADMSG,
+		"missing space after quote",
+	},
+	{
+		"policy_name=\"test\xFF\xEF\" policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"op=EXECUTE dmverity_signature=TRUE action=ALLOW",
+		0,
+		"expanded ascii",
+	},
+	{
+		"policy_name=\"test\xFF\xEF\" policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"op=EXECUTE dmverity_roothash=\"GOOD DOG\" action=ALLOW",
+		-EBADMSG,
+		"invalid property value (2)",
+	},
+	{
+		"policy_name='test' policy_version=0.0.0\n"
+		"policy_name='test' policy_version=0.1.0\n"
+		"DEFAULT action=ALLOW",
+		-EBADMSG,
+		"double header"
+	},
+	{
+		"policy_name='test' policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"DEFAULT action=ALLOW\n",
+		-EBADMSG,
+		"double default"
+	},
+	{
+		"policy_name='test' policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"DEFAULT op=EXECUTE action=DENY\n"
+		"DEFAULT op=EXECUTE action=ALLOW\n",
+		-EBADMSG,
+		"double operation default"
+	},
+	{
+		"policy_name='test' policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"DEFAULT op=EXECUTE action=DEN\n",
+		-EINVAL,
+		"invalid allow value"
+	},
+	{
+		"policy_name='test' policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"DEFAULT op=EXECUTE action\n",
+		-EINVAL,
+		"invalid allow value (2)"
+	},
+	{
+		"policy_name='test' policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"UNKNOWN value=true\n",
+		-EINVAL,
+		"unrecognized statement"
+	},
+	{
+		"policy_name='test' policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"op=EXECUTE dmverity_roothash=1c0d7ee1f8343b7fbe418378e8eb22c061d7dec7 action=DENY\n",
+		-EBADMSG,
+		"old-style digest"
+	},
+	{
+		"policy_name='test' policy_version=0.0.0\n"
+		"DEFAULT action=ALLOW\n"
+		"op=EXECUTE fsverity_digest=1c0d7ee1f8343b7fbe418378e8eb22c061d7dec7 action=DENY\n",
+		-EBADMSG,
+		"old-style digest"
+	}
+};
+
+static void pol_to_desc(const struct policy_case *c, char *desc)
+{
+	strncpy(desc, c->desc, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(ipe_policies, policy_cases, pol_to_desc);
+
+/**
+ * ipe_parser_unsigned_test: Throw policies at the parser and check the result.
+ * @test: Supplies a pointer to a kunit structure.
+ *
+ * This is called by the kunit harness. This test does not check the correctness
+ * of the policy, but ensures that errors are handled correctly. Functional
+ * validation of correctly-parsed policies are done in the evaluation unit tests.
+ */
+static void ipe_parser_unsigned_test(struct kunit *test)
+{
+	const struct policy_case *p = test->param_value;
+	struct ipe_policy *pol = ipe_new_policy(p->policy, strlen(p->policy), NULL, 0);
+
+	if (p->errno) {
+		KUNIT_EXPECT_EQ(test, PTR_ERR(pol), p->errno);
+		return;
+	}
+
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pol);
+	KUNIT_EXPECT_EQ(test, 1, refcount_read(&pol->refcount));
+	KUNIT_EXPECT_PTR_EQ(test, NULL, pol->ctx);
+	KUNIT_EXPECT_NOT_ERR_OR_NULL(test, pol->parsed);
+	KUNIT_EXPECT_PTR_EQ(test, NULL, pol->policyfs);
+	KUNIT_EXPECT_STREQ(test, pol->text, p->policy);
+	KUNIT_EXPECT_PTR_EQ(test, NULL, pol->pkcs7);
+	KUNIT_EXPECT_EQ(test, 0, pol->pkcs7len);
+	KUNIT_EXPECT_TRUE(test, list_empty(&pol->next));
+
+	ipe_put_policy(pol);
+}
+
+/**
+ * ipe_parser_widestring_test: Ensure a wide string policy causes a failure.
+ * @test: Supplies a pointer to a kunit structure.
+ *
+ * This is called by the kunit harness.
+ */
+static void ipe_parser_widestring_test(struct kunit *test)
+{
+	struct ipe_policy *pol = NULL;
+	const unsigned short policy[] = L"policy_name=Test policy_version=0.0.0\n"
+					L"DEFAULT action=ALLOW";
+
+	pol = ipe_new_policy((const char *)policy, (ARRAY_SIZE(policy) - 1) * 2, NULL, 0);
+	KUNIT_EXPECT_TRUE(test, IS_ERR_OR_NULL(pol));
+
+	ipe_put_policy(pol);
+}
+
+static struct kunit_case ipe_parser_test_cases[] = {
+	KUNIT_CASE_PARAM(ipe_parser_unsigned_test, ipe_policies_gen_params),
+	KUNIT_CASE(ipe_parser_widestring_test),
+};
+
+static struct kunit_suite ipe_parser_test_suite = {
+	.name = "ipe-parser",
+	.test_cases = ipe_parser_test_cases,
+};
+
+kunit_test_suite(ipe_parser_test_suite);
