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

http chunked response with buffer slices served over TLS corrupts input data #4509

Closed
conet opened this issue Oct 18, 2022 · 21 comments
Closed
Assignees
Labels
Milestone

Comments

@conet
Copy link

conet commented Oct 18, 2022

Version

4.3.4

Context

While working on a streaming http server I encountered data corruption if TLS was used, this did not happen if TLS was not present. As far as I can tell buffers sent as response should be considered read only so I consider this alteration of buffer contents as a bug. If this was not true it means that the data delivered on every response must be copied which is a huge performance issue.

Do you have a reproducer?

The reproducer contains all the details.

https://github.com/conet/vertx-tls-reproducer

Steps to reproduce

  1. Run the test with TLS enabled
  2. Compare the input file with the output file
  3. The actual result is that the files are different
  4. The files should be identical
  5. All diff instances involve http chunked encoding protocol data 0D 0A 66 61 30 0D 0A -> CR LF size CR LF which means that the chunked encoding protocol corrupts the input buffer (emitted in slices - read only references pointing to the same underlying buffer)
  6. This does not happen when TLS is disabled

Extra

Might be caused by something like this

@conet
Copy link
Author

conet commented Oct 19, 2022

I found out that if I transform the source buffers into a read only buffers like this:

.map(buffer -> Buffer.buffer(buffer.getByteBuf().asReadOnly()))

The corruption does not happen anymore. This is a solution that we can work with because sharing a read only buffer stream is really what we were looking for. Although I still think that there is a problem that the input buffer is modified in the write function.

@vietj vietj added this to the 4.3.5 milestone Oct 19, 2022
@vietj vietj self-assigned this Oct 19, 2022
@vietj
Copy link
Member

vietj commented Oct 19, 2022

vertx buffers can be shared only if they are not modified, usually sending buffer on a response should not modify them unless there is a bug (e.g like you pointed out there was in netty).

Can you try using a specific implementation of netty ByteBuf that logs when the buffer is actually (with a stacktrace) modified, instead of being read-only ?

@conet
Copy link
Author

conet commented Oct 19, 2022

I tried it but it didn't work, looks like the write does not go through the implementation that calls ensureWritable or maybe I'm missing something.

@conet
Copy link
Author

conet commented Oct 19, 2022

After some debugging and I think that the problem is here data is being accumulated to the vert.x buffer (the slice in this case) see the screenshot
debug

The workaround has an effect because the intermediate ReadOnlyByteBuf that wraps the data has this implementation:

    public int ensureWritable(int minWritableBytes, boolean force) {
        return 1;
    }

which prevents SSLHandler from appending bytes to the buffer. If this is the case then all normal buffers sent as a response are appended with bytes, in our case it became a corruption because we were sending slices.

debug_read_only

As you can see the cumulation member is the ReadOnlyByteBuf (when the workaround is applied)

@conet
Copy link
Author

conet commented Oct 19, 2022

Based on my previous comment it looks like this exactly an effect of netty/netty#11792

@vietj
Copy link
Member

vietj commented Oct 20, 2022

thanks for your investigation work @conet , can we reproduce this with a smaller reproducer (i.e with a minimum amount of data) ?

@vietj
Copy link
Member

vietj commented Oct 20, 2022

what is not clear to me with respect to the netty issue that is referenced is that

  • in the netty bug, the byte buf were corrupted because they are shared
  • in this case buffers are not shared since they originate from the file system

so I do not understand yet how the bug happens if the byte buf originating from the file system can be expanded

@vietj
Copy link
Member

vietj commented Oct 20, 2022

ah I think I understand what I was missing : it comes from the slice operation that is performed in the concatMap that shares the same underlying bigger byte buf.

@vietj
Copy link
Member

vietj commented Oct 20, 2022

so here is what happen:

  • the concatMap will slice vertx buffers that create netty slices, normally those buffers cannot be appended to
  • when such buffers are written to the HttpServerResponse, they are duplicated (so the netty slice is duplicated) and the duplicate does not respect the slice nature of the buffer, making the buffer appendable

e.g let's take this vertx code

    Buffer hello_world = Buffer.buffer("Hello World");
    Buffer hello = hello_world.slice(0, 5); // create a Netty slice
    hello.getByteBuf().writeByte('_'); // getByteBuf duplicates the bytebuf which is  a slice, it is duplicated as a regular byte buf and we get Hello_World which should not be possible
    ((BufferImpl)hello).byteBuf().writeByte(' '); // bytebuf() returns the original buffer, throws an exception because we are using a slice

@conet
Copy link
Author

conet commented Oct 20, 2022

I updated the project to make it a self contained test without external files. Yes the slice operation is the key cause of this but I think that the code should handle it without the corruption.

@vietj
Copy link
Member

vietj commented Oct 20, 2022

wondering first whether the netty slice duplicate should not itself duplicate into a slice as well, or vertx should ensure the correctness.

the javadoc of duplicate says:

A buffer whose readable content is equivalent to the buffer returned by slice(). However this buffer will share the capacity of the underlying buffer, and therefore allows access to all of the underlying content if necessary.

@conet
Copy link
Author

conet commented Oct 20, 2022

That is a good question, if you think that vert.x uses netty correctly then the problem is in netty otherwise it's a problem vert.x needs to fix. My feeling is that duplicate should preserve attributes such as readOnly.

@vietj
Copy link
Member

vietj commented Oct 20, 2022

I don't think read only is involved here, a slice is writable, however it cannot be expanded which cause the issue with SSL handler.

@conet
Copy link
Author

conet commented Oct 20, 2022

OK, I'm not that familiar with all netty buffer attributes, I was thinking read only because making the underlying buffer (the one that is sliced) read only fixes the problem.

@vietj
Copy link
Member

vietj commented Oct 20, 2022

the main difference now I can find is:

    System.out.println(hello.getByteBuf().isWritable(1)); // true
    System.out.println(((BufferImpl)hello).byteBuf().isWritable(1)); // false

so we could use this when we duplicate and determine whether we need to perform extra work, i.e in the case the buffer is not writable then duplicate would do

buffer.unwrap().slice(buffer.readerIndex(), buffer.writeIndex())

@conet
Copy link
Author

conet commented Oct 20, 2022

Yes, I think that would work, that is also the check used by SSLHandler.

@vietj
Copy link
Member

vietj commented Oct 20, 2022

that sound more complex than at first sight, I'm having a look but in reality a sliced buffer has an adjustment field (offset) and it is not possible to know this adjustment field when we use a slice, which means that a vertx sliced buffer should retain the original information when a slice operation happens in vertx I think.

@vietj
Copy link
Member

vietj commented Oct 20, 2022

I'll provide a patch soon and check against the reproducer you provided @conet

@vietj
Copy link
Member

vietj commented Oct 21, 2022

created this issue to check whether that's considered as a ByteBuf defect : netty/netty#12919

@vietj
Copy link
Member

vietj commented Oct 24, 2022

it is not considered as a defect, I believe one way we can do it, is simply by replacing the duplicate by a slice, so vertx buffers will always send slices through the pipeline which will always prevent the buffers to be appended to.

@vietj vietj closed this as completed Oct 25, 2022
@vietj
Copy link
Member

vietj commented Oct 25, 2022

fixed by #4517

gaetanmaisse added a commit to gravitee-io/gravitee-connector-http that referenced this issue Mar 21, 2023
Revert "fix: avoid chunk corruption with tls and http1.1"
This reverts commit a5da167.

BREAKING CHANGE: Need Vert.x `4.3.5` or higher

Need Vert.x `4.3.5` or higher due to this issue eclipse-vertx/vert.x#4509
gaetanmaisse added a commit to gravitee-io/gravitee-connector-http that referenced this issue Mar 21, 2023
Revert "fix: avoid chunk corruption with tls and http1.1"
This reverts commit a5da167.

BREAKING CHANGE: Need Vert.x `4.3.5` or higher

Need Vert.x `4.3.5` or higher due to this issue eclipse-vertx/vert.x#4509
gaetanmaisse added a commit to gravitee-io/gravitee-connector-http that referenced this issue Mar 22, 2023
Revert "fix: avoid chunk corruption with tls and http1.1"
This reverts commit a5da167.

BREAKING CHANGE: Need Vert.x `4.3.5` or higher

Need Vert.x `4.3.5` or higher due to this issue eclipse-vertx/vert.x#4509
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants