linux/security/ipe/policy_fs.c

// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2020-2024 Microsoft Corporation. All rights reserved.
 */
#include <linux/fs.h>
#include <linux/namei.h>
#include <linux/types.h>
#include <linux/dcache.h>
#include <linux/security.h>

#include "ipe.h"
#include "policy.h"
#include "eval.h"
#include "fs.h"

#define MAX_VERSION_SIZE ARRAY_SIZE("65535.65535.65535")

/**
 * ipefs_file - defines a file in securityfs.
 */
struct ipefs_file {
	const char *name;
	umode_t access;
	const struct file_operations *fops;
};

/**
 * read_pkcs7() - Read handler for "ipe/policies/$name/pkcs7".
 * @f: Supplies a file structure representing the securityfs node.
 * @data: Supplies a buffer passed to the write syscall.
 * @len: Supplies the length of @data.
 * @offset: unused.
 *
 * @data will be populated with the pkcs7 blob representing the policy
 * on success. If the policy is unsigned (like the boot policy), this
 * will return -ENOENT.
 *
 * Return:
 * * Length of buffer written	- Success
 * * %-ENOENT			- Policy initializing/deleted or is unsigned
 */
static ssize_t read_pkcs7(struct file *f, char __user *data,
			  size_t len, loff_t *offset)
{
	const struct ipe_policy *p = NULL;
	struct inode *root = NULL;
	int rc = 0;

	root = d_inode(f->f_path.dentry->d_parent);

	inode_lock_shared(root);
	p = (struct ipe_policy *)root->i_private;
	if (!p) {
		rc = -ENOENT;
		goto out;
	}

	if (!p->pkcs7) {
		rc = -ENOENT;
		goto out;
	}

	rc = simple_read_from_buffer(data, len, offset, p->pkcs7, p->pkcs7len);

out:
	inode_unlock_shared(root);

	return rc;
}

/**
 * read_policy() - Read handler for "ipe/policies/$name/policy".
 * @f: Supplies a file structure representing the securityfs node.
 * @data: Supplies a buffer passed to the write syscall.
 * @len: Supplies the length of @data.
 * @offset: unused.
 *
 * @data will be populated with the plain-text version of the policy
 * on success.
 *
 * Return:
 * * Length of buffer written	- Success
 * * %-ENOENT			- Policy initializing/deleted
 */
static ssize_t read_policy(struct file *f, char __user *data,
			   size_t len, loff_t *offset)
{
	const struct ipe_policy *p = NULL;
	struct inode *root = NULL;
	int rc = 0;

	root = d_inode(f->f_path.dentry->d_parent);

	inode_lock_shared(root);
	p = (struct ipe_policy *)root->i_private;
	if (!p) {
		rc = -ENOENT;
		goto out;
	}

	rc = simple_read_from_buffer(data, len, offset, p->text, p->textlen);

out:
	inode_unlock_shared(root);

	return rc;
}

/**
 * read_name() - Read handler for "ipe/policies/$name/name".
 * @f: Supplies a file structure representing the securityfs node.
 * @data: Supplies a buffer passed to the write syscall.
 * @len: Supplies the length of @data.
 * @offset: unused.
 *
 * @data will be populated with the policy_name attribute on success.
 *
 * Return:
 * * Length of buffer written	- Success
 * * %-ENOENT			- Policy initializing/deleted
 */
static ssize_t read_name(struct file *f, char __user *data,
			 size_t len, loff_t *offset)
{
	const struct ipe_policy *p = NULL;
	struct inode *root = NULL;
	int rc = 0;

	root = d_inode(f->f_path.dentry->d_parent);

	inode_lock_shared(root);
	p = (struct ipe_policy *)root->i_private;
	if (!p) {
		rc = -ENOENT;
		goto out;
	}

	rc = simple_read_from_buffer(data, len, offset, p->parsed->name,
				     strlen(p->parsed->name));

out:
	inode_unlock_shared(root);

	return rc;
}

/**
 * read_version() - Read handler for "ipe/policies/$name/version".
 * @f: Supplies a file structure representing the securityfs node.
 * @data: Supplies a buffer passed to the write syscall.
 * @len: Supplies the length of @data.
 * @offset: unused.
 *
 * @data will be populated with the version string on success.
 *
 * Return:
 * * Length of buffer written	- Success
 * * %-ENOENT			- Policy initializing/deleted
 */
