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

Data corruption due to interaction between SslHandler and HttpObjectEncoder #11792

Closed
trustin opened this issue Oct 25, 2021 · 5 comments · Fixed by #11802
Closed

Data corruption due to interaction between SslHandler and HttpObjectEncoder #11792

trustin opened this issue Oct 25, 2021 · 5 comments · Fixed by #11802
Labels

Comments

@trustin
Copy link
Member

trustin commented Oct 25, 2021

TL;DR:

HttpObjectEncoder sends a duplicate of its static final ByteBuf field to encode HTTP chunks, such as 0\r\n\r\n. This reused buffer is neither read-only nor unexpandable, and thus anyone in the pipeline can modify or expand it. As a result, SslHandler can sometimes expand it and append stuff to it, causing unexpected data corruption. It'd be nice if this issue is fixed soon and a new release is out given its severity.

Here's the full analysis from my colleague @alexc-db:

  1. When Channel.flush() is called, SslHandler starts getting items from the pendingUnencryptedWrites queue [1] - it gets entries from the internal bufAndListenerPairs and accumulates them in the toReturn buffer [2].
  2. For the first entry, toReturn is null and thus composeFirst is called. That method by default just returns the reference to the first entry (it’s overridden in SslHandlerCoalescingBufferQueue to have special handling for CompositeByteBuf, but it’s not relevant to the issue here). The following calls are calling compose and pass toReturn there.
  3. Internally, compose calls attemptCopyToCumulation which checks if toReturn is writable and its capacity can be expanded to write new bytes from the following queued buffers - if it passes checks in [3], attemptCopyToCumulation copies bytes from the next entries to the original toReturn buffer (as we saw above it can be the first entry), if it does not pass the checks - SslHandlerCoalescingBufferQueue calls copyAndCompose which creates a new bigger buf and copies bytes from toReturn and next entry there, making it a new toReturn.
  4. Given the behavior above - if flush is called and bufAndListenerPairs has multiple buffers queued where the first entry is writable, had enough capacity for the bytes in the next entries, and shared across multiple threads - two threads will start copying bytes into the same memory addresses which will cause corruption. In our local reproducer that has more dependencies that just Netty, it was ZERO_CRLF_CRLF_BUF [4] (duplicate() call doesn’t make a copy, just wraps the original direct buffer).
    I was able to make tests pass by marking ZERO_CRLF_CRLF_BUF read-only [5].
    [1] https://github.com/netty/netty/blob/netty-4.1.68.Final/handler/src/main/java/io/netty/handler/ssl/SslHandler.java#L817
    [2] https://github.com/netty/netty/blob/netty-4.1.68.Final/transport/src/main/java/io/netty/channel/AbstractCoalescingBufferQueue.java#L175
    [3] https://github.com/netty/netty/blob/netty-4.1.68.Final/handler/src/main/java/io/netty/handler/ssl/SslHandler.java#L2326-L2334
    [4] https://github.com/netty/netty/blob/netty-4.1.68.Final/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java#L210
    [5]
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java (revision 7d34282f9d2ffdd64c91cb4780b09902d9779b92)
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java (date 1635112426650)
@@ -50,9 +50,9 @@
     static final int CRLF_SHORT = (CR << 8) | LF;
     private static final int ZERO_CRLF_MEDIUM = ('0' << 16) | CRLF_SHORT;
     private static final byte[] ZERO_CRLF_CRLF = { '0', CR, LF, CR, LF };
-    private static final ByteBuf CRLF_BUF = unreleasableBuffer(directBuffer(2).writeByte(CR).writeByte(LF));
+    private static final ByteBuf CRLF_BUF = unreleasableBuffer(directBuffer(2).writeByte(CR).writeByte(LF).asReadOnly());
     private static final ByteBuf ZERO_CRLF_CRLF_BUF = unreleasableBuffer(directBuffer(ZERO_CRLF_CRLF.length)
-            .writeBytes(ZERO_CRLF_CRLF));
+            .writeBytes(ZERO_CRLF_CRLF).asReadOnly());
     private static final float HEADERS_WEIGHT_NEW = 1 / 5f;
     private static final float HEADERS_WEIGHT_HISTORICAL = 1 - HEADERS_WEIGHT_NEW;
     private static final float TRAILERS_WEIGHT_NEW = HEADERS_WEIGHT_NEW;

Netty version

4.1.68.Final and 4.1.69.Final and of course possible in older versions.

@trustin trustin added the defect label Oct 25, 2021
trustin added a commit to trustin/armeria that referenced this issue Oct 25, 2021
Motivation:

There's a chance of HTTP content corruption when:

- The current connection is TLS;
- The current response is using chunked encoding; and
- The content length is greater than 16378 (16384 - 6).

.. due to a bug in Netty: netty/netty#11792

Modifications:

- Fixed the math mistake in `Http1ObjectEncoder.MAX_TLS_DATA_LENGTH`,
  to prevent `HttpObjectEncoder` from putting its static final `ByteBuf`
  in the first place of `SslHandler`'s internal queue.

Result:

- No more HTTP content conrruption.
trustin added a commit to trustin/armeria that referenced this issue Oct 25, 2021
Motivation:

There's a chance of HTTP content corruption when:

- The current connection is TLS;
- The current response is using chunked encoding; and
- The content length is greater than 16378 (16384 - 6).

.. due to a bug in Netty: netty/netty#11792

Modifications:

- Fixed the math mistake in `Http1ObjectEncoder.MAX_TLS_DATA_LENGTH`,
  to prevent `HttpObjectEncoder` from putting its static final `ByteBuf`
  in the first place of `SslHandler`'s internal queue.

Result:

- No more HTTP content conrruption.
@normanmaurer
Copy link
Member

@trustin can you just do a PR so we can easily merge and attribute ?

trustin added a commit to line/armeria that referenced this issue Oct 25, 2021
Motivation:

There's a chance of HTTP content corruption when:

- The current connection is HTTP/1 over TLS;
- The current response is using chunked encoding; and
- The content length is greater than 16378 (16384 - 6).

.. due to a bug in Netty: netty/netty#11792

Modifications:

- Fixed the math mistake in `Http1ObjectEncoder.MAX_TLS_DATA_LENGTH`,
  to prevent `HttpObjectEncoder` from putting its static final `ByteBuf`
  in the first place of `SslHandler`'s internal queue.

Result:

- No more HTTP content corruption.
@alexc-db
Copy link
Contributor

@normanmaurer @trustin Do you have a preference/opinion on how to prevent this class of issues going forward? There are a few more instances where static buffers are not marked as read-only, quick search found [1][2], there may be more. We can add unit-tests for the affected classes, but it will not prevent new instances in other places. Ideally, there should be a compile-time check or validation that'll catch issues like that during development, before changes are merged.

[1] https://github.com/netty/netty/blob/netty-4.1.68.Final/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameEncoder.java#L34-L39
[2] https://github.com/netty/netty/blob/netty-4.1.68.Final/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java#L36-L37

@trustin
Copy link
Member Author

trustin commented Oct 26, 2021

@normanmaurer, @alexc-db will send out a PR.

@alexc-db, I'd recommend 1) making sure all static final ByteBuf instantiation involves a certain new utility method that makes a buffer read-only, e.g. ByteBufUtil.newConstantBuf(ByteBuf) and 2) writing a Checkstyle rule for it, as we discussed offline. What do you think, @normanmaurer?

@normanmaurer
Copy link
Member

@trustin sounds good!

@alexc-db
Copy link
Contributor

Added validation check in netty/netty-build#12, seems like I don't have permissions to assign reviewers, @normanmaurer - could you please take a look?

normanmaurer pushed a commit to netty/netty-build that referenced this issue Oct 29, 2021
Add validation for static final byte buffers to make sure they are unreleasable and read-only as a follow up for netty/netty#11792. Also enabled support for checkstyle warning suppression using `SuppressWarnings` annotation.

Verified by installing it locally and running it against local Netty code. Example:

```
→ mvn validate
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Detecting the operating system and CPU architecture
[INFO] ------------------------------------------------------------------------
[INFO] os.detected.name: osx
[INFO] os.detected.arch: x86_64
[INFO] os.detected.bitness: 64
[INFO] os.detected.version: 10.16
[INFO] os.detected.version.major: 10
[INFO] os.detected.version.minor: 16
[INFO] os.detected.classifier: osx-x86_64
[INFO]
[INFO] ---------------------< io.netty:netty-codec-http >----------------------
[INFO] Building Netty/Codec/HTTP 4.1.70.Final-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-enforcer-plugin:1.4.1:enforce (enforce-maven) @ netty-codec-http ---
[INFO]
[INFO] --- maven-enforcer-plugin:1.4.1:enforce (enforce-tools) @ netty-codec-http ---
[INFO]
[INFO] --- maven-checkstyle-plugin:3.1.0:check (check-style) @ netty-codec-http ---
[INFO] Starting audit...
[ERROR] /Users/alex.chekunkov/Dev/github.com/alexc-db/netty/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java:53: static final buffer assignment should match pattern (Unpooled\s*\.)?unreleasableBuffer\(.*?\)\s*\.asReadOnly\(\) [StaticFinalBuffer]
[ERROR] /Users/alex.chekunkov/Dev/github.com/alexc-db/netty/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java:54: static final buffer assignment should match pattern (Unpooled\s*\.)?unreleasableBuffer\(.*?\)\s*\.asReadOnly\(\) [StaticFinalBuffer]
[ERROR] /Users/alex.chekunkov/Dev/github.com/alexc-db/netty/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameEncoder.java:34: static final buffer assignment should match pattern (Unpooled\s*\.)?unreleasableBuffer\(.*?\)\s*\.asReadOnly\(\) [StaticFinalBuffer]
[ERROR] /Users/alex.chekunkov/Dev/github.com/alexc-db/netty/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameEncoder.java:36: static final buffer assignment should match pattern (Unpooled\s*\.)?unreleasableBuffer\(.*?\)\s*\.asReadOnly\(\) [StaticFinalBuffer]
[ERROR] /Users/alex.chekunkov/Dev/github.com/alexc-db/netty/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameEncoder.java:38: static final buffer assignment should match pattern (Unpooled\s*\.)?unreleasableBuffer\(.*?\)\s*\.asReadOnly\(\) [StaticFinalBuffer]
Audit done.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.755 s
[INFO] Finished at: 2021-10-26T20:00:56-07:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-checkstyle-plugin:3.1.0:check (check-style) on project netty-codec-http: Failed during checkstyle execution: There are 5 errors reported by Checkstyle 8.29 with io/netty/checkstyle.xml ruleset. -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
```

Findings from the check are fixed in netty/netty#11802.
normanmaurer pushed a commit that referenced this issue Nov 3, 2021
Motivation:

Fix #11792 and avoid potential corruption issues caused by writable shared buffers.

TL;DR:

`HttpObjectEncoder` sends a duplicate of its static final `ByteBuf` field to encode HTTP chunks, such as `0\r\n\r\n`. This reused buffer is neither read-only nor unexpandable, and thus anyone in the pipeline can modify or expand it. As a result, `SslHandler` can sometimes expand it and append stuff to it, causing unexpected data corruption.

Modification:

Most of the static final buffers in the project are made unreleasable and read-only by wrapping them in `Unpooled.unreleasableBuffer(...).asReadOnly()`. Buffers that were excluded:

- Unpooled.EMPTY_BUFFER - EmptyByteBuf instances are not readable/writable/releasable. Wrapping this buffer in `Unpooled.unreleasableBuffer(...).asReadOnly()` would change the instance type, since it's a public API it could be a potential breaking change.
- ReplayingDecoderByteBuf.EMPTY_BUFFER - since it uses Unpooled.EMPTY_BUFFER and not releasable too.

Excluded buffers are marked with `SuppressWarnings` to skip check added in netty/netty-build#12.

Result:

Fixes #11792.
normanmaurer pushed a commit that referenced this issue Nov 3, 2021
Motivation:

Fix #11792 and avoid potential corruption issues caused by writable shared buffers.

TL;DR:

`HttpObjectEncoder` sends a duplicate of its static final `ByteBuf` field to encode HTTP chunks, such as `0\r\n\r\n`. This reused buffer is neither read-only nor unexpandable, and thus anyone in the pipeline can modify or expand it. As a result, `SslHandler` can sometimes expand it and append stuff to it, causing unexpected data corruption.

Modification:

Most of the static final buffers in the project are made unreleasable and read-only by wrapping them in `Unpooled.unreleasableBuffer(...).asReadOnly()`. Buffers that were excluded:

