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

Avoid double-free in content branch of NettyHttpRequest.buildBody #6712

Merged
merged 1 commit into from
Feb 8, 2022

Conversation

yawkat
Copy link
Member

@yawkat yawkat commented Jan 5, 2022

When concatenating receivedContent data into a CompositeByteBuf, we call addComponent with the content buffers of the individual receivedContent items. addComponent takes "ownership" of the buffers, so we need to retain them once so that freeing the composite buffer does not free the receivedContent items.

@yawkat yawkat added the type: bug Something isn't working label Jan 5, 2022
@yawkat yawkat added this to the 3.2.5 milestone Jan 5, 2022
@jameskleeh
Copy link
Contributor

How is this issue manifesting itself? Are you seeing warnings etc?

@yawkat
Copy link
Member Author

yawkat commented Jan 5, 2022

You get this stack trace in logs:

15:44:01.360 [default-nioEventLoopGroup-1-3] WARN  i.n.util.concurrent.DefaultPromise - An exception was thrown by io.micronaut.http.server.netty.RoutingInBoundHandler$$Lambda$873/0x00000001008b4040.operationComplete()
io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1
	at io.netty.util.internal.ReferenceCountUpdater.toLiveRealRefCnt(ReferenceCountUpdater.java:74)
	at io.netty.util.internal.ReferenceCountUpdater.release(ReferenceCountUpdater.java:138)
	at io.netty.buffer.AbstractReferenceCountedByteBuf.release(AbstractReferenceCountedByteBuf.java:100)
	at io.netty.buffer.WrappedByteBuf.release(WrappedByteBuf.java:1037)
	at io.netty.buffer.SimpleLeakAwareByteBuf.release(SimpleLeakAwareByteBuf.java:102)
	at io.netty.buffer.AdvancedLeakAwareByteBuf.release(AdvancedLeakAwareByteBuf.java:942)
	at io.netty.handler.codec.http.multipart.AbstractMemoryHttpData.delete(AbstractMemoryHttpData.java:183)
	at io.netty.handler.codec.http.multipart.AbstractHttpData.deallocate(AbstractHttpData.java:120)
	at io.netty.util.AbstractReferenceCounted.handleRelease(AbstractReferenceCounted.java:86)
	at io.netty.util.AbstractReferenceCounted.release(AbstractReferenceCounted.java:76)
	at io.netty.handler.codec.http.multipart.MixedAttribute.release(MixedAttribute.java:352)
	at io.micronaut.http.server.netty.NettyHttpRequest.releaseIfNecessary(NettyHttpRequest.java:368)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at io.micronaut.http.server.netty.NettyHttpRequest.release(NettyHttpRequest.java:346)
	at io.micronaut.http.server.netty.RoutingInBoundHandler.cleanupRequest(RoutingInBoundHandler.java:215)
	at io.micronaut.http.server.netty.RoutingInBoundHandler.lambda$writeFinalNettyResponse$12(RoutingInBoundHandler.java:1194)
	at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:578)
	at io.netty.util.concurrent.DefaultPromise.notifyListeners0(DefaultPromise.java:571)
	at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:550)
	at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:491)
	at io.netty.util.concurrent.DefaultPromise.setValue0(DefaultPromise.java:616)
	at io.netty.util.concurrent.DefaultPromise.setSuccess0(DefaultPromise.java:605)
	at io.netty.util.concurrent.DefaultPromise.trySuccess(DefaultPromise.java:104)
	at io.netty.util.internal.PromiseNotificationUtil.trySuccess(PromiseNotificationUtil.java:48)
	at io.netty.channel.ChannelOutboundBuffer.safeSuccess(ChannelOutboundBuffer.java:717)
	at io.netty.channel.ChannelOutboundBuffer.remove(ChannelOutboundBuffer.java:272)
	at io.netty.channel.ChannelOutboundBuffer.removeBytes(ChannelOutboundBuffer.java:352)
	at io.netty.channel.socket.nio.NioSocketChannel.doWrite(NioSocketChannel.java:414)
	at io.netty.channel.AbstractChannel$AbstractUnsafe.flush0(AbstractChannel.java:949)
	at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.flush0(AbstractNioChannel.java:354)
	at io.netty.channel.AbstractChannel$AbstractUnsafe.flush(AbstractChannel.java:913)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.flush(DefaultChannelPipeline.java:1372)
	at io.netty.channel.AbstractChannelHandlerContext.invokeFlush0(AbstractChannelHandlerContext.java:750)
	at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:742)
	at io.netty.channel.AbstractChannelHandlerContext.flush(AbstractChannelHandlerContext.java:728)
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.flush(CombinedChannelDuplexHandler.java:531)
	at io.netty.channel.ChannelOutboundHandlerAdapter.flush(ChannelOutboundHandlerAdapter.java:125)
	at io.netty.channel.CombinedChannelDuplexHandler.flush(CombinedChannelDuplexHandler.java:356)
	at io.netty.channel.AbstractChannelHandlerContext.invokeFlush0(AbstractChannelHandlerContext.java:750)
	at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:742)
	at io.netty.channel.AbstractChannelHandlerContext.flush(AbstractChannelHandlerContext.java:728)
	at io.netty.channel.ChannelDuplexHandler.flush(ChannelDuplexHandler.java:127)
	at io.netty.channel.AbstractChannelHandlerContext.invokeFlush0(AbstractChannelHandlerContext.java:750)
	at io.netty.channel.AbstractChannelHandlerContext.invokeWriteAndFlush(AbstractChannelHandlerContext.java:765)
	at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:790)
	at io.netty.channel.AbstractChannelHandlerContext.writeAndFlush(AbstractChannelHandlerContext.java:758)
	at io.micronaut.http.netty.stream.HttpStreamsHandler.unbufferedWrite(HttpStreamsHandler.java:385)
	at io.micronaut.http.netty.stream.HttpStreamsServerHandler.unbufferedWrite(HttpStreamsServerHandler.java:178)
	at io.micronaut.http.netty.stream.HttpStreamsServerHandler.unbufferedWrite(HttpStreamsServerHandler.java:58)
	at io.micronaut.http.netty.stream.HttpStreamsHandler.proceedWriteOutgoing(HttpStreamsHandler.java:372)
	at io.micronaut.http.netty.stream.HttpStreamsHandler.write(HttpStreamsHandler.java:352)
	at io.micronaut.http.netty.stream.HttpStreamsServerHandler.write(HttpStreamsServerHandler.java:58)
	at io.netty.channel.AbstractChannelHandlerContext.invokeWrite0(AbstractChannelHandlerContext.java:717)
	at io.netty.channel.AbstractChannelHandlerContext.invokeWrite(AbstractChannelHandlerContext.java:709)
	at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:792)
	at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:702)
	at io.netty.handler.stream.ChunkedWriteHandler.doFlush(ChunkedWriteHandler.java:302)
	at io.netty.handler.stream.ChunkedWriteHandler.flush(ChunkedWriteHandler.java:131)
	at io.netty.channel.AbstractChannelHandlerContext.invokeFlush0(AbstractChannelHandlerContext.java:750)
	at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:742)
	at io.netty.channel.AbstractChannelHandlerContext.flush(AbstractChannelHandlerContext.java:728)
	at io.netty.channel.ChannelOutboundHandlerAdapter.flush(ChannelOutboundHandlerAdapter.java:125)
	at io.netty.channel.AbstractChannelHandlerContext.invokeFlush0(AbstractChannelHandlerContext.java:750)
	at io.netty.channel.AbstractChannelHandlerContext.invokeWriteAndFlush(AbstractChannelHandlerContext.java:765)
	at io.netty.channel.AbstractChannelHandlerContext$WriteTask.run(AbstractChannelHandlerContext.java:1071)
	at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
	at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:469)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:503)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:829)

