Repository navigation
GH-1337: Skip unknown fields in FlightData parsing (ArrowMessage.frame) #1338
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
Open
laurentgo
wants to merge
1
commit into
apache:main
Choose a base branch
from
laurentgo:claude/arrow-java-unknown-fields-74336a
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+378
−2
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,17 +55,26 @@ | |
| import org.apache.arrow.vector.ipc.message.MessageSerializer; | ||
| import org.apache.arrow.vector.types.MetadataVersion; | ||
| import org.apache.arrow.vector.types.pojo.Schema; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** The in-memory representation of FlightData used to manage a stream of Arrow messages. */ | ||
| class ArrowMessage implements AutoCloseable { | ||
|
|
||
| // Must be declared before the static initializer below, which logs invalid settings. | ||
| private static final Logger logger = LoggerFactory.getLogger(ArrowMessage.class); | ||
|
|
||
| // If true, deserialize Arrow data by giving Arrow a reference to the underlying gRPC buffer | ||
| // instead of copying the data. Defaults to true. | ||
| public static final boolean ENABLE_ZERO_COPY_READ; | ||
| // If true, serialize Arrow data by giving gRPC a reference to the underlying Arrow buffer | ||
| // instead of copying the data. Defaults to false. | ||
| public static final boolean ENABLE_ZERO_COPY_WRITE; | ||
|
|
||
| // Maximum nesting depth of groups in unknown fields that are skipped. Defaults to 100, like the | ||
| // recursion limit of protobuf-java. | ||
| static final int MAX_PROTOBUF_SKIP_GROUP_DEPTH; | ||
|
|
||
| static { | ||
| String zeroCopyReadFlag = System.getProperty("arrow.flight.enable_zero_copy_read"); | ||
| if (zeroCopyReadFlag == null) { | ||
|
|
@@ -77,6 +86,33 @@ class ArrowMessage implements AutoCloseable { | |
| } | ||
| ENABLE_ZERO_COPY_READ = !"false".equalsIgnoreCase(zeroCopyReadFlag); | ||
| ENABLE_ZERO_COPY_WRITE = "true".equalsIgnoreCase(zeroCopyWriteFlag); | ||
|
|
||
| String maxGroupDepth = System.getProperty("arrow.flight.max_protobuf_skip_group_depth"); | ||
| if (maxGroupDepth == null) { | ||
| maxGroupDepth = System.getenv("ARROW_FLIGHT_MAX_PROTOBUF_SKIP_GROUP_DEPTH"); | ||
| } | ||
| MAX_PROTOBUF_SKIP_GROUP_DEPTH = parseMaxGroupDepth(maxGroupDepth); | ||
| } | ||
|
|
||
| private static final int DEFAULT_MAX_PROTOBUF_SKIP_GROUP_DEPTH = 100; | ||
|
|
||
| private static int parseMaxGroupDepth(String value) { | ||
| if (value == null) { | ||
| return DEFAULT_MAX_PROTOBUF_SKIP_GROUP_DEPTH; | ||
| } | ||
| try { | ||
| final int depth = Integer.parseInt(value.trim()); | ||
| if (depth >= 0) { | ||
| return depth; | ||
| } | ||
| } catch (NumberFormatException e) { | ||
| // fall through to the default | ||
| } | ||
| logger.warn( | ||
| "Invalid maximum protobuf group depth '{}', using the default of {}", | ||
| value, | ||
| DEFAULT_MAX_PROTOBUF_SKIP_GROUP_DEPTH); | ||
| return DEFAULT_MAX_PROTOBUF_SKIP_GROUP_DEPTH; | ||
| } | ||
|
|
||
| private static final int DESCRIPTOR_TAG = | ||
|
|
@@ -88,6 +124,18 @@ class ArrowMessage implements AutoCloseable { | |
| private static final int APP_METADATA_TAG = | ||
| (FlightData.APP_METADATA_FIELD_NUMBER << 3) | WireFormat.WIRETYPE_LENGTH_DELIMITED; | ||
|
|
||
| // Wire sizes of protobuf values. protobuf-java declares these in WireFormat with the same names, | ||
| // but they are package-private. | ||
|
|
||
| /** Size in bytes of a fixed32 value. */ | ||
| private static final int FIXED32_SIZE = 4; | ||
|
|
||
| /** Size in bytes of a fixed64 value. */ | ||
| private static final int FIXED64_SIZE = 8; | ||
|
|
||
| /** Maximum size in bytes of an encoded varint, i.e. of a 64-bit value. */ | ||
| private static final int MAX_VARINT_SIZE = 10; | ||
|
|
||
| private static final Marshaller<FlightData> NO_BODY_MARSHALLER = | ||
| ProtoUtils.marshaller(FlightData.getDefaultInstance()); | ||
|
|
||
|
|
@@ -325,7 +373,10 @@ private static ArrowMessage frame(BufferAllocator allocator, final InputStream s | |
| break; | ||
|
|
||
| default: | ||
| // ignore unknown fields. | ||
| // Anything else is an unknown field, including a known field number sent with another | ||
| // wire type than expected. As in protobuf, it is skipped, not rejected. | ||
| skipField(stream, tag); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe a tag of protobuf rejects field number 0, so we could do the same at the top of private static void skipField(InputStream stream, int tag, int depth) throws IOException {
if (WireFormat.getTagFieldNumber(tag) == 0) {
throw new IOException("Malformed FlightData frame: invalid tag " + tag);
}
switch (WireFormat.getTagWireType(tag)) {The |
||
| break; | ||
| } | ||
| } | ||
| // Protobuf implementations can omit empty fields, such as body; for some message types, like | ||
|
|
@@ -432,7 +483,101 @@ private static ArrowBuf readFieldBuffer(BufferAllocator allocator, InputStream s | |
| return buf; | ||
| } | ||
|
|
||
| private static IOException fieldTooLong(int size, int remaining) { | ||
| /** Skip the value of a field whose tag has just been read, without buffering it. */ | ||
| private static void skipField(InputStream stream, int tag) throws IOException { | ||
| skipField(stream, tag, 0); | ||
| } | ||
|
|
||
| private static void skipField(InputStream stream, int tag, int depth) throws IOException { | ||
| switch (WireFormat.getTagWireType(tag)) { | ||
| case WireFormat.WIRETYPE_VARINT: | ||
| skipVarint(stream); | ||
| break; | ||
| case WireFormat.WIRETYPE_FIXED64: | ||
| skipFully(stream, FIXED64_SIZE); | ||
| break; | ||
| case WireFormat.WIRETYPE_LENGTH_DELIMITED: | ||
| skipFully(stream, readFieldLength(stream)); | ||
| break; | ||
| case WireFormat.WIRETYPE_START_GROUP: | ||
| if (depth >= MAX_PROTOBUF_SKIP_GROUP_DEPTH) { | ||
| throw new IOException( | ||
| "Malformed FlightData frame: groups nested deeper than " | ||
| + MAX_PROTOBUF_SKIP_GROUP_DEPTH | ||
| + " levels"); | ||
| } | ||
| skipGroup(stream, WireFormat.getTagFieldNumber(tag), depth + 1); | ||
| break; | ||
| case WireFormat.WIRETYPE_FIXED32: | ||
| skipFully(stream, FIXED32_SIZE); | ||
| break; | ||
| default: | ||
| // An end group without a matching start group, or an invalid wire type. | ||
| throw new IOException( | ||
| "Malformed FlightData frame: unexpected wire type " | ||
| + WireFormat.getTagWireType(tag) | ||
| + " for field " | ||
| + WireFormat.getTagFieldNumber(tag)); | ||
| } | ||
| } | ||
|
|
||
| /** Skip the fields of a group up to and including its matching end group tag. */ | ||
| private static void skipGroup(InputStream stream, int fieldNumber, int depth) throws IOException { | ||
| while (true) { | ||
| final int firstByte = stream.read(); | ||
| if (firstByte == -1) { | ||
| throw new IOException("Malformed FlightData frame: unterminated group " + fieldNumber); | ||
| } | ||
| final int tag = readRawVarint32(firstByte, stream); | ||
| if (tag == 0) { | ||
| throw new IOException("Malformed FlightData frame: invalid tag 0 in group " + fieldNumber); | ||
| } | ||
| if (WireFormat.getTagWireType(tag) == WireFormat.WIRETYPE_END_GROUP) { | ||
| if (WireFormat.getTagFieldNumber(tag) != fieldNumber) { | ||
| throw new IOException( | ||
| "Malformed FlightData frame: group " | ||
| + fieldNumber | ||
| + " ended by group " | ||
| + WireFormat.getTagFieldNumber(tag)); | ||
| } | ||
| return; | ||
| } | ||
| skipField(stream, tag, depth); | ||
| } | ||
| } | ||
|
|
||
| /** Skip a varint of up to {@link #MAX_VARINT_SIZE} bytes. */ | ||
| private static void skipVarint(InputStream stream) throws IOException { | ||
| for (int i = 0; i < MAX_VARINT_SIZE; i++) { | ||
| final int b = stream.read(); | ||
| if (b == -1) { | ||
| throw new IOException("Malformed FlightData frame: truncated varint"); | ||
| } | ||
| if ((b & 0x80) == 0) { | ||
| return; | ||
| } | ||
| } | ||
| throw new IOException( | ||
| "Malformed FlightData frame: varint longer than " + MAX_VARINT_SIZE + " bytes"); | ||
| } | ||
|
|
||
| /** Skip exactly {@code size} bytes, failing if the stream ends first. */ | ||
| private static void skipFully(InputStream stream, long size) throws IOException { | ||
| long remaining = size; | ||
| while (remaining > 0) { | ||
| final long skipped = stream.skip(remaining); | ||
| if (skipped > 0) { | ||
| remaining -= skipped; | ||
| } else if (stream.read() == -1) { | ||
| // skip() may return 0 without being at EOF, so probe with a single-byte read. | ||
| throw fieldTooLong(size, size - remaining); | ||
| } else { | ||
| remaining--; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static IOException fieldTooLong(long size, long remaining) { | ||
| return new IOException( | ||
| "Malformed FlightData frame: field length " | ||
| + size | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should set an upper bound here, and skip group which is recursive (
skipField->skipGroup->skipField). With a large setting (like-Darrow.flight.max_protobuf_skip_group_depth=10000000), a peer can send a deeply nested message and overflow the stack.frame()only catchesException, so theStackOverFlowErrorskips the cleanup:appMetadataandbodyare never released and the allocator leaks.