Commit a77c2cfd authored by Alexei Starovoitov's avatar Alexei Starovoitov

Merge branch 'bpf-lsm: Extend interoperability with IMA'

Roberto Sassu says:

====================
Extend the interoperability with IMA, to give wider flexibility for the
implementation of integrity-focused LSMs based on eBPF.

Patch 1 fixes some style issues.

Patches 2-6 give the ability to eBPF-based LSMs to take advantage of the
measurement capability of IMA without needing to setup a policy in IMA
(those LSMs might implement the policy capability themselves).

Patches 7-9 allow eBPF-based LSMs to evaluate files read by the kernel.

Changelog

v2:
- Add better description to patch 1 (suggested by Shuah)
- Recalculate digest if it is not fresh (when IMA_COLLECTED flag not set)
- Move declaration of bpf_ima_file_hash() at the end (suggested by
  Yonghong)
- Add tests to check if the digest has been recalculated
- Add deny test for bpf_kernel_read_file()
- Add description to tests

v1:
- Modify ima_file_hash() only and allow the usage of the function with the
  modified behavior by eBPF-based LSMs through the new function
  bpf_ima_file_hash() (suggested by Mimi)
- Make bpf_lsm_kernel_read_file() sleepable so that bpf_ima_inode_hash()
  and bpf_ima_file_hash() can be called inside the implementation of
  eBPF-based LSMs for this hook
====================
Signed-off-by: default avatarAlexei Starovoitov <ast@kernel.org>
parents 357b3cc3 7bae42b6
......@@ -5119,6 +5119,16 @@ union bpf_attr {
* 0 on success.
* **-EINVAL** for invalid input
* **-EOPNOTSUPP** for unsupported protocol
*
* long bpf_ima_file_hash(struct file *file, void *dst, u32 size)
* Description
* Returns a calculated IMA hash of the *file*.
* If the hash is larger than *size*, then only *size*
* bytes will be copied to *dst*
* Return
* The **hash_algo** is returned on success,
* **-EOPNOTSUP** if the hash calculation failed or **-EINVAL** if
* invalid arguments are passed.
*/
#define __BPF_FUNC_MAPPER(FN) \
FN(unspec), \
......@@ -5314,6 +5324,7 @@ union bpf_attr {
FN(xdp_store_bytes), \
FN(copy_from_user_task), \
FN(skb_set_tstamp), \
FN(ima_file_hash), \
/* */
/* integer value in 'imm' field of BPF_CALL instruction selects which helper
......
......@@ -99,6 +99,24 @@ static const struct bpf_func_proto bpf_ima_inode_hash_proto = {
.allowed = bpf_ima_inode_hash_allowed,
};
BPF_CALL_3(bpf_ima_file_hash, struct file *, file, void *, dst, u32, size)
{
return ima_file_hash(file, dst, size);
}
BTF_ID_LIST_SINGLE(bpf_ima_file_hash_btf_ids, struct, file)
static const struct bpf_func_proto bpf_ima_file_hash_proto = {
.func = bpf_ima_file_hash,
.gpl_only = false,
.ret_type = RET_INTEGER,
.arg1_type = ARG_PTR_TO_BTF_ID,
.arg1_btf_id = &bpf_ima_file_hash_btf_ids[0],
.arg2_type = ARG_PTR_TO_UNINIT_MEM,
.arg3_type = ARG_CONST_SIZE,
.allowed = bpf_ima_inode_hash_allowed,
};
static const struct bpf_func_proto *
bpf_lsm_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
......@@ -121,6 +139,8 @@ bpf_lsm_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
return &bpf_bprm_opts_set_proto;
case BPF_FUNC_ima_inode_hash:
return prog->aux->sleepable ? &bpf_ima_inode_hash_proto : NULL;
case BPF_FUNC_ima_file_hash:
return prog->aux->sleepable ? &bpf_ima_file_hash_proto : NULL;
default:
return tracing_prog_func_proto(func_id, prog);
}
......@@ -167,6 +187,7 @@ BTF_ID(func, bpf_lsm_inode_setxattr)
BTF_ID(func, bpf_lsm_inode_symlink)
BTF_ID(func, bpf_lsm_inode_unlink)
BTF_ID(func, bpf_lsm_kernel_module_request)
BTF_ID(func, bpf_lsm_kernel_read_file)
BTF_ID(func, bpf_lsm_kernfs_init_security)
#ifdef CONFIG_KEYS
......
......@@ -418,6 +418,7 @@ int ima_file_mmap(struct file *file, unsigned long prot)
/**
* ima_file_mprotect - based on policy, limit mprotect change
* @vma: vm_area_struct protection is set to
* @prot: contains the protection that will be applied by the kernel.
*
* Files can be mmap'ed read/write and later changed to execute to circumvent
......@@ -519,20 +520,38 @@ int ima_file_check(struct file *file, int mask)
}
EXPORT_SYMBOL_GPL(ima_file_check);
static int __ima_inode_hash(struct inode *inode, char *buf, size_t buf_size)
static int __ima_inode_hash(struct inode *inode, struct file *file, char *buf,
size_t buf_size)
{
struct integrity_iint_cache *iint;
int hash_algo;
struct integrity_iint_cache *iint = NULL, tmp_iint;
int rc, hash_algo;
if (!ima_policy_flag)
return -EOPNOTSUPP;
if (ima_policy_flag) {
iint = integrity_iint_find(inode);
if (iint)
mutex_lock(&iint->mutex);
}
if ((!iint || !(iint->flags & IMA_COLLECTED)) && file) {
if (iint)
mutex_unlock(&iint->mutex);
memset(&tmp_iint, 0, sizeof(tmp_iint));
tmp_iint.inode = inode;
mutex_init(&tmp_iint.mutex);
rc = ima_collect_measurement(&tmp_iint, file, NULL, 0,
ima_hash_algo, NULL);
if (rc < 0)
return -EOPNOTSUPP;
iint = &tmp_iint;
mutex_lock(&iint->mutex);
}
iint = integrity_iint_find(inode);
if (!iint)
return -EOPNOTSUPP;
mutex_lock(&iint->mutex);
/*
* ima_file_hash can be called when ima_collect_measurement has still
* not been called, we might not always have a hash.
......@@ -551,12 +570,14 @@ static int __ima_inode_hash(struct inode *inode, char *buf, size_t buf_size)
hash_algo = iint->ima_hash->algo;
mutex_unlock(&iint->mutex);
if (iint == &tmp_iint)
kfree(iint->ima_hash);
return hash_algo;
}
/**
* ima_file_hash - return the stored measurement if a file has been hashed and
* is in the iint cache.
* ima_file_hash - return a measurement of the file
* @file: pointer to the file
* @buf: buffer in which to store the hash
* @buf_size: length of the buffer
......@@ -569,7 +590,7 @@ static int __ima_inode_hash(struct inode *inode, char *buf, size_t buf_size)
* The file hash returned is based on the entire file, including the appended
* signature.
*
* If IMA is disabled or if no measurement is available, return -EOPNOTSUPP.
* If the measurement cannot be performed, return -EOPNOTSUPP.
* If the parameters are incorrect, return -EINVAL.
*/
int ima_file_hash(struct file *file, char *buf, size_t buf_size)
......@@ -577,7 +598,7 @@ int ima_file_hash(struct file *file, char *buf, size_t buf_size)
if (!file)
return -EINVAL;
return __ima_inode_hash(file_inode(file), buf, buf_size);
return __ima_inode_hash(file_inode(file), file, buf, buf_size);
}
EXPORT_SYMBOL_GPL(ima_file_hash);
......@@ -604,14 +625,14 @@ int ima_inode_hash(struct inode *inode, char *buf, size_t buf_size)
if (!inode)
return -EINVAL;
return __ima_inode_hash(inode, buf, buf_size);
return __ima_inode_hash(inode, NULL, buf, buf_size);
}
EXPORT_SYMBOL_GPL(ima_inode_hash);
/**
* ima_post_create_tmpfile - mark newly created tmpfile as new
* @mnt_userns: user namespace of the mount the inode was found from
* @file : newly created tmpfile
* @mnt_userns: user namespace of the mount the inode was found from
* @inode: inode of the newly created tmpfile
*
* No measuring, appraising or auditing of newly created tmpfiles is needed.
* Skip calling process_measurement(), but indicate which newly, created
......@@ -643,7 +664,7 @@ void ima_post_create_tmpfile(struct user_namespace *mnt_userns,
/**
* ima_post_path_mknod - mark as a new inode
* @mnt_userns: user namespace of the mount the inode was found from
* @mnt_userns: user namespace of the mount the inode was found from
* @dentry: newly created dentry
*
* Mark files created via the mknodat syscall as new, so that the
......@@ -814,8 +835,8 @@ int ima_load_data(enum kernel_load_data_id id, bool contents)
* ima_post_load_data - appraise decision based on policy
* @buf: pointer to in memory file contents
* @size: size of in memory file contents
* @id: kernel load data caller identifier
* @description: @id-specific description of contents
* @load_id: kernel load data caller identifier
* @description: @load_id-specific description of contents
*
* Measure/appraise/audit in memory buffer based on policy. Policy rules
* are written in terms of a policy identifier.
......
......@@ -5119,6 +5119,16 @@ union bpf_attr {
* 0 on success.
* **-EINVAL** for invalid input
* **-EOPNOTSUPP** for unsupported protocol
*
* long bpf_ima_file_hash(struct file *file, void *dst, u32 size)
* Description
* Returns a calculated IMA hash of the *file*.
* If the hash is larger than *size*, then only *size*
* bytes will be copied to *dst*
* Return
* The **hash_algo** is returned on success,
* **-EOPNOTSUP** if the hash calculation failed or **-EINVAL** if
* invalid arguments are passed.
*/
#define __BPF_FUNC_MAPPER(FN) \
FN(unspec), \
......@@ -5314,6 +5324,7 @@ union bpf_attr {
FN(xdp_store_bytes), \
FN(copy_from_user_task), \
FN(skb_set_tstamp), \
FN(ima_file_hash), \
/* */
/* integer value in 'imm' field of BPF_CALL instruction selects which helper
......
......@@ -12,7 +12,7 @@ LOG_FILE="$(mktemp /tmp/ima_setup.XXXX.log)"
usage()
{
echo "Usage: $0 <setup|cleanup|run> <existing_tmp_dir>"
echo "Usage: $0 <setup|cleanup|run|modify-bin|restore-bin|load-policy> <existing_tmp_dir>"
exit 1
}
......@@ -51,6 +51,7 @@ setup()
ensure_mount_securityfs
echo "measure func=BPRM_CHECK fsuuid=${mount_uuid}" > ${IMA_POLICY_FILE}
echo "measure func=BPRM_CHECK fsuuid=${mount_uuid}" > ${mount_dir}/policy_test
}
cleanup() {
......@@ -77,6 +78,32 @@ run()
exec "${copied_bin_path}"
}
modify_bin()
{
local tmp_dir="$1"
local mount_dir="${tmp_dir}/mnt"
local copied_bin_path="${mount_dir}/$(basename ${TEST_BINARY})"
echo "mod" >> "${copied_bin_path}"
}
restore_bin()
{
local tmp_dir="$1"
local mount_dir="${tmp_dir}/mnt"
local copied_bin_path="${mount_dir}/$(basename ${TEST_BINARY})"
truncate -s -4 "${copied_bin_path}"
}
load_policy()
{
local tmp_dir="$1"
local mount_dir="${tmp_dir}/mnt"
echo ${mount_dir}/policy_test > ${IMA_POLICY_FILE} 2> /dev/null
}
catch()
{
local exit_code="$1"
......@@ -105,6 +132,12 @@ main()
cleanup "${tmp_dir}"
elif [[ "${action}" == "run" ]]; then
run "${tmp_dir}"
elif [[ "${action}" == "modify-bin" ]]; then
modify_bin "${tmp_dir}"
elif [[ "${action}" == "restore-bin" ]]; then
restore_bin "${tmp_dir}"
elif [[ "${action}" == "load-policy" ]]; then
load_policy "${tmp_dir}"
else
echo "Unknown action: ${action}"
exit 1
......
......@@ -13,14 +13,17 @@
#include "ima.skel.h"
static int run_measured_process(const char *measured_dir, u32 *monitored_pid)
#define MAX_SAMPLES 4
static int _run_measured_process(const char *measured_dir, u32 *monitored_pid,
const char *cmd)
{
int child_pid, child_status;
child_pid = fork();
if (child_pid == 0) {
*monitored_pid = getpid();
execlp("./ima_setup.sh", "./ima_setup.sh", "run", measured_dir,
execlp("./ima_setup.sh", "./ima_setup.sh", cmd, measured_dir,
NULL);
exit(errno);
......@@ -32,19 +35,39 @@ static int run_measured_process(const char *measured_dir, u32 *monitored_pid)
return -EINVAL;
}
static u64 ima_hash_from_bpf;
static int run_measured_process(const char *measured_dir, u32 *monitored_pid)
{
return _run_measured_process(measured_dir, monitored_pid, "run");
}
static u64 ima_hash_from_bpf[MAX_SAMPLES];
static int ima_hash_from_bpf_idx;
static int process_sample(void *ctx, void *data, size_t len)
{
ima_hash_from_bpf = *((u64 *)data);
if (ima_hash_from_bpf_idx >= MAX_SAMPLES)
return -ENOSPC;
ima_hash_from_bpf[ima_hash_from_bpf_idx++] = *((u64 *)data);
return 0;
}
static void test_init(struct ima__bss *bss)
{
ima_hash_from_bpf_idx = 0;
bss->use_ima_file_hash = false;
bss->enable_bprm_creds_for_exec = false;
bss->enable_kernel_read_file = false;
bss->test_deny = false;
}
void test_test_ima(void)
{
char measured_dir_template[] = "/tmp/ima_measuredXXXXXX";
struct ring_buffer *ringbuf = NULL;
const char *measured_dir;
u64 bin_true_sample;
char cmd[256];
int err, duration = 0;
......@@ -72,13 +95,127 @@ void test_test_ima(void)
if (CHECK(err, "failed to run command", "%s, errno = %d\n", cmd, errno))
goto close_clean;
/*
* Test #1
* - Goal: obtain a sample with the bpf_ima_inode_hash() helper
* - Expected result: 1 sample (/bin/true)
*/
test_init(skel->bss);
err = run_measured_process(measured_dir, &skel->bss->monitored_pid);
if (CHECK(err, "run_measured_process", "err = %d\n", err))
if (CHECK(err, "run_measured_process #1", "err = %d\n", err))
goto close_clean;
err = ring_buffer__consume(ringbuf);
ASSERT_EQ(err, 1, "num_samples_or_err");
ASSERT_NEQ(ima_hash_from_bpf, 0, "ima_hash");
ASSERT_NEQ(ima_hash_from_bpf[0], 0, "ima_hash");
/*
* Test #2
* - Goal: obtain samples with the bpf_ima_file_hash() helper
* - Expected result: 2 samples (./ima_setup.sh, /bin/true)
*/
test_init(skel->bss);
skel->bss->use_ima_file_hash = true;
err = run_measured_process(measured_dir, &skel->bss->monitored_pid);
if (CHECK(err, "run_measured_process #2", "err = %d\n", err))
goto close_clean;
err = ring_buffer__consume(ringbuf);
ASSERT_EQ(err, 2, "num_samples_or_err");
ASSERT_NEQ(ima_hash_from_bpf[0], 0, "ima_hash");
ASSERT_NEQ(ima_hash_from_bpf[1], 0, "ima_hash");
bin_true_sample = ima_hash_from_bpf[1];
/*
* Test #3
* - Goal: confirm that bpf_ima_inode_hash() returns a non-fresh digest
* - Expected result: 2 samples (/bin/true: non-fresh, fresh)
*/
test_init(skel->bss);
err = _run_measured_process(measured_dir, &skel->bss->monitored_pid,
"modify-bin");
if (CHECK(err, "modify-bin #3", "err = %d\n", err))
goto close_clean;
skel->bss->enable_bprm_creds_for_exec = true;
err = run_measured_process(measured_dir, &skel->bss->monitored_pid);
if (CHECK(err, "run_measured_process #3", "err = %d\n", err))
goto close_clean;
err = ring_buffer__consume(ringbuf);
ASSERT_EQ(err, 2, "num_samples_or_err");
ASSERT_NEQ(ima_hash_from_bpf[0], 0, "ima_hash");
ASSERT_NEQ(ima_hash_from_bpf[1], 0, "ima_hash");
ASSERT_EQ(ima_hash_from_bpf[0], bin_true_sample, "sample_equal_or_err");
/* IMA refreshed the digest. */
ASSERT_NEQ(ima_hash_from_bpf[1], bin_true_sample,
"sample_different_or_err");
/*
* Test #4
* - Goal: verify that bpf_ima_file_hash() returns a fresh digest
* - Expected result: 4 samples (./ima_setup.sh: fresh, fresh;
* /bin/true: fresh, fresh)
*/
test_init(skel->bss);
skel->bss->use_ima_file_hash = true;
skel->bss->enable_bprm_creds_for_exec = true;
err = run_measured_process(measured_dir, &skel->bss->monitored_pid);
if (CHECK(err, "run_measured_process #4", "err = %d\n", err))
goto close_clean;
err = ring_buffer__consume(ringbuf);
ASSERT_EQ(err, 4, "num_samples_or_err");
ASSERT_NEQ(ima_hash_from_bpf[0], 0, "ima_hash");
ASSERT_NEQ(ima_hash_from_bpf[1], 0, "ima_hash");
ASSERT_NEQ(ima_hash_from_bpf[2], 0, "ima_hash");
ASSERT_NEQ(ima_hash_from_bpf[3], 0, "ima_hash");
ASSERT_NEQ(ima_hash_from_bpf[2], bin_true_sample,
"sample_different_or_err");
ASSERT_EQ(ima_hash_from_bpf[3], ima_hash_from_bpf[2],
"sample_equal_or_err");
skel->bss->use_ima_file_hash = false;
skel->bss->enable_bprm_creds_for_exec = false;
err = _run_measured_process(measured_dir, &skel->bss->monitored_pid,
"restore-bin");
if (CHECK(err, "restore-bin #3", "err = %d\n", err))
goto close_clean;
/*
* Test #5
* - Goal: obtain a sample from the kernel_read_file hook
* - Expected result: 2 samples (./ima_setup.sh, policy_test)
*/
test_init(skel->bss);
skel->bss->use_ima_file_hash = true;
skel->bss->enable_kernel_read_file = true;
err = _run_measured_process(measured_dir, &skel->bss->monitored_pid,
"load-policy");
if (CHECK(err, "run_measured_process #5", "err = %d\n", err))
goto close_clean;
err = ring_buffer__consume(ringbuf);
ASSERT_EQ(err, 2, "num_samples_or_err");
ASSERT_NEQ(ima_hash_from_bpf[0], 0, "ima_hash");
ASSERT_NEQ(ima_hash_from_bpf[1], 0, "ima_hash");
/*
* Test #6
* - Goal: ensure that the kernel_read_file hook denies an operation
* - Expected result: 0 samples
*/
test_init(skel->bss);
skel->bss->enable_kernel_read_file = true;
skel->bss->test_deny = true;
err = _run_measured_process(measured_dir, &skel->bss->monitored_pid,
"load-policy");
if (CHECK(!err, "run_measured_process #6", "err = %d\n", err))
goto close_clean;
err = ring_buffer__consume(ringbuf);
ASSERT_EQ(err, 0, "num_samples_or_err");
close_clean:
snprintf(cmd, sizeof(cmd), "./ima_setup.sh cleanup %s", measured_dir);
......
......@@ -18,8 +18,12 @@ struct {
char _license[] SEC("license") = "GPL";
SEC("lsm.s/bprm_committed_creds")
void BPF_PROG(ima, struct linux_binprm *bprm)
bool use_ima_file_hash;
bool enable_bprm_creds_for_exec;
bool enable_kernel_read_file;
bool test_deny;
static void ima_test_common(struct file *file)
{
u64 ima_hash = 0;
u64 *sample;
......@@ -28,8 +32,12 @@ void BPF_PROG(ima, struct linux_binprm *bprm)
pid = bpf_get_current_pid_tgid() >> 32;
if (pid == monitored_pid) {
ret = bpf_ima_inode_hash(bprm->file->f_inode, &ima_hash,
sizeof(ima_hash));
if (!use_ima_file_hash)
ret = bpf_ima_inode_hash(file->f_inode, &ima_hash,
sizeof(ima_hash));
else
ret = bpf_ima_file_hash(file, &ima_hash,
sizeof(ima_hash));
if (ret < 0 || ima_hash == 0)
return;
......@@ -43,3 +51,53 @@ void BPF_PROG(ima, struct linux_binprm *bprm)
return;
}
static int ima_test_deny(void)
{
u32 pid;
pid = bpf_get_current_pid_tgid() >> 32;
if (pid == monitored_pid && test_deny)
return -EPERM;
return 0;
}
SEC("lsm.s/bprm_committed_creds")
void BPF_PROG(bprm_committed_creds, struct linux_binprm *bprm)
{
ima_test_common(bprm->file);
}
SEC("lsm.s/bprm_creds_for_exec")
int BPF_PROG(bprm_creds_for_exec, struct linux_binprm *bprm)
{
if (!enable_bprm_creds_for_exec)
return 0;
ima_test_common(bprm->file);
return 0;
}
SEC("lsm.s/kernel_read_file")
int BPF_PROG(kernel_read_file, struct file *file, enum kernel_read_file_id id,
bool contents)
{
int ret;
if (!enable_kernel_read_file)
return 0;
if (!contents)
return 0;
if (id != READING_POLICY)
return 0;
ret = ima_test_deny();
if (ret < 0)
return ret;
ima_test_common(file);
return 0;
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment