You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@servicecomb.apache.org by li...@apache.org on 2018/10/23 08:43:16 UTC

[incubator-servicecomb-java-chassis] 02/02: [SCB-965] to avoid copy too many code, use javassist to fix the DoS problem

This is an automated email from the ASF dual-hosted git repository.

liubao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-servicecomb-java-chassis.git

commit e17c9fd91792ea068341363205fca4d7b4ae5b43
Author: wujimin <wu...@huawei.com>
AuthorDate: Thu Oct 18 12:13:02 2018 +0800

    [SCB-965] to avoid copy too many code, use javassist to fix the DoS problem
---
 .../com/fasterxml/jackson/core/base/DoSFix.java    | 151 +++++++++++++
 .../jackson/core/base/DoSParserFixed.java          |  76 +++++++
 .../rest/codec/AbstractRestObjectMapper.java       |   5 +
 .../common/rest/codec/RestObjectMapper.java        |   7 +
 .../codec/produce/ProduceProcessorManager.java     |   4 +-
 .../common/rest/codec/fix/TestDoSFix.java          | 246 +++++++++++++++++++++
 6 files changed, 488 insertions(+), 1 deletion(-)

diff --git a/common/common-rest/src/main/java/com/fasterxml/jackson/core/base/DoSFix.java b/common/common-rest/src/main/java/com/fasterxml/jackson/core/base/DoSFix.java
new file mode 100644
index 0000000..6c46eca
--- /dev/null
+++ b/common/common-rest/src/main/java/com/fasterxml/jackson/core/base/DoSFix.java
@@ -0,0 +1,151 @@
+/*
+ * 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 com.fasterxml.jackson.core.base;
+
+import org.apache.servicecomb.foundation.common.utils.JvmUtils;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper;
+import com.fasterxml.jackson.core.json.ReaderBasedJsonParser;
+import com.fasterxml.jackson.core.json.UTF8StreamJsonParser;
+import com.fasterxml.jackson.databind.MappingJsonFactory;
+import com.netflix.config.DynamicPropertyFactory;
+
+import javassist.CannotCompileException;
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.CtMethod;
+import javassist.LoaderClassPath;
+import javassist.NotFoundException;
+
+/**
+ * will be deleted after jackson fix the DoS problem:
+ * https://github.com/FasterXML/jackson-databind/issues/2157
+ */
+public class DoSFix {
+  private static final String SUFFIX = "Fixed";
+
+  private static boolean enabled = DynamicPropertyFactory.getInstance()
+      .getBooleanProperty("servicecomb.jackson.fix.DoS.enabled", true).get();
+
+  private static boolean fixed;
+
+  private static Class<?> mappingJsonFactoryClass;
+
+  public static synchronized void init() {
+    if (fixed || !enabled) {
+      return;
+    }
+
+    fix();
+  }
+
+  public static JsonFactory createJsonFactory() {
+    try {
+      return (JsonFactory) mappingJsonFactoryClass.newInstance();
+    } catch (Throwable e) {
+      throw new IllegalStateException("Failed to create JsonFactory.", e);
+    }
+  }
+
+  private static void fix() {
+    try {
+      ClassLoader classLoader = JvmUtils.correctClassLoader(DoSFix.class.getClassLoader());
+      ClassPool pool = new ClassPool(ClassPool.getDefault());
+      pool.appendClassPath(new LoaderClassPath(classLoader));
+
+      fixParserBase(classLoader, pool);
+      fixReaderParser(classLoader, pool);
+      fixStreamParser(classLoader, pool);
+      fixByteSourceJsonBootstrapper(classLoader, pool);
+
+      CtClass ctJsonFactoryFixedClass = fixJsonFactory(classLoader, pool);
+      fixMappingJsonFactoryClass(classLoader, pool, ctJsonFactoryFixedClass);
+
+      fixed = true;
+    } catch (Throwable e) {
+      throw new IllegalStateException(
+          "Failed to fix jackson DoS bug.",
+          e);
+    }
+  }
+
+  private static void fixMappingJsonFactoryClass(ClassLoader classLoader, ClassPool pool,
+      CtClass ctJsonFactoryFixedClass) throws NotFoundException, CannotCompileException {
+    CtClass ctMappingJsonFactoryClass = pool
+        .getAndRename(MappingJsonFactory.class.getName(), MappingJsonFactory.class.getName() + SUFFIX);
+    ctMappingJsonFactoryClass.setSuperclass(ctJsonFactoryFixedClass);
+    mappingJsonFactoryClass = ctMappingJsonFactoryClass.toClass(classLoader, null);
+  }
+
+  private static CtClass fixJsonFactory(ClassLoader classLoader, ClassPool pool)
+      throws NotFoundException, CannotCompileException {
+    CtClass ctJsonFactoryClass = pool.getCtClass(JsonFactory.class.getName());
+    CtClass ctJsonFactoryFixedClass = pool.makeClass(JsonFactory.class.getName() + SUFFIX);
+    ctJsonFactoryFixedClass.setSuperclass(ctJsonFactoryClass);
+    for (CtMethod ctMethod : ctJsonFactoryClass.getDeclaredMethods()) {
+      if (ctMethod.getName().equals("_createParser")) {
+        ctJsonFactoryFixedClass.addMethod(new CtMethod(ctMethod, ctJsonFactoryFixedClass, null));
+      }
+    }
+    ctJsonFactoryFixedClass
+        .replaceClassName(ReaderBasedJsonParser.class.getName(), ReaderBasedJsonParser.class.getName() + SUFFIX);
+    ctJsonFactoryFixedClass
+        .replaceClassName(UTF8StreamJsonParser.class.getName(), UTF8StreamJsonParser.class.getName() + SUFFIX);
+    ctJsonFactoryFixedClass.replaceClassName(ByteSourceJsonBootstrapper.class.getName(),
+        ByteSourceJsonBootstrapper.class.getName() + SUFFIX);
+    ctJsonFactoryFixedClass.toClass(classLoader, null);
+
+    return ctJsonFactoryFixedClass;
+  }
+
+  private static void fixByteSourceJsonBootstrapper(ClassLoader classLoader, ClassPool pool)
+      throws NotFoundException, CannotCompileException {
+    CtClass ctByteSourceJsonBootstrapper = pool
+        .getAndRename(ByteSourceJsonBootstrapper.class.getName(), ByteSourceJsonBootstrapper.class.getName() + SUFFIX);
+    ctByteSourceJsonBootstrapper
+        .replaceClassName(UTF8StreamJsonParser.class.getName(), UTF8StreamJsonParser.class.getName() + SUFFIX);
+    ctByteSourceJsonBootstrapper
+        .replaceClassName(ReaderBasedJsonParser.class.getName(), ReaderBasedJsonParser.class.getName() + SUFFIX);
+    ctByteSourceJsonBootstrapper.toClass(classLoader, null);
+  }
+
+  private static void fixStreamParser(ClassLoader classLoader, ClassPool pool)
+      throws NotFoundException, CannotCompileException {
+    CtClass ctStreamClass = pool
+        .getAndRename(UTF8StreamJsonParser.class.getName(), UTF8StreamJsonParser.class.getName() + SUFFIX);
+    ctStreamClass.replaceClassName(ParserBase.class.getName(), ParserBase.class.getName() + SUFFIX);
+    ctStreamClass.toClass(classLoader, null);
+  }
+
+  private static void fixReaderParser(ClassLoader classLoader, ClassPool pool)
+      throws NotFoundException, CannotCompileException {
+    CtClass ctReaderClass = pool
+        .getAndRename(ReaderBasedJsonParser.class.getName(), ReaderBasedJsonParser.class.getName() + SUFFIX);
+    ctReaderClass.replaceClassName(ParserBase.class.getName(), ParserBase.class.getName() + SUFFIX);
+    ctReaderClass.toClass(classLoader, null);
+  }
+
+  private static void fixParserBase(ClassLoader classLoader, ClassPool pool)
+      throws NotFoundException, CannotCompileException {
+    CtMethod ctMethodFixed = pool.get(DoSParserFixed.class.getName()).getDeclaredMethod("_parseSlowInt");
+    CtClass baseClass = pool.getAndRename(ParserBase.class.getName(), ParserBase.class.getName() + SUFFIX);
+    baseClass.removeMethod(baseClass.getDeclaredMethod("_parseSlowInt"));
+    baseClass.addMethod(new CtMethod(ctMethodFixed, baseClass, null));
+    baseClass.toClass(classLoader, null);
+  }
+}
diff --git a/common/common-rest/src/main/java/com/fasterxml/jackson/core/base/DoSParserFixed.java b/common/common-rest/src/main/java/com/fasterxml/jackson/core/base/DoSParserFixed.java
new file mode 100644
index 0000000..71fbdc6
--- /dev/null
+++ b/common/common-rest/src/main/java/com/fasterxml/jackson/core/base/DoSParserFixed.java
@@ -0,0 +1,76 @@
+/*
+ * 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 com.fasterxml.jackson.core.base;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.math.BigInteger;
+
+import com.fasterxml.jackson.core.ObjectCodec;
+import com.fasterxml.jackson.core.io.IOContext;
+import com.fasterxml.jackson.core.io.NumberInput;
+import com.fasterxml.jackson.core.json.ReaderBasedJsonParser;
+import com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer;
+
+/**
+ * will not be use directly
+ * just get _parseSlowInt/_parseSlowFloat bytecode and replace to ParserBase
+ */
+public abstract class DoSParserFixed extends ReaderBasedJsonParser {
+  public DoSParserFixed(IOContext ctxt, int features, Reader r,
+      ObjectCodec codec, CharsToNameCanonicalizer st,
+      char[] inputBuffer, int start, int end, boolean bufferRecyclable) {
+    super(ctxt, features, r, codec, st, inputBuffer, start, end, bufferRecyclable);
+  }
+
+  private void _parseSlowInt(int expType) throws IOException {
+    String numStr = _textBuffer.contentsAsString();
+    try {
+      int len = _intLength;
+      char[] buf = _textBuffer.getTextBuffer();
+      int offset = _textBuffer.getTextOffset();
+      if (_numberNegative) {
+        ++offset;
+      }
+      // Some long cases still...
+      if (NumberInput.inLongRange(buf, offset, len, _numberNegative)) {
+        // Probably faster to construct a String, call parse, than to use BigInteger
+        _numberLong = Long.parseLong(numStr);
+        _numTypesValid = NR_LONG;
+      } else {
+        // nope, need the heavy guns... (rare case)
+
+        // *** fix DoS attack begin ***
+        if (NR_DOUBLE == expType || NR_FLOAT == expType) {
+          _numberDouble = Double.parseDouble(numStr);
+          _numTypesValid = NR_DOUBLE;
+          return;
+        }
+        if (NR_BIGINT != expType) {
+          throw new NumberFormatException("invalid numeric value '" + numStr + "'");
+        }
+        // *** fix DoS attack end ***
+
+        _numberBigInt = new BigInteger(numStr);
+        _numTypesValid = NR_BIGINT;
+      }
+    } catch (NumberFormatException nex) {
+      // Can this ever occur? Due to overflow, maybe?
+      _wrapError("Malformed numeric value '" + numStr + "'", nex);
+    }
+  }
+}
diff --git a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/AbstractRestObjectMapper.java b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/AbstractRestObjectMapper.java
index 0ca5fa6..1f64d3d 100644
--- a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/AbstractRestObjectMapper.java
+++ b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/AbstractRestObjectMapper.java
@@ -17,10 +17,15 @@
 
 package org.apache.servicecomb.common.rest.codec;
 
+import com.fasterxml.jackson.core.JsonFactory;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 public abstract class AbstractRestObjectMapper extends ObjectMapper {
   private static final long serialVersionUID = 189026839992490564L;
 
+  public AbstractRestObjectMapper(JsonFactory jsonFactory) {
+    super(jsonFactory);
+  }
+
   abstract public String convertToString(Object value) throws Exception;
 }
diff --git a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/RestObjectMapper.java b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/RestObjectMapper.java
index 8f20acb..ae1a45f 100644
--- a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/RestObjectMapper.java
+++ b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/RestObjectMapper.java
@@ -23,6 +23,7 @@ import java.util.Date;
 
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.core.JsonParser.Feature;
+import com.fasterxml.jackson.core.base.DoSFix;
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.JavaType;
 import com.fasterxml.jackson.databind.JsonSerializer;
@@ -34,6 +35,10 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
 import io.vertx.core.json.JsonObject;
 
 public class RestObjectMapper extends AbstractRestObjectMapper {
+  static {
+    DoSFix.init();
+  }
+
   private static class JsonObjectSerializer extends JsonSerializer<JsonObject> {
     @Override
     public void serialize(JsonObject value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
@@ -47,6 +52,8 @@ public class RestObjectMapper extends AbstractRestObjectMapper {
 
   @SuppressWarnings("deprecation")
   public RestObjectMapper() {
+    super(DoSFix.createJsonFactory());
+
     // swagger中要求date使用ISO8601格式传递,这里与之做了功能绑定,这在cse中是没有问题的
     setDateFormat(new com.fasterxml.jackson.databind.util.ISO8601DateFormat() {
       private static final long serialVersionUID = 7798938088541203312L;
diff --git a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/produce/ProduceProcessorManager.java b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/produce/ProduceProcessorManager.java
index 255b35d..6bd5bab 100644
--- a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/produce/ProduceProcessorManager.java
+++ b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/produce/ProduceProcessorManager.java
@@ -20,6 +20,7 @@ package org.apache.servicecomb.common.rest.codec.produce;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+
 import javax.ws.rs.core.MediaType;
 
 import org.apache.servicecomb.foundation.common.RegisterManager;
@@ -47,8 +48,9 @@ public final class ProduceProcessorManager extends RegisterManager<String, Produ
     super(NAME);
     Set<String> set = new HashSet<>();
     produceProcessor.forEach(processor -> {
-      if (set.add(processor.getName()))
+      if (set.add(processor.getName())) {
         register(processor.getName(), processor);
+      }
     });
   }
 }
diff --git a/common/common-rest/src/test/java/org/apache/servicecomb/common/rest/codec/fix/TestDoSFix.java b/common/common-rest/src/test/java/org/apache/servicecomb/common/rest/codec/fix/TestDoSFix.java
new file mode 100644
index 0000000..baff897
--- /dev/null
+++ b/common/common-rest/src/test/java/org/apache/servicecomb/common/rest/codec/fix/TestDoSFix.java
@@ -0,0 +1,246 @@
+/*
+ * 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.servicecomb.common.rest.codec.fix;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.util.concurrent.Callable;
+
+import org.apache.servicecomb.common.rest.codec.RestObjectMapper;
+import org.apache.servicecomb.foundation.test.scaffolding.model.Color;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.InvalidFormatException;
+import com.fasterxml.jackson.databind.exc.MismatchedInputException;
+import com.google.common.base.Strings;
+
+public class TestDoSFix {
+  static ObjectMapper mapper = new RestObjectMapper();
+
+  static String invalidNum = Strings.repeat("9", 100_0000);
+
+  static String invalidStr = "\"" + invalidNum + "\"";
+
+  static String invalidArrNum = "[" + invalidNum + "]";
+
+  static String invalidArrStr = "[\"" + invalidNum + "\"]";
+
+  public static class Model {
+    public Color color;
+
+    public char cValue;
+
+    public Character cObjValue;
+
+    public byte bValue;
+
+    public Byte bObjValue;
+
+    public short sValue;
+
+    public Short sObjValue;
+
+    public int iValue;
+
+    public Integer iObjValue;
+
+    public long lValue;
+
+    public Long lObjValue;
+
+    public float fValue;
+
+    public Float fObjValue;
+
+    public double dValue;
+
+    public Double dObjValue;
+  }
+
+  void fastFail(Callable<?> callable, Class<?> eCls) {
+    long start = System.currentTimeMillis();
+    try {
+      Object ret = callable.call();
+      Assert.fail("expect failed, but succes to be " + ret);
+    } catch (AssertionError e) {
+      throw e;
+    } catch (Throwable e) {
+      if (eCls != e.getClass()) {
+        e.printStackTrace();
+      }
+      Assert.assertEquals(eCls, e.getClass());
+    }
+
+    long time = System.currentTimeMillis() - start;
+    Assert.assertTrue("did not fix DoS problem, time:" + time, time < 1000);
+  }
+
+  void fastFail(String input, Class<?> cls, Class<?> eCls) {
+    fastFail(() -> mapper.readValue(input, cls), eCls);
+
+    fastFail(() -> mapper.readValue(new ByteArrayInputStream(input.getBytes()), cls), eCls);
+  }
+
+  void batFastFail(Class cls, Class<?> e1, Class<?> e2) {
+    fastFail(invalidNum, cls, e1);
+    fastFail(invalidStr, cls, e2);
+    fastFail(invalidArrNum, cls, e1);
+    fastFail(invalidArrStr, cls, e2);
+  }
+
+  void batFastFail(Class cls) {
+    batFastFail(cls, JsonParseException.class, InvalidFormatException.class);
+  }
+
+  void batFastFail(String fieldName, Class<?> e1, Class<?> e2) {
+    fastFail("{\"" + fieldName + "\":" + invalidNum + "}", Model.class, e1);
+    fastFail("{\"" + fieldName + "\":\"" + invalidNum + "\"}", Model.class, e2);
+    fastFail("{\"" + fieldName + "\":[" + invalidNum + "]}", Model.class, e1);
+    fastFail("{\"" + fieldName + "\":[\"" + invalidNum + "\"]}", Model.class, e2);
+  }
+
+  void batFastFail(String fieldName) {
+    batFastFail(fieldName, JsonMappingException.class, InvalidFormatException.class);
+  }
+
+  @Test
+  public void testEnum() {
+    batFastFail(Color.class);
+    batFastFail("color");
+  }
+
+  @Test
+  public void testChar() {
+    batFastFail(char.class, JsonParseException.class, MismatchedInputException.class);
+    batFastFail(Character.class, JsonParseException.class, MismatchedInputException.class);
+
+    batFastFail("cValue", JsonMappingException.class, MismatchedInputException.class);
+    batFastFail("cObjValue", JsonMappingException.class, MismatchedInputException.class);
+  }
+
+  @Test
+  public void testByte() {
+    batFastFail(byte.class);
+    batFastFail(Byte.class);
+
+    batFastFail("bValue");
+    batFastFail("bObjValue");
+  }
+
+  @Test
+  public void testShort() {
+    batFastFail(short.class);
+    batFastFail(Short.class);
+
+    batFastFail("sValue");
+    batFastFail("sObjValue");
+  }
+
+  @Test
+  public void testInt() {
+    batFastFail(int.class);
+    batFastFail(Integer.class);
+
+    batFastFail("iValue");
+    batFastFail("iObjValue");
+  }
+
+  @Test
+  public void testLong() {
+    batFastFail(long.class);
+    batFastFail(Long.class);
+
+    batFastFail("lValue");
+    batFastFail("lObjValue");
+  }
+
+  Object fastSucc(Callable<?> callable) {
+    long start = System.currentTimeMillis();
+    try {
+      Object ret = callable.call();
+      Assert.assertTrue(System.currentTimeMillis() - start < 1000);
+      return ret;
+    } catch (Throwable e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  Object fastSucc(String input, Class<?> cls) {
+    return fastSucc(() -> mapper.readValue(input, cls));
+  }
+
+  Object fastSucc(InputStream input, Class<?> cls) {
+    return fastSucc(() -> {
+      input.reset();
+      return mapper.readValue(input, cls);
+    });
+  }
+
+  void batFastSucc(Class cls, Object expected) {
+    Assert.assertEquals(expected, fastSucc(invalidNum, cls));
+    Assert.assertEquals(expected, fastSucc(new ByteArrayInputStream(invalidNum.getBytes()), cls));
+
+    Assert.assertEquals(expected, fastSucc(invalidStr, cls));
+    Assert.assertEquals(expected, fastSucc(new ByteArrayInputStream(invalidStr.getBytes()), cls));
+
+    Assert.assertEquals(expected, fastSucc(invalidArrNum, cls));
+    Assert.assertEquals(expected, fastSucc(new ByteArrayInputStream(invalidArrNum.getBytes()), cls));
+
+    Assert.assertEquals(expected, fastSucc(invalidArrStr, cls));
+    Assert.assertEquals(expected, fastSucc(new ByteArrayInputStream(invalidArrStr.getBytes()), cls));
+  }
+
+  void checkField(Model model, String fieldName, Object expected) {
+    try {
+      Field field = Model.class.getField(fieldName);
+      Object value = field.get(model);
+      Assert.assertEquals(expected, value);
+    } catch (Throwable e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  void batFastSucc(String fieldName, Object expected) {
+    checkField((Model) fastSucc("{\"" + fieldName + "\":" + invalidNum + "}", Model.class), fieldName, expected);
+    checkField((Model) fastSucc("{\"" + fieldName + "\":\"" + invalidNum + "\"}", Model.class), fieldName, expected);
+    checkField((Model) fastSucc("{\"" + fieldName + "\":[" + invalidNum + "]}", Model.class), fieldName, expected);
+    checkField((Model) fastSucc("{\"" + fieldName + "\":[\"" + invalidNum + "\"]}", Model.class), fieldName, expected);
+  }
+
+  @Test
+  public void testFloat() {
+    batFastSucc(float.class, Float.POSITIVE_INFINITY);
+    batFastSucc(Float.class, Float.POSITIVE_INFINITY);
+
+    batFastSucc("fValue", Float.POSITIVE_INFINITY);
+    batFastSucc("fObjValue", Float.POSITIVE_INFINITY);
+  }
+
+  @Test
+  public void testDouble() {
+    batFastSucc(double.class, Double.POSITIVE_INFINITY);
+    batFastSucc(Double.class, Double.POSITIVE_INFINITY);
+
+    batFastSucc("dValue", Double.POSITIVE_INFINITY);
+    batFastSucc("dObjValue", Double.POSITIVE_INFINITY);
+  }
+}