[apparmor] [PATCH 25/32] apparmor: Add interface files for profiles and namespaces
John Johansen
john.johansen at canonical.com
Wed Jan 16 21:28:54 UTC 2013
Add basic interface files to access namespace and profile information.
The interface files are created when a profile is loaded and removed
when the profile or namespace is removed.
Signed-off-by: John Johansen <john.johansen at canonical.com>
---
security/apparmor/apparmorfs.c | 327 ++++++++++++++++++++++++++++++--
security/apparmor/audit.c | 6 +
security/apparmor/include/apparmorfs.h | 38 ++++
security/apparmor/include/audit.h | 2 +
security/apparmor/include/policy.h | 11 ++
security/apparmor/policy.c | 93 +++++++--
6 files changed, 448 insertions(+), 29 deletions(-)
diff --git a/security/apparmor/apparmorfs.c b/security/apparmor/apparmorfs.c
index 1266724..1156936 100644
--- a/security/apparmor/apparmorfs.c
+++ b/security/apparmor/apparmorfs.c
@@ -12,6 +12,7 @@
* License.
*/
+#include <linux/ctype.h>
#include <linux/security.h>
#include <linux/vmalloc.h>
#include <linux/module.h>
@@ -28,6 +29,49 @@
#include "include/resource.h"
/**
+ * 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(char *name, char *target)
+{
+ char *t = target;
+
+ while (*name == '/' || *name == '.')
+ name++;
+
+ if (target) {
+ for (; *name; name++) {
+ if (strchr("\"\'(){}[]", *name))
+ continue;
+ else if (*name == '/')
+ *(t)++ = '.';
+ else if (isspace(*name))
+ *(t)++ = '_';
+ else if (strchr("*?^$\\", *name))
+ *(t)++ = 'X';
+ else if (isgraph(*name))
+ *(t)++ = *name;
+ }
+
+ *t = 0;
+ } else {
+ int len = 0;
+ for (; *name; name++) {
+ if (strchr("\"\'(){}[]", *name))
+ continue;
+ len++;
+ }
+
+ return len;
+ }
+
+ return t - target;
+}
+
+/**
* aa_simple_write_to_buffer - common routine for getting policy from user
* @op: operation doing the user buffer copy
* @userbuf: user buffer to copy data from (NOT NULL)
@@ -182,8 +226,252 @@ const struct file_operations aa_fs_seq_file_ops = {
.release = single_release,
};
-/** Base file system setup **/
+static int aa_fs_seq_profile_open(struct inode *inode, struct file *file,
+ int (*show)(struct seq_file *, void *))
+{
+ struct aa_replacedby *r = aa_get_replacedby(inode->i_private);
+ int error = single_open(file, show, r);
+
+ if (error)
+ aa_put_replacedby(r);
+
+ return error;
+}
+
+static int aa_fs_seq_profile_release(struct inode *inode, struct file *file)
+{
+ aa_put_replacedby(inode->i_private);
+ return single_release(inode, file);
+}
+
+static int aa_fs_seq_profname_show(struct seq_file *seq, void *v)
+{
+ struct aa_replacedby *r = seq->private;
+ struct aa_profile *profile = aa_get_profile_rcu(&r->profile);
+ seq_printf(seq, "%s\n", profile->base.name);
+ aa_put_profile(profile);
+
+ return 0;
+}
+
+static int aa_fs_seq_profname_open(struct inode *inode, struct file *file)
+{
+ return aa_fs_seq_profile_open(inode, file, aa_fs_seq_profname_show);
+}
+
+const struct file_operations aa_fs_profname_fops = {
+ .owner = THIS_MODULE,
+ .open = aa_fs_seq_profname_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = aa_fs_seq_profile_release,
+};
+
+static int aa_fs_seq_profmode_show(struct seq_file *seq, void *v)
+{
+ struct aa_replacedby *r = seq->private;
+ struct aa_profile *profile = aa_get_profile_rcu(&r->profile);
+ seq_printf(seq, "%s\n", aa_profile_mode_names[profile->mode]);
+ aa_put_profile(profile);
+
+ return 0;
+}
+
+static int aa_fs_seq_profmode_open(struct inode *inode, struct file *file)
+{
+ return aa_fs_seq_profile_open(inode, file, aa_fs_seq_profmode_show);
+}
+
+const struct file_operations aa_fs_profmode_fops = {
+ .owner = THIS_MODULE,
+ .open = aa_fs_seq_profmode_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = aa_fs_seq_profile_release,
+};
+
+/** fns to setup dynamic per profile/namespace files **/
+void __aa_fs_profile_rmdir(struct aa_profile *profile)
+{
+ struct aa_profile *child;
+ int i;
+
+ if (!profile)
+ return;
+
+ list_for_each_entry(child, &profile->base.profiles, base.list)
+ __aa_fs_profile_rmdir(child);
+
+ for (i = AAFS_PROF_SIZEOF - 1; i >= 0; --i) {
+ struct aa_replacedby *r;
+ if (!profile->dents[i])
+ continue;
+
+ r = profile->dents[i]->d_inode->i_private;
+ securityfs_remove(profile->dents[i]);
+ aa_put_replacedby(r);
+ profile->dents[i] = NULL;
+ }
+}
+
+void __aa_fs_profile_migrate_dents(struct aa_profile *old,
+ struct aa_profile *new)
+{
+ int i;
+
+ for (i = 0; i < AAFS_PROF_SIZEOF; i++) {
+ new->dents[i] = old->dents[i];
+ old->dents[i] = NULL;
+ }
+}
+
+static struct dentry *create_profile_file(struct dentry *dir, const char *name,
+ struct aa_profile *profile,
+ const struct file_operations *fops)
+{
+ struct aa_replacedby *r = aa_get_replacedby(profile->replacedby);
+ struct dentry *dent;
+
+ dent = securityfs_create_file(name, S_IFREG | 0444, dir, r, fops);
+ if (IS_ERR(dent))
+ aa_put_replacedby(r);
+
+ return dent;
+}
+/* requires lock be held */
+int __aa_fs_profile_mkdir(struct aa_profile *profile, struct dentry *parent)
+{
+ struct aa_profile *child;
+ struct dentry *dent = NULL, *dir;
+ int error;
+
+ if (!parent) {
+ dent = prof_dir(profile->parent);
+ /* adding to parent that previously didn't have children */
+ dent = securityfs_create_dir("profiles", dent);
+ if (IS_ERR(dent))
+ goto fail;
+ prof_child_dir(profile->parent) = parent = dent;
+ }
+
+ if (!profile->dirname) {
+ int len, id_len;
+ len = mangle_name(profile->base.name, NULL);
+ id_len = snprintf(NULL, 0, ".%ld", profile->ns->uniq_id);
+
+ profile->dirname = kmalloc(len + id_len + 1, GFP_KERNEL);
+ if (!profile->dirname)
+ goto fail;
+
+ mangle_name(profile->base.name, profile->dirname);
+ sprintf(profile->dirname + len, ".%ld", profile->ns->uniq_id++);
+ }
+
+ dent = securityfs_create_dir(profile->dirname, parent);
+ if (IS_ERR(dent))
+ goto fail;
+ prof_dir(profile) = dir = dent;
+
+ dent = create_profile_file(dir, "name", profile, &aa_fs_profname_fops);
+ if (IS_ERR(dent))
+ goto fail;
+ profile->dents[AAFS_PROF_NAME] = dent;
+
+ dent = create_profile_file(dir, "mode", profile, &aa_fs_profmode_fops);
+ if (IS_ERR(dent))
+ goto fail;
+ profile->dents[AAFS_PROF_MODE] = dent;
+
+ list_for_each_entry(child, &profile->base.profiles, base.list) {
+ error = __aa_fs_profile_mkdir(child, prof_child_dir(profile));
+ if (error)
+ goto fail2;
+ }
+
+ return 0;
+
+fail:
+ error = PTR_ERR(dent);
+
+fail2:
+ __aa_fs_profile_rmdir(profile);
+
+ return error;
+}
+
+void __aa_fs_namespace_rmdir(struct aa_namespace *ns)
+{
+ struct aa_namespace *sub;
+ struct aa_profile *child;
+ int i;
+
+ if (!ns)
+ return;
+
+ list_for_each_entry(child, &ns->base.profiles, base.list)
+ __aa_fs_profile_rmdir(child);
+
+ list_for_each_entry(sub, &ns->sub_ns, base.list)
+ __aa_fs_namespace_rmdir(sub);
+
+ for (i = AAFS_NS_SIZEOF - 1; i >= 0 ; --i) {
+ securityfs_remove(ns->dents[i]);
+ ns->dents[i] = NULL;
+ }
+}
+
+int __aa_fs_namespace_mkdir(struct aa_namespace *ns, struct dentry *parent,
+ const char *name)
+{
+ struct aa_namespace *sub;
+ struct aa_profile *child;
+ struct dentry *dent, *dir;
+ int error;
+
+ if (!name)
+ name = ns->base.name;
+
+ dent = securityfs_create_dir(name, parent);
+ if (IS_ERR(dent))
+ goto fail;
+ ns_dir(ns) = dir = dent;
+
+ dent = securityfs_create_dir("profiles", dir);
+ if (IS_ERR(dent))
+ goto fail;
+ ns_subprofs_dir(ns) = dent;
+
+ dent = securityfs_create_dir("namespaces", dir);
+ if (IS_ERR(dent))
+ goto fail;
+ ns_subns_dir(ns) = dent;
+
+ list_for_each_entry(child, &ns->base.profiles, base.list) {
+ error = __aa_fs_profile_mkdir(child, ns_subprofs_dir(ns));
+ if (error)
+ goto fail2;
+ }
+
+ list_for_each_entry(sub, &ns->sub_ns, base.list) {
+ error = __aa_fs_namespace_mkdir(sub, ns_subns_dir(ns), NULL);
+ if (error)
+ goto fail2;
+ }
+
+ return 0;
+
+fail:
+ error = PTR_ERR(dent);
+
+fail2:
+ __aa_fs_namespace_rmdir(ns);
+
+ return error;
+}
+
+
+/** Base file system setup **/
static struct aa_fs_entry aa_fs_entry_file[] = {
AA_FS_FILE_STRING("mask", "create read write exec append mmap_exec " \
"link lock"),
@@ -221,8 +509,10 @@ static struct aa_fs_entry aa_fs_entry_apparmor[] = {
{ }
};
-static struct aa_fs_entry aa_fs_entry =
- AA_FS_DIR("apparmor", aa_fs_entry_apparmor);
+static struct aa_fs_entry aa_fs_entry[] = {
+ AA_FS_DIR("apparmor", aa_fs_entry_apparmor),
+ { }
+};
/**
* aafs_create_file - create a file entry in the apparmor securityfs
@@ -247,6 +537,7 @@ static int __init aafs_create_file(struct aa_fs_entry *fs_file,
return error;
}
+static void __init aafs_remove_dir(struct aa_fs_entry *fs_dir);
/**
* aafs_create_dir - recursively create a directory entry in the securityfs
* @fs_dir: aa_fs_entry (and all child entries) to build (NOT NULL)
@@ -257,17 +548,16 @@ static int __init aafs_create_file(struct aa_fs_entry *fs_file,
static int __init aafs_create_dir(struct aa_fs_entry *fs_dir,
struct dentry *parent)
{
- int error;
struct aa_fs_entry *fs_file;
+ struct dentry *dir;
+ int error;
- fs_dir->dentry = securityfs_create_dir(fs_dir->name, parent);
- if (IS_ERR(fs_dir->dentry)) {
- error = PTR_ERR(fs_dir->dentry);
- fs_dir->dentry = NULL;
- goto failed;
- }
+ dir = securityfs_create_dir(fs_dir->name, parent);
+ if (IS_ERR(dir))
+ return PTR_ERR(dir);
+ fs_dir->dentry = dir;
- for (fs_file = fs_dir->v.files; fs_file->name; ++fs_file) {
+ for (fs_file = fs_dir->v.files; fs_file && fs_file->name; ++fs_file) {
if (fs_file->v_type == AA_FS_TYPE_DIR)
error = aafs_create_dir(fs_file, fs_dir->dentry);
else
@@ -279,6 +569,8 @@ static int __init aafs_create_dir(struct aa_fs_entry *fs_dir,
return 0;
failed:
+ aafs_remove_dir(fs_dir);
+
return error;
}
@@ -303,7 +595,7 @@ static void __init aafs_remove_dir(struct aa_fs_entry *fs_dir)
{
struct aa_fs_entry *fs_file;
- for (fs_file = fs_dir->v.files; fs_file->name; ++fs_file) {
+ for (fs_file = fs_dir->v.files; fs_file && fs_file->name; ++fs_file) {
if (fs_file->v_type == AA_FS_TYPE_DIR)
aafs_remove_dir(fs_file);
else
@@ -320,7 +612,7 @@ static void __init aafs_remove_dir(struct aa_fs_entry *fs_dir)
*/
void __init aa_destroy_aafs(void)
{
- aafs_remove_dir(&aa_fs_entry);
+ aafs_remove_dir(aa_fs_entry);
}
/**
@@ -337,13 +629,18 @@ static int __init aa_create_aafs(void)
if (!apparmor_initialized)
return 0;
- if (aa_fs_entry.dentry) {
+ if (aa_fs_entry[0].dentry) {
AA_ERROR("%s: AppArmor securityfs already exists\n", __func__);
return -EEXIST;
}
/* Populate fs tree. */
- error = aafs_create_dir(&aa_fs_entry, NULL);
+ error = aafs_create_dir(aa_fs_entry, NULL);
+ if (error)
+ goto error;
+
+ error = __aa_fs_namespace_mkdir(root_ns, aa_fs_entry[0].dentry,
+ "policy");
if (error)
goto error;
diff --git a/security/apparmor/audit.c b/security/apparmor/audit.c
index 031d2d9..3f221c7 100644
--- a/security/apparmor/audit.c
+++ b/security/apparmor/audit.c
@@ -73,6 +73,12 @@ const char *const op_table[] = {
"profile_remove"
};
+const char *const aa_profile_mode_names[] = {
+ "enforce",
+ "complain",
+ "kill"
+};
+
const char *const audit_mode_names[] = {
"normal",
"quiet_denied",
diff --git a/security/apparmor/include/apparmorfs.h b/security/apparmor/include/apparmorfs.h
index 7ea4769..2494e11 100644
--- a/security/apparmor/include/apparmorfs.h
+++ b/security/apparmor/include/apparmorfs.h
@@ -61,4 +61,42 @@ extern const struct file_operations aa_fs_seq_file_ops;
extern void __init aa_destroy_aafs(void);
+struct aa_profile;
+struct aa_namespace;
+
+enum aafs_ns_type {
+ AAFS_NS_DIR,
+ AAFS_NS_PROFS,
+ AAFS_NS_NS,
+ AAFS_NS_COUNT,
+ AAFS_NS_MAX_COUNT,
+ AAFS_NS_SIZE,
+ AAFS_NS_MAX_SIZE,
+ AAFS_NS_OWNER,
+ AAFS_NS_SIZEOF,
+};
+
+enum aafs_prof_type {
+ AAFS_PROF_DIR,
+ AAFS_PROF_PROFS,
+ AAFS_PROF_NAME,
+ AAFS_PROF_MODE,
+ AAFS_PROF_SIZEOF,
+};
+
+#define ns_dir(X) ((X)->dents[AAFS_NS_DIR])
+#define ns_subns_dir(X) ((X)->dents[AAFS_NS_NS])
+#define ns_subprofs_dir(X) ((X)->dents[AAFS_NS_PROFS])
+
+#define prof_dir(X) ((X)->dents[AAFS_PROF_DIR])
+#define prof_child_dir(X) ((X)->dents[AAFS_PROF_PROFS])
+
+void __aa_fs_profile_rmdir(struct aa_profile *profile);
+void __aa_fs_profile_migrate_dents(struct aa_profile *old,
+ struct aa_profile *new);
+int __aa_fs_profile_mkdir(struct aa_profile *profile, struct dentry *parent);
+void __aa_fs_namespace_rmdir(struct aa_namespace *ns);
+int __aa_fs_namespace_mkdir(struct aa_namespace *ns, struct dentry *parent,
+ const char *name);
+
#endif /* __AA_APPARMORFS_H */
diff --git a/security/apparmor/include/audit.h b/security/apparmor/include/audit.h
index 69d8cae..6539ab3 100644
--- a/security/apparmor/include/audit.h
+++ b/security/apparmor/include/audit.h
@@ -28,6 +28,8 @@ struct aa_profile;
extern const char *const audit_mode_names[];
#define AUDIT_MAX_INDEX 5
+extern const char *const aa_profile_mode_names[];
+
enum audit_mode {
AUDIT_NORMAL, /* follow normal auditing of accesses */
AUDIT_QUIET_DENIED, /* quiet all denied access messages */
diff --git a/security/apparmor/include/policy.h b/security/apparmor/include/policy.h
index dd53798..0e07045 100644
--- a/security/apparmor/include/policy.h
+++ b/security/apparmor/include/policy.h
@@ -111,6 +111,8 @@ struct aa_ns_acct {
* @unconfined: special unconfined profile for the namespace
* @sub_ns: list of namespaces under the current namespace.
* @uniq_null: uniq value used for null learning profiles
+ * @uniq_id: a unique id count for the profiles in the namespace
+ * @dents: dentries for the namespaces file entries in apparmorfs
*
* An aa_namespace defines the set profiles that are searched to determine
* which profile to attach to a task. Profiles can not be shared between
@@ -134,6 +136,9 @@ struct aa_namespace {
struct aa_profile *unconfined;
struct list_head sub_ns;
atomic_t uniq_null;
+ long uniq_id;
+
+ struct dentry *dents[AAFS_NS_SIZEOF];
};
/* struct aa_policydb - match engine for a policy
@@ -172,6 +177,9 @@ struct aa_replacedby {
* @caps: capabilities for the profile
* @rlimits: rlimits for the profile
*
+ * @dents: dentries for the profiles file entries in apparmorfs
+ * @dirname: name of the profile dir in apparmorfs
+ *
* The AppArmor profile contains the basic confinement data. Each profile
* has a name, and exists in a namespace. The @name and @exec_match are
* used to determine profile attachment against unconfined tasks. All other
@@ -207,6 +215,9 @@ struct aa_profile {
struct aa_file_rules file;
struct aa_caps caps;
struct aa_rlimit rlimits;
+
+ char *dirname;
+ struct dentry *dents[AAFS_PROF_SIZEOF];
};
extern struct aa_namespace *root_ns;
diff --git a/security/apparmor/policy.c b/security/apparmor/policy.c
index 4949595..9c78706 100644
--- a/security/apparmor/policy.c
+++ b/security/apparmor/policy.c
@@ -405,7 +405,13 @@ static struct aa_namespace *aa_prepare_namespace(const char *name)
ns = alloc_namespace(root->base.hname, name);
if (!ns)
goto out;
- /* add parent ref */
+ if (__aa_fs_namespace_mkdir(ns, ns_subns_dir(root), name)) {
+ AA_ERROR("Failed to create interface for ns %s\n",
+ ns->base.name);
+ free_namespace(ns);
+ ns = NULL;
+ goto out;
+ }
ns->parent = aa_get_namespace(root);
list_add_rcu(&ns->base.list, &root->sub_ns);
/* add list ref */
@@ -486,6 +492,7 @@ static void __replace_profile(struct aa_profile *old, struct aa_profile *new)
}
__aa_update_replacedby(old, new);
list_replace_rcu(&old->base.list, &new->base.list);
+ __aa_fs_profile_migrate_dents(old, new);
aa_get_profile(new);
aa_put_profile(old);
}
@@ -505,6 +512,7 @@ static void __remove_profile(struct aa_profile *profile)
/* released by free_profile */
__aa_update_replacedby(profile, profile->ns->unconfined);
__list_remove_profile(profile);
+ __aa_fs_profile_rmdir(profile);
}
/**
@@ -540,6 +548,7 @@ static void destroy_namespace(struct aa_namespace *ns)
if (ns->parent)
__aa_update_replacedby(ns->unconfined, ns->parent->unconfined);
+ __aa_fs_namespace_rmdir(ns);
mutex_unlock(&ns->lock);
}
@@ -661,6 +670,7 @@ static void free_profile(struct aa_profile *profile)
aa_free_cap_rules(&profile->caps);
aa_free_rlimit_rules(&profile->rlimits);
+ kzfree(profile->dirname);
aa_put_dfa(profile->xmatch);
aa_put_dfa(profile->policy.dfa);
aa_put_replacedby(profile->replacedby);
@@ -1016,11 +1026,11 @@ bool aa_may_manage_policy(int op)
* @ns - namespace the lookup occurs in
* @new - profile to lookup who it is replacing
* @noreplace - true if not replacing an existing profile
- * @old - Returns: pointer to profile to replace (NO REFCOUNT)
- * @rename - Returns: pointer to profile to rename (NO REFCOUNT)
+ * @old - Returns: pointer to profile to replace (ref counted)
+ * @rename - Returns: pointer to profile to rename (ref counted)
* @info - Returns: info string on why lookup failed
*
- * Returns: policy (no ref) profile is in on success else ptr error
+ * Returns: policy (no ref count) profile is in on success else ptr error
*/
static struct aa_policy *__lookup_replace(struct aa_namespace *ns,
struct aa_profile *new,
@@ -1111,16 +1121,61 @@ ssize_t aa_replace_profiles(void *udata, size_t size, bool noreplace)
policy = __lookup_replace(ns, new, noreplace, NULL, NULL,
&info);
if (IS_ERR(policy)) {
- mutex_unlock(&ns->lock);
error = PTR_ERR(policy);
name = new->base.hname;
- goto fail;
+ goto fail_lock;
}
/* released when @new is freed */
new->ns = aa_get_namespace(ns);
if (policy != &ns->base)
/* released on profile replacement or free_profile */
new->parent = aa_get_profile((struct aa_profile *) policy);
+ /* TODO: check against list for parent if child lookup fails
+ for atomic load
+ */
+ /* TODO: check renaming replace for child conflicts if old
+ * exists
+ */
+
+ }
+
+ /* create new fs entries for introspection if needed */
+ list_for_each_entry(new, &lh, base.list) {
+ struct aa_profile *old, *rename;
+ policy = __lookup_replace(ns, new, noreplace, &old, &rename,
+ &info);
+ if (old) {
+ aa_put_replacedby(new->replacedby);
+ new->replacedby = aa_get_replacedby(old->replacedby);
+
+ if (rename) {
+ // ????? mkfiles for children
+ // replacedby not updated here
+ }
+ } else if (rename) {
+ aa_put_replacedby(new->replacedby);
+ new->replacedby = aa_get_replacedby(rename->replacedby);
+
+ // ????
+ }
+ if (!old) {
+ struct dentry *parent;
+ if (new->parent)
+ parent = prof_child_dir(new->parent);
+ else
+ parent = ns_subprofs_dir(new->ns);
+ error = __aa_fs_profile_mkdir(new, parent);
+ /* TODO: rename allocation
+ * what of repointing the files to the correct profile
+ */
+ }
+ aa_put_profile(old);
+ aa_put_profile(rename);
+
+ if (error) {
+ info = "failed to create ";
+ goto fail_lock;
+ }
}
/* do actual replacement */
@@ -1137,22 +1192,29 @@ ssize_t aa_replace_profiles(void *udata, size_t size, bool noreplace)
if (old) {
__replace_profile(old, new);
- aa_put_replacedby(new->replacedby);
- new->replacedby = aa_get_replacedby(old->replacedby);
-
- if (rename)
+ if (rename) {
+ /* TODO:
__replace_profile(rename, new);
+ */
+ }
} else if (rename) {
__replace_profile(rename, new);
- aa_put_replacedby(new->replacedby);
- new->replacedby = aa_get_replacedby(rename->replacedby);
- } else
+ /* TODO: move children to new */
+ __aa_fs_profile_rmdir(rename);
+ } else {
+ /*
+ * set replacedby backref to new only when, it
+ * has a file in the interface fs. The backref will
+ * be removed by replacement/removal
+ */
+ new->replacedby->profile = aa_get_profile(new);
__list_add_profile(&policy->profiles, new);
-
+ }
aa_put_profile(rename);
aa_put_profile(old);
aa_put_profile(new);
}
+
mutex_unlock(&ns->lock);
out:
@@ -1162,6 +1224,9 @@ out:
return error;
return size;
+fail_lock:
+ mutex_unlock(&ns->lock);
+
fail:
error = audit_policy(op, GFP_KERNEL, name, info, error);
--
1.7.10.4
More information about the AppArmor
mailing list