Commit c8e33141 authored by Linus Torvalds's avatar Linus Torvalds

tty: Make flush_to_ldisc() locking more robust

The locking logic in this function is extremely subtle, and it broke
when we started doing potentially concurrent 'flush_to_ldisc()' calls in
commit e043e42b ("pty: avoid forcing
'low_latency' tty flag").

The code in flush_to_ldisc() used to set 'tty->buf.head' to NULL, with
the intention that this would then cause any other concurrent calls to
not do anything (locking note: we have to drop the buf.lock over the
call to ->receive_buf that can block, which is why we can have
concurrency here at all in the first place).

It also used to set the TTY_FLUSHING bit, which would then cause any
concurrent 'tty_buffer_flush()' to not free all the tty buffers and
clear 'tty->buf.tail'.  And with 'buf.head' being NULL, and 'buf.tail'
being non-NULL, new data would never touch 'buf.head'.

Does that sound a bit too subtle? It was.  If another concurrent call to
'flush_to_ldisc()' were to come in, the NULL buf.head would indeed cause
it to not process the buffer list, but it would still clear TTY_FLUSHING
afterwards, making the buffer protection against 'tty_buffer_flush()' no
longer work.

So this clears it all up.  We depend purely on TTY_FLUSHING for handling
re-entrancy, and stop playing games with the buffer list entirely.  In
fact, the buffer list handling is now robust enough that we could
probably stop doing the whole "protect against 'tty_buffer_flush()'"
thing entirely.

However, Alan also points out that we would probably be better off
simplifying the locking even further, and just take the tty ldisc_mutex
around all the buffer flushing calls.  That seems like a good idea, but
in the meantime this is a conceptually minimal fix (with the patch
itself being bigger than required just to clean the code up and make it
readable).

This fixes keyboard trouble under X:

	http://bugzilla.kernel.org/show_bug.cgi?id=14388Reported-and-tested-by: default avatarFrédéric Meunier <fredlwm@gmail.com>
Reported-and-tested-by: default avatarBoyan <btanastasov@yahoo.co.uk>
Cc: Alan Cox <alan@lxorguk.ukuu.org.uk>
Cc: Paul Fulghum <paulkf@microgate.com>
Signed-off-by: default avatarLinus Torvalds <torvalds@linux-foundation.org>
parent 80f50691
...@@ -402,28 +402,26 @@ static void flush_to_ldisc(struct work_struct *work) ...@@ -402,28 +402,26 @@ static void flush_to_ldisc(struct work_struct *work)
container_of(work, struct tty_struct, buf.work.work); container_of(work, struct tty_struct, buf.work.work);
unsigned long flags; unsigned long flags;
struct tty_ldisc *disc; struct tty_ldisc *disc;
struct tty_buffer *tbuf, *head;
char *char_buf;
unsigned char *flag_buf;
disc = tty_ldisc_ref(tty); disc = tty_ldisc_ref(tty);
if (disc == NULL) /* !TTY_LDISC */ if (disc == NULL) /* !TTY_LDISC */
return; return;
spin_lock_irqsave(&tty->buf.lock, flags); spin_lock_irqsave(&tty->buf.lock, flags);
/* So we know a flush is running */
set_bit(TTY_FLUSHING, &tty->flags); if (!test_and_set_bit(TTY_FLUSHING, &tty->flags)) {
head = tty->buf.head; struct tty_buffer *head;
if (head != NULL) { while ((head = tty->buf.head) != NULL) {
tty->buf.head = NULL; int count;
for (;;) { char *char_buf;
int count = head->commit - head->read; unsigned char *flag_buf;
count = head->commit - head->read;
if (!count) { if (!count) {
if (head->next == NULL) if (head->next == NULL)
break; break;
tbuf = head; tty->buf.head = head->next;
head = head->next; tty_buffer_free(tty, head);
tty_buffer_free(tty, tbuf);
continue; continue;
} }
/* Ldisc or user is trying to flush the buffers /* Ldisc or user is trying to flush the buffers
...@@ -445,9 +443,9 @@ static void flush_to_ldisc(struct work_struct *work) ...@@ -445,9 +443,9 @@ static void flush_to_ldisc(struct work_struct *work)
flag_buf, count); flag_buf, count);
spin_lock_irqsave(&tty->buf.lock, flags); spin_lock_irqsave(&tty->buf.lock, flags);
} }
/* Restore the queue head */ clear_bit(TTY_FLUSHING, &tty->flags);
tty->buf.head = head;
} }
/* We may have a deferred request to flush the input buffer, /* We may have a deferred request to flush the input buffer,
if so pull the chain under the lock and empty the queue */ if so pull the chain under the lock and empty the queue */
if (test_bit(TTY_FLUSHPENDING, &tty->flags)) { if (test_bit(TTY_FLUSHPENDING, &tty->flags)) {
...@@ -455,7 +453,6 @@ static void flush_to_ldisc(struct work_struct *work) ...@@ -455,7 +453,6 @@ static void flush_to_ldisc(struct work_struct *work)
clear_bit(TTY_FLUSHPENDING, &tty->flags); clear_bit(TTY_FLUSHPENDING, &tty->flags);
wake_up(&tty->read_wait); wake_up(&tty->read_wait);
} }
clear_bit(TTY_FLUSHING, &tty->flags);
spin_unlock_irqrestore(&tty->buf.lock, flags); spin_unlock_irqrestore(&tty->buf.lock, flags);
tty_ldisc_deref(disc); tty_ldisc_deref(disc);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment