From: Peter Hurley <peter@hurleysoftware.com>
To: Dmitry Vyukov <dvyukov@google.com>
Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
jslaby@suse.com, LKML <linux-kernel@vger.kernel.org>,
Jiri Slaby <jslaby@suse.cz>,
Andrey Konovalov <andreyknvl@google.com>,
Kostya Serebryany <kcc@google.com>,
Alexander Potapenko <glider@google.com>,
Paul McKenney <paulmck@linux.vnet.ibm.com>,
Hans Boehm <hboehm@google.com>
Subject: Re: [PATCH] tty: fix data races on tty_buffer.commit
Date: Fri, 4 Sep 2015 15:49:37 -0400 [thread overview]
Message-ID: <55E9F5D1.5000208@hurleysoftware.com> (raw)
In-Reply-To: <CACT4Y+Y09n8S_pzYNAC4FJAhpSftypEZnpqq_F8QRwOffrOUYA@mail.gmail.com>
On 09/04/2015 03:37 PM, Dmitry Vyukov wrote:
> On Fri, Sep 4, 2015 at 9:34 PM, Peter Hurley <peter@hurleysoftware.com> wrote:
>> Hi Dmitry,
>>
>> On 09/04/2015 03:09 PM, Dmitry Vyukov wrote:
>>> Race on buffer data happens in the following scenario:
>>> __tty_buffer_request_room does a plain write of tail->commit,
>>> no barriers were executed before that.
>>> At this point flush_to_ldisc reads this new value of commit,
>>> and reads buffer data, no barriers in between.
>>> The committed buffer data is not necessary visible to flush_to_ldisc.
>>
>> Please submit one patch for each "fix", because it is not possible
>> to review what you believe you're fixing.
>>
>> See below for an example.
>>
>>> Similar bug happens when tty_schedule_flip commits data.
>>>
>>> Another race happens in tty_buffer_flush. It uses plain reads
>>> to read tty_buffer.next, as the result it can free a buffer
>>> which has pending writes in __tty_buffer_request_room thread.
>>> For example, tty_buffer_flush calls tty_buffer_free which
>>> reads b->size, the size may not be visible to this thread.
>>> As the result a large buffer can hang in the freelist.
>>>
>>> Update commit with smp_store_release and read commit with
>>> smp_load_acquire, as it is commit that signals data readiness.
>>> This is orthogonal to the existing synchronization on tty_buffer.next,
>>> which is required to not dismiss a buffer with unconsumed data.
>>>
>>> The data race was found with KernelThreadSanitizer (KTSAN).
>>>
>>> Signed-off-by: Dmitry Vyukov <dvyukov@google.com>
>>> ---
>>> drivers/tty/tty_buffer.c | 38 ++++++++++++++++++++++++--------------
>>> 1 file changed, 24 insertions(+), 14 deletions(-)
>>>
>>> diff --git a/drivers/tty/tty_buffer.c b/drivers/tty/tty_buffer.c
>>> index 4cf263d..4fae5d1 100644
>>> --- a/drivers/tty/tty_buffer.c
>>> +++ b/drivers/tty/tty_buffer.c
>>> @@ -89,7 +89,7 @@ void tty_buffer_unlock_exclusive(struct tty_port *port)
>>> struct tty_bufhead *buf = &port->buf;
>>> int restart;
>>>
>>> - restart = buf->head->commit != buf->head->read;
>>> + restart = READ_ONCE(buf->head->commit) != buf->head->read;
>>>
>>> atomic_dec(&buf->priority);
>>> mutex_unlock(&buf->lock);
>>> @@ -242,11 +242,14 @@ void tty_buffer_flush(struct tty_struct *tty, struct tty_ldisc *ld)
>>> atomic_inc(&buf->priority);
>>>
>>> mutex_lock(&buf->lock);
>>> - while ((next = buf->head->next) != NULL) {
>>> + /* paired with smp_store_release in __tty_buffer_request_room();
>>> + * ensures there are no outstanding writes to buf->head when we free it
>>> + */
>>> + while ((next = smp_load_acquire(&buf->head->next)) != NULL) {
>>> tty_buffer_free(port, buf->head);
>>> buf->head = next;
>>> }
>>> - buf->head->read = buf->head->commit;
>>> + buf->head->read = READ_ONCE(buf->head->commit);
>>>
>>> if (ld && ld->ops->flush_buffer)
>>> ld->ops->flush_buffer(tty);
>>> @@ -290,13 +293,15 @@ static int __tty_buffer_request_room(struct tty_port *port, size_t size,
>>> if (n != NULL) {
>>> n->flags = flags;
>>> buf->tail = n;
>>> - b->commit = b->used;
>>> - /* paired w/ barrier in flush_to_ldisc(); ensures the
>>> - * latest commit value can be read before the head is
>>> - * advanced to the next buffer
>>> + /* paired with smp_load_acquire in flush_to_ldisc();
>>> + * ensures flush_to_ldisc() sees buffer data.
>>> */
>>> - smp_wmb();
>>> - b->next = n;
>>> + smp_store_release(&b->commit, b->used);
>>> + /* paired with smp_load_acquire in flush_to_ldisc();
>>> + * ensures the latest commit value can be read before
>>> + * the head is advanced to the next buffer
>>> + */
>>> + smp_store_release(&b->next, n);
>>> } else if (change)
>>> size = 0;
>>> else
>>> @@ -394,7 +399,10 @@ void tty_schedule_flip(struct tty_port *port)
>>> {
>>> struct tty_bufhead *buf = &port->buf;
>>>
>>> - buf->tail->commit = buf->tail->used;
>>> + /* paired with smp_load_acquire in flush_to_ldisc(); ensures the
>>> + * committed data is visible to flush_to_ldisc()
>>> + */
>>> + smp_store_release(&buf->tail->commit, buf->tail->used);
>>> schedule_work(&buf->work);
>>
>> schedule_work() is an implied barrier for obvious reasons.
>
> OK, I will split.
> To answer this particular question: you need release/write barrier
> _before_ the synchronizing store, not _after_. Once the store to
> commit happened, another thread can start reading buffer data, this
> thread has not yet executed schedule_work at this point.
No.
If the work is already running, a new work will be scheduled, and the
new work will pick up the changed commit index.
If the work is already running /and it happens to see the new commit index/,
it will process the buffer. The new work will start and discover there is
nothing to do.
Regards,
Peter Hurley
PS - You need to base your patches on current mainline. You'll see that I already
converted the smp_rmb()/smp_wmb() of 'next' to load_acquire/store_release. FWIW,
that's not a fix, but a minor optimization. Commit sha 069f38b4983efaea9
>>> }
>>> EXPORT_SYMBOL(tty_schedule_flip);
>>> @@ -488,13 +496,15 @@ static void flush_to_ldisc(struct work_struct *work)
>>> if (atomic_read(&buf->priority))
>>> break;
>>>
>>> - next = head->next;
>>> - /* paired w/ barrier in __tty_buffer_request_room();
>>> + /* paired with smp_store_release in __tty_buffer_request_room();
>>> * ensures commit value read is not stale if the head
>>> * is advancing to the next buffer
>>> */
>>> - smp_rmb();
>>> - count = head->commit - head->read;
>>> + next = smp_load_acquire(&head->next);
>>> + /* paired with smp_store_release in __tty_buffer_request_room();
>>> + * ensures we see the committed buffer data
>>> + */
>>> + count = smp_load_acquire(&head->commit) - head->read;
>>> if (!count) {
>>> if (next == NULL) {
>>> check_other_closed(tty);
>>>
>>
>
>
>
next prev parent reply other threads:[~2015-09-04 19:49 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2015-09-04 19:09 [PATCH] tty: fix data races on tty_buffer.commit Dmitry Vyukov
2015-09-04 19:34 ` Peter Hurley
2015-09-04 19:37 ` Dmitry Vyukov
2015-09-04 19:49 ` Peter Hurley [this message]
2015-09-04 20:13 ` Dmitry Vyukov
2015-09-07 12:31 ` Dmitry Vyukov
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=55E9F5D1.5000208@hurleysoftware.com \
--to=peter@hurleysoftware.com \
--cc=andreyknvl@google.com \
--cc=dvyukov@google.com \
--cc=glider@google.com \
--cc=gregkh@linuxfoundation.org \
--cc=hboehm@google.com \
--cc=jslaby@suse.com \
--cc=jslaby@suse.cz \
--cc=kcc@google.com \
--cc=linux-kernel@vger.kernel.org \
--cc=paulmck@linux.vnet.ibm.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.