basically, AbstractMemoryHttpData tries to release its content buffer, but it has already been freed.

@morki
Copy link
Contributor

morki commented Jan 5, 2022

Is this related to #6705?

@yawkat
Copy link
Member Author

yawkat commented Jan 5, 2022

I found it when debugging #6705 but it's not directly related

@yawkat
Copy link
Member Author

yawkat commented Jan 6, 2022

I don't understand the CI failures. I can't reproduce them locally, and the github actions logs aren't helpful. Is there a way to see more details, maybe a gradle build scan?

@yawkat yawkat force-pushed the mixed-refcnt branch 2 times, most recently from 74352ee to 5fb78d8 Compare January 10, 2022 08:34
When concatenating `receivedContent` data into a CompositeByteBuf, we call `addComponent` with the content buffers of the individual `receivedContent` items. `addComponent` takes "ownership" of the buffers, so we need to retain them once so that freeing the composite buffer does not free the `receivedContent` items.
@jameskleeh jameskleeh modified the milestones: 3.2.6, 3.2.7, 3.2.8 Jan 18, 2022
@yawkat yawkat modified the milestones: 3.2.8, 3.3.0 Jan 25, 2022
@jameskleeh jameskleeh modified the milestones: 3.3.0, 3.3.1 Feb 7, 2022
@@ -304,7 +304,8 @@ protected Object buildBody() {
for (ByteBufHolder holder : receivedContent) {
ByteBuf content = holder.content();
if (content != null) {
byteBufs.addComponent(true, content);
// need to retain content, because for addComponent "ownership of buffer is transferred to this CompositeByteBuf."
byteBufs.addComponent(true, content.retain());
Copy link
Contributor

Choose a reason for hiding this comment

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

The content is already retained when the addContent method is called. Can you explain why it needs to be retained again and how retaining the content twice wouldn't lead to a memory leak without an additional release?

Copy link
Member Author

Choose a reason for hiding this comment

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

The retain in addContent is on the attribute. It does not affect the buffer itself.

The release of the attribute in receivedContent is done in NettyHttpRequest.release

Copy link
Contributor

Choose a reason for hiding this comment

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

How can you be sure that addContent is always called with an attribute? It can be called even if the request is not multipart

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

The other cases are probably HttpData, which is handled by the receivedData branch?

But it should be safe anyway. When a holder is added, it is retained, and it is released when the request is released. For this new retain, the corresponding release is when the composite buffer is released by the caller. So there is a corresponding release for each retain.

@jameskleeh jameskleeh merged commit e4847a7 into 3.2.x Feb 8, 2022
@jameskleeh jameskleeh deleted the mixed-refcnt branch February 8, 2022 17:02
yawkat added a commit that referenced this pull request Feb 9, 2022
This is an attempt at a fix for the flaky FuzzyInputSpec. Can't reproduce that locally, but this code is obviously buggy, so this might help.

`body` is a memoized supplier, but `NettyHttpRequest.release` checks whether it's a reference counted object. The supplier is never reference counted, but the object it supplies might be. This patch stores the unwrapped value as a separate field so that it can be released properly.

The test for #6712 still passes, so this doesn't seem to regress the issue that the patch that caused this leak fixed.
jameskleeh added a commit that referenced this pull request Feb 10, 2022
* Release unwrapped body in NettyHttpRequest
This is an attempt at a fix for the flaky FuzzyInputSpec. Can't reproduce that locally, but this code is obviously buggy, so this might help.

`body` is a memoized supplier, but `NettyHttpRequest.release` checks whether it's a reference counted object. The supplier is never reference counted, but the object it supplies might be. This patch stores the unwrapped value as a separate field so that it can be released properly.

The test for #6712 still passes, so this doesn't seem to regress the issue that the patch that caused this leak fixed.

* fix setBody

* Release buffers in tests

Co-authored-by: jameskleeh <james.kleeh@gmail.com>
alvarosanchez pushed a commit that referenced this pull request Feb 17, 2022
* Release unwrapped body in NettyHttpRequest
This is an attempt at a fix for the flaky FuzzyInputSpec. Can't reproduce that locally, but this code is obviously buggy, so this might help.

`body` is a memoized supplier, but `NettyHttpRequest.release` checks whether it's a reference counted object. The supplier is never reference counted, but the object it supplies might be. This patch stores the unwrapped value as a separate field so that it can be released properly.

The test for #6712 still passes, so this doesn't seem to regress the issue that the patch that caused this leak fixed.

* fix setBody

* Release buffers in tests

Co-authored-by: jameskleeh <james.kleeh@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants