Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

tty: Fix race condition between __tty_buffer_request_room and flush_to_ldisc

The race was introduced while development of linux-3.11 by
e8437d7ecbc50198705331449367d401ebb3181f and
e9975fdec0138f1b2a85b9624e41660abd9865d4.
Originally it was found and reproduced on linux-3.12.15 and
linux-3.12.15-rt25, by sending 500 byte blocks with 115kbaud to the
target uart in a loop with 100 milliseconds delay.

In short:
1. The consumer flush_to_ldisc is on to remove the head tty_buffer.
2. The producer adds a number of bytes, so that a new tty_buffer must
be allocated and added by __tty_buffer_request_room.
3. The consumer removes the head tty_buffer element, without handling
newly committed data.

Detailed example:
* Initial buffer:
* Head, Tail -> 0: used=250; commit=250; read=240; next=NULL
* Consumer: ''flush_to_ldisc''
* consumed 10 Byte
* buffer:
* Head, Tail -> 0: used=250; commit=250; read=250; next=NULL
{{{
count = head->commit - head->read; // count = 0
if (!count) { // enter
// INTERRUPTED BY PRODUCER ->
if (head->next == NULL)
break;
buf->head = head->next;
tty_buffer_free(port, head);
continue;
}
}}}
* Producer: tty_insert_flip_... 10 bytes + tty_flip_buffer_push
* buffer:
* Head, Tail -> 0: used=250; commit=250; read=250; next=NULL
* added 6 bytes: head-element filled to maximum.
* buffer:
* Head, Tail -> 0: used=256; commit=250; read=250; next=NULL
* added 4 bytes: __tty_buffer_request_room is called
* buffer:
* Head -> 0: used=256; commit=256; read=250; next=1
* Tail -> 1: used=4; commit=0; read=250 next=NULL
* push (tty_flip_buffer_push)
* buffer:
* Head -> 0: used=256; commit=256; read=250; next=1
* Tail -> 1: used=4; commit=4; read=250 next=NULL
* Consumer
{{{
count = head->commit - head->read;
if (!count) {
// INTERRUPTED BY PRODUCER <-
if (head->next == NULL) // -> no break
break;
buf->head = head->next;
tty_buffer_free(port, head);
// ERROR: tty_buffer head freed -> 6 bytes lost
continue;
}
}}}

This patch reintroduces a spin_lock to protect this case. Perhaps later
a lock-less solution could be found.

Signed-off-by: Manfred Schlaegl <manfred.schlaegl@gmx.at>
Cc: stable <stable@vger.kernel.org> # 3.11
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>

authored by

Manfred Schlaegl and committed by
Greg Kroah-Hartman
6a20dbd6 a798c10f

+15 -2
+14 -2
drivers/tty/tty_buffer.c
··· 255 255 if (change || left < size) { 256 256 /* This is the slow path - looking for new buffers to use */ 257 257 if ((n = tty_buffer_alloc(port, size)) != NULL) { 258 + unsigned long iflags; 259 + 258 260 n->flags = flags; 259 261 buf->tail = n; 262 + 263 + spin_lock_irqsave(&buf->flush_lock, iflags); 260 264 b->commit = b->used; 261 - smp_mb(); 262 265 b->next = n; 266 + spin_unlock_irqrestore(&buf->flush_lock, iflags); 267 + 263 268 } else if (change) 264 269 size = 0; 265 270 else ··· 448 443 mutex_lock(&buf->lock); 449 444 450 445 while (1) { 446 + unsigned long flags; 451 447 struct tty_buffer *head = buf->head; 452 448 int count; 453 449 ··· 456 450 if (atomic_read(&buf->priority)) 457 451 break; 458 452 453 + spin_lock_irqsave(&buf->flush_lock, flags); 459 454 count = head->commit - head->read; 460 455 if (!count) { 461 - if (head->next == NULL) 456 + if (head->next == NULL) { 457 + spin_unlock_irqrestore(&buf->flush_lock, flags); 462 458 break; 459 + } 463 460 buf->head = head->next; 461 + spin_unlock_irqrestore(&buf->flush_lock, flags); 464 462 tty_buffer_free(port, head); 465 463 continue; 466 464 } 465 + spin_unlock_irqrestore(&buf->flush_lock, flags); 467 466 468 467 count = receive_buf(tty, head, count); 469 468 if (!count) ··· 523 512 struct tty_bufhead *buf = &port->buf; 524 513 525 514 mutex_init(&buf->lock); 515 + spin_lock_init(&buf->flush_lock); 526 516 tty_buffer_reset(&buf->sentinel, 0); 527 517 buf->head = &buf->sentinel; 528 518 buf->tail = &buf->sentinel;
+1
include/linux/tty.h
··· 61 61 struct tty_buffer *head; /* Queue head */ 62 62 struct work_struct work; 63 63 struct mutex lock; 64 + spinlock_t flush_lock; 64 65 atomic_t priority; 65 66 struct tty_buffer sentinel; 66 67 struct llist_head free; /* Free queue head */