Skip to content
This repository has been archived by the owner on Feb 16, 2023. It is now read-only.

Fix masker data race #320

Merged
merged 3 commits into from
Jul 23, 2020
Merged

Fix masker data race #320

merged 3 commits into from
Jul 23, 2020

Conversation

Marton6
Copy link
Member

@Marton6 Marton6 commented Jul 21, 2020

The indexedBuffer was not goroutine safe, as the upToIndex method returned the result of bytes.Buffer.Next, which is a slice of the underlying buffer. This slice, according to the documentation, is only valid until the next read or write operation on the buffer.

Therefore it might happen that after calling upToIndex but before using the value it returns, another goroutine writes to the buffer and invalidates the result of upToIndex.

This issue can also cause a data race, as the access to the underlying buffer through the result of upToIndex is not guarded with a mutex.

This PR fixes the issue by updating the upToIndex method to directly write these bytes to a writer, thus ensuring that any access to the underlying buffer is protected with the mutex.

@Marton6 Marton6 requested a review from jpcoenen July 21, 2020 15:51
@SimonBarendse SimonBarendse added the bug Something isn't working label Jul 21, 2020
Copy link
Member

@jpcoenen jpcoenen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find! I completely looked over the fact that a read or a write to the buffer invalidates the slice.

Regarding the solution: this definitely works. But looking at the explanation you give in the PR, I would expect a different approach. upToIndex is only called in 3 places. In one of those, the output is ignored and in the other two, it is directly written to an io.Writer.

So could we avoid copying the whole buffer (which is potentially big) by just making sure that the Write also happens within the lock? Or wouldn't that solve the problem? Especially if the masker is processing a lot of output, it is a waste to having to copy every byte an extra time. Those are CPU cycles we're stealing from the main process that's running. Normally I'd say: don't care too much about this, but the masker is a special case where we have to find a balance between performance, readability of the code, and security.

This change ensures that the underlying buffer is protected by the
mutex, by enforcing that all writes happen before the call to
mutex.Unlock, while also ensuring high performance for the masker.
@Marton6
Copy link
Member Author

Marton6 commented Jul 22, 2020

Good find! I completely looked over the fact that a read or a write to the buffer invalidates the slice.

Regarding the solution: this definitely works. But looking at the explanation you give in the PR, I would expect a different approach. upToIndex is only called in 3 places. In one of those, the output is ignored and in the other two, it is directly written to an io.Writer.

So could we avoid copying the whole buffer (which is potentially big) by just making sure that the Write also happens within the lock? Or wouldn't that solve the problem? Especially if the masker is processing a lot of output, it is a waste to having to copy every byte an extra time. Those are CPU cycles we're stealing from the main process that's running. Normally I'd say: don't care too much about this, but the masker is a special case where we have to find a balance between performance, readability of the code, and security.

I was not aware of the need for having an efficient masker.

I updated the indexedBuffer again, now it does not copy the bytes, but it is a bit less readable than before.

@Marton6 Marton6 requested a review from jpcoenen July 22, 2020 11:47
Copy link
Member

@jpcoenen jpcoenen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice solution. Only have one tiny question.

_, err = s.dest.Write([]byte("<redacted by SecretHub>"))
if err != nil {
return err
}
}

// Drop all bytes until the end of the mask.
_ = s.buf.upToIndex(i + int64(length))
_, _ = s.buf.writeUpToIndex(ioutil.Discard, i+int64(length))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the current setup writeUpToIndex can never return an error here, but the function signature does not enforce this. Since flush() does have error as a return type, would it be a problem if we just forward the error? That way we prevent any bugs when writeUpToIndex is changed so that it can return when a ioutil.Discarder is provided as an argument.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good point. This seems like a place where we could easily introduce a bug by mistake later on unless we handle the error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the error handling.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

@Marton6 Marton6 merged commit 29212c7 into develop Jul 23, 2020
@Marton6 Marton6 deleted the fix/masker-data-race branch July 23, 2020 08:21
@florisvdg florisvdg mentioned this pull request Sep 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants