Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add better guards again re-entrance in ByteToMessageDecoder #370

Merged
merged 1 commit into from
May 8, 2018

Conversation

normanmaurer
Copy link
Member

@normanmaurer normanmaurer commented Apr 28, 2018

Motivation:

When the ByteToMessageDecoder re-entranced in its decode(...) / decodeLast(...) methods it could be possible that the ordering of processing and so the seen bytes are mixed.

Modifications:

  • Refresh the buffer on each loop iteration and update the cumulationBuffers indicies.

Result:

More robust code.

fileprivate var state = HTTPParserState()

fileprivate init(type: HTTPMessageT.Type) {
/* this is a private init, the public versions only allow HTTPClientResponsePart and HTTPServerRequestPart */
assert(HTTPMessageT.self == HTTPClientResponsePart.self || HTTPMessageT.self == HTTPServerRequestPart.self)
}

deinit {
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 used a deinit { } for it as decoderRemoved may be called while still in the decode(...) and so I can not modify the parser.

/// Decode in a loop until there is nothing more to decode.
private func decodeLoop(ctx: ChannelHandlerContext, decodeFunc: (ChannelHandlerContext, inout ByteBuffer) throws -> DecodingState) {
assert(self.cumulationBuffer != nil)
ctx.withThrowingToFireErrorAndClose {
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably better to hoist this up to the caller.

Copy link
Member Author

Choose a reason for hiding this comment

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

You mean let the error bubble up ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Let decodeLoop throw, place the ctx.thenThrowingToFireErrorAndClose in the function that calls decodeLoop.

let writerIndex = buffer.writerIndex
let result = try decodeFunc(ctx, &buffer)
if self.cumulationBuffer != nil {
self.cumulationBuffer!.moveReaderIndex(forwardBy: readable - buffer.readableBytes)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this right? We’re passing a local copy of the buffer, but then mutating the cumulation buffer. Why not just preserve the buffer returned from the call to decode, especially as we’re bothering to pass it inout.

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 think it’s right but I am all ears for better ideas. Can you show me some code with your suggestion as I don’t understand what exactly you suggest here

Copy link
Contributor

Choose a reason for hiding this comment

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

Basically I'm just saying why not just do self.cumulationBuffer = buffer?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I know why: the cumulationBuffer may have been mutated. That's awkward.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hrm, I'm inclined to say that we should probably pass &buffer as a slice instead, and then come up with some custom logic to reconcile the changes from that slice with the parent.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah it’s because of the reason you stated... a slice sounds ok, that said I am not sure if it really buys us anything

buffer.discardReadBytes()
if self.cumulationBuffer != nil {
if self.cumulationBuffer!.readableBytes > 0 {
if self.shouldReclaimBytes(buffer: self.cumulationBuffer!) && self.cumulationBuffer!.discardReadBytes() && self.cumulationBuffer!.readableBytes == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let’s make this clearer.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok let me break up the if statement and add some comments

@normanmaurer
Copy link
Member Author

@swift-nio-bot test this please

@normanmaurer
Copy link
Member Author

Also tests pass in docker locally... we may need to update timeouts

parser.data = UnsafeMutableRawPointer(bitPattern: 0x0000deadbeef0000)

// Set the callbacks to nil as we dont need these anymore
settings.on_body = nil
Copy link
Member

Choose a reason for hiding this comment

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

this should have the same effect:

settings = http_parser_settings()

@tomerd
Copy link
Member

tomerd commented Apr 30, 2018

@swift-nio-bot test this please

@normanmaurer normanmaurer changed the title [DONT-MERGE_YET] ByteToMessageDecoder improvements / HTTPDecoder allocations Add better guards again re-entrance in ByteToMessageDecoder May 4, 2018
@normanmaurer
Copy link
Member Author

Let me update the PR to only include the ByteToMessageDecoder changes.

Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

Can we have a test for this change?

@normanmaurer normanmaurer force-pushed the byte_to_message_decoder branch 3 times, most recently from 4a0a64a to 58ff97f Compare May 4, 2018 18:00
@normanmaurer
Copy link
Member Author

@weissi @Lukasa PTAL again... Will add some more tests as well.

@normanmaurer
Copy link
Member Author

@Lukasa haha you were faster then me (and I did not refresh yet). Yep will add a few tests.

@weissi
Copy link
Member

weissi commented May 4, 2018

@normanmaurer awesome!

18:30:29 info: 1000_reqs_1_conn: total number of mallocs: 46730
18:30:29 info: 1_reqs_1000_conn: allocations not freed: 0
18:30:29 info: 1_reqs_1000_conn: total number of mallocs: 775997
18:30:29 info: ping_pong_1000_reqs_1_conn: allocations not freed: 0
18:30:29 info: ping_pong_1000_reqs_1_conn: total number of mallocs: 4513
18:30:29 info: bytebuffer_lots_of_rw: allocations not freed: 0
18:30:29 info: bytebuffer_lots_of_rw: total number of mallocs: 3011
18:30:29 info: future_lots_of_callbacks: allocations not freed: 0
18:30:29 info: future_lots_of_callbacks: total number of mallocs: 99001

Please edit the docker compose files and lower the limits once again. Just add the changes to this PR

if self.shouldReclaimBytes(buffer: buffer) {
buffer.discardReadBytes()
// Discard the cumulationBuffer or discard read bytes if needed.
guard self.cumulationBuffer != nil else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason not to use guard let here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because we want to act on the self.cumulationBuffer on the following lines and not on a different copy ?

}

if self.shouldReclaimBytes(buffer: self.cumulationBuffer!) && self.cumulationBuffer!.discardReadBytes() {
if self.cumulationBuffer!.readableBytes == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason not to just hoist this check up? It won’t be changed by reclaiming bytes.

Copy link
Member Author

Choose a reason for hiding this comment

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

@Lukasa you are right this can be removed.

@@ -96,64 +106,95 @@ private extension ChannelHandlerContext {

extension ByteToMessageDecoder {

/// Decode in a loop until there is nothing more to decode.
private func decodeLoop(ctx: ChannelHandlerContext, decodeFunc: (ChannelHandlerContext, inout ByteBuffer) throws -> DecodingState) throws {
while var slice = self.cumulationBuffer?.slice(), slice.readableBytes > 0 {
Copy link
Member Author

Choose a reason for hiding this comment

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

@weissi @Lukasa so I started to write some unit tests and notice we can not do this.. The problem here is that it is possible that decodeLast will be triggered from within decodeFunc (due a close(...) call for example). In this case decodeLast will by default call decode(...) which will then see the same bytes again even if decode(...) before increased the readerIndex as we had no chance to replicate these changes to the cumulationBuffer yet :(

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, there's no way to entirely prevent re-entrancy without removing decodeLast.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah... so I wonder what we should do here in the case... any suggestions ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean, I'm inclined to sit on it and wait until 2.0, when decodeLast is removed. In the meantime we can recommend users override decodeLast or channelInactive.

Copy link
Member Author

Choose a reason for hiding this comment

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

@Lukasa when you say sit on it you suggest just not pass in a slice but self.cumulationBuffer or pass in the slice (as I did here) ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean, it doesn't matter what you do, the issue is still the same: decodeLast is not safe from avoid reentrancy. So you may as well keep using the slice.

}

guard slice.writerIndex == sliceWriterIndex else {
fatalError("Writing to the buffer is not allowed")
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor nit, but I tend to prefer these to be preconditionFailure.

Copy link
Member Author

Choose a reason for hiding this comment

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

sure why not... I dont care and I think at the end it makes no difference as long as it crashes :)

typealias InboundOut = ByteBuffer

var cumulationBuffer: ByteBuffer?
var triggeredReentrace: Bool = false
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: "reentrance"

}

if !self.triggeredReentrace {
self.triggeredReentrace = true
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than set this here, it might be better to let the re-entrant call set it (where you currently have an XCTAssertTrue) and then check it at the end of the test. Otherwise this test could pass if you never re-entered at all.

Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

Cool, I'm basically happy. @weissi?

/// 1. Moving the reader index forward persists across calls. When your method returns, if the reader index has advanced, those bytes are considered "consumed" and will not be available in future calls to `decode`.
/// Please note, however, that the numerical value of the `readerIndex` itself is not preserved, and may not be the same from one call to the next. Please do not rely on this numerical value: if you need
/// to recall where a byte is relative to the `readerIndex`, use an offset rather than an absolute value.
/// 2. Mutating the bytes in the buffer will cause a `fatalError` and so is not allowed. You are only allowed to move the readerIndex forward or consume from the buffer.
Copy link
Member

Choose a reason for hiding this comment

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

should we say 'mutating the bytes or the readerIndex will cause undefined behaviour and likely crash your program' or something?

Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

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

mostly nits and can you lower the allocation limits?

break
}

guard slice.writerIndex == sliceWriterIndex else {
Copy link
Member

Choose a reason for hiding this comment

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

why not just precondition(slice.writeIndex == sliceWriterIndex, "...")?

Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

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

nice one, ta

Motivation:

When the ByteToMessageDecoder re-entranced in its decode(...) / decodeLast(...) methods it could be possible that the ordering of processing and so the seen bytes are mixed.

Modifications:

- Refresh the buffer (and take a slice)  on each loop iteration and update the cumulationBuffers indicies.

Result:

More robust code.
@normanmaurer normanmaurer self-assigned this May 8, 2018
@normanmaurer normanmaurer added this to the 1.7.0 milestone May 8, 2018
@normanmaurer normanmaurer merged commit 4cbafde into apple:master May 8, 2018
@normanmaurer normanmaurer deleted the byte_to_message_decoder branch May 8, 2018 16:29
@Lukasa Lukasa added the semver/patch No public API change. label May 11, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants