linux/security/ipe/policy_parser.c

// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2020-2024 Microsoft Corporation. All rights reserved.
 */

#include <linux/err.h>
#include <linux/slab.h>
#include <linux/parser.h>
#include <linux/types.h>
#include <linux/ctype.h>

#include "policy.h"
#include "policy_parser.h"
#include "digest.h"

#define START_COMMENT	'#'
#define IPE_POLICY_DELIM " \t"
#define IPE_LINE_DELIM "\n\r"

/**
 * new_parsed_policy() - Allocate and initialize a parsed policy.
 *
 * Return:
 * * a pointer to the ipe_parsed_policy structure	- Success
 * * %-ENOMEM						- Out of memory (OOM)
 */
static struct ipe_parsed_policy *new_parsed_policy(void)
{
	struct ipe_parsed_policy *p = NULL;
	struct ipe_op_table *t = NULL;
	size_t i = 0;

	p = kzalloc(sizeof(*p), GFP_KERNEL);
	if (!p)
		return ERR_PTR(-ENOMEM);

	p->global_default_action = IPE_ACTION_INVALID;

	for (i = 0; i < ARRAY_SIZE(p->rules); ++i) {
		t = &p->rules[i];

		t->default_action = IPE_ACTION_INVALID;
		INIT_LIST_HEAD(&t->rules);
	}

	return p;
}

/**
 * remove_comment() - Truncate all chars following START_COMMENT in a string.
 *
 * @line: Supplies a policy line string for preprocessing.
 */
static void remove_comment(char *line)
{
	line = strchr(line, START_COMMENT);

	if (line)
		*line = '\0';
}

/**
 * remove_trailing_spaces() - Truncate all trailing spaces in a string.
 *
 * @line: Supplies a policy line string for preprocessing.
 *
 * Return: The length of truncated string.
 */
static size_t remove_trailing_spaces(char *line)
{
	size_t i = 0;

	i = strlen(line);
	while (i > 0 && isspace(line[i - 1]))
		i--;

	line[i] = '\0';

	return i;
}

/**
 * parse_version() - Parse policy version.
 * @ver: Supplies a version string to be parsed.
 * @p: Supplies the partial parsed policy.
 *
 * Return:
 * * %0		- Success
 * * %-EBADMSG	- Version string is invalid
 * * %-ERANGE	- Version number overflow
 * * %-EINVAL	- Parsing error
 */
static int parse_version(char *ver, struct ipe_parsed_policy *p)
{
	u16 *const cv[] = { &p->version.major, &p->version.minor, &p->version.rev };
	size_t sep_count = 0;
	char *token;
	int rc = 0;

	while ((token = strsep(&ver, ".")) != NULL) {
		/* prevent overflow */
		if (sep_count >= ARRAY_SIZE(cv))
			return -EBADMSG;

		rc = kstrtou16(token, 10, cv[sep_count]);
		if (rc)
			return rc;

		++sep_count;
	}

	/* prevent underflow */
	if (sep_count != ARRAY_SIZE(cv))
		return -EBADMSG;

	return 0;
}

enum header_opt {
	IPE_HEADER_POLICY_NAME = 0,
	IPE_HEADER_POLICY_VERSION,
	__IPE_HEADER_MAX
};

static const match_table_t header_tokens = {
	{IPE_HEADER_POLICY_NAME,	"policy_name=%s"},
	{IPE_HEADER_POLICY_VERSION,	"policy_version=%s"},
	{__IPE_HEADER_MAX,		NULL}
};

/**
 * parse_header() - Parse policy header information.
 * @line: Supplies header line to be parsed.
 * @p: Supplies the partial parsed policy.
 *
 * Return:
 * * %0		- Success
 * * %-EBADMSG	- Header string is invalid
 * * %-ENOMEM	- Out of memory (OOM)
 * * %-ERANGE	- Version number overflow
 * * %-EINVAL	- Version parsing error
 */
static int parse_header(char *line, struct ipe_parsed_policy *p)
{
	substring_t args[MAX_OPT_ARGS];
	char *t, *ver = NULL;
	size_t idx = 0;
	int rc = 0;

	while ((t = strsep(&line, IPE_POLICY_DELIM)) != NULL) {
		int token;

		if (*t == '\0')
			continue;
		if (idx >= __IPE_HEADER_MAX) {
			rc = -EBADMSG;
			goto out;
		}

		token = match_token(t, header_tokens, args);
		if (token != idx) {
			rc = -EBADMSG;
			goto out;
		}

		switch (token) {
		case IPE_HEADER_POLICY_NAME:
			p->name = match_strdup(&args[0]);
			if (!p->name)
				rc = -ENOMEM;
			break;
		case IPE_HEADER_POLICY_VERSION:
			ver = match_strdup(&args[0]);
			if (!ver) {
				rc = -ENOMEM;
				break;
			}
			rc = parse_version(ver, p);
			break;
		default:
			rc = -EBADMSG;
		}
		if (rc)
			goto out;
		++idx;
	}

	if (idx != __IPE_HEADER_MAX)
		rc = -EBADMSG;

out:
	kfree(ver);
	return rc;
}

/**
 * token_default() - Determine if the given token is "DEFAULT".
 * @token: Supplies the token string to be compared.
 *
 * Return:
 * * %false	- The token is not "DEFAULT"
 * * %true	- The token is "DEFAULT"
 */
static bool token_default(char *token)
{
	return !strcmp(token, "DEFAULT");
}

/**
 * free_rule() - Free the supplied ipe_rule struct.
 * @r: Supplies the ipe_rule struct to be freed.
 *
 * Free a ipe_rule struct @r. Note @r must be removed from any lists before
 * calling this function.
 */
static void free_rule(struct ipe_rule *r)
{
	struct ipe_prop *p, *t;

	if (IS_ERR_OR_NULL(r))
		return;

	list_for_each_entry_safe(p, t, &r->props, next) {
		list_del(&p->next);
		ipe_digest_free(p->value);
		kfree(p);
	}

	kfree(r);
}

static const match_table_t operation_tokens = {
	{IPE_OP_EXEC,			"op=EXECUTE"},
	{IPE_OP_FIRMWARE,		"op=FIRMWARE"},
	{IPE_OP_KERNEL_MODULE,		"op=KMODULE"},
	{IPE_OP_KEXEC_IMAGE,		"op=KEXEC_IMAGE"},
	{IPE_OP_KEXEC_INITRAMFS,	"op=KEXEC_INITRAMFS"},
	{IPE_OP_POLICY,			"op=POLICY"},
	{IPE_OP_X509,			"op=X509_CERT"},
	{IPE_OP_INVALID,		NULL}
};

/**
 * parse_operation() - Parse the operation type given a token string.
 * @t: Supplies the token string to be parsed.
 *
 * Return: The parsed operation type.
 */
static enum ipe_op_type parse_operation(char *t)
{
	substring_t args[MAX_OPT_ARGS];

	return match_token(t, operation_tokens, args);
}

static const match_table_t action_tokens = {
	{IPE_ACTION_ALLOW,	"action=ALLOW"},
	{IPE_ACTION_DENY,	"action=DENY"},
	{IPE_ACTION_INVALID,	NULL}
};

/**
 * parse_action() - Parse the action type given a token string.
 * @t: Supplies the token string to be parsed.
 *
 * Return: The parsed action type.
 */
static enum ipe_action_type parse_action(char *t)
{
	substring_t args[MAX_OPT_ARGS];

	return match_token(t, action_tokens, args);
}

static const match_table_t property_tokens = {
	{IPE_PROP_BOOT_VERIFIED_FALSE,	"boot_verified=FALSE"},
	{IPE_PROP_BOOT_VERIFIED_TRUE,	"boot_verified=TRUE"},
	{IPE_PROP_DMV_ROOTHASH,		"dmverity_roothash=%s"},
	{IPE_PROP_DMV_SIG_FALSE,	"dmverity_signature=FALSE"},
	{IPE_PROP_DMV_SIG_TRUE,		"dmverity_signature=TRUE"},
	{IPE_PROP_FSV_DIGEST,		"fsverity_digest=%s"},
	{IPE_PROP_FSV_SIG_FALSE,	"fsverity_signature=FALSE"},
	{IPE_PROP_FSV_SIG_TRUE,		"fsverity_signature=TRUE"},
	{IPE_PROP_INVALID,		NULL}
};

/**
 * parse_property() - Parse a rule property given a token string.
 * @t: Supplies the token string to be parsed.
 * @r: Supplies the ipe_rule the parsed property will be associated with.
 *
 * This function parses and associates a property with an IPE rule based
 * on a token string.
 *
 * Return:
 * * %0		- Success
 * * %-ENOMEM	- Out of memory (OOM)
 * * %-EBADMSG	- The supplied token cannot be parsed
 */
static int parse_property(char *t, struct ipe_rule *r)
{
	substring_t args[MAX_OPT_ARGS];
	struct ipe_prop *p = NULL;
	int rc = 0;
	int token;
	char *dup = NULL;

	p = kzalloc(sizeof(*p), GFP_KERNEL);
	if (!p)
		return -ENOMEM;

	token = match_token(t, property_tokens, args);

	switch (token) {
	case IPE_PROP_DMV_ROOTHASH:
	case IPE_PROP_FSV_DIGEST:
		dup = match_strdup(&args[0]);
		if (!dup) {
			rc = -ENOMEM;
			goto err;
		}
		p->value = ipe_digest_parse(dup);
		if (IS_ERR(p->value)) {
			rc = PTR_ERR(p->value);
			goto err;
		}
		fallthrough;
	case IPE_PROP_BOOT_VERIFIED_FALSE:
	case IPE_PROP_BOOT_VERIFIED_TRUE:
	case IPE_PROP_DMV_SIG_FALSE:
	case IPE_PROP_DMV_SIG_TRUE:
	case IPE_PROP_FSV_SIG_FALSE:
	case IPE_PROP_FSV_SIG_TRUE:
		p->type = token;
		break;
	default:
		rc = -EBADMSG;
		break;
	}
	if (rc)
		goto err;
	list_add_tail(&p->next, &r->props);

out:
	kfree(dup);
	return rc;
err:
	kfree(p);
	goto out;
}

/**
 * parse_rule() - parse a policy rule line.
 * @line: Supplies rule line to be parsed.
 * @p: Supplies the partial parsed policy.
 *
 * Return:
 * * 0		- Success
 * * %-ENOMEM	- Out of memory (OOM)
 * * %-EBADMSG	- Policy syntax error
 */
static int parse_rule(char *line, struct ipe_parsed_policy *p)
{
	enum ipe_action_type action = IPE_ACTION_INVALID;
	enum ipe_op_type op = IPE_OP_INVALID;
	bool is_default_rule = false;
	struct ipe_rule *r = NULL;
	bool first_token = true;
	bool op_parsed = false;
	int rc = 0;
	char *t;

	if (IS_ERR_OR_NULL(line))
		return -EBADMSG;

	r = kzalloc(sizeof(*r), GFP_KERNEL);
	if (!r)
		return -ENOMEM;

	INIT_LIST_HEAD(&r->next);
	INIT_LIST_HEAD(&r->props);

	while (t = strsep(&line, IPE_POLICY_DELIM), line) {
		if (*t == '\0')
			continue;
		if (first_token && token_default(t)) {
			is_default_rule = true;
		} else {
			if (!op_parsed) {
				op = parse_operation(t);
				if (op == IPE_OP_INVALID)
					rc = -EBADMSG;
				else
					op_parsed = true;
			} else {
				rc = parse_property(t, r);
			}
		}

		if (rc)
			goto err;
		first_token = false;
	}

	action = parse_action(t);
	if (action == IPE_ACTION_INVALID) {
		rc = -EBADMSG;
		goto err;
	}

	if (is_default_rule) {
		if (!list_empty(&r->props)) {
			rc = -EBADMSG;
		} else if (op == IPE_OP_INVALID) {
			if (p->global_default_action != IPE_ACTION_INVALID)
				rc = -EBADMSG;
			else
				p->global_default_action = action;
		} else {
			if (p->rules[op].default_action != IPE_ACTION_INVALID)
				rc = -EBADMSG;
			else
				p->rules[op].default_action = action;
		}
	} else if (op != IPE_OP_INVALID && action != IPE_ACTION_INVALID) {
		r->op = op;
		r->action = action;
	} else {
		rc = -EBADMSG;
	}

	if (rc)
		goto err;
	if (!is_default_rule)
		list_add_tail(&r->next, &p->rules[op].rules);
	else
		free_rule(r);

	return rc;
err:
	free_rule(r);
	return rc;
}

