Skip to content

Commit c6e03a5

Browse files
committed
Release retained mplex frame slice on invalid stream tag
MplexFrameCodec.decode() called data.retain() before validating the stream tag via MplexFlag.getByValue(). For streamTag == 7 the lookup throws IllegalArgumentException and the retained slice leaks the underlying inbound buffer. A remote peer could repeatedly send such frames to exhaust direct memory.
1 parent 461a526 commit c6e03a5

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrameCodec.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,12 @@ class MplexFrameCodec(
7373
val streamId = header.shr(3)
7474
val data = msg.readSlice(lenData.toInt())
7575
data.retain() // MessageToMessageCodec releases original buffer, but it needs to be relayed
76-
val flag = MplexFlag.getByValue(streamTag)
76+
val flag = try {
77+
MplexFlag.getByValue(streamTag)
78+
} catch (e: IllegalArgumentException) {
79+
data.release()
80+
throw e
81+
}
7782
val mplexFrame = MplexFrame(MplexId(ctx.channel().id(), streamId, !flag.isInitiator), flag, data)
7883
out.add(mplexFrame)
7984
}

libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexFrameCodecTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,29 @@ class MplexFrameCodecTest {
117117
}
118118
}
119119

120+
@Test
121+
fun `invalid stream tag does not leak inbound buffer`() {
122+
// streamTag = 7 (header & 0x07) is not mapped in MplexFlag, so
123+
// MplexFlag.getByValue() throws AFTER data.retain() has been called.
124+
// Without releasing the retained slice, the parent inbound buffer leaks.
125+
val rawBytes = Unpooled.buffer()
126+
.writeByte(0x07) // varint header: streamId=0, streamTag=7 (invalid)
127+
.writeByte(0x05) // varint lenData = 5
128+
.writeBytes(byteArrayOf(1, 2, 3, 4, 5))
129+
130+
rawBytes.retain() // keep an external ref to observe the codec's net effect
131+
assertEquals(2, rawBytes.refCnt())
132+
133+
assertThrows<DecoderException> {
134+
channel.writeInbound(rawBytes)
135+
}
136+
137+
// Only our external ref should remain. Without the fix, the retained
138+
// slice keeps an extra ref alive (refCnt == 2 instead of 1).
139+
assertEquals(1, rawBytes.refCnt())
140+
rawBytes.release()
141+
}
142+
120143
@Test
121144
fun `check the frame underlying buffer is released after send`() {
122145
val frameDataBuf = "Hello-1".toByteArray().toByteBuf()

0 commit comments

Comments
 (0)