blob: c6192b4b201a298180bbc835ec32da6a0946f432 [file] [log] [blame]
From 0b10440b61f3328d7478b93fbf27e879a0dbac91 Mon Sep 17 00:00:00 2001
From: Andrew Purtell <apurtell@salesforce.com>
Date: Mon, 14 Feb 2022 15:00:03 -0800
Subject: [PATCH] CVE-2021-22569: Improve performance of parsing unknown fields
in Java
An issue in protobuf-java allowed the interleaving of UnknownFieldSet fields
in such a way that would be processed out of order. A small malicious payload
can occupy the parser for several minutes by creating large numbers of short-
lived objects that cause frequent, repeated pauses.
Port fix from https://github.com/protocolbuffers/protobuf/pull/9371. It uses
a TreeMap to process fields in a well ordered way.
Reason: Security
Test Plan: Unit tests
---
Makefile.am | 1 +
.../com/google/protobuf/UnknownFieldSet.java | 179 +++++++++---------
.../UnknownFieldSetPerformanceTest.java | 76 ++++++++
4 files changed, 171 insertions(+), 86 deletions(-)
create mode 100644 java/src/test/java/com/google/protobuf/UnknownFieldSetPerformanceTest.java
diff --git a/Makefile.am b/Makefile.am
index c9286132b..aedd021c3 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -140,6 +140,7 @@ EXTRA_DIST = \
java/src/test/java/com/google/protobuf/TestUtil.java \
java/src/test/java/com/google/protobuf/TextFormatTest.java \
java/src/test/java/com/google/protobuf/UnknownFieldSetTest.java \
+ java/src/test/java/com/google/protobuf/UnknownFieldSetPerformanceTest.java \
java/src/test/java/com/google/protobuf/UnmodifiableLazyStringListTest.java \
java/src/test/java/com/google/protobuf/WireFormatTest.java \
java/src/test/java/com/google/protobuf/multiple_files_test.proto \
diff --git a/java/src/main/java/com/google/protobuf/UnknownFieldSet.java b/java/src/main/java/com/google/protobuf/UnknownFieldSet.java
index 45e2e6e40..f02377cf0 100644
--- a/java/src/main/java/com/google/protobuf/UnknownFieldSet.java
+++ b/java/src/main/java/com/google/protobuf/UnknownFieldSet.java
@@ -57,7 +57,6 @@ import java.util.TreeMap;
* @author kenton@google.com Kenton Varda
*/
public final class UnknownFieldSet implements MessageLite {
- private UnknownFieldSet() {}
/** Create a new {@link Builder}. */
public static Builder newBuilder() {
@@ -80,16 +79,20 @@ public final class UnknownFieldSet implements MessageLite {
return defaultInstance;
}
private static final UnknownFieldSet defaultInstance =
- new UnknownFieldSet(Collections.<Integer, Field>emptyMap());
+ new UnknownFieldSet(new TreeMap<Integer, Field>());
+
+ private UnknownFieldSet() {
+ fields = new TreeMap<Integer, Field>();
+ }
/**
* Construct an {@code UnknownFieldSet} around the given map. The map is
* expected to be immutable.
*/
- private UnknownFieldSet(final Map<Integer, Field> fields) {
+ private UnknownFieldSet(final TreeMap<Integer, Field> fields) {
this.fields = fields;
}
- private Map<Integer, Field> fields;
+ private final TreeMap<Integer, Field> fields;
@Override
public boolean equals(final Object other) {
@@ -196,8 +199,10 @@ public final class UnknownFieldSet implements MessageLite {
/** Get the number of bytes required to encode this set. */
public int getSerializedSize() {
int result = 0;
- for (final Map.Entry<Integer, Field> entry : fields.entrySet()) {
- result += entry.getValue().getSerializedSize(entry.getKey());
+ if (!fields.isEmpty()) {
+ for (final Map.Entry<Integer, Field> entry : fields.entrySet()) {
+ result += entry.getValue().getSerializedSize(entry.getKey());
+ }
}
return result;
}
@@ -281,62 +286,44 @@ public final class UnknownFieldSet implements MessageLite {
// This constructor should never be called directly (except from 'create').
private Builder() {}
- private Map<Integer, Field> fields;
-
- // Optimization: We keep around a builder for the last field that was
- // modified so that we can efficiently add to it multiple times in a
- // row (important when parsing an unknown repeated field).
- private int lastFieldNumber;
- private Field.Builder lastField;
+ private TreeMap<Integer, Field.Builder> fieldBuilders =
+ new TreeMap<Integer, Field.Builder>();
private static Builder create() {
- Builder builder = new Builder();
- builder.reinitialize();
- return builder;
+ return new Builder();
}
/**
* Get a field builder for the given field number which includes any
* values that already exist.
*/
- private Field.Builder getFieldBuilder(final int number) {
- if (lastField != null) {
- if (number == lastFieldNumber) {
- return lastField;
- }
- // Note: addField() will reset lastField and lastFieldNumber.
- addField(lastFieldNumber, lastField.build());
- }
+ private Field.Builder getFieldBuilder(int number) {
if (number == 0) {
return null;
} else {
- final Field existing = fields.get(number);
- lastFieldNumber = number;
- lastField = Field.newBuilder();
- if (existing != null) {
- lastField.mergeFrom(existing);
+ Field.Builder builder = fieldBuilders.get(number);
+ if (builder == null) {
+ builder = Field.newBuilder();
+ fieldBuilders.put(number, builder);
}
- return lastField;
+ return builder;
}
}
/**
* Build the {@link UnknownFieldSet} and return it.
- *
- * <p>Once {@code build()} has been called, the {@code Builder} will no
- * longer be usable. Calling any method after {@code build()} will result
- * in undefined behavior and can cause a {@code NullPointerException} to be
- * thrown.
*/
public UnknownFieldSet build() {
- getFieldBuilder(0); // Force lastField to be built.
- final UnknownFieldSet result;
- if (fields.isEmpty()) {
+ UnknownFieldSet result;
+ if (fieldBuilders.isEmpty()) {
result = getDefaultInstance();
} else {
- result = new UnknownFieldSet(Collections.unmodifiableMap(fields));
+ TreeMap<Integer, Field> fields = new TreeMap<Integer, Field>();
+ for (Map.Entry<Integer, Field.Builder> entry : fieldBuilders.entrySet()) {
+ fields.put(entry.getKey(), entry.getValue().build());
+ }
+ result = new UnknownFieldSet(fields);
}
- fields = null;
return result;
}
@@ -347,24 +334,22 @@ public final class UnknownFieldSet implements MessageLite {
@Override
public Builder clone() {
- getFieldBuilder(0); // Force lastField to be built.
- return UnknownFieldSet.newBuilder().mergeFrom(
- new UnknownFieldSet(fields));
+ Builder clone = UnknownFieldSet.newBuilder();
+ for (Map.Entry<Integer, Field.Builder> entry : fieldBuilders.entrySet()) {
+ Integer key = entry.getKey();
+ Field.Builder value = entry.getValue();
+ clone.fieldBuilders.put(key, value.clone());
+ }
+ return clone;
}
public UnknownFieldSet getDefaultInstanceForType() {
return UnknownFieldSet.getDefaultInstance();
}
- private void reinitialize() {
- fields = Collections.emptyMap();
- lastFieldNumber = 0;
- lastField = null;
- }
-
/** Reset the builder to an empty set. */
public Builder clear() {
- reinitialize();
+ fieldBuilders = new TreeMap<Integer, Field.Builder>();
return this;
}
@@ -415,11 +400,8 @@ public final class UnknownFieldSet implements MessageLite {
}
/** Check if the given field number is present in the set. */
- public boolean hasField(final int number) {
- if (number == 0) {
- throw new IllegalArgumentException("Zero is not a valid field number.");
- }
- return number == lastFieldNumber || fields.containsKey(number);
+ public boolean hasField(int number) {
+ return fieldBuilders.containsKey(number);
}
/**
@@ -430,15 +412,7 @@ public final class UnknownFieldSet implements MessageLite {
if (number == 0) {
throw new IllegalArgumentException("Zero is not a valid field number.");
}
- if (lastField != null && lastFieldNumber == number) {
- // Discard this.
- lastField = null;
- lastFieldNumber = 0;
- }
- if (fields.isEmpty()) {
- fields = new TreeMap<Integer,Field>();
- }
- fields.put(number, field);
+ fieldBuilders.put(number, Field.newBuilder(field));
return this;
}
@@ -447,7 +421,10 @@ public final class UnknownFieldSet implements MessageLite {
* fields are added, the changes may or may not be reflected in this map.
*/
public Map<Integer, Field> asMap() {
- getFieldBuilder(0); // Force lastField to be built.
+ TreeMap<Integer, Field> fields = new TreeMap<Integer, Field>();
+ for (Map.Entry<Integer, Field.Builder> entry : fieldBuilders.entrySet()) {
+ fields.put(entry.getKey(), entry.getValue().build());
+ }
return Collections.unmodifiableMap(fields);
}
@@ -809,54 +786,84 @@ public final class UnknownFieldSet implements MessageLite {
* <p>Use {@link Field#newBuilder()} to construct a {@code Builder}.
*/
public static final class Builder {
- // This constructor should never be called directly (except from 'create').
- private Builder() {}
+ // This constructor should only be called directly from 'create' and 'clone'.
+ private Builder() {
+ result = new Field();
+ }
private static Builder create() {
Builder builder = new Builder();
- builder.result = new Field();
return builder;
}
private Field result;
+ @Override
+ public Builder clone() {
+ Field copy = new Field();
+ if (result.varint == null) {
+ copy.varint = null;
+ } else {
+ copy.varint = new ArrayList<Long>(result.varint);
+ }
+ if (result.fixed32 == null) {
+ copy.fixed32 = null;
+ } else {
+ copy.fixed32 = new ArrayList<Integer>(result.fixed32);
+ }
+ if (result.fixed64 == null) {
+ copy.fixed64 = null;
+ } else {
+ copy.fixed64 = new ArrayList<Long>(result.fixed64);
+ }
+ if (result.lengthDelimited == null) {
+ copy.lengthDelimited = null;
+ } else {
+ copy.lengthDelimited = new ArrayList<ByteString>(result.lengthDelimited);
+ }
+ if (result.group == null) {
+ copy.group = null;
+ } else {
+ copy.group = new ArrayList<UnknownFieldSet>(result.group);
+ }
+
+ Builder clone = new Builder();
+ clone.result = copy;
+ return clone;
+ }
+
/**
- * Build the field. After {@code build()} has been called, the
- * {@code Builder} is no longer usable. Calling any other method will
- * result in undefined behavior and can cause a
- * {@code NullPointerException} to be thrown.
+ * Build the field.
*/
public Field build() {
+ Field built = new Field();
if (result.varint == null) {
- result.varint = Collections.emptyList();
+ built.varint = Collections.emptyList();
} else {
- result.varint = Collections.unmodifiableList(result.varint);
+ built.varint = Collections.unmodifiableList(result.varint);
}
if (result.fixed32 == null) {
- result.fixed32 = Collections.emptyList();
+ built.fixed32 = Collections.emptyList();
} else {
- result.fixed32 = Collections.unmodifiableList(result.fixed32);
+ built.fixed32 = Collections.unmodifiableList(result.fixed32);
}
if (result.fixed64 == null) {
- result.fixed64 = Collections.emptyList();
+ built.fixed64 = Collections.emptyList();
} else {
- result.fixed64 = Collections.unmodifiableList(result.fixed64);
+ built.fixed64 = Collections.unmodifiableList(result.fixed64);
}
if (result.lengthDelimited == null) {
- result.lengthDelimited = Collections.emptyList();
+ built.lengthDelimited = Collections.emptyList();
} else {
- result.lengthDelimited =
+ built.lengthDelimited =
Collections.unmodifiableList(result.lengthDelimited);
}
if (result.group == null) {
- result.group = Collections.emptyList();
+ built.group = Collections.emptyList();
} else {
- result.group = Collections.unmodifiableList(result.group);
+ built.group = Collections.unmodifiableList(result.group);
}
-
- final Field returnMe = result;
- result = null;
- return returnMe;
+ return built;
}
/** Discard the field's contents. */
diff --git a/java/src/test/java/com/google/protobuf/UnknownFieldSetPerformanceTest.java b/java/src/test/java/com/google/protobuf/UnknownFieldSetPerformanceTest.java
new file mode 100644
index 000000000..9b913b50f
--- /dev/null
+++ b/java/src/test/java/com/google/protobuf/UnknownFieldSetPerformanceTest.java
@@ -0,0 +1,76 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package com.google.protobuf;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.junit.Assert;
+
+import junit.framework.TestCase;
+
+public final class UnknownFieldSetPerformanceTest extends TestCase {
+
+ private static byte[] generateBytes(int length) {
+ Assert.assertTrue((length % 4) == 0);
+ byte[] input = new byte[length];
+ for (int i = 0; i < length; i += 4) {
+ input[i] = (byte) 0x08; // field 1, wiretype 0
+ input[i + 1] = (byte) 0x08; // field 1, payload 8
+ input[i + 2] = (byte) 0x20; // field 4, wiretype 0
+ input[i + 3] = (byte) 0x20; // field 4, payload 32
+ }
+ return input;
+ }
+
+ // This is a performance test. Failure here is a timeout.
+ public void testAlternatingFieldNumbers() throws IOException {
+ byte[] input = generateBytes(800000);
+ InputStream in = new ByteArrayInputStream(input);
+ UnknownFieldSet.Builder builder = UnknownFieldSet.newBuilder();
+ CodedInputStream codedInput = CodedInputStream.newInstance(in);
+ builder.mergeFrom(codedInput);
+ }
+
+ // This is a performance test. Failure here is a timeout.
+ public void testAddField() {
+ UnknownFieldSet.Builder builder = UnknownFieldSet.newBuilder();
+ for (int i = 1; i <= 100000; i++) {
+ UnknownFieldSet.Field field = UnknownFieldSet.Field.newBuilder().addFixed32(i).build();
+ builder.addField(i, field);
+ }
+ UnknownFieldSet fieldSet = builder.build();
+ assertTrue(fieldSet.getField(100000).getFixed32List().get(0) == 100000);
+ }
+
+}
+
--
2.33.0