Vulnerability: Race condition in kernel/events/core.c in the Linux kernel before 4.9.7 allows local users to gain privileges via a crafted application that makes concurrent perf_event_open system calls for moving a software group into a hardware context.  NOTE: this vulnerability exists because of an incomplete fix for CVE-2016-6786.
Vulnerability ID: CVE-2017-6001
CWE: CWE-362
Severity score: 10
Project: https://github.com/torvalds/linux
Published: 2017-02-18
File: kernel/events/core.c
Commit message: perf/core: Fix concurrent sys_perf_event_open() vs. 'move_group' race

Di Shen reported a race between two concurrent sys_perf_event_open()
calls where both try and move the same pre-existing software group
into a hardware context.

The problem is exactly that described in commit:

  f63a8daa5812 ("perf: Fix event->ctx locking")

... where, while we wait for a ctx->mutex acquisition, the event->ctx
relation can have changed under us.

That very same commit failed to recognise sys_perf_event_context() as an
external access vector to the events and thereby didn't apply the
established locking rules correctly.

So while one sys_perf_event_open() call is stuck waiting on
mutex_lock_double(), the other (which owns said locks) moves the group
about. So by the time the former sys_perf_event_open() acquires the
locks, the context we've acquired is stale (and possibly dead).

Apply the established locking rules as per perf_event_ctx_lock_nested()
to the mutex_lock_double() for the 'move_group' case. This obviously means
we need to validate state after we acquire the locks.

Reported-by: Di Shen (Keen Lab)
Tested-by: John Dias <joaodias@google.com>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Cc: Alexander Shishkin <alexander.shishkin@linux.intel.com>
Cc: Arnaldo Carvalho de Melo <acme@kernel.org>
Cc: Arnaldo Carvalho de Melo <acme@redhat.com>
Cc: Jiri Olsa <jolsa@redhat.com>
Cc: Kees Cook <keescook@chromium.org>
Cc: Linus Torvalds <torvalds@linux-foundation.org>
Cc: Min Chong <mchong@google.com>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Stephane Eranian <eranian@google.com>
Cc: Thomas Gleixner <tglx@linutronix.de>
Cc: Vince Weaver <vincent.weaver@maine.edu>
Fixes: f63a8daa5812 ("perf: Fix event->ctx locking")
Link: http://lkml.kernel.org/r/20170106131444.GZ3174@twins.programming.kicks-ass.net
Signed-off-by: Ingo Molnar <mingo@kernel.org>
Commit author: Peter Zijlstra

Vulnerable version (before fix):
 static int perf_event_set_clock(struct perf_event *event, clockid_t clk_id) 	return 0; }  /**  * sys_perf_event_open - open a performance event, associate it to a task/cpu  * SYSCALL_DEFINE5(perf_event_open, 	}  	if (move_group) { 		gctx = group_leader->ctx; 		mutex_lock_double(&gctx->mutex, &ctx->mutex); 		if (gctx->task == TASK_TOMBSTONE) { 			err = -ESRCH; 			goto err_locked; 		} 	} else { 		mutex_lock(&ctx->mutex); 	} SYSCALL_DEFINE5(perf_event_open, 	perf_unpin_context(ctx);  	if (move_group) 		mutex_unlock(&gctx->mutex); 	mutex_unlock(&ctx->mutex);  	if (task) { SYSCALL_DEFINE5(perf_event_open,  err_locked: 	if (move_group) 		mutex_unlock(&gctx->mutex); 	mutex_unlock(&ctx->mutex); /* err_file: */ 	fput(event_file);

