Skip to content

Fix reentrancy bug in ByteToMessageDecoder#15833

Merged
chrisvest merged 8 commits into
netty:4.2from
chrisvest:4.2-fix-btmd-reentrancy
Nov 12, 2025
Merged

Fix reentrancy bug in ByteToMessageDecoder#15833
chrisvest merged 8 commits into
netty:4.2from
chrisvest:4.2-fix-btmd-reentrancy

Conversation

@chrisvest
Copy link
Copy Markdown
Member

Motivation:
If the ByteToMessageDecoder gets a reentrant channelRead call, we need to avoid closing or otherwise manipulating the cumulation buffer.

Modification:
Guard reentrant channelRead calls by queueing up messages and letting the top-level call process them all in order.

Result:
Reentrant calls to ByteToMessageDecoder.channelRead will no longer cause weird IllegalReferenceCountException on the cumulation buffer.

Motivation:
If the ByteToMessageDecoder gets a reentrant channelRead call, we need to avoid closing or otherwise manipulating the cumulation buffer.

Modification:
Guard reentrant channelRead calls by queueing up messages and letting the top-level call process them all in order.

Result:
Reentrant calls to ByteToMessageDecoder.channelRead will no longer cause weird IllegalReferenceCountException on the cumulation buffer.
@chrisvest
Copy link
Copy Markdown
Member Author

This will fix the build errors in #15830

@chrisvest chrisvest added this to the 4.2.8.Final milestone Nov 8, 2025
@chrisvest
Copy link
Copy Markdown
Member Author

This needs to be back ported to 4.1 as well.

@chrisvest
Copy link
Copy Markdown
Member Author

Opened 4.1 version of this PR in #15834

Comment thread codec-base/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java Outdated
Comment thread codec-base/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java Outdated
ByteBuf buf2 = channel.alloc().buffer();
buf2.writeLong(42); // Adding 8 bytes.
channel.writeInbound(buf2); // Reentrant call back into ByteToMessageDecoder
ctx.read(); // With LocalChannel this would be a reentrant call. For EmbeddedChannel it's harmless.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

so do you want to do also add a test for LocalChannel ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've been trying to do this. It gets pretty complicated and hard to follow.

Comment thread codec-base/src/test/java/io/netty/handler/codec/ByteToMessageDecoderTest.java Outdated
Comment thread codec-base/src/test/java/io/netty/handler/codec/ByteToMessageDecoderTest.java Outdated
Comment thread codec-base/src/test/java/io/netty/handler/codec/ByteToMessageDecoderTest.java Outdated
Comment thread codec-base/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java Outdated
@yawkat
Copy link
Copy Markdown
Contributor

yawkat commented Nov 8, 2025

btw what exactly triggers this? ive tried to design handlers that can produce multiple elements at a time (eg the decompressors) in such a way that nested channelRead calls aren't triggered in the first place, and are instead "held back" upstream

@chrisvest
Copy link
Copy Markdown
Member Author

@yawkat the case I ran into was using x25519mlkem key exchange in the SslHandler on the local transport. I think the exchange messages got so big that they needed multiple reads to deliver, so when autoRead is off, the SslHandler will at one point do an unwrap, get buffer underflow, then call ctx.read(), which reenters the ByteToMessageDecoder.channelRead.

@yawkat
Copy link
Copy Markdown
Contributor

yawkat commented Nov 9, 2025

Sounds like sslhandler should wait until channelReadComplete to do the read call

@normanmaurer
Copy link
Copy Markdown
Member

Sounds like sslhandler should wait until channelReadComplete to do the read call

I think while this might also fix it we should better ensure the ByteToMessageDecoder does not mess up internally when reentrancy happens.

Comment thread codec-base/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java Outdated
Comment thread codec-base/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java Outdated
Comment thread codec-base/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java Outdated
Comment thread codec-base/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java Outdated
ctx.fireChannelRead(input);
}
}
} while (inputMessages != null && (input = inputMessages.pollFirst()) != null);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I also wonder if we need to do anything special in handlerRemoved now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

handlerRemoved is fine because it just punts the removal to the decodeRemovalReentryProtection method… but there, I wonder if we should also wait until the inputMessages is null or empty

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added a test to capture this.

@chrisvest
Copy link
Copy Markdown
Member Author

I got an ok from Norman after he closed #15844

Merging

@chrisvest chrisvest merged commit 6e199a9 into netty:4.2 Nov 12, 2025
34 of 35 checks passed
@chrisvest chrisvest deleted the 4.2-fix-btmd-reentrancy branch November 12, 2025 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants