Skip to content

Commit e0ed7ff

Browse files
committed
fix(noise): rethrow the underlying Error, not its wrapper
NoiseXXCodec.exceptionCaught detected a fatal JVM Error nested in the causal chain via hasCauseOfType(Error::class) but then did throw cause, rethrowing whatever Netty handed in. On the encode() path Netty wraps throwables in EncoderException, so an OutOfMemoryError was rethrown as a RuntimeException wrapper, downgrading the fatal JVM signal. Add findCauseOfType to extract the actual Error from the causal chain and rethrow that. Covered by a test feeding EncoderException(OutOfMemoryError).
1 parent e9480e3 commit e0ed7ff

3 files changed

Lines changed: 28 additions & 3 deletions

File tree

libp2p/src/main/kotlin/io/libp2p/etc/types/OtherExt.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,9 @@ fun <T : Throwable> Throwable.hasCauseOfType(clazz: KClass<T>) =
4444
Throwables.getCausalChain(this)
4545
.filter(clazz::isInstance)
4646
.any()
47+
48+
fun <T : Throwable> Throwable.findCauseOfType(clazz: KClass<T>): T? =
49+
Throwables.getCausalChain(this)
50+
.filter(clazz::isInstance)
51+
.map { clazz.java.cast(it) }
52+
.firstOrNull()

libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXCodec.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.libp2p.security.noise
22

33
import com.southernstorm.noise.protocol.CipherState
4+
import io.libp2p.etc.types.findCauseOfType
45
import io.libp2p.etc.types.hasCauseOfType
56
import io.libp2p.etc.types.toByteArray
67
import io.libp2p.security.CantDecryptInboundException
@@ -49,13 +50,17 @@ class NoiseXXCodec(val aliceCipher: CipherState, val bobCipher: CipherState) :
4950
} else if (cause.hasCauseOfType(SecureChannelError::class)) {
5051
logger.debug("Invalid Noise content", cause)
5152
closeAbruptly(ctx)
52-
} else if (cause.hasCauseOfType(Error::class)) {
53+
} else if (cause.findCauseOfType(Error::class) != null) {
5354
// A fatal JVM error (e.g. OutOfMemoryError) must never be swallowed: close the
5455
// channel to release resources, then rethrow so the failure is surfaced rather
5556
// than downgraded to a single log line while the event loop keeps running.
56-
logger.error("Fatal error in Noise channel, closing", cause)
57+
// Netty wraps throwables from encode()/decode() (e.g. in EncoderException), so we
58+
// extract and rethrow the actual Error rather than the runtime wrapper, which would
59+
// otherwise downgrade the fatal JVM signal.
60+
val fatal = cause.findCauseOfType(Error::class)!!
61+
logger.error("Fatal error in Noise channel, closing", fatal)
5762
closeAbruptly(ctx)
58-
throw cause
63+
throw fatal
5964
} else {
6065
logger.error("Unexpected error in Noise channel", cause)
6166
closeAbruptly(ctx)

libp2p/src/test/kotlin/io/libp2p/security/noise/NoiseXXCodecTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.southernstorm.noise.protocol.CipherState
44
import io.mockk.mockk
55
import io.netty.channel.ChannelHandlerContext
66
import io.netty.channel.embedded.EmbeddedChannel
7+
import io.netty.handler.codec.EncoderException
78
import org.assertj.core.api.Assertions.assertThat
89
import org.assertj.core.api.Assertions.assertThatThrownBy
910
import org.junit.jupiter.api.Test
@@ -40,6 +41,19 @@ class NoiseXXCodecTest {
4041
assertThat(channel.isOpen).isFalse()
4142
}
4243

44+
@Test
45+
fun `wrapped fatal Error is unwrapped and rethrown as the Error`() {
46+
val codec = codec()
47+
val (_, ctx) = channelWith(codec)
48+
val oom = OutOfMemoryError("Java heap space")
49+
// Netty wraps throwables from encode() in an EncoderException; the fatal JVM signal
50+
// must be surfaced as the Error itself, not downgraded to the runtime wrapper.
51+
val wrapped = EncoderException(oom)
52+
53+
assertThatThrownBy { codec.exceptionCaught(ctx, wrapped) }
54+
.isSameAs(oom)
55+
}
56+
4357
@Test
4458
fun `unexpected runtime exception closes the channel`() {
4559
val codec = codec()

0 commit comments

Comments
 (0)