Vulnerability: The timer_create syscall implementation in kernel/time/posix-timers.c in the Linux kernel before 4.14.8 doesn't properly validate the sigevent->sigev_notify field, which leads to out-of-bounds access in the show_timer function (called when /proc/$PID/timers is read). This allows userspace applications to read arbitrary kernel memory (on a kernel built with CONFIG_POSIX_TIMERS and CONFIG_CHECKPOINT_RESTORE).
Vulnerability ID: CVE-2017-18344
CWE: CWE-125
Severity score: 2.9
Project: https://github.com/torvalds/linux
Published: 2018-07-26
File: kernel/time/posix-timers.c
Commit message: posix-timer: Properly check sigevent->sigev_notify

timer_create() specifies via sigevent->sigev_notify the signal delivery for
the new timer. The valid modes are SIGEV_NONE, SIGEV_SIGNAL, SIGEV_THREAD
and (SIGEV_SIGNAL | SIGEV_THREAD_ID).

The sanity check in good_sigevent() is only checking the valid combination
for the SIGEV_THREAD_ID bit, i.e. SIGEV_SIGNAL, but if SIGEV_THREAD_ID is
not set it accepts any random value.

This has no real effects on the posix timer and signal delivery code, but
it affects show_timer() which handles the output of /proc/$PID/timers. That
function uses a string array to pretty print sigev_notify. The access to
that array has no bound checks, so random sigev_notify cause access beyond
the array bounds.

Add proper checks for the valid notify modes and remove the SIGEV_THREAD_ID
masking from various code pathes as SIGEV_NONE can never be set in
combination with SIGEV_THREAD_ID.

Reported-by: Eric Biggers <ebiggers3@gmail.com>
Reported-by: Dmitry Vyukov <dvyukov@google.com>
Reported-by: Alexey Dobriyan <adobriyan@gmail.com>
Signed-off-by: Thomas Gleixner <tglx@linutronix.de>
Cc: John Stultz <john.stultz@linaro.org>
Cc: stable@vger.kernel.org
Commit author: Thomas Gleixner

Vulnerable version (before fix):
 static struct pid *good_sigevent(sigevent_t * event) { 	struct task_struct *rtn = current->group_leader;  	if ((event->sigev_notify & SIGEV_THREAD_ID ) && 		(!(rtn = find_task_by_vpid(event->sigev_notify_thread_id)) || 		 !same_thread_group(rtn, current) || 		 (event->sigev_notify & ~SIGEV_THREAD_ID) != SIGEV_SIGNAL)) 		return NULL;  	if (((event->sigev_notify & ~SIGEV_THREAD_ID) != SIGEV_NONE) && 	    ((event->sigev_signo <= 0) || (event->sigev_signo > SIGRTMAX))) 		return NULL;  	return task_pid(rtn); }  static struct k_itimer * alloc_posix_timer(void) void common_timer_get(struct k_itimer *timr, struct itimerspec64 *cur_setting) 	struct timespec64 ts64; 	bool sig_none;  	sig_none = (timr->it_sigev_notify & ~SIGEV_THREAD_ID) == SIGEV_NONE; 	iv = timr->it_interval;  	/* interval timer ? */ int common_timer_set(struct k_itimer *timr, int flags,  	timr->it_interval = timespec64_to_ktime(new_setting->it_interval); 	expires = timespec64_to_ktime(new_setting->it_value); 	sigev_none = (timr->it_sigev_notify & ~SIGEV_THREAD_ID) == SIGEV_NONE;  	kc->timer_arm(timr, expires, flags & TIMER_ABSTIME, sigev_none); 	timr->it_active = !sigev_none;

Fixed version (after fix):
 static struct pid *good_sigevent(sigevent_t * event) { 	struct task_struct *rtn = current->group_leader;  	switch (event->sigev_notify) { 	case SIGEV_SIGNAL | SIGEV_THREAD_ID: 		rtn = find_task_by_vpid(event->sigev_notify_thread_id); 		if (!rtn || !same_thread_group(rtn, current)) 			return NULL; 		/* FALLTHRU */ 	case SIGEV_SIGNAL: 	case SIGEV_THREAD: 		if (event->sigev_signo <= 0 || event->sigev_signo > SIGRTMAX) 			return NULL; 		/* FALLTHRU */ 	case SIGEV_NONE: 		return task_pid(rtn); 	default: 		return NULL; 	} }  static struct k_itimer * alloc_posix_timer(void) void common_timer_get(struct k_itimer *timr, struct itimerspec64 *cur_setting) 	struct timespec64 ts64; 	bool sig_none;  	sig_none = timr->it_sigev_notify == SIGEV_NONE; 	iv = timr->it_interval;  	/* interval timer ? */ int common_timer_set(struct k_itimer *timr, int flags,  	timr->it_interval = timespec64_to_ktime(new_setting->it_interval); 	expires = timespec64_to_ktime(new_setting->it_value); 	sigev_none = timr->it_sigev_notify == SIGEV_NONE;  	kc->timer_arm(timr, expires, flags & TIMER_ABSTIME, sigev_none); 	timr->it_active = !sigev_none;

Patch:
patch: @@ -434,17 +434,22 @@ static struct pid *good_sigevent(sigevent_t * event)
 {
 	struct task_struct *rtn = current->group_leader;
 
-	if ((event->sigev_notify & SIGEV_THREAD_ID ) &&
-		(!(rtn = find_task_by_vpid(event->sigev_notify_thread_id)) ||
-		 !same_thread_group(rtn, current) ||
-		 (event->sigev_notify & ~SIGEV_THREAD_ID) != SIGEV_SIGNAL))
+	switch (event->sigev_notify) {
+	case SIGEV_SIGNAL | SIGEV_THREAD_ID:
+		rtn = find_task_by_vpid(event->sigev_notify_thread_id);
+		if (!rtn || !same_thread_group(rtn, current))
+			return NULL;
+		/* FALLTHRU */
+	case SIGEV_SIGNAL:
+	case SIGEV_THREAD:
+		if (event->sigev_signo <= 0 || event->sigev_signo > SIGRTMAX)
+			return NULL;
+		/* FALLTHRU */
+	case SIGEV_NONE:
+		return task_pid(rtn);
+	default:
 		return NULL;
-
-	if (((event->sigev_notify & ~SIGEV_THREAD_ID) != SIGEV_NONE) &&
-	    ((event->sigev_signo <= 0) || (event->sigev_signo > SIGRTMAX)))
-		return NULL;
-
-	return task_pid(rtn);
+	}
 }
 
 static struct k_itimer * alloc_posix_timer(void)
@@ -669,7 +674,7 @@ void common_timer_get(struct k_itimer *timr, struct itimerspec64 *cur_setting)
 	struct timespec64 ts64;
 	bool sig_none;
 
-	sig_none = (timr->it_sigev_notify & ~SIGEV_THREAD_ID) == SIGEV_NONE;
+	sig_none = timr->it_sigev_notify == SIGEV_NONE;
 	iv = timr->it_interval;
 
 	/* interval timer ? */
@@ -856,7 +861,7 @@ int common_timer_set(struct k_itimer *timr, int flags,
 
 	timr->it_interval = timespec64_to_ktime(new_setting->it_interval);
 	expires = timespec64_to_ktime(new_setting->it_value);
-	sigev_none = (timr->it_sigev_notify & ~SIGEV_THREAD_ID) == SIGEV_NONE;
+	sigev_none = timr->it_sigev_notify == SIGEV_NONE;
 
 	kc->timer_arm(timr, expires, flags & TIMER_ABSTIME, sigev_none);
 	timr->it_active = !sigev_none;
path: kernel/time/posix-timers.c