[PATCH 10/12] AppArmor: domain functions for domain transition

From: John Johansen
Date: Tue Nov 10 2009 - 11:14:47 EST


AppArmor routines for controling domain transitions, which can occur at
exec or through self directed change_profile/change_hat calls.

Unconfined tasks are checked at exec against the profiles in the confining
profile namespace to determine if a profile should be attached to the task.

Confined tasks execs are controlled by the profile which provides rules
determining which execs are allowed and if so which profiles should be
transitioned to.

Self directed domain transitions allow a task to request transition
to a given profile. If the transition is allowed then the profile will
be applied, either immeditately or at exec time depending on the request.
Immeditate self directed transitions have several security limitations
but have uses in setting up stub transition profiles and other limited
cases.

Signed-off-by: John Johansen <john.johansen@xxxxxxxxxxxxx>
---
security/apparmor/domain.c | 691 ++++++++++++++++++++++++++++++++++++
security/apparmor/include/domain.h | 36 ++
2 files changed, 727 insertions(+), 0 deletions(-)
create mode 100644 security/apparmor/domain.c
create mode 100644 security/apparmor/include/domain.h

diff --git a/security/apparmor/domain.c b/security/apparmor/domain.c
new file mode 100644
index 0000000..d58231a
--- /dev/null
+++ b/security/apparmor/domain.c
@@ -0,0 +1,691 @@
+/*
+ * AppArmor security module
+ *
+ * This file contains AppArmor policy attachment and domain transitions
+ *
+ * Copyright (C) 2002-2008 Novell/SUSE
+ * Copyright 2009 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, version 2 of the
+ * License.
+ */
+
+#include <linux/errno.h>
+#include <linux/fdtable.h>
+#include <linux/file.h>
+#include <linux/mount.h>
+#include <linux/syscalls.h>
+#include <linux/tracehook.h>
+#include <linux/personality.h>
+
+#include "include/audit.h"
+#include "include/apparmorfs.h"
+#include "include/context.h"
+#include "include/domain.h"
+#include "include/file.h"
+#include "include/ipc.h"
+#include "include/match.h"
+#include "include/path.h"
+#include "include/policy.h"
+
+/**
+ * aa_free_domain_entries - free entries in a domain table
+ * @domain: the domain table to free
+ */
+void aa_free_domain_entries(struct aa_domain *domain)
+{
+ int i;
+
+ if (!domain->table)
+ return;
+
+ for (i = 0; i < domain->size; i++)
+ kfree(domain->table[i]);
+ kfree(domain->table);
+}
+
+/*
+ * check if the task is ptraced and if so if the tracing task is allowed
+ * to trace the new domain
+ */
+static int aa_may_change_ptraced_domain(struct task_struct *task,
+ struct aa_profile *to_profile)
+{
+ struct task_struct *tracer;
+ struct cred *cred = NULL;
+ struct aa_profile *tracerp = NULL;
+ int error = 0;
+
+ rcu_read_lock();
+ tracer = tracehook_tracer_task(task);
+ if (tracer)
+ /* released below */
+ cred = aa_get_task_policy(tracer, &tracerp);
+ rcu_read_unlock();
+
+ /* not ptraced */
+ if (!tracer)
+ return 0;
+
+ if (!tracerp)
+ goto out;
+
+ error = aa_may_ptrace(tracer, tracerp, to_profile, PTRACE_MODE_ATTACH);
+
+out:
+ put_cred(cred);
+
+ return error;
+}
+
+/**
+ * change_profile_perms - find permissions for change_profile
+ * @profile: the current profile
+ * @ns: the namespace being switched to
+ * @name: the name of the profile to change to
+ * @rstate: if !NULL will contain the state the match finished in
+ */
+static struct file_perms change_profile_perms(struct aa_profile *profile,
+ struct aa_namespace *ns,
+ const char *name,
+ unsigned int *rstate)
+{
+ struct file_perms perms;
+ struct path_cond cond = { };
+ unsigned int state;
+
+ if (!profile) {
+ /* unconfined */
+ perms.allowed = AA_MAY_CHANGE_PROFILE;
+ perms.xindex = perms.dindex = 0;
+ perms.audit = perms.quiet = perms.kill = 0;
+ if (rstate)
+ *rstate = 0;
+ return perms;
+ } else if (!profile->file.dfa) {
+ return nullperms;
+ } else if ((ns == profile->ns)) {
+ /* try matching against rules with out namespace prependend */
+ perms = aa_str_perms(profile->file.dfa, DFA_START, name, &cond,
+ rstate);
+ if (COMBINED_PERM_MASK(perms) & AA_MAY_CHANGE_PROFILE)
+ return perms;
+ }
+
+ /* try matching with namespace name and then profile */
+ state = aa_dfa_match(profile->file.dfa, DFA_START, ns->base.name);
+ state = aa_dfa_null_transition(profile->file.dfa, state);
+ return aa_str_perms(profile->file.dfa, state, name, &cond, rstate);
+}
+
+static const char *next_name(int xtype, const char *name)
+{
+ return NULL;
+}
+
+/* __aa_attach_match_ - find an attachment match
+ * @name - to match against
+ * @head - profile list to walk
+ *
+ * Do a linear search on the profiles in the list. There is a matching
+ * preference where an exact match is prefered over a name which uses
+ * expressions to match, and matching expressions with the greatest
+ * xmatch_len are prefered.
+ */
+static struct aa_profile *__aa_attach_match(const char *name,
+ struct list_head *head)
+{
+ int len = 0;
+ struct aa_profile *profile, *candidate = NULL;
+
+ list_for_each_entry(profile, head, base.list) {
+ if (profile->flags & PFLAG_NULL)
+ continue;
+ if (profile->xmatch && profile->xmatch_len > len) {
+ unsigned int state = aa_dfa_match(profile->xmatch,
+ DFA_START, name);
+ u16 perm = dfa_user_allow(profile->xmatch, state);
+ /* any accepting state means a valid match. */
+ if (perm & MAY_EXEC) {
+ candidate = profile;
+ len = profile->xmatch_len;
+ }
+ } else if (!strcmp(profile->base.name, name))
+ /* exact non-re match, no more searching required */
+ return profile;
+ }
+
+ return candidate;
+}
+
+/**
+ * aa_sys_find_attach - do attachment search for sys unconfined processes
+ * @base: the base to search
+ * name: the executable name to match against
+ */
+static struct aa_profile *aa_sys_find_attach(struct aa_policy_common *base,
+ const char *name)
+{
+ struct aa_profile *profile;
+
+ read_lock(&base->lock);
+ profile = aa_get_profile(__aa_attach_match(name, &base->profiles));
+ read_unlock(&base->lock);
+
+ return profile;
+}
+
+/**
+ * x_to_profile - get target profile for a given xindex
+ * @ns: namespace of profile
+ * @profile: current profile
+ * @name: to to lookup if specified
+ * @xindex: index into x transition table
+ *
+ * find profile for a transition index
+ *
+ * Returns: refcounted profile or ERR_PTR
+ */
+static struct aa_profile *x_to_profile(struct aa_namespace *ns,
+ struct aa_profile *profile,
+ const char *name, u16 xindex)
+{
+ struct aa_profile *new_profile = NULL;
+ u16 xtype = xindex & AA_X_TYPE_MASK;
+ int index = xindex & AA_X_INDEX_MASK;
+
+ if (!profile)
+ profile = ns->unconfined;
+
+ switch (xtype) {
+ case AA_X_NONE:
+ /* fail exec unless ix || ux fallback - handled by caller */
+ return ERR_PTR(-EACCES);
+ case AA_X_NAME:
+ if (xindex & AA_X_CHILD)
+ /* released by caller */
+ new_profile = aa_sys_find_attach(&profile->base, name);
+ else
+ /* released by caller */
+ new_profile = aa_sys_find_attach(&ns->base, name);
+
+ goto out;
+ case AA_X_TABLE:
+ if (index > profile->file.trans.size) {
+ AA_ERROR("Invalid named transition\n");
+ return ERR_PTR(-EACCES);
+ }
+ name = profile->file.trans.table[index];
+ break;
+ }
+
+ for (; !new_profile && name; name = next_name(xtype, name)) {
+ struct aa_namespace *new_ns;
+ const char *xname = NULL;
+
+ new_ns = NULL;
+ if (xindex & AA_X_CHILD) {
+ /* release by caller */
+ new_profile = aa_find_child(profile, name);
+ if (new_profile)
+ return new_profile;
+ continue;
+ } else if (*name == ':') {
+ /* switching namespace */
+ const char *ns_name = name + 1;
+ name = xname = ns_name + strlen(ns_name) + 1;
+ if (!*xname)
+ /* no name so use profile name */
+ xname = profile->fqname;
+ if (*ns_name == '@') {
+ /* TODO: variable support */
+ ;
+ }
+ /* released below */
+ new_ns = aa_find_namespace(ns_name);
+ if (!new_ns)
+ continue;
+ } else if (*name == '@') {
+ /* TODO: variable support */
+
+ } else {
+ xname = name;
+ }
+
+ /* released by caller */
+ new_profile = aa_find_profile(new_ns ? new_ns : ns, xname);
+ aa_put_namespace(new_ns);
+ }
+
+out:
+ if (!new_profile)
+ return ERR_PTR(-ENOENT);
+
+ /* released by caller */
+ return new_profile;
+}
+
+/**
+ * apparmor_bprm_set_creds - set the new creds on the bprm struct
+ * @bprm: binprm for the exec
+ */
+int apparmor_bprm_set_creds(struct linux_binprm *bprm)
+{
+ struct aa_task_context *cxt;
+ struct aa_profile *profile, *new_profile = NULL;
+ struct aa_namespace *ns;
+ char *buffer = NULL;
+ unsigned int state = DFA_START;
+ struct path_cond cond = {
+ bprm->file->f_path.dentry->d_inode->i_uid,
+ bprm->file->f_path.dentry->d_inode->i_mode
+ };
+ struct aa_audit_file sa = {
+ .base.operation = "exec",
+ .base.gfp_mask = GFP_KERNEL,
+ .request = MAY_EXEC,
+ .cond = &cond,
+ };
+
+ sa.base.error = cap_bprm_set_creds(bprm);
+ if (sa.base.error)
+ return sa.base.error;
+
+ if (bprm->cred_prepared)
+ return 0;
+
+ cxt = bprm->cred->security;
+ BUG_ON(!cxt);
+
+ profile = aa_confining_profile(cxt->sys.profile);
+ ns = profile->ns;
+
+ /* buffer freed below, name is pointer inside of buffer */
+ sa.base.error = aa_get_name(&bprm->file->f_path, 0, &buffer,
+ (char **)&sa.name);
+ if (sa.base.error) {
+ if (!profile || profile->flags & PFLAG_IX_ON_NAME_ERROR)
+ sa.base.error = 0;
+ sa.base.info = "Exec failed name resolution";
+ sa.name = bprm->filename;
+ goto audit;
+ }
+
+ if (!profile) {
+ /* unconfined task - attach profile if one matches */
+ new_profile = aa_sys_find_attach(&ns->base, sa.name);
+ if (!new_profile)
+ goto cleanup;
+ goto apply;
+ } else if (cxt->sys.onexec) {
+ /*
+ * onexec permissions are stored in a pair, rewalk the
+ * dfa to get start of the exec path match.
+ */
+ sa.perms = change_profile_perms(profile, cxt->sys.onexec->ns,
+ sa.name, &state);
+ state = aa_dfa_null_transition(profile->file.dfa, state);
+ }
+ sa.perms = aa_str_perms(profile->file.dfa, state, sa.name, &cond, NULL);
+ if (cxt->sys.onexec && sa.perms.allowed & AA_MAY_ONEXEC) {
+ new_profile = cxt->sys.onexec;
+ cxt->sys.onexec = NULL;
+ sa.base.info = "change_profile onexec";
+ } else if (sa.perms.allowed & MAY_EXEC) {
+ new_profile = x_to_profile(ns, profile, sa.name,
+ sa.perms.xindex);
+ if (IS_ERR(new_profile)) {
+ if (sa.perms.xindex & AA_X_INHERIT) {
+ /* (p|c|n)ix - don't change profile */
+ sa.base.info = "ix fallback";
+ goto x_clear;
+ } else if (sa.perms.xindex & AA_X_UNCONFINED) {
+ new_profile = aa_get_profile(ns->unconfined);
+ sa.base.info = "ux fallback";
+ } else {
+ sa.base.error = PTR_ERR(new_profile);
+ if (sa.base.error == -ENOENT)
+ sa.base.info = "profile not found";
+ new_profile = NULL;
+ }
+ }
+ } else if (PROFILE_COMPLAIN(profile)) {
+ new_profile = aa_alloc_null_profile(profile, 0);
+ sa.base.error = -EACCES;
+ if (!new_profile)
+ sa.base.error = -ENOMEM;
+ sa.name2 = new_profile->fqname;
+ sa.perms.xindex |= AA_X_UNSAFE;
+ } else {
+ sa.base.error = -EACCES;
+ }
+
+ if (!new_profile)
+ goto audit;
+
+ if (profile == new_profile) {
+ aa_put_profile(new_profile);
+ goto audit;
+ }
+
+ if (bprm->unsafe & LSM_UNSAFE_SHARE) {
+ /* FIXME: currently don't mediate shared state */
+ ;
+ }
+
+ if (bprm->unsafe & (LSM_UNSAFE_PTRACE | LSM_UNSAFE_PTRACE_CAP)) {
+ sa.base.error = aa_may_change_ptraced_domain(current,
+ new_profile);
+ if (sa.base.error)
+ goto audit;
+ }
+
+ /* Determine if secure exec is needed.
+ * Can be at this point for the following reasons:
+ * 1. unconfined switching to confined
+ * 2. confined switching to different confinement
+ * 3. confined switching to unconfined
+ *
+ * Cases 2 and 3 are marked as requiring secure exec
+ * (unless policy specified "unsafe exec")
+ *
+ * bprm->unsafe is used to cache the AA_X_UNSAFE permission
+ * to avoid having to recompute in secureexec
+ */
+ if (!(sa.perms.xindex & AA_X_UNSAFE))
+ bprm->unsafe |= AA_SECURE_X_NEEDED;
+
+apply:
+ sa.name2 = new_profile->fqname;
+ /* When switching namespace ensure its part of audit message */
+ if (new_profile->ns != ns)
+ sa.name3 = new_profile->ns->base.name;
+
+ /* when transitioning profiles clear unsafe personality bits */
+ bprm->per_clear |= PER_CLEAR_ON_SETID;
+
+ aa_put_profile(cxt->sys.profile);
+ /* transfer new profile reference will be released when cxt is freed */
+ cxt->sys.profile = new_profile;
+
+x_clear:
+ aa_put_profile(cxt->sys.previous);
+ aa_put_profile(cxt->sys.onexec);
+ cxt->sys.previous = NULL;
+ cxt->sys.onexec = NULL;
+ cxt->sys.token = 0;
+
+audit:
+ sa.base.error = aa_audit_file(profile, &sa);
+
+cleanup:
+ kfree(buffer);
+
+ return sa.base.error;
+}
+
+int apparmor_bprm_secureexec(struct linux_binprm *bprm)
+{
+ int ret = cap_bprm_secureexec(bprm);
+
+ /* the decision to use secure exec is computed in set_creds
+ * and stored in bprm->unsafe.
+ */
+ if (!ret && (bprm->unsafe & AA_SECURE_X_NEEDED))
+ ret = 1;
+
+ return ret;
+}
+
+void apparmor_bprm_committing_creds(struct linux_binprm *bprm)
+{
+ struct aa_profile *profile;
+ /* ref released below */
+ struct cred *cred = aa_get_task_policy(current, &profile);
+ struct aa_task_context *new_cxt = bprm->cred->security;
+
+ /* bail out if unconfiged or not changing profile */
+ if ((new_cxt->sys.profile == profile) ||
+ (new_cxt->sys.profile->flags & PFLAG_UNCONFINED)) {
+ put_cred(cred);
+ return;
+ }
+ put_cred(cred);
+
+ current->pdeath_signal = 0;
+
+ /* reset soft limits and set hard limits for the new profile */
+ __aa_transition_rlimits(profile, new_cxt->sys.profile);
+}
+
+void apparmor_bprm_committed_creds(struct linux_binprm *bprm)
+{
+ /* TODO: cleanup signals - ipc mediation */
+ return;
+}
+
+/*
+ * Functions for self directed profile change
+ */
+
+static char *new_compound_name(const char *n1, const char *n2)
+{
+ char *name = kmalloc(strlen(n1) + strlen(n2) + 3, GFP_KERNEL);
+ if (name)
+ sprintf(name, "%s//%s", n1, n2);
+ return name;
+}
+
+/**
+ * aa_change_hat - change hat to/from subprofile
+ * @hat_name: hat to change to
+ * @token: magic value to validate the hat change
+ * @permtest: true if this is just a permission test
+ *
+ * Change to new @hat_name, and store the @hat_magic in the current task
+ * context. If the new @hat_name is %NULL and the @token matches that
+ * stored in the current task context and is not 0, return to the top level
+ * profile.
+ * Returns %0 on success, error otherwise.
+ */
+int aa_change_hat(const char *hat_name, u64 token, int permtest)
+{
+ const struct cred *cred;
+ struct aa_task_context *cxt;
+ struct aa_profile *profile, *previous_profile, *hat = NULL;
+ struct aa_audit_file sa = {
+ .base.gfp_mask = GFP_KERNEL,
+ .base.operation = "change_hat",
+ .request = AA_MAY_CHANGEHAT,
+ };
+ char *name = NULL;
+
+ cred = aa_current_policy(&profile);
+ cxt = cred->security;
+ previous_profile = cxt->sys.previous;
+
+ if (!profile) {
+ sa.base.info = "unconfined";
+ sa.base.error = -EPERM;
+ goto audit;
+ }
+
+ if (hat_name) {
+ struct aa_profile *root;
+ root = PROFILE_IS_HAT(profile) ? profile->parent : profile;
+ sa.name2 = profile->ns->base.name;
+
+ /* released below */
+ hat = aa_find_child(root, hat_name);
+ if (!hat) {
+ if (permtest || !PROFILE_COMPLAIN(root))
+ /* probing is an expected unfortunate behavior
+ * of the change_hat api is traditionally quiet
+ */
+ goto out;
+
+ /* freed below */
+ name = new_compound_name(root->fqname, hat_name);
+
+ sa.name = name;
+ sa.base.info = "hat not found";
+ sa.base.error = -ENOENT;
+ /* released below */
+ hat = aa_alloc_null_profile(profile, 1);
+ if (!hat) {
+ sa.base.info = "failed null profile create";
+ sa.base.error = -ENOMEM;
+ goto audit;
+ }
+ } else {
+ sa.name = hat->fqname;
+ if (!PROFILE_IS_HAT(hat)) {
+ sa.base.info = "target not hat";
+ sa.base.error = -EPERM;
+ goto audit;
+ }
+ }
+
+ sa.base.error = aa_may_change_ptraced_domain(current, hat);
+ if (sa.base.error) {
+ sa.base.info = "ptraced";
+ sa.base.error = -EPERM;
+ goto audit;
+ }
+
+ if (!permtest) {
+ sa.base.error = aa_set_current_hat(hat, token);
+ if (sa.base.error == -EACCES)
+ sa.perms.kill = AA_MAY_CHANGEHAT;
+ else if (name && !sa.base.error)
+ /* reset error for learning of new hats */
+ sa.base.error = -ENOENT;
+ }
+ } else if (previous_profile) {
+ sa.name = previous_profile->fqname;
+ sa.base.error = aa_restore_previous_profile(token);
+ sa.perms.kill = AA_MAY_CHANGEHAT;
+ } else
+ /* ignore restores when there is no saved profile */
+ goto out;
+
+audit:
+ if (!permtest)
+ sa.base.error = aa_audit_file(profile, &sa);
+
+out:
+ aa_put_profile(hat);
+ kfree(name);
+
+ return sa.base.error;
+}
+
+/**
+ * aa_change_profile - perform a one-way profile transition
+ * @ns_name: name of the profile namespace to change to
+ * @fqname: name of profile to change to
+ * @onexec: whether this transition is to take place immediately or at exec
+ * @permtest: true if this is just a permission test
+ *
+ * Change to new profile @name. Unlike with hats, there is no way
+ * to change back. If @onexec then the transition is delayed until
+ * the next exec.
+ *
+ * Returns %0 on success, error otherwise.
+ */
+int aa_change_profile(const char *ns_name, const char *fqname, int onexec,
+ int permtest)
+{
+ const struct cred *cred;
+ struct aa_task_context *cxt;
+ struct aa_profile *profile, *target = NULL;
+ struct aa_namespace *ns = NULL;
+ struct aa_audit_file sa = {
+ .request = AA_MAY_CHANGE_PROFILE,
+ .base.gfp_mask = GFP_KERNEL,
+ };
+
+ if (!fqname && !ns_name)
+ return -EINVAL;
+
+ if (onexec)
+ sa.base.operation = "change_onexec";
+ else
+ sa.base.operation = "change_profile";
+
+ cred = aa_current_policy(&profile);
+ cxt = cred->security;
+
+ if (ns_name) {
+ sa.name2 = ns_name;
+ /* released below */
+ ns = aa_find_namespace(ns_name);
+ if (!ns) {
+ /* we don't create new namespace in complain mode */
+ sa.base.info = "namespace not found";
+ sa.base.error = -ENOENT;
+ goto audit;
+ }
+ } else {
+ /* released below */
+ ns = aa_get_namespace(cxt->sys.profile->ns);
+ sa.name2 = ns->base.name;
+ }
+
+ /* if the name was not specified, use the name of the current profile */
+ if (!fqname) {
+ if (!profile)
+ fqname = ns->unconfined->fqname;
+ else
+ fqname = profile->fqname;
+ }
+ sa.name = fqname;
+
+ sa.perms = change_profile_perms(profile, ns, fqname, NULL);
+ if (!(sa.perms.allowed & AA_MAY_CHANGE_PROFILE)) {
+ sa.base.error = -EACCES;
+ goto audit;
+ }
+
+ /* released below */
+ target = aa_find_profile(ns, fqname);
+ if (!target) {
+ sa.base.info = "profile not found";
+ sa.base.error = -ENOENT;
+ if (permtest || !PROFILE_COMPLAIN(profile))
+ goto audit;
+ /* release below */
+ target = aa_alloc_null_profile(profile, 0);
+ if (!target) {
+ sa.base.info = "failed null profile create";
+ sa.base.error = -ENOMEM;
+ goto audit;
+ }
+ }
+
+ /* check if tracing task is allowed to trace target domain */
+ sa.base.error = aa_may_change_ptraced_domain(current, target);
+ if (sa.base.error) {
+ sa.base.info = "ptrace prevents transition";
+ goto audit;
+ }
+
+ if (permtest)
+ goto audit;
+
+ if (onexec)
+ sa.base.error = aa_set_current_onexec(target);
+ else
+ sa.base.error = aa_replace_current_profiles(target);
+
+audit:
+ if (!permtest)
+ sa.base.error = aa_audit_file(profile, &sa);
+
+ aa_put_namespace(ns);
+ aa_put_profile(target);
+
+ return sa.base.error;
+}
diff --git a/security/apparmor/include/domain.h b/security/apparmor/include/domain.h
new file mode 100644
index 0000000..c7fd38f
--- /dev/null
+++ b/security/apparmor/include/domain.h
@@ -0,0 +1,36 @@
+/*
+ * AppArmor security module
+ *
+ * This file contains AppArmor security domain transition function definitions.
+ *
+ * Copyright (C) 1998-2008 Novell/SUSE
+ * Copyright 2009 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, version 2 of the
+ * License.
+ */
+
+#include <linux/binfmts.h>
+#include <linux/types.h>
+
+#ifndef __AA_DOMAIN_H
+#define __AA_DOMAIN_H
+
+struct aa_domain {
+ int size;
+ char **table;
+};
+
+int apparmor_bprm_set_creds(struct linux_binprm *bprm);
+int apparmor_bprm_secureexec(struct linux_binprm *bprm);
+void apparmor_bprm_committing_creds(struct linux_binprm *bprm);
+void apparmor_bprm_committed_creds(struct linux_binprm *bprm);
+
+void aa_free_domain_entries(struct aa_domain *domain);
+int aa_change_hat(const char *hat_name, u64 token, int permtest);
+int aa_change_profile(const char *ns_name, const char *name, int onexec,
+ int permtest);
+
+#endif /* __AA_DOMAIN_H */
--
1.6.3.3

--
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/