// SPDX-License-Identifier: GPL-2.0-only /* * AppArmor security module * * This file contains AppArmor /sys/kernel/security/apparmor interface functions * * Copyright (C) 1998-2008 Novell/SUSE * Copyright 2009-2010 Canonical Ltd. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "include/apparmor.h" #include "include/apparmorfs.h" #include "include/audit.h" #include "include/cred.h" #include "include/crypto.h" #include "include/ipc.h" #include "include/label.h" #include "include/policy.h" #include "include/policy_ns.h" #include "include/resource.h" #include "include/policy_unpack.h" /* * The apparmor filesystem interface used for policy load and introspection * The interface is split into two main components based on their function * a securityfs component: * used for static files that are always available, and which allows * userspace to specificy the location of the security filesystem. * * fns and data are prefixed with * aa_sfs_ * * an apparmorfs component: * used loaded policy content and introspection. It is not part of a * regular mounted filesystem and is available only through the magic * policy symlink in the root of the securityfs apparmor/ directory. * Tasks queries will be magically redirected to the correct portion * of the policy tree based on their confinement. * * fns and data are prefixed with * aafs_ * * The aa_fs_ prefix is used to indicate the fn is used by both the * securityfs and apparmorfs filesystems. */ /* * support fns */ struct rawdata_f_data { struct aa_loaddata *loaddata; }; #ifdef CONFIG_SECURITY_APPARMOR_EXPORT_BINARY #define RAWDATA_F_DATA_BUF(p) (char *)(p + 1) static void rawdata_f_data_free(struct rawdata_f_data *private) { if (!private) return; aa_put_loaddata(private->loaddata); kvfree(private); } static struct rawdata_f_data *rawdata_f_data_alloc(size_t size) { struct rawdata_f_data *ret; if (size > SIZE_MAX - sizeof(*ret)) return ERR_PTR(-EINVAL); ret = kvzalloc(sizeof(*ret) + size, GFP_KERNEL); if (!ret) return ERR_PTR(-ENOMEM); return ret; } #endif /** * aa_mangle_name - mangle a profile name to std profile layout form * @name: profile name to mangle (NOT NULL) * @target: buffer to store mangled name, same length as @name (MAYBE NULL) * * Returns: length of mangled name */ static int mangle_name(const char *name, char *target) { char *t = target; while (*name == '/' || *name == '.') name++; if (target) { for (; *name; name++) { if (*name == '/') *(t)++ = '.'; else if (isspace(*name)) *(t)++ = '_'; else if (isalnum(*name) || strchr("._-", *name)) *(t)++ = *name; } *t = 0; } else { int len = 0; for (; *name; name++) { if (isalnum(*name) || isspace(*name) || strchr("/._-", *name)) len++; } return len; } return t - target; } /* * aafs - core fns and data for the policy tree */ #define AAFS_NAME "apparmorfs" static struct vfsmount *aafs_mnt; static int aafs_count; static int aafs_show_path(struct seq_file *seq, struct dentry *dentry) { seq_printf(seq, "%s:[%lu]", AAFS_NAME, d_inode(dentry)->i_ino); return 0; } static void aafs_free_inode(struct inode *inode) { if (S_ISLNK(inode->i_mode)) kfree(inode->i_link); free_inode_nonrcu(inode); } static const struct super_operations aafs_super_ops = { .statfs = simple_statfs, .free_inode = aafs_free_inode, .show_path = aafs_show_path, }; static int apparmorfs_fill_super(struct super_block *sb, struct fs_context *fc) { static struct tree_descr files[] = { {""} }; int error; error = simple_fill_super(sb, AAFS_MAGIC, files); if (error) return error; sb->s_op = &aafs_super_ops; return 0; } static int apparmorfs_get_tree(struct fs_context *fc) { return get_tree_single(fc, apparmorfs_fill_super); } static const struct fs_context_operations apparmorfs_context_ops = { .get_tree = apparmorfs_get_tree, }; static int apparmorfs_init_fs_context(struct fs_context *fc) { fc->ops = &apparmorfs_context_ops; return 0; } static struct file_system_type aafs_ops = { .owner = THIS_MODULE, .name = AAFS_NAME, .init_fs_context = apparmorfs_init_fs_context, .kill_sb = kill_anon_super, }; /** * __aafs_setup_d_inode - basic inode setup for apparmorfs * @dir: parent directory for the dentry * @dentry: dentry we are seting the inode up for * @mode: permissions the file should have * @data: data to store on inode.i_private, available in open() * @link: if symlink, symlink target string * @fops: struct file_operations that should be used * @iops: struct of inode_operations that should be used */ static int __aafs_setup_d_inode(struct inode *dir, struct dentry *dentry, umode_t mode, void *data, char *link, const struct file_operations *fops, const struct inode_operations *iops) { struct inode *inode = new_inode(dir->i_sb); AA_BUG(!dir); AA_BUG(!dentry); if (!inode) return -ENOMEM; inode->i_ino = get_next_ino(); inode->i_mode = mode; inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode); inode->i_private = data; if (S_ISDIR(mode)) { inode->i_op = iops ? iops : &simple_dir_inode_operations; inode->i_fop = &simple_dir_operations; inc_nlink(inode); inc_nlink(dir); } else if (S_ISLNK(mode)) { inode->i_op = iops ? iops : &simple_symlink_inode_operations; inode->i_link = link; } else { inode->i_fop = fops; } d_instantiate(dentry, inode); dget(dentry); return 0; } /** * aafs_create - create a dentry in the apparmorfs filesystem * * @name: name of dentry to create * @mode: permissions the file should have * @parent: parent directory for this dentry * @data: data to store on inode.i_private, available in open() * @link: if symlink, symlink target string * @fops: struct file_operations that should be used for * @iops: struct of inode_operations that should be used * * This is the basic "create a xxx" function for apparmorfs. * * Returns a pointer to a dentry if it succeeds, that must be free with * aafs_remove(). Will return ERR_PTR on failure. */ static struct dentry *aafs_create(const char *name, umode_t mode, struct dentry *parent, void *data, void *link, const struct file_operations *fops, const struct inode_operations *iops) { struct dentry *dentry; struct inode *dir; int error; AA_BUG(!name); AA_BUG(!parent); if (!(mode & S_IFMT)) mode = (mode & S_IALLUGO) | S_IFREG; error = simple_pin_fs(&aafs_ops, &aafs_mnt, &aafs_count); if (error) return ERR_PTR(error); dir = d_inode(parent); inode_lock(dir); dentry = lookup_one_len(name, parent, strlen(name)); if (IS_ERR(dentry)) { error = PTR_ERR(dentry); goto fail_lock; } if (d_really_is_positive(dentry)) { error = -EEXIST; goto fail_dentry; } error = __aafs_setup_d_inode(dir, dentry, mode, data, link, fops, iops); if (error) goto fail_dentry; inode_unlock(dir); return dentry; fail_dentry: dput(dentry); fail_lock: inode_unlock(dir); simple_release_fs(&aafs_mnt, &aafs_count); return ERR_PTR(error); } /** * aafs_create_file - create a file in the apparmorfs filesystem * * @name: name of dentry to create * @mode: permissions the file should have * @parent: parent directory for this dentry * @data: data to store on inode.i_private, available in open() * @fops: struct file_operations that should be used for * * see aafs_create */ static struct dentry *aafs_create_file(const char *name, umode_t mode, struct dentry *parent, void *data, const struct file_operations *fops) { return aafs_create(name, mode, parent, data, NULL, fops, NULL); } /** * aafs_create_dir - create a directory in the apparmorfs filesystem * * @name: name of dentry to create * @parent: parent directory for this dentry * * see aafs_create */ static struct dentry *aafs_create_dir(const char *name, struct dentry *parent) { return aafs_create(name, S_IFDIR | 0755, parent, NULL, NULL, NULL, NULL); } /** * aafs_remove - removes a file or directory from the apparmorfs filesystem * * @dentry: dentry of the file/directory/symlink to removed. */ static void aafs_remove(struct dentry *dentry) { struct inode *dir; if (!dentry || IS_ERR(dentry)) return; dir = d_inode(dentry->d_parent); inode_lock(dir); if (simple_positive(dentry)) { if (d_is_dir(dentry)) simple_rmdir(dir, dentry); else simple_unlink(dir, dentry); d_delete(dentry); dput(dentry); } inode_unlock(dir); simple_release_fs(&aafs_mnt, &aafs_count); } /* * aa_fs - policy load/replace/remove */ /** * aa_simple_write_to_buffer - common routine for getting policy from user * @userbuf: user buffer to copy data from (NOT NULL) * @alloc_size: size of user buffer (REQUIRES: @alloc_size >= @copy_size) * @copy_size: size of data to copy from user buffer * @pos: position write is at in the file (NOT NULL) * * Returns: kernel buffer containing copy of user buffer data or an * ERR_PTR on failure. */ static struct aa_loaddata *aa_simple_write_to_buffer(const char __user *userbuf, size_t alloc_size, size_t copy_size, loff_t *pos) { struct aa_loaddata *data; AA_BUG(copy_size > alloc_size); if (*pos != 0) /* only writes from pos 0, that is complete writes */ return ERR_PTR(-ESPIPE); /* freed by caller to simple_write_to_buffer */ data = aa_loaddata_alloc(alloc_size); if (IS_ERR(data)) return data; data->size = copy_size; if (copy_from_user(data->data, userbuf, copy_size)) { kvfree(data); return ERR_PTR(-EFAULT); } return data; } static ssize_t policy_update(u32 mask, const char __user *buf, size_t size, loff_t *pos, struct aa_ns *ns) { struct aa_loaddata *data; struct aa_label *label; ssize_t error; label = begin_current_label_crit_section(); /* high level check about policy management - fine grained in * below after unpack */ error = aa_may_manage_policy(label, ns, mask); if (error) goto end_section; data = aa_simple_write_to_buffer(buf, size, size, pos); error = PTR_ERR(data); if (!IS_ERR(data)) { error = aa_replace_profiles(ns, label, mask, data); aa_put_loaddata(data); } end_section: end_current_label_crit_section(label); return error; } /* .load file hook fn to load policy */ static ssize_t profile_load(struct file *f, const char __user *buf, size_t size, loff_t *pos) { struct aa_ns *ns = aa_get_ns(f->f_inode->i_private); int error = policy_update(AA_MAY_LOAD_POLICY, buf, size, pos, ns); aa_put_ns(ns); return error; } static const struct file_operations aa_fs_profile_load = { .write = profile_load, .llseek = default_llseek, }; /* .replace file hook fn to load and/or replace policy */ static ssize_t profile_replace(struct file *f, const char __user *buf, size_t size, loff_t *pos) { struct aa_ns *ns = aa_get_ns(f->f_inode->i_private); int error = policy_update(AA_MAY_LOAD_POLICY | AA_MAY_REPLACE_POLICY, buf, size, pos, ns); aa_put_ns(ns); return error; } static const struct file_operations aa_fs_profile_replace = { .write = profile_replace, .llseek = default_llseek, }; /* .remove file hook fn to remove loaded policy */ static ssize_t profile_remove(struct file *f, const char __user *buf, size_t size, loff_t *pos) { struct aa_loaddata *data; struct aa_label *label; ssize_t error; struct aa_ns *ns = aa_get_ns(f->f_inode->i_private); label = begin_current_label_crit_section(); /* high level check about policy management - fine grained in * below after unpack */ error = aa_may_manage_policy(label, ns, AA_MAY_REMOVE_POLICY); if (error) goto out; /* * aa_remove_profile needs a null terminated string so 1 extra * byte is allocated and the copied data is null terminated. */ data = aa_simple_write_to_buffer(buf, size + 1, size, pos); error = PTR_ERR(data); if (!IS_ERR(data)) { data->data[size] = 0; error = aa_remove_profiles(ns, label, data->data, size); aa_put_loaddata(data); } out: end_current_label_crit_section(label); aa_put_ns(ns); return error; } static const struct file_operations aa_fs_profile_remove = { .write = profile_remove, .llseek = default_llseek, }; struct aa_revision { struct aa_ns *ns; long last_read; }; /* revision file hook fn for policy loads */ static int ns_revision_release(struct inode *inode, struct file *file) { struct aa_revision *rev = file->private_data; if (rev) { aa_put_ns(rev->ns); kfree(rev); } return 0; } static ssize_t ns_revision_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) { struct aa_revision *rev = file->private_data; char buffer[32]; long last_read; int avail; mutex_lock_nested(&rev->ns->lock, rev->ns->level); last_read = rev->last_read; if (last_read == rev->ns->revision) { mutex_unlock(&rev->ns->lock); if (file->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(rev->ns->wait, last_read != READ_ONCE(rev->ns->revision))) return -ERESTARTSYS; mutex_lock_nested(&rev->ns->lock, rev->ns->level); } avail = sprintf(buffer, "%ld\n", rev->ns->revision); if (*ppos + size > avail) { rev->last_read = rev->ns->revision; *ppos = 0; } mutex_unlock(&rev->ns->lock); return simple_read_from_buffer(buf, size, ppos, buffer, avail); } static int ns_revision_open(struct inode *inode, struct file *file) { struct aa_revision *rev = kzalloc(sizeof(*rev), GFP_KERNEL); if (!rev) return -ENOMEM; rev->ns = aa_get_ns(inode->i_private); if (!rev->ns) rev->ns = aa_get_current_ns(); file->private_data = rev; return 0; } static __poll_t ns_revision_poll(struct file *file, poll_table *pt) { struct aa_revision *rev = file->private_data; __poll_t mask = 0; if (rev) { mutex_lock_nested(&rev->ns->lock, rev->ns->level); poll_wait(file, &rev->ns->wait, pt); if (rev->last_read < rev->ns->revision) mask |= EPOLLIN | EPOLLRDNORM; mutex_unlock(&rev->ns->lock); } return mask; } void __aa_bump_ns_revision(struct aa_ns *ns) { WRITE_ONCE(ns->revision, READ_ONCE(ns->revision) + 1); wake_up_interruptible(&ns->wait); } static const struct file_operations aa_fs_ns_revision_fops = { .owner = THIS_MODULE, .open = ns_revision_open, .poll = ns_revision_poll, .read = ns_revision_read, .llseek = generic_file_llseek, .release = ns_revision_release, }; static void profile_query_cb(struct aa_profile *profile, struct aa_perms *perms, const char *match_str, size_t match_len) { struct aa_perms tmp = { }; struct aa_dfa *dfa; unsigned int state = 0; if (profile_unconfined(profile)) return; if (profile->file.dfa && *match_str == AA_CLASS_FILE) { dfa = profile->file.dfa; state = aa_dfa_match_len(dfa, profile->file.start, match_str + 1, match_len - 1); if (state) { struct path_cond cond = { }; tmp = aa_compute_fperms(dfa, state, &cond); } } else if (profile->policy.dfa) { if (!PROFILE_MEDIATES(profile, *match_str)) return; /* no change to current perms */ dfa = profile->policy.dfa; state = aa_dfa_match_len(dfa, profile->policy.start[0], match_str, match_len); if (state) aa_compute_perms(dfa, state, &tmp); } aa_apply_modes_to_perms(profile, &tmp); aa_perms_accum_raw(perms, &tmp); } /** * query_data - queries a policy and writes its data to buf * @buf: the resulting data is stored here (NOT NULL) * @buf_len: size of buf * @query: query string used to retrieve data * @query_len: size of query including second NUL byte * * The buffers pointed to by buf and query may overlap. The query buffer is * parsed before buf is written to. * * The query should look like "