apparmor: fix race between freeing data and fs accessing it

AppArmor was putting the reference to i_private data on its end after
removing the original entry from the file system. However the inode
can aand does live beyond that point and it is possible that some of
the fs call back functions will be invoked after the reference has
been put, which results in a race between freeing the data and
accessing it through the fs.

While the rawdata/loaddata is the most likely candidate to fail the
race, as it has the fewest references. If properly crafted it might be
possible to trigger a race for the other types stored in i_private.

Fix this by moving the put of i_private referenced data to the correct
place which is during inode eviction.

Fixes: c961ee5f21 ("apparmor: convert from securityfs to apparmorfs for policy ns files")
Reported-by: Qualys Security Advisory <qsa@qualys.com>
Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
Reviewed-by: Maxime Bélair <maxime.belair@canonical.com>
Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
Signed-off-by: John Johansen <john.johansen@canonical.com>
This commit is contained in:
John Johansen
2026-03-01 16:10:51 -08:00
parent a0b7091c4d
commit 8e135b8aee
7 changed files with 153 additions and 101 deletions

View File

@@ -32,6 +32,7 @@
#include "include/crypto.h"
#include "include/ipc.h"
#include "include/label.h"
#include "include/lib.h"
#include "include/policy.h"
#include "include/policy_ns.h"
#include "include/resource.h"
@@ -62,6 +63,7 @@
* securityfs and apparmorfs filesystems.
*/
#define IREF_POISON 101
/*
* support fns
@@ -153,6 +155,71 @@ static int aafs_show_path(struct seq_file *seq, struct dentry *dentry)
return 0;
}
static struct aa_ns *get_ns_common_ref(struct aa_common_ref *ref)
{
if (ref) {
struct aa_label *reflabel = container_of(ref, struct aa_label,
count);
return aa_get_ns(labels_ns(reflabel));
}
return NULL;
}
static struct aa_proxy *get_proxy_common_ref(struct aa_common_ref *ref)
{
if (ref)
return aa_get_proxy(container_of(ref, struct aa_proxy, count));
return NULL;
}
static struct aa_loaddata *get_loaddata_common_ref(struct aa_common_ref *ref)
{
if (ref)
return aa_get_i_loaddata(container_of(ref, struct aa_loaddata,
count));
return NULL;
}
static void aa_put_common_ref(struct aa_common_ref *ref)
{
if (!ref)
return;
switch (ref->reftype) {
case REF_RAWDATA:
aa_put_i_loaddata(container_of(ref, struct aa_loaddata,
count));
break;
case REF_PROXY:
aa_put_proxy(container_of(ref, struct aa_proxy,
count));
break;
case REF_NS:
/* ns count is held on its unconfined label */
aa_put_ns(labels_ns(container_of(ref, struct aa_label, count)));
break;
default:
AA_BUG(true, "unknown refcount type");
break;
}
}
static void aa_get_common_ref(struct aa_common_ref *ref)
{
kref_get(&ref->count);
}
static void aafs_evict(struct inode *inode)
{
struct aa_common_ref *ref = inode->i_private;
clear_inode(inode);
aa_put_common_ref(ref);
inode->i_private = (void *) IREF_POISON;
}
static void aafs_free_inode(struct inode *inode)
{
if (S_ISLNK(inode->i_mode))
@@ -162,6 +229,7 @@ static void aafs_free_inode(struct inode *inode)
static const struct super_operations aafs_super_ops = {
.statfs = simple_statfs,
.evict_inode = aafs_evict,
.free_inode = aafs_free_inode,
.show_path = aafs_show_path,
};
@@ -262,7 +330,8 @@ static int __aafs_setup_d_inode(struct inode *dir, struct dentry *dentry,
* 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,
struct dentry *parent,
struct aa_common_ref *data, void *link,
const struct file_operations *fops,
const struct inode_operations *iops)
{
@@ -299,6 +368,9 @@ static struct dentry *aafs_create(const char *name, umode_t mode,
goto fail_dentry;
inode_unlock(dir);
if (data)
aa_get_common_ref(data);
return dentry;
fail_dentry:
@@ -323,7 +395,8 @@ fail_lock:
* see aafs_create
*/
static struct dentry *aafs_create_file(const char *name, umode_t mode,
struct dentry *parent, void *data,
struct dentry *parent,
struct aa_common_ref *data,
const struct file_operations *fops)
{
return aafs_create(name, mode, parent, data, NULL, fops, NULL);
@@ -453,7 +526,7 @@ end_section:
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);
struct aa_ns *ns = get_ns_common_ref(f->f_inode->i_private);
int error = policy_update(AA_MAY_LOAD_POLICY, buf, size, pos, ns,
f->f_cred);
@@ -471,7 +544,7 @@ static const struct file_operations aa_fs_profile_load = {
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);
struct aa_ns *ns = get_ns_common_ref(f->f_inode->i_private);
int error = policy_update(AA_MAY_LOAD_POLICY | AA_MAY_REPLACE_POLICY,
buf, size, pos, ns, f->f_cred);
aa_put_ns(ns);
@@ -491,7 +564,7 @@ static ssize_t profile_remove(struct file *f, const char __user *buf,
struct aa_loaddata *data;
struct aa_label *label;
ssize_t error;
struct aa_ns *ns = aa_get_ns(f->f_inode->i_private);
struct aa_ns *ns = get_ns_common_ref(f->f_inode->i_private);
label = begin_current_label_crit_section();
/* high level check about policy management - fine grained in
@@ -581,7 +654,7 @@ static int ns_revision_open(struct inode *inode, struct file *file)
if (!rev)
return -ENOMEM;
rev->ns = aa_get_ns(inode->i_private);
rev->ns = get_ns_common_ref(inode->i_private);
if (!rev->ns)
rev->ns = aa_get_current_ns();
file->private_data = rev;
@@ -1067,7 +1140,7 @@ static const struct file_operations seq_profile_ ##NAME ##_fops = { \
static int seq_profile_open(struct inode *inode, struct file *file,
int (*show)(struct seq_file *, void *))
{
struct aa_proxy *proxy = aa_get_proxy(inode->i_private);
struct aa_proxy *proxy = get_proxy_common_ref(inode->i_private);
int error = single_open(file, show, proxy);
if (error) {
@@ -1259,7 +1332,7 @@ static const struct file_operations seq_rawdata_ ##NAME ##_fops = { \
static int seq_rawdata_open(struct inode *inode, struct file *file,
int (*show)(struct seq_file *, void *))
{
struct aa_loaddata *data = aa_get_i_loaddata(inode->i_private);
struct aa_loaddata *data = get_loaddata_common_ref(inode->i_private);
int error;
if (!data)
@@ -1392,7 +1465,7 @@ static int rawdata_open(struct inode *inode, struct file *file)
if (!aa_current_policy_view_capable(NULL))
return -EACCES;
loaddata = aa_get_i_loaddata(inode->i_private);
loaddata = get_loaddata_common_ref(inode->i_private);
if (!loaddata)
return -ENOENT;
@@ -1437,7 +1510,6 @@ static void remove_rawdata_dents(struct aa_loaddata *rawdata)
if (!IS_ERR_OR_NULL(rawdata->dents[i])) {
aafs_remove(rawdata->dents[i]);
rawdata->dents[i] = NULL;
aa_put_i_loaddata(rawdata);
}
}
}
@@ -1476,45 +1548,41 @@ int __aa_fs_create_rawdata(struct aa_ns *ns, struct aa_loaddata *rawdata)
if (IS_ERR(dir))
/* ->name freed when rawdata freed */
return PTR_ERR(dir);
aa_get_i_loaddata(rawdata);
rawdata->dents[AAFS_LOADDATA_DIR] = dir;
dent = aafs_create_file("abi", S_IFREG | 0444, dir, rawdata,
dent = aafs_create_file("abi", S_IFREG | 0444, dir, &rawdata->count,
&seq_rawdata_abi_fops);
if (IS_ERR(dent))
goto fail;
aa_get_i_loaddata(rawdata);
rawdata->dents[AAFS_LOADDATA_ABI] = dent;
dent = aafs_create_file("revision", S_IFREG | 0444, dir, rawdata,
&seq_rawdata_revision_fops);
dent = aafs_create_file("revision", S_IFREG | 0444, dir,
&rawdata->count,
&seq_rawdata_revision_fops);
if (IS_ERR(dent))
goto fail;
aa_get_i_loaddata(rawdata);
rawdata->dents[AAFS_LOADDATA_REVISION] = dent;
if (aa_g_hash_policy) {
dent = aafs_create_file("sha256", S_IFREG | 0444, dir,
rawdata, &seq_rawdata_hash_fops);
&rawdata->count,
&seq_rawdata_hash_fops);
if (IS_ERR(dent))
goto fail;
aa_get_i_loaddata(rawdata);
rawdata->dents[AAFS_LOADDATA_HASH] = dent;
}
dent = aafs_create_file("compressed_size", S_IFREG | 0444, dir,
rawdata,
&rawdata->count,
&seq_rawdata_compressed_size_fops);
if (IS_ERR(dent))
goto fail;
aa_get_i_loaddata(rawdata);
rawdata->dents[AAFS_LOADDATA_COMPRESSED_SIZE] = dent;
dent = aafs_create_file("raw_data", S_IFREG | 0444,
dir, rawdata, &rawdata_fops);
dent = aafs_create_file("raw_data", S_IFREG | 0444, dir,
&rawdata->count, &rawdata_fops);
if (IS_ERR(dent))
goto fail;
aa_get_i_loaddata(rawdata);
rawdata->dents[AAFS_LOADDATA_DATA] = dent;
d_inode(dent)->i_size = rawdata->size;
@@ -1525,7 +1593,6 @@ int __aa_fs_create_rawdata(struct aa_ns *ns, struct aa_loaddata *rawdata)
fail:
remove_rawdata_dents(rawdata);
aa_put_i_loaddata(rawdata);
return PTR_ERR(dent);
}
#endif /* CONFIG_SECURITY_APPARMOR_EXPORT_BINARY */
@@ -1549,13 +1616,10 @@ void __aafs_profile_rmdir(struct aa_profile *profile)
__aafs_profile_rmdir(child);
for (i = AAFS_PROF_SIZEOF - 1; i >= 0; --i) {
struct aa_proxy *proxy;
if (!profile->dents[i])
continue;
proxy = d_inode(profile->dents[i])->i_private;
aafs_remove(profile->dents[i]);
aa_put_proxy(proxy);
profile->dents[i] = NULL;
}
}
@@ -1589,14 +1653,7 @@ static struct dentry *create_profile_file(struct dentry *dir, const char *name,
struct aa_profile *profile,
const struct file_operations *fops)
{
struct aa_proxy *proxy = aa_get_proxy(profile->label.proxy);
struct dentry *dent;
dent = aafs_create_file(name, S_IFREG | 0444, dir, proxy, fops);
if (IS_ERR(dent))
aa_put_proxy(proxy);
return dent;
return aafs_create_file(name, S_IFREG | 0444, dir, &profile->label.proxy->count, fops);
}
#ifdef CONFIG_SECURITY_APPARMOR_EXPORT_BINARY
@@ -1646,7 +1703,8 @@ static const char *rawdata_get_link_base(struct dentry *dentry,
struct delayed_call *done,
const char *name)
{
struct aa_proxy *proxy = inode->i_private;
struct aa_common_ref *ref = inode->i_private;
struct aa_proxy *proxy = container_of(ref, struct aa_proxy, count);
struct aa_label *label;
struct aa_profile *profile;
char *target;
@@ -1788,27 +1846,24 @@ int __aafs_profile_mkdir(struct aa_profile *profile, struct dentry *parent)
if (profile->rawdata) {
if (aa_g_hash_policy) {
dent = aafs_create("raw_sha256", S_IFLNK | 0444, dir,
profile->label.proxy, NULL, NULL,
&rawdata_link_sha256_iops);
&profile->label.proxy->count, NULL,
NULL, &rawdata_link_sha256_iops);
if (IS_ERR(dent))
goto fail;
aa_get_proxy(profile->label.proxy);
profile->dents[AAFS_PROF_RAW_HASH] = dent;
}
dent = aafs_create("raw_abi", S_IFLNK | 0444, dir,
profile->label.proxy, NULL, NULL,
&profile->label.proxy->count, NULL, NULL,
&rawdata_link_abi_iops);
if (IS_ERR(dent))
goto fail;
aa_get_proxy(profile->label.proxy);
profile->dents[AAFS_PROF_RAW_ABI] = dent;
dent = aafs_create("raw_data", S_IFLNK | 0444, dir,
profile->label.proxy, NULL, NULL,
&profile->label.proxy->count, NULL, NULL,
&rawdata_link_data_iops);
if (IS_ERR(dent))
goto fail;
aa_get_proxy(profile->label.proxy);
profile->dents[AAFS_PROF_RAW_DATA] = dent;
}
#endif /*CONFIG_SECURITY_APPARMOR_EXPORT_BINARY */
@@ -1845,7 +1900,7 @@ static struct dentry *ns_mkdir_op(struct mnt_idmap *idmap, struct inode *dir,
if (error)
return ERR_PTR(error);
parent = aa_get_ns(dir->i_private);
parent = get_ns_common_ref(dir->i_private);
AA_BUG(d_inode(ns_subns_dir(parent)) != dir);
/* we have to unlock and then relock to get locking order right
@@ -1895,7 +1950,7 @@ static int ns_rmdir_op(struct inode *dir, struct dentry *dentry)
if (error)
return error;
parent = aa_get_ns(dir->i_private);
parent = get_ns_common_ref(dir->i_private);
/* rmdir calls the generic securityfs functions to remove files
* from the apparmor dir. It is up to the apparmor ns locking
* to avoid races.
@@ -1965,27 +2020,6 @@ void __aafs_ns_rmdir(struct aa_ns *ns)
__aa_fs_list_remove_rawdata(ns);
if (ns_subns_dir(ns)) {
sub = d_inode(ns_subns_dir(ns))->i_private;
aa_put_ns(sub);
}
if (ns_subload(ns)) {
sub = d_inode(ns_subload(ns))->i_private;
aa_put_ns(sub);
}
if (ns_subreplace(ns)) {
sub = d_inode(ns_subreplace(ns))->i_private;
aa_put_ns(sub);
}
if (ns_subremove(ns)) {
sub = d_inode(ns_subremove(ns))->i_private;
aa_put_ns(sub);
}
if (ns_subrevision(ns)) {
sub = d_inode(ns_subrevision(ns))->i_private;
aa_put_ns(sub);
}
for (i = AAFS_NS_SIZEOF - 1; i >= 0; --i) {
aafs_remove(ns->dents[i]);
ns->dents[i] = NULL;
@@ -2010,40 +2044,40 @@ static int __aafs_ns_mkdir_entries(struct aa_ns *ns, struct dentry *dir)
return PTR_ERR(dent);
ns_subdata_dir(ns) = dent;
dent = aafs_create_file("revision", 0444, dir, ns,
dent = aafs_create_file("revision", 0444, dir,
&ns->unconfined->label.count,
&aa_fs_ns_revision_fops);
if (IS_ERR(dent))
return PTR_ERR(dent);
aa_get_ns(ns);
ns_subrevision(ns) = dent;
dent = aafs_create_file(".load", 0640, dir, ns,
&aa_fs_profile_load);
dent = aafs_create_file(".load", 0640, dir,
&ns->unconfined->label.count,
&aa_fs_profile_load);
if (IS_ERR(dent))
return PTR_ERR(dent);
aa_get_ns(ns);
ns_subload(ns) = dent;
dent = aafs_create_file(".replace", 0640, dir, ns,
&aa_fs_profile_replace);
dent = aafs_create_file(".replace", 0640, dir,
&ns->unconfined->label.count,
&aa_fs_profile_replace);
if (IS_ERR(dent))
return PTR_ERR(dent);
aa_get_ns(ns);
ns_subreplace(ns) = dent;
dent = aafs_create_file(".remove", 0640, dir, ns,
&aa_fs_profile_remove);
dent = aafs_create_file(".remove", 0640, dir,
&ns->unconfined->label.count,
&aa_fs_profile_remove);
if (IS_ERR(dent))
return PTR_ERR(dent);
aa_get_ns(ns);
ns_subremove(ns) = dent;
/* use create_dentry so we can supply private data */
dent = aafs_create("namespaces", S_IFDIR | 0755, dir, ns, NULL, NULL,
&ns_dir_inode_operations);
dent = aafs_create("namespaces", S_IFDIR | 0755, dir,
&ns->unconfined->label.count,
NULL, NULL, &ns_dir_inode_operations);
if (IS_ERR(dent))
return PTR_ERR(dent);
aa_get_ns(ns);
ns_subns_dir(ns) = dent;
return 0;

View File

@@ -102,7 +102,7 @@ enum label_flags {
struct aa_label;
struct aa_proxy {
struct kref count;
struct aa_common_ref count;
struct aa_label __rcu *label;
};
@@ -125,7 +125,7 @@ struct label_it {
* vec: vector of profiles comprising the compound label
*/
struct aa_label {
struct kref count;
struct aa_common_ref count;
struct rb_node node;
struct rcu_head rcu;
struct aa_proxy *proxy;
@@ -357,7 +357,7 @@ int aa_label_match(struct aa_profile *profile, struct aa_ruleset *rules,
*/
static inline struct aa_label *__aa_get_label(struct aa_label *l)
{
if (l && kref_get_unless_zero(&l->count))
if (l && kref_get_unless_zero(&l->count.count))
return l;
return NULL;
@@ -366,7 +366,7 @@ static inline struct aa_label *__aa_get_label(struct aa_label *l)
static inline struct aa_label *aa_get_label(struct aa_label *l)
{
if (l)
kref_get(&(l->count));
kref_get(&(l->count.count));
return l;
}
@@ -386,7 +386,7 @@ static inline struct aa_label *aa_get_label_rcu(struct aa_label __rcu **l)
rcu_read_lock();
do {
c = rcu_dereference(*l);
} while (c && !kref_get_unless_zero(&c->count));
} while (c && !kref_get_unless_zero(&c->count.count));
rcu_read_unlock();
return c;
@@ -426,7 +426,7 @@ static inline struct aa_label *aa_get_newest_label(struct aa_label *l)
static inline void aa_put_label(struct aa_label *l)
{
if (l)
kref_put(&l->count, aa_label_kref);
kref_put(&l->count.count, aa_label_kref);
}
/* wrapper fn to indicate semantics of the check */
@@ -443,7 +443,7 @@ void aa_proxy_kref(struct kref *kref);
static inline struct aa_proxy *aa_get_proxy(struct aa_proxy *proxy)
{
if (proxy)
kref_get(&(proxy->count));
kref_get(&(proxy->count.count));
return proxy;
}
@@ -451,7 +451,7 @@ static inline struct aa_proxy *aa_get_proxy(struct aa_proxy *proxy)
static inline void aa_put_proxy(struct aa_proxy *proxy)
{
if (proxy)
kref_put(&proxy->count, aa_proxy_kref);
kref_put(&proxy->count.count, aa_proxy_kref);
}
void __aa_proxy_redirect(struct aa_label *orig, struct aa_label *new);

View File

@@ -102,6 +102,18 @@ void aa_info_message(const char *str);
/* Security blob offsets */
extern struct lsm_blob_sizes apparmor_blob_sizes;
enum reftype {
REF_NS,
REF_PROXY,
REF_RAWDATA,
};
/* common reference count used by data the shows up in aafs */
struct aa_common_ref {
struct kref count;
enum reftype reftype;
};
/**
* aa_strneq - compare null terminated @str to a non null terminated substring
* @str: a null terminated string

View File

@@ -379,7 +379,7 @@ static inline bool profile_mediates_safe(struct aa_profile *profile,
static inline struct aa_profile *aa_get_profile(struct aa_profile *p)
{
if (p)
kref_get(&(p->label.count));
kref_get(&(p->label.count.count));
return p;
}
@@ -393,7 +393,7 @@ static inline struct aa_profile *aa_get_profile(struct aa_profile *p)
*/
static inline struct aa_profile *aa_get_profile_not0(struct aa_profile *p)
{
if (p && kref_get_unless_zero(&p->label.count))
if (p && kref_get_unless_zero(&p->label.count.count))
return p;
return NULL;
@@ -413,7 +413,7 @@ static inline struct aa_profile *aa_get_profile_rcu(struct aa_profile __rcu **p)
rcu_read_lock();
do {
c = rcu_dereference(*p);
} while (c && !kref_get_unless_zero(&c->label.count));
} while (c && !kref_get_unless_zero(&c->label.count.count));
rcu_read_unlock();
return c;
@@ -426,7 +426,7 @@ static inline struct aa_profile *aa_get_profile_rcu(struct aa_profile __rcu **p)
static inline void aa_put_profile(struct aa_profile *p)
{
if (p)
kref_put(&p->label.count, aa_label_kref);
kref_put(&p->label.count.count, aa_label_kref);
}
static inline int AUDIT_MODE(struct aa_profile *profile)

View File

@@ -108,7 +108,7 @@ struct aa_ext {
* fs entries and drops the associated @count ref.
*/
struct aa_loaddata {
struct kref count;
struct aa_common_ref count;
struct kref pcount;
struct list_head list;
struct work_struct work;
@@ -143,7 +143,7 @@ aa_get_i_loaddata(struct aa_loaddata *data)
{
if (data)
kref_get(&(data->count));
kref_get(&(data->count.count));
return data;
}
@@ -171,7 +171,7 @@ struct aa_loaddata *aa_loaddata_alloc(size_t size);
static inline void aa_put_i_loaddata(struct aa_loaddata *data)
{
if (data)
kref_put(&data->count, aa_loaddata_kref);
kref_put(&data->count.count, aa_loaddata_kref);
}
static inline void aa_put_profile_loaddata(struct aa_loaddata *data)

View File

@@ -52,7 +52,8 @@ static void free_proxy(struct aa_proxy *proxy)
void aa_proxy_kref(struct kref *kref)
{
struct aa_proxy *proxy = container_of(kref, struct aa_proxy, count);
struct aa_proxy *proxy = container_of(kref, struct aa_proxy,
count.count);
free_proxy(proxy);
}
@@ -63,7 +64,8 @@ struct aa_proxy *aa_alloc_proxy(struct aa_label *label, gfp_t gfp)
new = kzalloc_obj(struct aa_proxy, gfp);
if (new) {
kref_init(&new->count);
kref_init(&new->count.count);
new->count.reftype = REF_PROXY;
rcu_assign_pointer(new->label, aa_get_label(label));
}
return new;
@@ -375,7 +377,8 @@ static void label_free_rcu(struct rcu_head *head)
void aa_label_kref(struct kref *kref)
{
struct aa_label *label = container_of(kref, struct aa_label, count);
struct aa_label *label = container_of(kref, struct aa_label,
count.count);
struct aa_ns *ns = labels_ns(label);
if (!ns) {
@@ -412,7 +415,8 @@ bool aa_label_init(struct aa_label *label, int size, gfp_t gfp)
label->size = size; /* doesn't include null */
label->vec[size] = NULL; /* null terminate */
kref_init(&label->count);
kref_init(&label->count.count);
label->count.reftype = REF_NS; /* for aafs purposes */
RB_CLEAR_NODE(&label->node);
return true;

View File

@@ -119,7 +119,8 @@ static void do_loaddata_free(struct aa_loaddata *d)
void aa_loaddata_kref(struct kref *kref)
{
struct aa_loaddata *d = container_of(kref, struct aa_loaddata, count);
struct aa_loaddata *d = container_of(kref, struct aa_loaddata,
count.count);
do_loaddata_free(d);
}
@@ -166,7 +167,8 @@ struct aa_loaddata *aa_loaddata_alloc(size_t size)
kfree(d);
return ERR_PTR(-ENOMEM);
}
kref_init(&d->count);
kref_init(&d->count.count);
d->count.reftype = REF_RAWDATA;
kref_init(&d->pcount);
INIT_LIST_HEAD(&d->list);