This is an automated email from the ASF dual-hosted git repository.
HTHou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 40e7ce62936 Reject truncated Thrift containers before allocation
(#18631)
40e7ce62936 is described below
commit 40e7ce629363734f7983626f18967f4ceedcb86d
Author: Haonan <[email protected]>
AuthorDate: Tue Oct 6 22:13:11 2026 +0800
Reject truncated Thrift containers before allocation (#18631)
* Harden Thrift container deserialization limits
* Add generated Thrift request regression test
* Validate Thrift reads against remaining frame data
---
.../en/org/apache/iotdb/rpc/i18n/RpcMessages.java | 3 +
.../zh/org/apache/iotdb/rpc/i18n/RpcMessages.java | 3 +
.../apache/iotdb/rpc/TElasticFramedTransport.java | 42 +++-
.../iotdb/rpc/TElasticFramedTransportReadTest.java | 266 +++++++++++++++++++++
4 files changed, 302 insertions(+), 12 deletions(-)
diff --git
a/iotdb-client/service-rpc/src/main/i18n/en/org/apache/iotdb/rpc/i18n/RpcMessages.java
b/iotdb-client/service-rpc/src/main/i18n/en/org/apache/iotdb/rpc/i18n/RpcMessages.java
index b9d76aa4fd9..33778bf8fb2 100644
---
a/iotdb-client/service-rpc/src/main/i18n/en/org/apache/iotdb/rpc/i18n/RpcMessages.java
+++
b/iotdb-client/service-rpc/src/main/i18n/en/org/apache/iotdb/rpc/i18n/RpcMessages.java
@@ -35,6 +35,9 @@ public final class RpcMessages {
"Frame size (%d) larger than protect max size (%d)%s!";
public static final String FRAME_ERROR_STRING_LENGTH_EXCEEDED =
"String length (%d) larger than protect max size (%d)%s!";
+ public static final String
+
EXCEPTION_REQUIRED_READ_SIZE_ARG_EXCEEDS_REMAINING_FRAME_SIZE_ARG_ARG_9C0541EE =
+ "Required read size (%d) exceeds remaining frame size (%d)%s!";
// TElasticFramedTransport - SSL
public static final String NON_SSL_TO_SSL_PORT =
diff --git
a/iotdb-client/service-rpc/src/main/i18n/zh/org/apache/iotdb/rpc/i18n/RpcMessages.java
b/iotdb-client/service-rpc/src/main/i18n/zh/org/apache/iotdb/rpc/i18n/RpcMessages.java
index 5ddfe9fc0b2..81b519fd477 100644
---
a/iotdb-client/service-rpc/src/main/i18n/zh/org/apache/iotdb/rpc/i18n/RpcMessages.java
+++
b/iotdb-client/service-rpc/src/main/i18n/zh/org/apache/iotdb/rpc/i18n/RpcMessages.java
@@ -32,6 +32,9 @@ public final class RpcMessages {
"帧大小 (%d) 超过保护最大值 (%d)%s!";
public static final String FRAME_ERROR_STRING_LENGTH_EXCEEDED =
"字符串长度 (%d) 超过保护最大值 (%d)%s!";
+ public static final String
+
EXCEPTION_REQUIRED_READ_SIZE_ARG_EXCEEDS_REMAINING_FRAME_SIZE_ARG_ARG_9C0541EE =
+ "请求读取的大小 (%d) 超过当前帧剩余大小 (%d)%s!";
// TElasticFramedTransport - SSL
public static final String NON_SSL_TO_SSL_PORT =
diff --git
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TElasticFramedTransport.java
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TElasticFramedTransport.java
index 31f20f5c381..d0c6a3d24ca 100644
---
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TElasticFramedTransport.java
+++
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TElasticFramedTransport.java
@@ -245,7 +245,9 @@ public class TElasticFramedTransport extends TTransport {
TLS_REQUEST(RpcMessages.FRAME_ERROR_TLS_REQUEST),
NEGATIVE_FRAME_SIZE(RpcMessages.FRAME_ERROR_NEGATIVE_FRAME_SIZE),
FRAME_SIZE_EXCEEDED(RpcMessages.FRAME_ERROR_FRAME_SIZE_EXCEEDED),
- STRING_LENGTH_EXCEEDED(RpcMessages.FRAME_ERROR_STRING_LENGTH_EXCEEDED);
+ STRING_LENGTH_EXCEEDED(RpcMessages.FRAME_ERROR_STRING_LENGTH_EXCEEDED),
+ INSUFFICIENT_FRAME_DATA(
+
RpcMessages.EXCEPTION_REQUIRED_READ_SIZE_ARG_EXCEEDS_REMAINING_FRAME_SIZE_ARG_ARG_9C0541EE);
private final String messageFormat;
@@ -255,7 +257,9 @@ public class TElasticFramedTransport extends TTransport {
void throwException(long size, String remoteInfo, int maxSize) throws
TTransportException {
String message =
- (this == FRAME_SIZE_EXCEEDED || this == STRING_LENGTH_EXCEEDED)
+ (this == FRAME_SIZE_EXCEEDED
+ || this == STRING_LENGTH_EXCEEDED
+ || this == INSUFFICIENT_FRAME_DATA)
? String.format(messageFormat, size, maxSize, remoteInfo)
: String.format(messageFormat, size, remoteInfo);
throw new TTransportException(TTransportException.CORRUPTED_DATA,
message);
@@ -308,18 +312,32 @@ public class TElasticFramedTransport extends TTransport {
@Override
public void checkReadBytesAvailable(long numBytes) throws
TTransportException {
+ // RPC messages are flushed as complete frames. Container checks pass
their minimum encoded
+ // size here, before generated code allocates the container. Compare it
with actual buffered
+ // data, not just the configured frame cap. Use readBuffer directly
because copyBinary makes
+ // this transport's getBytesRemainingInBuffer() return -1.
+ int remaining = readBuffer.getBytesRemainingInBuffer();
+ FrameError error;
+ int limit;
if (numBytes >= thriftMaxFrameSize) {
- SocketAddress remoteAddress = null;
- if (underlying instanceof TSocket) {
- remoteAddress = ((TSocket)
underlying).getSocket().getRemoteSocketAddress();
- }
- String remoteInfo =
- (remoteAddress == null)
- ? RpcMessages.EMPTY_MESSAGE
- : RpcMessages.REMOTE_ADDRESS_PREFIX + remoteAddress;
- close();
- FrameError.STRING_LENGTH_EXCEEDED.throwException(numBytes, remoteInfo,
thriftMaxFrameSize);
+ error = FrameError.STRING_LENGTH_EXCEEDED;
+ limit = thriftMaxFrameSize;
+ } else if (numBytes > remaining) {
+ error = FrameError.INSUFFICIENT_FRAME_DATA;
+ limit = remaining;
+ } else {
+ return;
+ }
+ SocketAddress remoteAddress = null;
+ if (underlying instanceof TSocket) {
+ remoteAddress = ((TSocket)
underlying).getSocket().getRemoteSocketAddress();
}
+ String remoteInfo =
+ (remoteAddress == null)
+ ? RpcMessages.EMPTY_MESSAGE
+ : RpcMessages.REMOTE_ADDRESS_PREFIX + remoteAddress;
+ close();
+ error.throwException(numBytes, remoteInfo, limit);
}
@Override
diff --git
a/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/TElasticFramedTransportReadTest.java
b/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/TElasticFramedTransportReadTest.java
new file mode 100644
index 00000000000..93d12fd32b2
--- /dev/null
+++
b/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/TElasticFramedTransportReadTest.java
@@ -0,0 +1,266 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.rpc;
+
+import org.apache.iotdb.rpc.i18n.RpcMessages;
+import org.apache.iotdb.service.rpc.thrift.IClientRPCService;
+import org.apache.iotdb.service.rpc.thrift.TSInsertRecordReq;
+
+import org.apache.thrift.TException;
+import org.apache.thrift.protocol.TBinaryProtocol;
+import org.apache.thrift.protocol.TCompactProtocol;
+import org.apache.thrift.protocol.TField;
+import org.apache.thrift.protocol.TList;
+import org.apache.thrift.protocol.TMap;
+import org.apache.thrift.protocol.TMessage;
+import org.apache.thrift.protocol.TMessageType;
+import org.apache.thrift.protocol.TProtocol;
+import org.apache.thrift.protocol.TSet;
+import org.apache.thrift.protocol.TStruct;
+import org.apache.thrift.protocol.TType;
+import org.apache.thrift.transport.TMemoryBuffer;
+import org.apache.thrift.transport.TMemoryInputTransport;
+import org.apache.thrift.transport.TTransport;
+import org.apache.thrift.transport.TTransportException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+@RunWith(Parameterized.class)
+public class TElasticFramedTransportReadTest {
+ private static final int MAX_FRAME_SIZE = 1024 * 1024;
+ private static final int ELEMENT_COUNT = 200_001;
+
+ private final boolean compact;
+ private final boolean copyBinary;
+ private final boolean snappy;
+
+ @Parameterized.Parameters(name = "compact={0}, copyBinary={1}, snappy={2}")
+ public static Collection<Object[]> parameters() {
+ Collection<Object[]> parameters = new ArrayList<>();
+ for (boolean compact : new boolean[] {false, true}) {
+ for (boolean copyBinary : new boolean[] {false, true}) {
+ for (boolean snappy : new boolean[] {false, true}) {
+ parameters.add(new Object[] {compact, copyBinary, snappy});
+ }
+ }
+ }
+ return parameters;
+ }
+
+ public TElasticFramedTransportReadTest(boolean compact, boolean copyBinary,
boolean snappy) {
+ this.compact = compact;
+ this.copyBinary = copyBinary;
+ this.snappy = snappy;
+ }
+
+ @Test
+ public void testTruncatedRequestRejectedBeforeContainerAllocation() throws
Exception {
+ byte[] frame =
+ serialize(
+ p -> {
+ p.writeMessageBegin(new TMessage("insertRecord",
TMessageType.CALL, 1));
+ p.writeStructBegin(new TStruct("insertRecord_args"));
+ p.writeFieldBegin(new TField("req", TType.STRUCT, (short) 1));
+ p.writeStructBegin(new TStruct("TSInsertRecordReq"));
+ p.writeFieldBegin(new TField("columnCategoryies", TType.LIST,
(short) 8));
+ p.writeListBegin(new TList(TType.BYTE, ELEMENT_COUNT));
+ // Flush a complete frame that ends at the container header,
without any elements.
+ });
+ try (TElasticFramedTransport transport = transport(new
TMemoryInputTransport(frame))) {
+ TProtocol protocol = protocol(transport);
+ assertEquals("insertRecord", protocol.readMessageBegin().name);
+ IClientRPCService.insertRecord_args args = new
IClientRPCService.insertRecord_args();
+ TTransportException exception =
+ assertThrows(TTransportException.class, () -> args.read(protocol));
+ assertNotNull(args.req);
+ assertNull(args.req.columnCategoryies);
+ assertInsufficientFrame(exception, ELEMENT_COUNT, 0);
+ }
+ }
+
+ @Test
+ public void testLargeCompleteRequestsInSuccessiveFrames() throws Exception {
+ TSInsertRecordReq large =
+ new TSInsertRecordReq(1, "root.sg.d", Collections.emptyList(),
ByteBuffer.allocate(0), 1)
+ .setColumnCategoryies(Collections.nCopies(ELEMENT_COUNT, (byte)
0));
+ TSInsertRecordReq empty =
+ new TSInsertRecordReq(1, "root.sg.d", Collections.emptyList(),
ByteBuffer.allocate(0), 2)
+ .setColumnCategoryies(Collections.emptyList());
+ TMemoryBuffer wire = new TMemoryBuffer(128);
+ try (TElasticFramedTransport output = transport(wire)) {
+ IClientRPCService.Client client = new
IClientRPCService.Client(protocol(output));
+ client.send_insertRecord(large);
+ client.send_insertRecord(empty);
+ }
+ try (TElasticFramedTransport input =
+ transport(new TMemoryInputTransport(Arrays.copyOf(wire.getArray(),
wire.length())))) {
+ TProtocol protocol = protocol(input);
+ for (TSInsertRecordReq expected : new TSInsertRecordReq[] {large,
empty}) {
+ assertEquals("insertRecord", protocol.readMessageBegin().name);
+ IClientRPCService.insertRecord_args args = new
IClientRPCService.insertRecord_args();
+ args.read(protocol);
+ protocol.readMessageEnd();
+ assertEquals(expected, args.req);
+ }
+ }
+ }
+
+ @Test
+ public void testTruncatedContainerHeaders() throws Exception {
+ for (byte elementType : new byte[] {TType.BYTE, TType.STRUCT, TType.LIST,
TType.MAP}) {
+ for (byte containerType : new byte[] {TType.LIST, TType.SET, TType.MAP})
{
+ byte[] frame =
+ serialize(
+ p -> {
+ if (containerType == TType.LIST) {
+ p.writeListBegin(new TList(elementType, ELEMENT_COUNT));
+ } else if (containerType == TType.SET) {
+ p.writeSetBegin(new TSet(elementType, ELEMENT_COUNT));
+ } else {
+ p.writeMapBegin(new TMap(TType.BYTE, elementType,
ELEMENT_COUNT));
+ }
+ });
+ try (TElasticFramedTransport input = transport(new
TMemoryInputTransport(frame))) {
+ TProtocol p = protocol(input);
+ long minimumBytes =
+ (long) ELEMENT_COUNT
+ * (p.getMinSerializedSize(elementType) + (containerType ==
TType.MAP ? 1 : 0));
+ TTransportException exception =
+ assertThrows(
+ TTransportException.class,
+ () -> {
+ if (containerType == TType.LIST) {
+ p.readListBegin();
+ } else if (containerType == TType.SET) {
+ p.readSetBegin();
+ } else {
+ p.readMapBegin();
+ }
+ });
+ assertInsufficientFrame(exception, minimumBytes, 0);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testCompleteBinaryAtFrameBoundary() throws Exception {
+ ByteBuffer binary = ByteBuffer.wrap(new byte[ELEMENT_COUNT]);
+ byte[] frame =
+ serialize(
+ p -> {
+ p.writeString("");
+ p.writeBinary(binary);
+ });
+ try (TElasticFramedTransport input = transport(new
TMemoryInputTransport(frame))) {
+ TProtocol protocol = protocol(input);
+ assertEquals("", protocol.readString());
+ assertEquals(binary, protocol.readBinary());
+ input.checkReadBytesAvailable(0);
+ }
+ }
+
+ @Test
+ public void testTruncatedStringAndBinary() throws Exception {
+ TMemoryBuffer buffer = new TMemoryBuffer(128);
+ protocol(buffer).writeBinary(ByteBuffer.wrap(new byte[ELEMENT_COUNT]));
+ int headerLength = buffer.length() - ELEMENT_COUNT;
+ byte[] frame = serialize(p -> p.getTransport().write(buffer.getArray(), 0,
headerLength));
+ for (boolean binary : new boolean[] {false, true}) {
+ try (TElasticFramedTransport input = transport(new
TMemoryInputTransport(frame))) {
+ TProtocol p = protocol(input);
+ TTransportException exception =
+ assertThrows(
+ TTransportException.class,
+ () -> {
+ if (binary) {
+ p.readBinary();
+ } else {
+ p.readString();
+ }
+ });
+ assertInsufficientFrame(exception, ELEMENT_COUNT, 0);
+ }
+ }
+ }
+
+ @Test
+ public void testExistingMaximumReadSizeProtection() throws Exception {
+ try (TElasticFramedTransport input = transport(new
TMemoryInputTransport(new byte[0]))) {
+ TTransportException exception =
+ assertThrows(
+ TTransportException.class, () ->
input.checkReadBytesAvailable(MAX_FRAME_SIZE));
+ assertEquals(TTransportException.CORRUPTED_DATA, exception.getType());
+ assertEquals(
+ String.format(
+ RpcMessages.FRAME_ERROR_STRING_LENGTH_EXCEEDED, MAX_FRAME_SIZE,
MAX_FRAME_SIZE, ""),
+ exception.getMessage());
+ }
+ }
+
+ private void assertInsufficientFrame(
+ TTransportException exception, long required, int remaining) {
+ assertEquals(TTransportException.CORRUPTED_DATA, exception.getType());
+ assertEquals(
+ String.format(
+ RpcMessages
+
.EXCEPTION_REQUIRED_READ_SIZE_ARG_EXCEEDS_REMAINING_FRAME_SIZE_ARG_ARG_9C0541EE,
+ required,
+ remaining,
+ ""),
+ exception.getMessage());
+ }
+
+ private TElasticFramedTransport transport(TTransport underlying) throws
TTransportException {
+ return snappy
+ ? new TSnappyElasticFramedTransport(underlying, 128, MAX_FRAME_SIZE,
copyBinary)
+ : new TElasticFramedTransport(underlying, 128, MAX_FRAME_SIZE,
copyBinary);
+ }
+
+ private TProtocol protocol(TTransport transport) {
+ return compact ? new TCompactProtocol(transport) : new
TBinaryProtocol(transport);
+ }
+
+ private byte[] serialize(ProtocolWriter writer) throws TException {
+ TMemoryBuffer wire = new TMemoryBuffer(128);
+ try (TElasticFramedTransport output = transport(wire)) {
+ writer.write(protocol(output));
+ output.flush();
+ }
+ return Arrays.copyOf(wire.getArray(), wire.length());
+ }
+
+ @FunctionalInterface
+ private interface ProtocolWriter {
+ void write(TProtocol protocol) throws TException;
+ }
+}