Fixed version (after fix):
 static int perf_event_set_clock(struct perf_event *event, clockid_t clk_id) 	return 0; }  /*  * Variation on perf_event_ctx_lock_nested(), except we take two context  * mutexes.  */ static struct perf_event_context * __perf_event_ctx_lock_double(struct perf_event *group_leader, 			     struct perf_event_context *ctx) { 	struct perf_event_context *gctx;  again: 	rcu_read_lock(); 	gctx = READ_ONCE(group_leader->ctx); 	if (!atomic_inc_not_zero(&gctx->refcount)) { 		rcu_read_unlock(); 		goto again; 	} 	rcu_read_unlock();  	mutex_lock_double(&gctx->mutex, &ctx->mutex);  	if (group_leader->ctx != gctx) { 		mutex_unlock(&ctx->mutex); 		mutex_unlock(&gctx->mutex); 		put_ctx(gctx); 		goto again; 	}  	return gctx; }  /**  * sys_perf_event_open - open a performance event, associate it to a task/cpu  * SYSCALL_DEFINE5(perf_event_open, 	}  	if (move_group) { 		gctx = __perf_event_ctx_lock_double(group_leader, ctx);  		if (gctx->task == TASK_TOMBSTONE) { 			err = -ESRCH; 			goto err_locked; 		}  		/* 		 * Check if we raced against another sys_perf_event_open() call 		 * moving the software group underneath us. 		 */ 		if (!(group_leader->group_caps & PERF_EV_CAP_SOFTWARE)) { 			/* 			 * If someone moved the group out from under us, check 			 * if this new event wound up on the same ctx, if so 			 * its the regular !move_group case, otherwise fail. 			 */ 			if (gctx != ctx) { 				err = -EINVAL; 				goto err_locked; 			} else { 				perf_event_ctx_unlock(group_leader, gctx); 				move_group = 0; 			} 		} 	} else { 		mutex_lock(&ctx->mutex); 	} SYSCALL_DEFINE5(perf_event_open, 	perf_unpin_context(ctx);  	if (move_group) 		perf_event_ctx_unlock(group_leader, gctx); 	mutex_unlock(&ctx->mutex);  	if (task) { SYSCALL_DEFINE5(perf_event_open,  err_locked: 	if (move_group) 		perf_event_ctx_unlock(group_leader, gctx); 	mutex_unlock(&ctx->mutex); /* err_file: */ 	fput(event_file);

Patch:
patch: @@ -9529,6 +9529,37 @@ static int perf_event_set_clock(struct perf_event *event, clockid_t clk_id)
 	return 0;
 }
 
+/*
+ * Variation on perf_event_ctx_lock_nested(), except we take two context
+ * mutexes.
+ */
+static struct perf_event_context *
+__perf_event_ctx_lock_double(struct perf_event *group_leader,
+			     struct perf_event_context *ctx)
+{
+	struct perf_event_context *gctx;
+
+again:
+	rcu_read_lock();
+	gctx = READ_ONCE(group_leader->ctx);
+	if (!atomic_inc_not_zero(&gctx->refcount)) {
+		rcu_read_unlock();
+		goto again;
+	}
+	rcu_read_unlock();
+
+	mutex_lock_double(&gctx->mutex, &ctx->mutex);
+
+	if (group_leader->ctx != gctx) {
+		mutex_unlock(&ctx->mutex);
+		mutex_unlock(&gctx->mutex);
+		put_ctx(gctx);
+		goto again;
+	}
+
+	return gctx;
+}
+
 /**
  * sys_perf_event_open - open a performance event, associate it to a task/cpu
  *
@@ -9772,12 +9803,31 @@ SYSCALL_DEFINE5(perf_event_open,
 	}
 
 	if (move_group) {
-		gctx = group_leader->ctx;
-		mutex_lock_double(&gctx->mutex, &ctx->mutex);
+		gctx = __perf_event_ctx_lock_double(group_leader, ctx);
+
 		if (gctx->task == TASK_TOMBSTONE) {
 			err = -ESRCH;
 			goto err_locked;
 		}
+
+		/*
+		 * Check if we raced against another sys_perf_event_open() call
+		 * moving the software group underneath us.
+		 */
+		if (!(group_leader->group_caps & PERF_EV_CAP_SOFTWARE)) {
+			/*
+			 * If someone moved the group out from under us, check
+			 * if this new event wound up on the same ctx, if so
+			 * its the regular !move_group case, otherwise fail.
+			 */
+			if (gctx != ctx) {
+				err = -EINVAL;
+				goto err_locked;
+			} else {
+				perf_event_ctx_unlock(group_leader, gctx);
+				move_group = 0;
+			}
+		}
 	} else {
 		mutex_lock(&ctx->mutex);
 	}
@@ -9879,7 +9929,7 @@ SYSCALL_DEFINE5(perf_event_open,
 	perf_unpin_context(ctx);
 
 	if (move_group)
-		mutex_unlock(&gctx->mutex);
+		perf_event_ctx_unlock(group_leader, gctx);
 	mutex_unlock(&ctx->mutex);
 
 	if (task) {
@@ -9905,7 +9955,7 @@ SYSCALL_DEFINE5(perf_event_open,
 
 err_locked:
 	if (move_group)
-		mutex_unlock(&gctx->mutex);
+		perf_event_ctx_unlock(group_leader, gctx);
 	mutex_unlock(&ctx->mutex);
 /* err_file: */
 	fput(event_file);
path: kernel/events/core.c