diff options
author | Linus Torvalds <torvalds@linux-foundation.org> | 2009-10-14 11:59:49 -0400 |
---|---|---|
committer | Linus Torvalds <torvalds@linux-foundation.org> | 2009-10-14 18:09:52 -0400 |
commit | c8e33141911bf8fe87dc6c92793b9a59b2be0130 (patch) | |
tree | 7b3561169050d242469df55ed338e752c9a35c48 | |
parent | 80f506918fdaaca6b574ba931536a58ce015c7be (diff) |
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 e043e42bdb66885b3ac10d27a01ccb9972e2b0a3 ("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=14388
Reported-and-tested-by: Frédéric Meunier <fredlwm@gmail.com>
Reported-and-tested-by: Boyan <btanastasov@yahoo.co.uk>
Cc: Alan Cox <alan@lxorguk.ukuu.org.uk>
Cc: Paul Fulghum <paulkf@microgate.com>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
-rw-r--r-- | drivers/char/tty_buffer.c | 29 |
1 files changed, 13 insertions, 16 deletions
diff --git a/drivers/char/tty_buffer.c b/drivers/char/tty_buffer.c index 3108991c5c8b..0296612cc7df 100644 --- a/drivers/char/tty_buffer.c +++ b/drivers/char/tty_buffer.c | |||
@@ -402,28 +402,26 @@ static void flush_to_ldisc(struct work_struct *work) | |||
402 | container_of(work, struct tty_struct, buf.work.work); | 402 | container_of(work, struct tty_struct, buf.work.work); |
403 | unsigned long flags; | 403 | unsigned long flags; |
404 | struct tty_ldisc *disc; | 404 | struct tty_ldisc *disc; |
405 | struct tty_buffer *tbuf, *head; | ||
406 | char *char_buf; | ||
407 | unsigned char *flag_buf; | ||
408 | 405 | ||
409 | disc = tty_ldisc_ref(tty); | 406 | disc = tty_ldisc_ref(tty); |
410 | if (disc == NULL) /* !TTY_LDISC */ | 407 | if (disc == NULL) /* !TTY_LDISC */ |
411 | return; | 408 | return; |
412 | 409 | ||
413 | spin_lock_irqsave(&tty->buf.lock, flags); | 410 | spin_lock_irqsave(&tty->buf.lock, flags); |
414 | /* So we know a flush is running */ | 411 | |
415 | set_bit(TTY_FLUSHING, &tty->flags); | 412 | if (!test_and_set_bit(TTY_FLUSHING, &tty->flags)) { |
416 | head = tty->buf.head; | 413 | struct tty_buffer *head; |
417 | if (head != NULL) { | 414 | while ((head = tty->buf.head) != NULL) { |
418 | tty->buf.head = NULL; | 415 | int count; |
419 | for (;;) { | 416 | char *char_buf; |
420 | int count = head->commit - head->read; | 417 | unsigned char *flag_buf; |
418 | |||
419 | count = head->commit - head->read; | ||
421 | if (!count) { | 420 | if (!count) { |
422 | if (head->next == NULL) | 421 | if (head->next == NULL) |
423 | break; | 422 | break; |
424 | tbuf = head; | 423 | tty->buf.head = head->next; |
425 | head = head->next; | 424 | tty_buffer_free(tty, head); |
426 | tty_buffer_free(tty, tbuf); | ||
427 | continue; | 425 | continue; |
428 | } | 426 | } |
429 | /* Ldisc or user is trying to flush the buffers | 427 | /* Ldisc or user is trying to flush the buffers |
@@ -445,9 +443,9 @@ static void flush_to_ldisc(struct work_struct *work) | |||
445 | flag_buf, count); | 443 | flag_buf, count); |
446 | spin_lock_irqsave(&tty->buf.lock, flags); | 444 | spin_lock_irqsave(&tty->buf.lock, flags); |
447 | } | 445 | } |
448 | /* Restore the queue head */ | 446 | clear_bit(TTY_FLUSHING, &tty->flags); |
449 | tty->buf.head = head; | ||
450 | } | 447 | } |
448 | |||
451 | /* We may have a deferred request to flush the input buffer, | 449 | /* We may have a deferred request to flush the input buffer, |
452 | if so pull the chain under the lock and empty the queue */ | 450 | if so pull the chain under the lock and empty the queue */ |
453 | if (test_bit(TTY_FLUSHPENDING, &tty->flags)) { | 451 | if (test_bit(TTY_FLUSHPENDING, &tty->flags)) { |
@@ -455,7 +453,6 @@ static void flush_to_ldisc(struct work_struct *work) | |||
455 | clear_bit(TTY_FLUSHPENDING, &tty->flags); | 453 | clear_bit(TTY_FLUSHPENDING, &tty->flags); |
456 | wake_up(&tty->read_wait); | 454 | wake_up(&tty->read_wait); |
457 | } | 455 | } |
458 | clear_bit(TTY_FLUSHING, &tty->flags); | ||
459 | spin_unlock_irqrestore(&tty->buf.lock, flags); | 456 | spin_unlock_irqrestore(&tty->buf.lock, flags); |
460 | 457 | ||
461 | tty_ldisc_deref(disc); | 458 | tty_ldisc_deref(disc); |