- Unpooled.EMPTY_BUFFER - EmptyByteBuf instances are not readable/writable/releasable. Wrapping this buffer in `Unpooled.unreleasableBuffer(...).asReadOnly()` would change the instance type, since it's a public API it could be a potential breaking change.
- ReplayingDecoderByteBuf.EMPTY_BUFFER - since it uses Unpooled.EMPTY_BUFFER and not releasable too.

Excluded buffers are marked with `SuppressWarnings` to skip check added in netty/netty-build#12.

Result:

Fixes #11792.
JeetKunDoug added a commit to JeetKunDoug/vertx-dependencies that referenced this issue Nov 12, 2021
laosijikaichele pushed a commit to laosijikaichele/netty that referenced this issue Dec 16, 2021
Motivation:

Fix netty#11792 and avoid potential corruption issues caused by writable shared buffers.

TL;DR:

`HttpObjectEncoder` sends a duplicate of its static final `ByteBuf` field to encode HTTP chunks, such as `0\r\n\r\n`. This reused buffer is neither read-only nor unexpandable, and thus anyone in the pipeline can modify or expand it. As a result, `SslHandler` can sometimes expand it and append stuff to it, causing unexpected data corruption.

Modification:

Most of the static final buffers in the project are made unreleasable and read-only by wrapping them in `Unpooled.unreleasableBuffer(...).asReadOnly()`. Buffers that were excluded:

- Unpooled.EMPTY_BUFFER - EmptyByteBuf instances are not readable/writable/releasable. Wrapping this buffer in `Unpooled.unreleasableBuffer(...).asReadOnly()` would change the instance type, since it's a public API it could be a potential breaking change.
- ReplayingDecoderByteBuf.EMPTY_BUFFER - since it uses Unpooled.EMPTY_BUFFER and not releasable too.

Excluded buffers are marked with `SuppressWarnings` to skip check added in netty/netty-build#12.

Result:

Fixes netty#11792.
laosijikaichele pushed a commit to laosijikaichele/netty that referenced this issue Dec 16, 2021
Motivation:

Fix netty#11792 and avoid potential corruption issues caused by writable shared buffers.

TL;DR:

`HttpObjectEncoder` sends a duplicate of its static final `ByteBuf` field to encode HTTP chunks, such as `0\r\n\r\n`. This reused buffer is neither read-only nor unexpandable, and thus anyone in the pipeline can modify or expand it. As a result, `SslHandler` can sometimes expand it and append stuff to it, causing unexpected data corruption.

Modification:

Most of the static final buffers in the project are made unreleasable and read-only by wrapping them in `Unpooled.unreleasableBuffer(...).asReadOnly()`. Buffers that were excluded:

- Unpooled.EMPTY_BUFFER - EmptyByteBuf instances are not readable/writable/releasable. Wrapping this buffer in `Unpooled.unreleasableBuffer(...).asReadOnly()` would change the instance type, since it's a public API it could be a potential breaking change.
- ReplayingDecoderByteBuf.EMPTY_BUFFER - since it uses Unpooled.EMPTY_BUFFER and not releasable too.

Excluded buffers are marked with `SuppressWarnings` to skip check added in netty/netty-build#12.

Result:

Fixes netty#11792.
raidyue pushed a commit to raidyue/netty that referenced this issue Jul 8, 2022
Motivation:

Fix netty#11792 and avoid potential corruption issues caused by writable shared buffers.

TL;DR:

`HttpObjectEncoder` sends a duplicate of its static final `ByteBuf` field to encode HTTP chunks, such as `0\r\n\r\n`. This reused buffer is neither read-only nor unexpandable, and thus anyone in the pipeline can modify or expand it. As a result, `SslHandler` can sometimes expand it and append stuff to it, causing unexpected data corruption.

Modification:

Most of the static final buffers in the project are made unreleasable and read-only by wrapping them in `Unpooled.unreleasableBuffer(...).asReadOnly()`. Buffers that were excluded:

- Unpooled.EMPTY_BUFFER - EmptyByteBuf instances are not readable/writable/releasable. Wrapping this buffer in `Unpooled.unreleasableBuffer(...).asReadOnly()` would change the instance type, since it's a public API it could be a potential breaking change.
- ReplayingDecoderByteBuf.EMPTY_BUFFER - since it uses Unpooled.EMPTY_BUFFER and not releasable too.

Excluded buffers are marked with `SuppressWarnings` to skip check added in netty/netty-build#12.

Result:

Fixes netty#11792.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants