Improve server-side validation of RPC payloads.
Dynamically allocate storage space for arrays to better handle large payloads.

Patch by: bobv
Review by: mmendez


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2260 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/rpc/impl/AbstractSerializationStreamReader.java b/user/src/com/google/gwt/user/client/rpc/impl/AbstractSerializationStreamReader.java
index 31f3a95..cc7118a 100644
--- a/user/src/com/google/gwt/user/client/rpc/impl/AbstractSerializationStreamReader.java
+++ b/user/src/com/google/gwt/user/client/rpc/impl/AbstractSerializationStreamReader.java
@@ -83,4 +83,8 @@
     seenArray.add(o);
   }
 
+  protected final void replaceRememberedObject(Object old, Object replacement) {
+    seenArray.set(seenArray.indexOf(old), replacement);
+  }
+
 }
diff --git a/user/src/com/google/gwt/user/client/rpc/impl/RemoteServiceProxy.java b/user/src/com/google/gwt/user/client/rpc/impl/RemoteServiceProxy.java
index 79832ce..063413f 100644
--- a/user/src/com/google/gwt/user/client/rpc/impl/RemoteServiceProxy.java
+++ b/user/src/com/google/gwt/user/client/rpc/impl/RemoteServiceProxy.java
@@ -233,6 +233,8 @@
         this, methodName, invocationCount, callback, responseReader);
     RequestBuilder rb = new RequestBuilder(RequestBuilder.POST,
         getServiceEntryPoint());
+    rb.setHeader("Content-Type", "text/x-gwt-rpc; charset=utf-8");
+
     try {
       return rb.sendRequest(requestData, responseHandler);
     } catch (RequestException ex) {
diff --git a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
index 5053f73..63179bd 100644
--- a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
+++ b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 Google Inc.
  * 
  * Licensed 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
@@ -32,13 +32,21 @@
 public class RPCServletUtils {
   private static final String ACCEPT_ENCODING = "Accept-Encoding";
 
+  private static final String ATTACHMENT = "attachment";
+
   private static final String CHARSET_UTF8 = "UTF-8";
 
+  private static final String CONTENT_DISPOSITION = "Content-Disposition";
+
   private static final String CONTENT_ENCODING = "Content-Encoding";
 
   private static final String CONTENT_ENCODING_GZIP = "gzip";
 
-  private static final String CONTENT_TYPE_TEXT_PLAIN_UTF8 = "text/plain; charset=utf-8";
+  private static final String CONTENT_TYPE_APPLICATION_JSON_UTF8 = "application/json; charset=utf-8";
+
+  private static final String EXPECTED_CHARSET = "charset=utf-8";
+
+  private static final String EXPECTED_CONTENT_TYPE = "text/x-gwt-rpc";
 
   private static final String GENERIC_FAILURE_MSG = "The call failed on the server; see server log for details";
 
@@ -90,8 +98,8 @@
    * @throws IOException if the requests input stream cannot be accessed, read
    *           from or closed
    * @throws ServletException if the content length of the request is not
-   *           specified of if the request's content type is not 'text/plain' or
-   *           'charset=utf-8'
+   *           specified of if the request's content type is not
+   *           'text/x-gwt-rpc' and 'charset=utf-8'
    */
   public static String readContentAsUtf8(HttpServletRequest request)
       throws IOException, ServletException {
@@ -106,21 +114,17 @@
     // Content-Type must be specified.
     if (contentType != null) {
       contentType = contentType.toLowerCase();
-      // The type must be plain text.
-      if (contentType.startsWith("text/plain")) {
-        // And it must be UTF-8 encoded (or unspecified, in which case we assume
-        // that it's either UTF-8 or ASCII).
-        if (contentType.indexOf("charset=") == -1) {
-          contentTypeIsOkay = true;
-        } else if (contentType.indexOf("charset=utf-8") != -1) {
+      // The type must be be distinct
+      if (contentType.startsWith(EXPECTED_CONTENT_TYPE)) {
+        if (contentType.indexOf(EXPECTED_CHARSET) != -1) {
           contentTypeIsOkay = true;
         }
       }
     }
 
     if (!contentTypeIsOkay) {
-      throw new ServletException(
-          "Content-Type must be 'text/plain' with 'charset=utf-8' (or unspecified charset)");
+      throw new ServletException("Content-Type must be '"
+          + EXPECTED_CONTENT_TYPE + "' with '" + EXPECTED_CHARSET + "'.");
     }
 
     InputStream in = request.getInputStream();
@@ -215,8 +219,9 @@
     // Send the reply.
     //
     response.setContentLength(responseBytes.length);
-    response.setContentType(CONTENT_TYPE_TEXT_PLAIN_UTF8);
+    response.setContentType(CONTENT_TYPE_APPLICATION_JSON_UTF8);
     response.setStatus(HttpServletResponse.SC_OK);
+    response.setHeader(CONTENT_DISPOSITION, ATTACHMENT);
     response.getOutputStream().write(responseBytes);
   }
 
diff --git a/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamReader.java b/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamReader.java
index 341ec10..4d568fc 100644
--- a/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamReader.java
+++ b/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamReader.java
@@ -30,6 +30,7 @@
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.IdentityHashMap;
+import java.util.LinkedList;
 import java.util.Map;
 
 /**
@@ -40,6 +41,37 @@
     AbstractSerializationStreamReader {
 
   /**
+   * Used to accumulate elements while deserializing array types. The generic
+   * type of the BoundedList will vary from the component type of the array it
+   * is intended to create when the array is of a primitive type.
+   * 
+   * @param <T> The type of object used to hold the data in the buffer
+   */
+  private static class BoundedList<T> extends LinkedList<T> {
+    private final Class<?> componentType;
+    private final int expectedSize;
+
+    public BoundedList(Class<?> componentType, int expectedSize) {
+      this.componentType = componentType;
+      this.expectedSize = expectedSize;
+    }
+
+    @Override
+    public boolean add(T o) {
+      assert size() < getExpectedSize();
+      return super.add(o);
+    }
+
+    public Class<?> getComponentType() {
+      return componentType;
+    }
+
+    public int getExpectedSize() {
+      return expectedSize;
+    }
+  }
+
+  /**
    * Enumeration used to provided typed instance readers.
    */
   private enum ValueReader {
@@ -115,99 +147,159 @@
   private enum VectorReader {
     BOOLEAN_VECTOR {
       @Override
-      void read(ServerSerializationStreamReader stream, Object instance) {
-        boolean[] vector = (boolean[]) instance;
-        for (int i = 0, n = vector.length; i < n; ++i) {
-          vector[i] = stream.readBoolean();
-        }
+      protected Object readSingleValue(ServerSerializationStreamReader stream)
+          throws SerializationException {
+        return stream.readBoolean();
+      }
+
+      @Override
+      protected void setSingleValue(Object array, int index, Object value) {
+        Array.setBoolean(array, index, (Boolean) value);
       }
     },
     BYTE_VECTOR {
       @Override
-      void read(ServerSerializationStreamReader stream, Object instance) {
-        byte[] vector = (byte[]) instance;
-        for (int i = 0, n = vector.length; i < n; ++i) {
-          vector[i] = stream.readByte();
-        }
+      protected Object readSingleValue(ServerSerializationStreamReader stream)
+          throws SerializationException {
+        return stream.readByte();
+      }
+
+      @Override
+      protected void setSingleValue(Object array, int index, Object value) {
+        Array.setByte(array, index, (Byte) value);
       }
     },
     CHAR_VECTOR {
       @Override
-      void read(ServerSerializationStreamReader stream, Object instance) {
-        char[] vector = (char[]) instance;
-        for (int i = 0, n = vector.length; i < n; ++i) {
-          vector[i] = stream.readChar();
-        }
+      protected Object readSingleValue(ServerSerializationStreamReader stream)
+          throws SerializationException {
+        return stream.readChar();
+      }
+
+      @Override
+      protected void setSingleValue(Object array, int index, Object value) {
+        Array.setChar(array, index, (Character) value);
       }
     },
     DOUBLE_VECTOR {
       @Override
-      void read(ServerSerializationStreamReader stream, Object instance) {
-        double[] vector = (double[]) instance;
-        for (int i = 0, n = vector.length; i < n; ++i) {
-          vector[i] = stream.readDouble();
-        }
+      protected Object readSingleValue(ServerSerializationStreamReader stream)
+          throws SerializationException {
+        return stream.readDouble();
+      }
+
+      @Override
+      protected void setSingleValue(Object array, int index, Object value) {
+        Array.setDouble(array, index, (Double) value);
       }
     },
     FLOAT_VECTOR {
       @Override
-      void read(ServerSerializationStreamReader stream, Object instance) {
-        float[] vector = (float[]) instance;
-        for (int i = 0, n = vector.length; i < n; ++i) {
-          vector[i] = stream.readFloat();
-        }
+      protected Object readSingleValue(ServerSerializationStreamReader stream)
+          throws SerializationException {
+        return stream.readFloat();
+      }
+
+      @Override
+      protected void setSingleValue(Object array, int index, Object value) {
+        Array.setFloat(array, index, (Float) value);
       }
     },
-
     INT_VECTOR {
       @Override
-      void read(ServerSerializationStreamReader stream, Object instance) {
-        int[] vector = (int[]) instance;
-        for (int i = 0, n = vector.length; i < n; ++i) {
-          vector[i] = stream.readInt();
-        }
+      protected Object readSingleValue(ServerSerializationStreamReader stream)
+          throws SerializationException {
+        return stream.readInt();
+      }
+
+      @Override
+      protected void setSingleValue(Object array, int index, Object value) {
+        Array.setInt(array, index, (Integer) value);
       }
     },
     LONG_VECTOR {
       @Override
-      void read(ServerSerializationStreamReader stream, Object instance) {
-        long[] vector = (long[]) instance;
-        for (int i = 0, n = vector.length; i < n; ++i) {
-          vector[i] = stream.readLong();
-        }
+      protected Object readSingleValue(ServerSerializationStreamReader stream)
+          throws SerializationException {
+        return stream.readLong();
+      }
+
+      @Override
+      protected void setSingleValue(Object array, int index, Object value) {
+        Array.setLong(array, index, (Long) value);
       }
     },
     OBJECT_VECTOR {
       @Override
-      void read(ServerSerializationStreamReader stream, Object instance)
+      protected Object readSingleValue(ServerSerializationStreamReader stream)
           throws SerializationException {
-        Object[] vector = (Object[]) instance;
-        for (int i = 0, n = vector.length; i < n; ++i) {
-          vector[i] = stream.readObject();
-        }
+        return stream.readObject();
+      }
+
+      @Override
+      protected void setSingleValue(Object array, int index, Object value) {
+        Array.set(array, index, value);
       }
     },
     SHORT_VECTOR {
       @Override
-      void read(ServerSerializationStreamReader stream, Object instance) {
-        short[] vector = (short[]) instance;
-        for (int i = 0, n = vector.length; i < n; ++i) {
-          vector[i] = stream.readShort();
-        }
+      protected Object readSingleValue(ServerSerializationStreamReader stream)
+          throws SerializationException {
+        return stream.readShort();
+      }
+
+      @Override
+      protected void setSingleValue(Object array, int index, Object value) {
+        Array.setShort(array, index, (Short) value);
       }
     },
     STRING_VECTOR {
       @Override
-      void read(ServerSerializationStreamReader stream, Object instance) {
-        String[] vector = (String[]) instance;
-        for (int i = 0, n = vector.length; i < n; ++i) {
-          vector[i] = stream.readString();
-        }
+      protected Object readSingleValue(ServerSerializationStreamReader stream)
+          throws SerializationException {
+        return stream.readString();
+      }
+
+      @Override
+      protected void setSingleValue(Object array, int index, Object value) {
+        Array.set(array, index, value);
       }
     };
 
-    abstract void read(ServerSerializationStreamReader stream, Object instance)
-        throws SerializationException;
+    protected abstract Object readSingleValue(
+        ServerSerializationStreamReader stream) throws SerializationException;
+
+    protected abstract void setSingleValue(Object array, int index, Object value);
+
+    /**
+     * Convert a BoundedList to an array of the correct type. This
+     * implementation consumes the BoundedList.
+     */
+    protected Object toArray(Class<?> componentType, BoundedList<Object> buffer)
+        throws SerializationException {
+      if (buffer.getExpectedSize() != buffer.size()) {
+        throw new SerializationException(
+            "Inconsistent number of elements received. Received "
+                + buffer.size() + " but expecting " + buffer.getExpectedSize());
+      }
+
+      Object arr = Array.newInstance(componentType, buffer.size());
+
+      for (int i = 0, n = buffer.size(); i < n; i++) {
+        setSingleValue(arr, i, buffer.removeFirst());
+      }
+
+      return arr;
+    }
+
+    Object read(ServerSerializationStreamReader stream,
+        BoundedList<Object> instance) throws SerializationException {
+      for (int i = 0, n = instance.getExpectedSize(); i < n; ++i) {
+        instance.add(readSingleValue(stream));
+      }
+
+      return toArray(instance.getComponentType(), instance);
+    }
   }
 
   /**
@@ -222,15 +314,15 @@
 
   private final ClassLoader classLoader;
 
+  private SerializationPolicy serializationPolicy = RPC.getDefaultSerializationPolicy();
+
+  private final SerializationPolicyProvider serializationPolicyProvider;
+
   private String[] stringTable;
 
   private final ArrayList<String> tokenList = new ArrayList<String>();
 
-  private SerializationPolicy serializationPolicy = RPC.getDefaultSerializationPolicy();
-
   private int tokenListIndex;
-
-  private final SerializationPolicyProvider serializationPolicyProvider;
   {
     CLASS_TO_VECTOR_READER.put(boolean[].class, VectorReader.BOOLEAN_VECTOR);
     CLASS_TO_VECTOR_READER.put(byte[].class, VectorReader.BYTE_VECTOR);
@@ -370,7 +462,15 @@
 
       rememberDecodedObject(instance);
 
-      deserializeImpl(customSerializer, instanceClass, instance);
+      Object replacement = deserializeImpl(customSerializer, instanceClass,
+          instance);
+
+      // It's possible that deserializing an object requires the original proxy
+      // object to be replaced.
+      if (instance != replacement) {
+        replaceRememberedObject(instance, replacement);
+        instance = replacement;
+      }
 
       return instance;
 
@@ -413,15 +513,17 @@
    * @param instance
    * @throws SerializationException
    */
-  private void deserializeArray(Class<?> instanceClass, Object instance)
+  @SuppressWarnings("unchecked")
+  private Object deserializeArray(Class<?> instanceClass, Object instance)
       throws SerializationException {
     assert (instanceClass.isArray());
 
+    BoundedList<Object> buffer = (BoundedList<Object>) instance;
     VectorReader instanceReader = CLASS_TO_VECTOR_READER.get(instanceClass);
     if (instanceReader != null) {
-      instanceReader.read(this, instance);
+      return instanceReader.read(this, buffer);
     } else {
-      VectorReader.OBJECT_VECTOR.read(this, instance);
+      return VectorReader.OBJECT_VECTOR.read(this, buffer);
     }
   }
 
@@ -453,7 +555,7 @@
     }
   }
 
-  private void deserializeImpl(Class<?> customSerializer,
+  private Object deserializeImpl(Class<?> customSerializer,
       Class<?> instanceClass, Object instance) throws NoSuchMethodException,
       IllegalArgumentException, IllegalAccessException,
       InvocationTargetException, SerializationException, ClassNotFoundException {
@@ -462,20 +564,30 @@
       deserializeWithCustomFieldDeserializer(customSerializer, instanceClass,
           instance);
     } else if (instanceClass.isArray()) {
-      deserializeArray(instanceClass, instance);
+      instance = deserializeArray(instanceClass, instance);
     } else if (instanceClass.isEnum()) {
       // Enums are deserialized when they are instantiated
     } else {
       deserializeClass(instanceClass, instance);
     }
+
+    return instance;
   }
 
-  private void deserializeStringTable() {
+  private void deserializeStringTable() throws SerializationException {
     int typeNameCount = readInt();
-    stringTable = new String[typeNameCount];
+    BoundedList<String> buffer = new BoundedList<String>(String.class,
+        typeNameCount);
     for (int typeNameIndex = 0; typeNameIndex < typeNameCount; ++typeNameIndex) {
-      stringTable[typeNameIndex] = extract();
+      buffer.add(extract());
     }
+
+    if (buffer.size() != buffer.getExpectedSize()) {
+      throw new SerializationException("Expected " + buffer.getExpectedSize()
+          + " string table elements; received " + buffer.size());
+    }
+
+    stringTable = buffer.toArray(new String[buffer.getExpectedSize()]);
   }
 
   private void deserializeWithCustomFieldDeserializer(
@@ -509,10 +621,10 @@
 
     if (instanceClass.isArray()) {
       int length = readInt();
-      Class<?> componentType = instanceClass.getComponentType();
-      return Array.newInstance(componentType, length);
+      // We don't pre-allocate the array; this prevents an allocation attack
+      return new BoundedList<Object>(instanceClass.getComponentType(), length);
     } else if (instanceClass.isEnum()) {
-      Enum[] enumConstants = (Enum[]) instanceClass.getEnumConstants();
+      Enum<?>[] enumConstants = (Enum[]) instanceClass.getEnumConstants();
       int ordinal = readInt();
       assert (ordinal >= 0 && ordinal < enumConstants.length);
       return enumConstants[ordinal];
diff --git a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTest.java b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTest.java
index 3bf3757..1819e85 100644
--- a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTest.java
+++ b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTest.java
@@ -19,6 +19,7 @@
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
 
 /**
  * TODO: document me.
@@ -84,6 +85,24 @@
     });
   }
   
+  public void testDoublyReferencedArray() {
+    delayTestFinish(TEST_DELAY);
+
+    ObjectGraphTestServiceAsync service = getServiceAsync();
+    final SerializableWithTwoArrays node = TestSetFactory.createDoublyReferencedArray();
+    service.echo_SerializableWithTwoArrays(node, new AsyncCallback() {
+      public void onFailure(Throwable caught) {
+        TestSetValidator.rethrowException(caught);
+      }
+
+      public void onSuccess(Object result) {
+        assertNotNull(result);
+        assertTrue(TestSetValidator.isValid((SerializableWithTwoArrays) result));
+        finishTest();
+      }
+    });
+  }
+  
   public void testPrivateNoArg() {
     delayTestFinish(TEST_DELAY);
 
diff --git a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestService.java b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestService.java
index abaa9f2..f806677 100644
--- a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestService.java
+++ b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestService.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
 
 /**
  * TODO: document me.
@@ -34,6 +35,8 @@
 
   SerializablePrivateNoArg echo_PrivateNoArg(SerializablePrivateNoArg node);
 
+  SerializableWithTwoArrays echo_SerializableWithTwoArrays(SerializableWithTwoArrays node);
+
   SerializableDoublyLinkedNode echo_TrivialCyclicGraph(
       SerializableDoublyLinkedNode node);
 }
diff --git a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestServiceAsync.java b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestServiceAsync.java
index 68ff8be..4b4d42b 100644
--- a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestServiceAsync.java
+++ b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestServiceAsync.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
 
 /**
  * TODO: document me.
@@ -33,6 +34,8 @@
 
   void echo_PrivateNoArg(SerializablePrivateNoArg node, AsyncCallback callback);
 
+  void echo_SerializableWithTwoArrays(SerializableWithTwoArrays node, AsyncCallback callback);
+
   void echo_TrivialCyclicGraph(SerializableDoublyLinkedNode node,
       AsyncCallback callback);
 }
diff --git a/user/test/com/google/gwt/user/client/rpc/TestSetFactory.java b/user/test/com/google/gwt/user/client/rpc/TestSetFactory.java
index 693bfdc..49ca013 100644
--- a/user/test/com/google/gwt/user/client/rpc/TestSetFactory.java
+++ b/user/test/com/google/gwt/user/client/rpc/TestSetFactory.java
@@ -196,6 +196,11 @@
   public static class SerializableVector extends Vector implements
       IsSerializable {
   }
+  
+  public static class SerializableWithTwoArrays implements IsSerializable {
+    String[] one;
+    String[] two;
+  }
 
   /**
    * TODO: document me.
@@ -393,6 +398,12 @@
     return n1;
   }
 
+  static SerializableWithTwoArrays createDoublyReferencedArray() {
+    SerializableWithTwoArrays o = new SerializableWithTwoArrays();
+    o.two = o.one = createStringArray();
+    return o;
+  }
+
   static SerializableClass createSerializableClass() {
     SerializableClass cls = new SerializableClass();
     IsSerializable[] elements = new IsSerializable[] {
diff --git a/user/test/com/google/gwt/user/client/rpc/TestSetValidator.java b/user/test/com/google/gwt/user/client/rpc/TestSetValidator.java
index 010ef97..188ba01 100644
--- a/user/test/com/google/gwt/user/client/rpc/TestSetValidator.java
+++ b/user/test/com/google/gwt/user/client/rpc/TestSetValidator.java
@@ -18,6 +18,7 @@
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableClass;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -304,6 +305,13 @@
 
     return actual.getValue() == 1;
   }
+  
+  /**
+   * We want to assert that the two fields have object identity.
+   */
+  public static boolean isValid(SerializableWithTwoArrays node) {
+    return node.one == node.two;
+  }
 
   public static boolean isValid(Vector expected, Vector actual) {
     if (actual == null) {
diff --git a/user/test/com/google/gwt/user/server/rpc/ObjectGraphTestServiceImpl.java b/user/test/com/google/gwt/user/server/rpc/ObjectGraphTestServiceImpl.java
index 5bcab6d..511fe0a 100644
--- a/user/test/com/google/gwt/user/server/rpc/ObjectGraphTestServiceImpl.java
+++ b/user/test/com/google/gwt/user/server/rpc/ObjectGraphTestServiceImpl.java
@@ -19,6 +19,7 @@
 import com.google.gwt.user.client.rpc.TestSetValidator;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
 import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
 
 /**
  * TODO: document me.
@@ -62,6 +63,15 @@
     return node;
   }
 
+  public SerializableWithTwoArrays echo_SerializableWithTwoArrays(
+      SerializableWithTwoArrays node) {
+    if (!TestSetValidator.isValid(node)) {
+      throw new RuntimeException();
+    }
+
+    return node;
+  }
+
   public SerializableDoublyLinkedNode echo_ComplexCyclicGraph(
       SerializableDoublyLinkedNode node1, SerializableDoublyLinkedNode node2) {
     if (node1 != node2) {