static ssize_t read_version(struct file *f, char __user *data,
			    size_t len, loff_t *offset)
{
	char buffer[MAX_VERSION_SIZE] = { 0 };
	const struct ipe_policy *p = NULL;
	struct inode *root = NULL;
	size_t strsize = 0;
	ssize_t rc = 0;

	root = d_inode(f->f_path.dentry->d_parent);

	inode_lock_shared(root);
	p = (struct ipe_policy *)root->i_private;
	if (!p) {
		rc = -ENOENT;
		goto out;
	}

	strsize = scnprintf(buffer, ARRAY_SIZE(buffer), "%hu.%hu.%hu",
			    p->parsed->version.major, p->parsed->version.minor,
			    p->parsed->version.rev);

	rc = simple_read_from_buffer(data, len, offset, buffer, strsize);

out:
	inode_unlock_shared(root);

	return rc;
}

/**
 * setactive() - Write handler for "ipe/policies/$name/active".
 * @f: Supplies a file structure representing the securityfs node.
 * @data: Supplies a buffer passed to the write syscall.
 * @len: Supplies the length of @data.
 * @offset: unused.
 *
 * Return:
 * * Length of buffer written	- Success
 * * %-EPERM			- Insufficient permission
 * * %-EINVAL			- Invalid input
 * * %-ENOENT			- Policy initializing/deleted
 */
static ssize_t setactive(struct file *f, const char __user *data,
			 size_t len, loff_t *offset)
{
	const struct ipe_policy *p = NULL;
	struct inode *root = NULL;
	bool value = false;
	int rc = 0;

	if (!file_ns_capable(f, &init_user_ns, CAP_MAC_ADMIN))
		return -EPERM;

	rc = kstrtobool_from_user(data, len, &value);
	if (rc)
		return rc;

	if (!value)
		return -EINVAL;

	root = d_inode(f->f_path.dentry->d_parent);
	inode_lock(root);

	p = (struct ipe_policy *)root->i_private;
	if (!p) {
		rc = -ENOENT;
		goto out;
	}

	rc = ipe_set_active_pol(p);

out:
	inode_unlock(root);
	return (rc < 0) ? rc : len;
}

/**
 * getactive() - Read handler for "ipe/policies/$name/active".
 * @f: Supplies a file structure representing the securityfs node.
 * @data: Supplies a buffer passed to the write syscall.
 * @len: Supplies the length of @data.
 * @offset: unused.
 *
 * @data will be populated with the 1 or 0 depending on if the
 * corresponding policy is active.
 *
 * Return:
 * * Length of buffer written	- Success
 * * %-ENOENT			- Policy initializing/deleted
 */
static ssize_t getactive(struct file *f, char __user *data,
			 size_t len, loff_t *offset)
{
	const struct ipe_policy *p = NULL;
	struct inode *root = NULL;
	const char *str;
	int rc = 0;

	root = d_inode(f->f_path.dentry->d_parent);

	inode_lock_shared(root);
	p = (struct ipe_policy *)root->i_private;
	if (!p) {
		inode_unlock_shared(root);
		return -ENOENT;
	}
	inode_unlock_shared(root);

	str = (p == rcu_access_pointer(ipe_active_policy)) ? "1" : "0";
	rc = simple_read_from_buffer(data, len, offset, str, 1);

	return rc;
}

/**
 * update_policy() - Write handler for "ipe/policies/$name/update".
 * @f: Supplies a file structure representing the securityfs node.
 * @data: Supplies a buffer passed to the write syscall.
 * @len: Supplies the length of @data.
 * @offset: unused.
 *
 * On success this updates the policy represented by $name,
 * in-place.
 *
 * Return: Length of buffer written on success. If an error occurs,
 * the function will return the -errno.
 */
static ssize_t update_policy(struct file *f, const char __user *data,
			     size_t len, loff_t *offset)
{
	struct inode *root = NULL;
	char *copy = NULL;
	int rc = 0;

	if (!file_ns_capable(f, &init_user_ns, CAP_MAC_ADMIN))
		return -EPERM;

	copy = memdup_user(data, len);
	if (IS_ERR(copy))
		return PTR_ERR(copy);

	root = d_inode(f->f_path.dentry->d_parent);
	inode_lock(root);
	rc = ipe_update_policy(root, NULL, 0, copy, len);
	inode_unlock(root);

	kfree(copy);
	if (rc)
		return rc;

	return len;
}

/**
 * delete_policy() - write handler for  "ipe/policies/$name/delete".
 * @f: Supplies a file structure representing the securityfs node.
 * @data: Supplies a buffer passed to the write syscall.
 * @len: Supplies the length of @data.
 * @offset: unused.
 *
 * On success this deletes the policy represented by $name.
 *
 * Return:
 * * Length of buffer written	- Success
 * * %-EPERM			- Insufficient permission/deleting active policy
 * * %-EINVAL			- Invalid input
 * * %-ENOENT			- Policy initializing/deleted
 */
static ssize_t delete_policy(struct file *f, const char __user *data,
			     size_t len, loff_t *offset)
{
	struct ipe_policy *ap = NULL;
	struct ipe_policy *p = NULL;
	struct inode *root = NULL;
	bool value = false;
	int rc = 0;

	if (!file_ns_capable(f, &init_user_ns, CAP_MAC_ADMIN))
		return -EPERM;

	rc = kstrtobool_from_user(data, len, &value);
	if (rc)
		return rc;

	if (!value)
		return -EINVAL;

	root = d_inode(f->f_path.dentry->d_parent);
	inode_lock(root);
	p = (struct ipe_policy *)root->i_private;
	if (!p) {
		inode_unlock(root);
		return -ENOENT;
	}

	mutex_lock(&ipe_policy_lock);
	ap = rcu_dereference_protected(ipe_active_policy,
				       lockdep_is_held(&ipe_policy_lock));
	if (p == ap) {
		mutex_unlock(&ipe_policy_lock);
		inode_unlock(root);
		return -EPERM;
	}
	mutex_unlock(&ipe_policy_lock);

	root->i_private = NULL;
	inode_unlock(root);

	synchronize_rcu();
	ipe_free_policy(p);

	return len;
}

static const struct file_operations content_fops = {
	.read = read_policy,
};

static const struct file_operations pkcs7_fops = {
	.read = read_pkcs7,
};

static const struct file_operations name_fops = {
	.read = read_name,
};

static const struct file_operations ver_fops = {
	.read = read_version,
};

static const struct file_operations active_fops = {
	.write = setactive,
	.read = getactive,
};

static const struct file_operations update_fops = {
	.write = update_policy,
};

static const struct file_operations delete_fops = {
	.write = delete_policy,
};

/**
 * policy_subdir - files under a policy subdirectory
 */
static const struct ipefs_file policy_subdir[] = {
	{ "pkcs7", 0444, &pkcs7_fops },
	{ "policy", 0444, &content_fops },
	{ "name", 0444, &name_fops },
	{ "version", 0444, &ver_fops },
	{ "active", 0600, &active_fops },
	{ "update", 0200, &update_fops },
	{ "delete", 0200, &delete_fops },
};

/**
 * ipe_del_policyfs_node() - Delete a securityfs entry for @p.
 * @p: Supplies a pointer to the policy to delete a securityfs entry for.
 */
void ipe_del_policyfs_node(struct ipe_policy *p)
{
	securityfs_recursive_remove(p->policyfs);
	p->policyfs = NULL;
}

/**
 * ipe_new_policyfs_node() - Create a securityfs entry for @p.
 * @p: Supplies a pointer to the policy to create a securityfs entry for.
 *
 * Return: %0 on success. If an error occurs, the function will return
 * the -errno.
 */
int ipe_new_policyfs_node(struct ipe_policy *p)
{
	const struct ipefs_file *f = NULL;
	struct dentry *policyfs = NULL;
	struct inode *root = NULL;
	struct dentry *d = NULL;
	size_t i = 0;
	int rc = 0;

	if (p->policyfs)
		return 0;

	policyfs = securityfs_create_dir(p->parsed->name, policy_root);
	if (IS_ERR(policyfs))
		return PTR_ERR(policyfs);

	root = d_inode(policyfs);

	for (i = 0; i < ARRAY_SIZE(policy_subdir); ++i) {
		f = &policy_subdir[i];

		d = securityfs_create_file(f->name, f->access, policyfs,
					   NULL, f->fops);
		if (IS_ERR(d)) {
			rc = PTR_ERR(d);
			goto err;
		}
	}

	inode_lock(root);
	p->policyfs = policyfs;
	root->i_private = p;
	inode_unlock(root);

	return 0;
err:
	securityfs_recursive_remove(policyfs);
	return rc;
}