Smack: limited capability for changing process label

This feature introduces new kernel interface:

- <smack_fs>/relabel-self - for setting transition labels list

This list is used to control smack label transition mechanism.
List is set by, and per process. Process can transit to new label only if
label is on the list. Only process with CAP_MAC_ADMIN capability can add
labels to this list. With this list, process can change it's label without
CAP_MAC_ADMIN but only once. After label changing, list is unset.

Changes in v2:
* use list_for_each_entry instead of _rcu during label write
* added missing description in security/Smack.txt

Changes in v3:
* squashed into one commit

Changes in v4:
* switch from global list to per-task list
* since the per-task list is accessed only by the task itself
  there is no need to use synchronization mechanisms on it

Changes in v5:
* change smackfs interface of relabel-self to the one used for onlycap
  multiple labels are accepted, separated by space, which
  replace the previous list upon write

Signed-off-by: Zbigniew Jasinski <z.jasinski@samsung.com>
Signed-off-by: Rafal Krypa <r.krypa@samsung.com>
Acked-by: Casey Schaufler <casey@schaufler-ca.com>
diff --git a/security/smack/smack_lsm.c b/security/smack/smack_lsm.c
index c2d66ca..ff81026 100644
--- a/security/smack/smack_lsm.c
+++ b/security/smack/smack_lsm.c
@@ -326,6 +326,7 @@
 	tsp->smk_task = task;
 	tsp->smk_forked = forked;
 	INIT_LIST_HEAD(&tsp->smk_rules);
+	INIT_LIST_HEAD(&tsp->smk_relabel);
 	mutex_init(&tsp->smk_rules_lock);
 
 	return tsp;
@@ -361,6 +362,35 @@
 }
 
 /**
+ * smk_copy_relabel - copy smk_relabel labels list
+ * @nhead: new rules header pointer
+ * @ohead: old rules header pointer
+ * @gfp: type of the memory for the allocation
+ *
+ * Returns 0 on success, -ENOMEM on error
+ */
+static int smk_copy_relabel(struct list_head *nhead, struct list_head *ohead,
+				gfp_t gfp)
+{
+	struct smack_known_list_elem *nklep;
+	struct smack_known_list_elem *oklep;
+
+	INIT_LIST_HEAD(nhead);
+
+	list_for_each_entry(oklep, ohead, list) {
+		nklep = kzalloc(sizeof(struct smack_known_list_elem), gfp);
+		if (nklep == NULL) {
+			smk_destroy_label_list(nhead);
+			return -ENOMEM;
+		}
+		nklep->smk_label = oklep->smk_label;
+		list_add(&nklep->list, nhead);
+	}
+
+	return 0;
+}
+
+/**
  * smk_ptrace_mode - helper function for converting PTRACE_MODE_* into MAY_*
  * @mode - input mode in form of PTRACE_MODE_*
  *
@@ -1922,6 +1952,8 @@
 		return;
 	cred->security = NULL;
 
+	smk_destroy_label_list(&tsp->smk_relabel);
+
 	list_for_each_safe(l, n, &tsp->smk_rules) {
 		rp = list_entry(l, struct smack_rule, list);
 		list_del(&rp->list);
@@ -1953,6 +1985,11 @@
 	if (rc != 0)
 		return rc;
 
+	rc = smk_copy_relabel(&new_tsp->smk_relabel, &old_tsp->smk_relabel,
+				gfp);
+	if (rc != 0)
+		return rc;
+
 	new->security = new_tsp;
 	return 0;
 }
@@ -3552,9 +3589,11 @@
 static int smack_setprocattr(struct task_struct *p, char *name,
 			     void *value, size_t size)
 {
-	struct task_smack *tsp;
+	struct task_smack *tsp = current_security();
 	struct cred *new;
 	struct smack_known *skp;
+	struct smack_known_list_elem *sklep;
+	int rc;
 
 	/*
 	 * Changing another process' Smack value is too dangerous
@@ -3563,7 +3602,7 @@
 	if (p != current)
 		return -EPERM;
 
-	if (!smack_privileged(CAP_MAC_ADMIN))
+	if (!smack_privileged(CAP_MAC_ADMIN) && list_empty(&tsp->smk_relabel))
 		return -EPERM;
 
 	if (value == NULL || size == 0 || size >= SMK_LONGLABEL)
@@ -3582,12 +3621,27 @@
 	if (skp == &smack_known_web)
 		return -EPERM;
 
+	if (!smack_privileged(CAP_MAC_ADMIN)) {
+		rc = -EPERM;
+		list_for_each_entry(sklep, &tsp->smk_relabel, list)
+			if (sklep->smk_label == skp) {
+				rc = 0;
+				break;
+			}
+		if (rc)
+			return rc;
+	}
+
 	new = prepare_creds();
 	if (new == NULL)
 		return -ENOMEM;
 
 	tsp = new->security;
 	tsp->smk_task = skp;
+	/*
+	 * process can change its label only once
+	 */
+	smk_destroy_label_list(&tsp->smk_relabel);
 
 	commit_creds(new);
 	return size;