commit
a0b7091c4de45a7325c8780e6934a894f92ac86b upstream.
Backport for conflicts introdued by
d61c57fde819 ("apparmor: make export of raw binary profile to userspace optional")
which nests the conflicting code inside an if condition
There is a race condition that leads to a use-after-free situation:
because the rawdata inodes are not refcounted, an attacker can start
open()ing one of the rawdata files, and at the same time remove the
last reference to this rawdata (by removing the corresponding profile,
for example), which frees its struct aa_loaddata; as a result, when
seq_rawdata_open() is reached, i_private is a dangling pointer and
freed memory is accessed.
The rawdata inodes weren't refcounted to avoid a circular refcount and
were supposed to be held by the profile rawdata reference. However
during profile removal there is a window where the vfs and profile
destruction race, resulting in the use after free.
Fix this by moving to a double refcount scheme. Where the profile
refcount on rawdata is used to break the circular dependency. Allowing
for freeing of the rawdata once all inode references to the rawdata
are put.
Fixes: 5d5182cae401 ("apparmor: move to per loaddata files, instead of replicating in profiles")
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>
Tested-by: Salvatore Bonaccorso <carnil@debian.org>
Signed-off-by: John Johansen <john.johansen@canonical.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
if (!private)
return;
- aa_put_loaddata(private->loaddata);
+ aa_put_i_loaddata(private->loaddata);
kvfree(private);
}
data->size = copy_size;
if (copy_from_user(data->data, userbuf, copy_size)) {
- aa_put_loaddata(data);
+ /* trigger free - don't need to put pcount */
+ aa_put_i_loaddata(data);
+
return ERR_PTR(-EFAULT);
}
error = PTR_ERR(data);
if (!IS_ERR(data)) {
error = aa_replace_profiles(ns, label, mask, data);
- aa_put_loaddata(data);
+ /* put pcount, which will put count and free if no
+ * profiles referencing it.
+ */
+ aa_put_profile_loaddata(data);
}
end_section:
end_current_label_crit_section(label);
if (!IS_ERR(data)) {
data->data[size] = 0;
error = aa_remove_profiles(ns, label, data->data, size);
- aa_put_loaddata(data);
+ aa_put_profile_loaddata(data);
}
out:
end_current_label_crit_section(label);
static int seq_rawdata_open(struct inode *inode, struct file *file,
int (*show)(struct seq_file *, void *))
{
- struct aa_loaddata *data = __aa_get_loaddata(inode->i_private);
+ struct aa_loaddata *data = aa_get_i_loaddata(inode->i_private);
int error;
if (!data)
- /* lost race this ent is being reaped */
return -ENOENT;
error = single_open(file, show, data);
if (error) {
AA_BUG(file->private_data &&
((struct seq_file *)file->private_data)->private);
- aa_put_loaddata(data);
+ aa_put_i_loaddata(data);
}
return error;
struct seq_file *seq = (struct seq_file *) file->private_data;
if (seq)
- aa_put_loaddata(seq->private);
+ aa_put_i_loaddata(seq->private);
return single_release(inode, file);
}
if (!policy_view_capable(NULL))
return -EACCES;
- loaddata = __aa_get_loaddata(inode->i_private);
+ loaddata = aa_get_i_loaddata(inode->i_private);
if (!loaddata)
- /* lost race: this entry is being reaped */
return -ENOENT;
private = rawdata_f_data_alloc(loaddata->size);
return error;
fail_private_alloc:
- aa_put_loaddata(loaddata);
+ aa_put_i_loaddata(loaddata);
return error;
}
for (i = 0; i < AAFS_LOADDATA_NDENTS; i++) {
if (!IS_ERR_OR_NULL(rawdata->dents[i])) {
- /* no refcounts on i_private */
aafs_remove(rawdata->dents[i]);
rawdata->dents[i] = NULL;
+ aa_put_i_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,
&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);
if (IS_ERR(dent))
goto fail;
+ aa_get_i_loaddata(rawdata);
rawdata->dents[AAFS_LOADDATA_REVISION] = dent;
if (aa_g_hash_policy) {
rawdata, &seq_rawdata_hash_fops);
if (IS_ERR(dent))
goto fail;
+ aa_get_i_loaddata(rawdata);
rawdata->dents[AAFS_LOADDATA_HASH] = dent;
}
&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);
if (IS_ERR(dent))
goto fail;
+ aa_get_i_loaddata(rawdata);
rawdata->dents[AAFS_LOADDATA_DATA] = dent;
d_inode(dent)->i_size = rawdata->size;
rawdata->ns = aa_get_ns(ns);
list_add(&rawdata->list, &ns->rawdata_list);
- /* no refcount on inode rawdata */
return 0;
fail:
remove_rawdata_dents(rawdata);
-
+ aa_put_i_loaddata(rawdata);
return PTR_ERR(dent);
}
AAFS_LOADDATA_NDENTS /* count of entries */
};
-/*
- * struct aa_loaddata - buffer of policy raw_data set
+/* struct aa_loaddata - buffer of policy raw_data set
+ * @count: inode/filesystem refcount - use aa_get_i_loaddata()
+ * @pcount: profile refcount - use aa_get_profile_loaddata()
+ * @list: list the loaddata is on
+ * @work: used to do a delayed cleanup
+ * @dents: refs to dents created in aafs
+ * @ns: the namespace this loaddata was loaded into
+ * @name:
+ * @size: the size of the data that was loaded
+ * @compressed_size: the size of the data when it is compressed
+ * @revision: unique revision count that this data was loaded as
+ * @abi: the abi number the loaddata uses
+ * @hash: a hash of the loaddata, used to help dedup data
*
- * there is no loaddata ref for being on ns list, nor a ref from
- * d_inode(@dentry) when grab a ref from these, @ns->lock must be held
- * && __aa_get_loaddata() needs to be used, and the return value
- * checked, if NULL the loaddata is already being reaped and should be
- * considered dead.
+ * There is no loaddata ref for being on ns->rawdata_list, so
+ * @ns->lock must be held when walking the list. Dentries and
+ * inode opens hold refs on @count; profiles hold refs on @pcount.
+ * When the last @pcount drops, do_ploaddata_rmfs() removes the
+ * fs entries and drops the associated @count ref.
*/
struct aa_loaddata {
struct kref count;
+ struct kref pcount;
struct list_head list;
struct work_struct work;
struct dentry *dents[AAFS_LOADDATA_NDENTS];
int aa_unpack(struct aa_loaddata *udata, struct list_head *lh, const char **ns);
/**
- * __aa_get_loaddata - get a reference count to uncounted data reference
+ * aa_get_loaddata - get a reference count from a counted data reference
* @data: reference to get a count on
*
- * Returns: pointer to reference OR NULL if race is lost and reference is
- * being repeated.
- * Requires: @data->ns->lock held, and the return code MUST be checked
- *
- * Use only from inode->i_private and @data->list found references
+ * Returns: pointer to reference
+ * Requires: @data to have a valid reference count on it. It is a bug
+ * if the race to reap can be encountered when it is used.
*/
static inline struct aa_loaddata *
-__aa_get_loaddata(struct aa_loaddata *data)
+aa_get_i_loaddata(struct aa_loaddata *data)
{
- if (data && kref_get_unless_zero(&(data->count)))
- return data;
- return NULL;
+ if (data)
+ kref_get(&(data->count));
+ return data;
}
+
/**
- * aa_get_loaddata - get a reference count from a counted data reference
+ * aa_get_profile_loaddata - get a profile reference count on loaddata
* @data: reference to get a count on
*
- * Returns: point to reference
- * Requires: @data to have a valid reference count on it. It is a bug
- * if the race to reap can be encountered when it is used.
+ * Returns: pointer to reference
+ * Requires: @data to have a valid reference count on it.
*/
static inline struct aa_loaddata *
-aa_get_loaddata(struct aa_loaddata *data)
+aa_get_profile_loaddata(struct aa_loaddata *data)
{
- struct aa_loaddata *tmp = __aa_get_loaddata(data);
-
- AA_BUG(data && !tmp);
-
- return tmp;
+ if (data)
+ kref_get(&(data->pcount));
+ return data;
}
void __aa_loaddata_update(struct aa_loaddata *data, long revision);
bool aa_rawdata_eq(struct aa_loaddata *l, struct aa_loaddata *r);
void aa_loaddata_kref(struct kref *kref);
+void aa_ploaddata_kref(struct kref *kref);
struct aa_loaddata *aa_loaddata_alloc(size_t size);
-static inline void aa_put_loaddata(struct aa_loaddata *data)
+static inline void aa_put_i_loaddata(struct aa_loaddata *data)
{
if (data)
kref_put(&data->count, aa_loaddata_kref);
}
+static inline void aa_put_profile_loaddata(struct aa_loaddata *data)
+{
+ if (data)
+ kref_put(&data->pcount, aa_ploaddata_kref);
+}
+
#endif /* __POLICY_INTERFACE_H */
}
kfree_sensitive(profile->hash);
- aa_put_loaddata(profile->rawdata);
+ aa_put_profile_loaddata(profile->rawdata);
aa_label_destroy(&profile->label);
kfree_sensitive(profile);
LIST_HEAD(lh);
op = mask & AA_MAY_REPLACE_POLICY ? OP_PROF_REPL : OP_PROF_LOAD;
- aa_get_loaddata(udata);
+ aa_get_profile_loaddata(udata);
/* released below */
error = aa_unpack(udata, &lh, &ns_name);
if (error)
if (aa_rawdata_eq(rawdata_ent, udata)) {
struct aa_loaddata *tmp;
- tmp = __aa_get_loaddata(rawdata_ent);
+ tmp = aa_get_profile_loaddata(rawdata_ent);
/* check we didn't fail the race */
if (tmp) {
- aa_put_loaddata(udata);
+ aa_put_profile_loaddata(udata);
udata = tmp;
break;
}
list_for_each_entry(ent, &lh, list) {
struct aa_policy *policy;
- ent->new->rawdata = aa_get_loaddata(udata);
+ ent->new->rawdata = aa_get_profile_loaddata(udata);
error = __lookup_replace(ns, ent->new->base.hname,
!(mask & AA_MAY_REPLACE_POLICY),
&ent->old, &info);
out:
aa_put_ns(ns);
- aa_put_loaddata(udata);
+ aa_put_profile_loaddata(udata);
kfree(ns_name);
if (error)
return memcmp(l->data, r->data, r->compressed_size ?: r->size) == 0;
}
+static void do_loaddata_free(struct aa_loaddata *d)
+{
+ kfree_sensitive(d->hash);
+ kfree_sensitive(d->name);
+ kvfree(d->data);
+ kfree_sensitive(d);
+}
+
+void aa_loaddata_kref(struct kref *kref)
+{
+ struct aa_loaddata *d = container_of(kref, struct aa_loaddata, count);
+
+ do_loaddata_free(d);
+}
+
/*
* need to take the ns mutex lock which is NOT safe most places that
* put_loaddata is called, so we have to delay freeing it
*/
-static void do_loaddata_free(struct work_struct *work)
+static void do_ploaddata_rmfs(struct work_struct *work)
{
struct aa_loaddata *d = container_of(work, struct aa_loaddata, work);
struct aa_ns *ns = aa_get_ns(d->ns);
if (ns) {
mutex_lock_nested(&ns->lock, ns->level);
+ /* remove fs ref to loaddata */
__aa_fs_remove_rawdata(d);
mutex_unlock(&ns->lock);
aa_put_ns(ns);
}
-
- kfree_sensitive(d->hash);
- kfree_sensitive(d->name);
- kvfree(d->data);
- kfree_sensitive(d);
+ /* called by dropping last pcount, so drop its associated icount */
+ aa_put_i_loaddata(d);
}
-void aa_loaddata_kref(struct kref *kref)
+void aa_ploaddata_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, pcount);
if (d) {
- INIT_WORK(&d->work, do_loaddata_free);
+ INIT_WORK(&d->work, do_ploaddata_rmfs);
schedule_work(&d->work);
}
}
return ERR_PTR(-ENOMEM);
}
kref_init(&d->count);
+ kref_init(&d->pcount);
INIT_LIST_HEAD(&d->list);
return d;