Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Fix a race in buffer usage of Http2Connection.ProcessPingFrame#39242

Merged
geoffkizer merged 5 commits intodotnet:masterfrom
scalablecory:38788-ping-race
Jul 12, 2019
Merged

Fix a race in buffer usage of Http2Connection.ProcessPingFrame#39242
geoffkizer merged 5 commits intodotnet:masterfrom
scalablecory:38788-ping-race

Conversation

@scalablecory
Copy link
Copy Markdown

Incoming buffer might be reused before we've sent it. Resolve #38788.

…ing buffer might be reused before we've sent it. Resolve #38788.
@scalablecory scalablecory added this to the 3.0 milestone Jul 5, 2019
@scalablecory scalablecory requested a review from a team July 5, 2019 23:06
@scalablecory scalablecory self-assigned this Jul 5, 2019
@scalablecory
Copy link
Copy Markdown
Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 4 pipeline(s).

// Send PING ACK
// Don't wait for completion, which could happen asynchronously.
LogExceptions(SendPingAckAsync(_incomingBuffer.ActiveMemory.Slice(0, FrameHeader.PingLength)));
byte[] pingContent = _incomingBuffer.ActiveMemory.Slice(0, FrameHeader.PingLength).ToArray();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think we could just do BinaryPrimitives.ReadInt64BigEndian or whatever it is, and avoid an alloc here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I considered it, but the rarity of PING makes me feel that this is not a worthwhile micro-optimization given that we'd then need to document endianness.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we'd then need to document endianness.

Why? Wouldn't this just be roundtripping the data? As long as you used routines for converting to and from a long that employed the same endianness, it wouldn't be visible. You can also use BitConverter.ToInt64/TryWriteBytes, which just uses the machine's endianness and won't force any reversals.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

When it doesn't matter, like in this case, I'd rather reduce complexities and have obvious code than need to comment something.

As long as you used routines for converting to and from a long that employed the same endianness, it wouldn't be visible. You can also use BitConverter.

Either way, this is going to need comments. If you know HTTP2 and know these are just opaque bytes and not an integer, you'll find it suspicious to see an endian-specific read. Even if you didn't know HTTP2, if you see BitConverter being used to read an integer in networking code you'd immediately flag it as a possible bug.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Either way, this is going to need comments

It needs comments even with ToArray. Someone reviewing the code in a year needs to know that this wasn't an oversight and what race conditions it's working around. Otherwise it's very likely to be reverted by someone trying to remove allocations.

When it doesn't matter, like in this case, I'd rather reduce complexities and have obvious code

It shouldn't be any more, or any more complicated, code. It's ToInt64 instead of ToArray, and TryWriteBytes instead of CopyTo. If anything it's arguably clearer code, because it makes the payload size crystal clear in the method contract, whereas today it appears to accept arbitrarily-sized data.

I assume pings are super uncommon, so from a perf perspective it probably doesn't matter. But at least from my perspective the code would be cleaner with a long, and so may as well get the perf benefit, too.

Up to you. If you choose not to switch, please do add a detailed comment about why ToArray is necessary.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We already read a bunch of stuff out of frames as big endian, so I don't see that adding one more here is much different.

@karelz
Copy link
Copy Markdown
Member

karelz commented Jul 10, 2019

What's blocking this PR from merge? Can we get it done by EOW?

@scalablecory
Copy link
Copy Markdown
Author

What's blocking this PR from merge? Can we get it done by EOW?

It'll be ready today.

// Send PING ACK
// Don't wait for completion, which could happen asynchronously.
LogExceptions(SendPingAckAsync(_incomingBuffer.ActiveMemory.Slice(0, FrameHeader.PingLength)));
// We discard the the frame from the incoming buffer, possibly before
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not sure what this comment means?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Generally speaking, all these routines read the frame completely before returning. So I don't think you need to comment specifically on that.

Copy link
Copy Markdown

@geoffkizer geoffkizer left a comment

Choose a reason for hiding this comment

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

Aside from clarifying the comment, LGTM

// We discard the the frame from the incoming buffer, possibly before
// SendPingAckAsync is completed, so we need to save our own copy of the data.
// The data is not an integer in the RFC, but this will avoid an array allocation.
long pingContent = BinaryPrimitives.ReadInt64BigEndian(_incomingBuffer.ActiveSpan.Slice(0, FrameHeader.PingLength));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If anything, move this line to above the "Send PING ACK" comment above, since that comment is specifically commenting on the call to SendPingAckAsync

// Don't wait for completion, which could happen asynchronously.
LogExceptions(SendPingAckAsync(_incomingBuffer.ActiveMemory.Slice(0, FrameHeader.PingLength)));

// SendPingAckAsync copies the buffer for us, so it is safe to not wait for completion.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think I read this code wrong when I initially reviewed it...

It seems better to just read the pingContent into a long here, in ProcessPingFrame, and then pass the resulting long into SendPingAckAsync. That's generally how these Process*Frame routines work, they read whatever data they need out of the frame. And it's more obviously correct.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

By reading the integer in SendPingAckAsync, it avoids depending on ProcessPingFrame (to have the same endianness scheme).

I agree that consistency with other Process*Frame is better here, so I've moved the read into ProcessPingFrame and added comments to note the dependency.


// Copy pingContent before we go async so the caller can
// discard their buffer without waiting for us to complete.
long pingContentLong = BitConverter.ToInt64(pingContent.Span);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is there a particular reason to use BitConverter here, as opposed to BinaryPrimitives.ReadUInt64BigEndian? We use BinaryPrimitives elsewhere in the code. Seems like we should be consistent.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I actually haven't found any instances of BinaryPrimitives being used to shuttle opaque bytes, so this isn't a consistency issue. It made sense to avoid byte shuffling when endianness concerns could be kept to a single small method.

@geoffkizer
Copy link
Copy Markdown

LGTM, thanks.

@geoffkizer geoffkizer merged commit 1d03a91 into dotnet:master Jul 12, 2019
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
Fix a race in buffer usage of Http2Connection.ProcessPingFrame

Commit migrated from dotnet/corefx@1d03a91
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

bug Product bug (most likely)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTP2: Race with ping data handling

4 participants