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

Fix a possible IllegalReferenceCountException in WebSocket handshake #5495

Merged
merged 2 commits into from
Mar 20, 2024

Conversation

ikhoon
Copy link
Contributor

@ikhoon ikhoon commented Mar 12, 2024

Motivation:

The following IllegalReferenceCountException could be raised during a HTTP/1 Websocket handshake.

01:54:00.031 [armeria-common-worker-kqueue-2-2] WARN  c.l.a.i.client.PendingExceptionUtil - [id: 0xd2fa853c, L:/10.31.29.254:65140 - R:vks-api-prod.linecorp-dev.com/10.120.97.166:443] Unexpected suppressed exception:
com.linecorp.armeria.common.ClosedSessionException: null
Caused by: io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1
	at io.netty.util.internal.ReferenceCountUpdater.toLiveRealRefCnt(ReferenceCountUpdater.java:83)
	at io.netty.util.internal.ReferenceCountUpdater.release(ReferenceCountUpdater.java:148)
	at io.netty.buffer.AbstractReferenceCountedByteBuf.release(AbstractReferenceCountedByteBuf.java:101)
	at io.netty.util.ReferenceCountUtil.release(ReferenceCountUtil.java:90)
	at com.linecorp.armeria.client.WebSocketHttp1ClientChannelHandler.channelRead(WebSocketHttp1ClientChannelHandler.java:246)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
	at io.netty.handler.codec.ByteToMessageDecoder.handlerRemoved(ByteToMessageDecoder.java:266)
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.remove0(CombinedChannelDuplexHandler.java:608)
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.remove(CombinedChannelDuplexHandler.java:593)
	at io.netty.channel.CombinedChannelDuplexHandler.handlerRemoved(CombinedChannelDuplexHandler.java:181)
	at io.netty.channel.AbstractChannelHandlerContext.callHandlerRemoved(AbstractChannelHandlerContext.java:1138)
	at io.netty.channel.DefaultChannelPipeline.callHandlerRemoved0(DefaultChannelPipeline.java:637)
	at io.netty.channel.DefaultChannelPipeline.remove(DefaultChannelPipeline.java:477)
	at io.netty.channel.DefaultChannelPipeline.remove(DefaultChannelPipeline.java:429)
	at com.linecorp.armeria.client.WebSocketHttp1ClientChannelHandler.channelRead(WebSocketHttp1ClientChannelHandler.java:209)

Modifications:

  • Retain a message before passing the next ChannelHandler because it is always released.
  • Remove HttpClientCodec after EMPTY_LAST_CONTENT is received and state becomes UPGRADE_COMPLTE to handle pending messages in the buffer correctly.

Result:

IllegalReferenceCountException no longer occurs during WebSocket handshake.

@ikhoon ikhoon added the defect label Mar 12, 2024
@ikhoon ikhoon added this to the 1.28.0 milestone Mar 12, 2024
Motivation:

The following `IllegalReferenceCountException` could be raised during
HTTP/1 Websocket handshake when `HttpClientCodec` is removed.
```
01:54:00.031 [armeria-common-worker-kqueue-2-2] WARN  c.l.a.i.client.PendingExceptionUtil - [id: 0xd2fa853c, L:/10.31.29.254:65140 - R:vks-api-prod.linecorp-dev.com/10.120.97.166:443] Unexpected suppressed exception:
com.linecorp.armeria.common.ClosedSessionException: null
Caused by: io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1
	at io.netty.util.internal.ReferenceCountUpdater.toLiveRealRefCnt(ReferenceCountUpdater.java:83)
	at io.netty.util.internal.ReferenceCountUpdater.release(ReferenceCountUpdater.java:148)
	at io.netty.buffer.AbstractReferenceCountedByteBuf.release(AbstractReferenceCountedByteBuf.java:101)
	at io.netty.util.ReferenceCountUtil.release(ReferenceCountUtil.java:90)
	at com.linecorp.armeria.client.WebSocketHttp1ClientChannelHandler.channelRead(WebSocketHttp1ClientChannelHandler.java:246)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
	at io.netty.handler.codec.ByteToMessageDecoder.handlerRemoved(ByteToMessageDecoder.java:266)
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.remove0(CombinedChannelDuplexHandler.java:608)
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.remove(CombinedChannelDuplexHandler.java:593)
	at io.netty.channel.CombinedChannelDuplexHandler.handlerRemoved(CombinedChannelDuplexHandler.java:181)
	at io.netty.channel.AbstractChannelHandlerContext.callHandlerRemoved(AbstractChannelHandlerContext.java:1138)
	at io.netty.channel.DefaultChannelPipeline.callHandlerRemoved0(DefaultChannelPipeline.java:637)
	at io.netty.channel.DefaultChannelPipeline.remove(DefaultChannelPipeline.java:477)
	at io.netty.channel.DefaultChannelPipeline.remove(DefaultChannelPipeline.java:429)
	at com.linecorp.armeria.client.WebSocketHttp1ClientChannelHandler.channelRead(WebSocketHttp1ClientChannelHandler.java:209)
```

- If 101 Switching Protocols response is received,
  `WebSocketHttp1ClientChannelHandler` removes `HttpClientCodec` from
  the channel pipeline.
  https://github.com/line/armeria/blob/75d5af1be6770e5e42690e1b6181c40fed1bf568/core/src/main/java/com/linecorp/armeria/client/WebSocketHttp1ClientChannelHandler.java#L195
