diff options
author | Peter Hurley <peter@hurleysoftware.com> | 2014-05-02 10:56:12 -0400 |
---|---|---|
committer | Greg Kroah-Hartman <gregkh@linuxfoundation.org> | 2014-05-03 18:14:28 -0400 |
commit | 62a0d8d7c2b29f92850e4ee3c38e5dfd936e92b2 (patch) | |
tree | ba32a5252d3cbfb5603da1d6391917415fb77b6b | |
parent | 5fbf1a65dd53ef313783c34a0e93a6e29def6136 (diff) |
tty: Fix lockless tty buffer race
Commit 6a20dbd6caa2358716136144bf524331d70b1e03,
"tty: Fix race condition between __tty_buffer_request_room and flush_to_ldisc"
correctly identifies an unsafe race condition between
__tty_buffer_request_room() and flush_to_ldisc(), where the consumer
flush_to_ldisc() prematurely advances the head before consuming the
last of the data committed. For example:
CPU 0 | CPU 1
__tty_buffer_request_room | flush_to_ldisc
... | ...
| count = head->commit - head->read
n = tty_buffer_alloc() |
b->commit = b->used |
b->next = n |
| if (!count) /* T */
| if (head->next == NULL) /* F */
| buf->head = head->next
In this case, buf->head has been advanced but head->commit may have
been updated with a new value.
Instead of reintroducing an unnecessary lock, fix the race locklessly.
Read the commit-next pair in the reverse order of writing, which guarantees
the commit value read is the latest value written if the head is
advancing.
Reported-by: Manfred Schlaegl <manfred.schlaegl@gmx.at>
Cc: <stable@vger.kernel.org> # 3.12.x+
Signed-off-by: Peter Hurley <peter@hurleysoftware.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
-rw-r--r-- | drivers/tty/tty_buffer.c | 17 |
1 files changed, 14 insertions, 3 deletions
diff --git a/drivers/tty/tty_buffer.c b/drivers/tty/tty_buffer.c index 8ebd9f88a6f6..cf78d1985cd8 100644 --- a/drivers/tty/tty_buffer.c +++ b/drivers/tty/tty_buffer.c | |||
@@ -258,7 +258,11 @@ static int __tty_buffer_request_room(struct tty_port *port, size_t size, | |||
258 | n->flags = flags; | 258 | n->flags = flags; |
259 | buf->tail = n; | 259 | buf->tail = n; |
260 | b->commit = b->used; | 260 | b->commit = b->used; |
261 | smp_mb(); | 261 | /* paired w/ barrier in flush_to_ldisc(); ensures the |
262 | * latest commit value can be read before the head is | ||
263 | * advanced to the next buffer | ||
264 | */ | ||
265 | smp_wmb(); | ||
262 | b->next = n; | 266 | b->next = n; |
263 | } else if (change) | 267 | } else if (change) |
264 | size = 0; | 268 | size = 0; |
@@ -444,17 +448,24 @@ static void flush_to_ldisc(struct work_struct *work) | |||
444 | 448 | ||
445 | while (1) { | 449 | while (1) { |
446 | struct tty_buffer *head = buf->head; | 450 | struct tty_buffer *head = buf->head; |
451 | struct tty_buffer *next; | ||
447 | int count; | 452 | int count; |
448 | 453 | ||
449 | /* Ldisc or user is trying to gain exclusive access */ | 454 | /* Ldisc or user is trying to gain exclusive access */ |
450 | if (atomic_read(&buf->priority)) | 455 | if (atomic_read(&buf->priority)) |
451 | break; | 456 | break; |
452 | 457 | ||
458 | next = head->next; | ||
459 | /* paired w/ barrier in __tty_buffer_request_room(); | ||
460 | * ensures commit value read is not stale if the head | ||
461 | * is advancing to the next buffer | ||
462 | */ | ||
463 | smp_rmb(); | ||
453 | count = head->commit - head->read; | 464 | count = head->commit - head->read; |
454 | if (!count) { | 465 | if (!count) { |
455 | if (head->next == NULL) | 466 | if (next == NULL) |
456 | break; | 467 | break; |
457 | buf->head = head->next; | 468 | buf->head = next; |
458 | tty_buffer_free(port, head); | 469 | tty_buffer_free(port, head); |
459 | continue; | 470 | continue; |
460 | } | 471 | } |