/**
 * ipe_free_parsed_policy() - free a parsed policy structure.
 * @p: Supplies the parsed policy.
 */
void ipe_free_parsed_policy(struct ipe_parsed_policy *p)
{
	struct ipe_rule *pp, *t;
	size_t i = 0;

	if (IS_ERR_OR_NULL(p))
		return;

	for (i = 0; i < ARRAY_SIZE(p->rules); ++i)
		list_for_each_entry_safe(pp, t, &p->rules[i].rules, next) {
			list_del(&pp->next);
			free_rule(pp);
		}

	kfree(p->name);
	kfree(p);
}

/**
 * validate_policy() - validate a parsed policy.
 * @p: Supplies the fully parsed policy.
 *
 * Given a policy structure that was just parsed, validate that all
 * operations have their default rules or a global default rule is set.
 *
 * Return:
 * * %0		- Success
 * * %-EBADMSG	- Policy is invalid
 */
static int validate_policy(const struct ipe_parsed_policy *p)
{
	size_t i = 0;

	if (p->global_default_action != IPE_ACTION_INVALID)
		return 0;

	for (i = 0; i < ARRAY_SIZE(p->rules); ++i) {
		if (p->rules[i].default_action == IPE_ACTION_INVALID)
			return -EBADMSG;
	}

	return 0;
}

/**
 * ipe_parse_policy() - Given a string, parse the string into an IPE policy.
 * @p: partially filled ipe_policy structure to populate with the result.
 *     it must have text and textlen set.
 *
 * Return:
 * * %0		- Success
 * * %-EBADMSG	- Policy is invalid
 * * %-ENOMEM	- Out of Memory
 * * %-ERANGE	- Policy version number overflow
 * * %-EINVAL	- Policy version parsing error
 */
int ipe_parse_policy(struct ipe_policy *p)
{
	struct ipe_parsed_policy *pp = NULL;
	char *policy = NULL, *dup = NULL;
	bool header_parsed = false;
	char *line = NULL;
	size_t len;
	int rc = 0;

	if (!p->textlen)
		return -EBADMSG;

	policy = kmemdup_nul(p->text, p->textlen, GFP_KERNEL);
	if (!policy)
		return -ENOMEM;
	dup = policy;

	pp = new_parsed_policy();
	if (IS_ERR(pp)) {
		rc = PTR_ERR(pp);
		goto out;
	}

	while ((line = strsep(&policy, IPE_LINE_DELIM)) != NULL) {
		remove_comment(line);
		len = remove_trailing_spaces(line);
		if (!len)
			continue;

		if (!header_parsed) {
			rc = parse_header(line, pp);
			if (rc)
				goto err;
			header_parsed = true;
		} else {
			rc = parse_rule(line, pp);
			if (rc)
				goto err;
		}
	}

	if (!header_parsed || validate_policy(pp)) {
		rc = -EBADMSG;
		goto err;
	}

	p->parsed = pp;

out:
	kfree(dup);
	return rc;
err:
	ipe_free_parsed_policy(pp);
	goto out;
}