- The removal may produce remaing buffers via `channelRead()`.
  https://github.com/netty/netty/blob/06a7e12df9118e3ffbeae3cdc50e638a999d93ec/codec/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java#L266-L267
- The packet after the WebSocket handshake comes in, but the state is
  still `NEEDS_HANDSHAKE_RESPONSE`, so the state machine is broken and
  the `ByteBuf` could be double released.
  https://github.com/line/armeria/blob/75d5af1be6770e5e42690e1b6181c40fed1bf568/core/src/main/java/com/linecorp/armeria/client/WebSocketHttp1ClientChannelHandler.java#L165-L167

Modifications:

- Retain a message before passing the next `ChannelHandler` because it
  is released always.
- Remove `HttpClientCodec` after `state` is transitted to `UPGRADE_COMPLTE`
  to correctly handle pending messages in the buffer.

Result:

`IllegalReferenceCountException` no longer occurs during WebSocket handshake.
@ikhoon ikhoon force-pushed the fix-websocket-illegal-release branch from ee642af to 0d28953 Compare March 12, 2024 03:27
Copy link
Member

@minwoox minwoox left a comment

Choose a reason for hiding this comment

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

Nice catch! Thanks!

Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

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

Very nice! Thanks for the analysis + fix! 🙇 👍 🙇

@ikhoon ikhoon modified the milestones: 1.28.0, 1.27.3 Mar 20, 2024
@ikhoon
Copy link
Contributor Author

ikhoon commented Mar 20, 2024

Thanks for all the reviews. 🙇‍♂️🙇‍♂️

@ikhoon ikhoon merged commit bc2454a into line:main Mar 20, 2024
15 of 16 checks passed
@ikhoon ikhoon deleted the fix-websocket-illegal-release branch March 26, 2024 02:52
ikhoon added a commit that referenced this pull request Mar 26, 2024
#5495)

Motivation:

The following `IllegalReferenceCountException` could be raised during a
HTTP/1 Websocket handshake.
```java
01:54:00.031 [armeria-common-worker-kqueue-2-2] WARN  c.l.a.i.client.PendingExceptionUtil - [id: 0xd2fa853c, L:/10.31.29.254:65140 - R:vks-api-prod.linecorp-dev.com/10.120.97.166:443] Unexpected suppressed exception:
com.linecorp.armeria.common.ClosedSessionException: null
Caused by: io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1
	at io.netty.util.internal.ReferenceCountUpdater.toLiveRealRefCnt(ReferenceCountUpdater.java:83)
	at io.netty.util.internal.ReferenceCountUpdater.release(ReferenceCountUpdater.java:148)
	at io.netty.buffer.AbstractReferenceCountedByteBuf.release(AbstractReferenceCountedByteBuf.java:101)
	at io.netty.util.ReferenceCountUtil.release(ReferenceCountUtil.java:90)
	at com.linecorp.armeria.client.WebSocketHttp1ClientChannelHandler.channelRead(WebSocketHttp1ClientChannelHandler.java:246)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
	at io.netty.handler.codec.ByteToMessageDecoder.handlerRemoved(ByteToMessageDecoder.java:266)
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.remove0(CombinedChannelDuplexHandler.java:608)
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.remove(CombinedChannelDuplexHandler.java:593)
	at io.netty.channel.CombinedChannelDuplexHandler.handlerRemoved(CombinedChannelDuplexHandler.java:181)
	at io.netty.channel.AbstractChannelHandlerContext.callHandlerRemoved(AbstractChannelHandlerContext.java:1138)
	at io.netty.channel.DefaultChannelPipeline.callHandlerRemoved0(DefaultChannelPipeline.java:637)
	at io.netty.channel.DefaultChannelPipeline.remove(DefaultChannelPipeline.java:477)
	at io.netty.channel.DefaultChannelPipeline.remove(DefaultChannelPipeline.java:429)
	at com.linecorp.armeria.client.WebSocketHttp1ClientChannelHandler.channelRead(WebSocketHttp1ClientChannelHandler.java:209)
```

- When 101 Switching Protocols response is received,
`WebSocketHttp1ClientChannelHandler` immediately removes
`HttpClientCodec` from the channel pipeline.
https://github.com/line/armeria/blob/75d5af1be6770e5e42690e1b6181c40fed1bf568/core/src/main/java/com/linecorp/armeria/client/WebSocketHttp1ClientChannelHandler.java#L195
- The removal may emit remaining buffers via `channelRead()`.
https://github.com/netty/netty/blob/06a7e12df9118e3ffbeae3cdc50e638a999d93ec/codec/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java#L266-L267
- WebSocket packets after the HTTP/1 WebSocket handshake are passed to
`WebSocketHttp1ClientChannelHandler`, but the state is still
`NEEDS_HANDSHAKE_RESPONSE`. As a result, the state machine becomes
broken and the `ByteBuf` could be double released.
https://github.com/line/armeria/blob/75d5af1be6770e5e42690e1b6181c40fed1bf568/core/src/main/java/com/linecorp/armeria/client/WebSocketHttp1ClientChannelHandler.java#L165-L167

Modifications:

- Retain a message before passing the next `ChannelHandler` because it
is always released.
- Remove `HttpClientCodec` after `EMPTY_LAST_CONTENT` is received and
`state` becomes `UPGRADE_COMPLTE` to handle pending messages in the
buffer correctly.

Result:

`IllegalReferenceCountException` no longer occurs during WebSocket
handshake.
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 this pull request may close these issues.

3 participants