Last active
January 6, 2018 03:53
-
-
Save liach/cdde4a79e8b493f76c34f43f50d331ba to your computer and use it in GitHub Desktop.
The modified version of Dynamic Enum Test that shows the bug
This file contains 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
import sun.reflect.ConstructorAccessor; | |
import sun.reflect.FieldAccessor; | |
import sun.reflect.ReflectionFactory; | |
import java.lang.reflect.AccessibleObject; | |
import java.lang.reflect.Array; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.Modifier; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.List; | |
public class DynamicEnumTest { | |
//original content | |
private static ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory(); | |
private static void setFailsafeFieldValue(Field field, Object target, Object value) throws NoSuchFieldException, | |
IllegalAccessException { | |
// let's make the field accessible | |
field.setAccessible(true); | |
// next we change the modifier in the Field instance to | |
// not be final anymore, thus tricking reflection into | |
// letting us modify the static final field | |
Field modifiersField = Field.class.getDeclaredField("modifiers"); | |
modifiersField.setAccessible(true); | |
int modifiers = modifiersField.getInt(field); | |
// blank out the final bit in the modifiers int | |
modifiers &= ~Modifier.FINAL; | |
modifiersField.setInt(field, modifiers); | |
FieldAccessor fa = reflectionFactory.newFieldAccessor(field, false); | |
fa.set(target, value); | |
} | |
private static void blankField(Class<?> enumClass, String fieldName) throws NoSuchFieldException, | |
IllegalAccessException { | |
for (Field field : Class.class.getDeclaredFields()) { | |
if (field.getName().contains(fieldName)) { | |
AccessibleObject.setAccessible(new Field[]{field}, true); | |
setFailsafeFieldValue(field, enumClass, null); | |
break; | |
} | |
} | |
} | |
private static void cleanEnumCache(Class<?> enumClass) throws NoSuchFieldException, IllegalAccessException { | |
blankField(enumClass, "enumConstantDirectory"); // Sun (Oracle?!?) JDK 1.5/6 | |
blankField(enumClass, "enumConstants"); // IBM JDK | |
} | |
private static ConstructorAccessor getConstructorAccessor(Class<?> enumClass, Class<?>[] additionalParameterTypes) | |
throws NoSuchMethodException { | |
Class<?>[] parameterTypes = new Class[additionalParameterTypes.length + 2]; | |
parameterTypes[0] = String.class; | |
parameterTypes[1] = int.class; | |
System.arraycopy(additionalParameterTypes, 0, parameterTypes, 2, additionalParameterTypes.length); | |
return reflectionFactory.newConstructorAccessor(enumClass.getDeclaredConstructor(parameterTypes)); | |
} | |
private static Object makeEnum(Class<?> enumClass, String value, int ordinal, Class<?>[] additionalTypes, | |
Object[] additionalValues) throws Exception { | |
Object[] parms = new Object[additionalValues.length + 2]; | |
parms[0] = value; | |
parms[1] = Integer.valueOf(ordinal); | |
System.arraycopy(additionalValues, 0, parms, 2, additionalValues.length); | |
return enumClass.cast(getConstructorAccessor(enumClass, additionalTypes).newInstance(parms)); | |
} | |
/** | |
* Add an enum instance to the enum class given as argument | |
* | |
* @param <T> the type of the enum (implicit) | |
* @param enumType the class of the enum to be modified | |
* @param enumName the name of the new enum instance to be added to the class. | |
*/ | |
@SuppressWarnings("unchecked") | |
public static <T extends Enum<?>> void addEnum(Class<T> enumType, String enumName) { | |
// 0. Sanity checks | |
if (!Enum.class.isAssignableFrom(enumType)) { | |
throw new RuntimeException("class " + enumType + " is not an instance of Enum"); | |
} | |
// 1. Lookup "$VALUES" holder in enum class and get previous enum instances | |
Field valuesField = null; | |
Field[] fields = TestEnum.class.getDeclaredFields(); | |
for (Field field : fields) { | |
if (field.getName().contains("$VALUES")) { | |
valuesField = field; | |
break; | |
} | |
} | |
AccessibleObject.setAccessible(new Field[]{valuesField}, true); | |
try { | |
// 2. Copy it | |
T[] previousValues = (T[]) valuesField.get(enumType); | |
List<T> values = new ArrayList<T>(Arrays.asList(previousValues)); | |
// 3. build new enum | |
T newValue = (T) makeEnum(enumType, // The target enum class | |
enumName, // THE NEW ENUM INSTANCE TO BE DYNAMICALLY ADDED | |
values.size(), | |
new Class<?>[]{}, // could be used to pass values to the enum constuctor if needed | |
new Object[]{}); // could be used to pass values to the enum constuctor if needed | |
// 4. add new value | |
values.add(newValue); | |
// 5. Set new values field | |
setFailsafeFieldValue(valuesField, null, values.toArray((T[]) Array.newInstance(enumType, 0))); | |
// 6. Clean enum cache | |
cleanEnumCache(enumType); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
throw new RuntimeException(e.getMessage(), e); | |
} | |
} | |
// -------------------------------------------------------------------------------------------- // | |
// custom content start | |
public static void injectField(Field fieldToHack, Object value) throws Exception { | |
AccessibleObject.setAccessible(new Field[]{fieldToHack}, true); | |
setFailsafeFieldValue(fieldToHack, null, value); // that hacked field is static | |
} | |
// We will add enum 10000 times so that there will be v_0 to v_10000 | |
private enum TestEnum { | |
v_0 | |
} | |
// This one is to mock Lex's annotation injection: we will inject everything from {0} to {10000} | |
private static final int[] ARRAY_FIELD = new int[] {0}; | |
private static final Custom CUSTOM_FIELD = new Custom(0); | |
private static final String STRING_FIELD = Integer.toString(0); | |
public static void main(String[] args) throws Exception { | |
//set up field | |
Field arrayField = DynamicEnumTest.class.getDeclaredField("ARRAY_FIELD"); | |
Field customField = DynamicEnumTest.class.getDeclaredField("CUSTOM_FIELD"); | |
Field stringField = DynamicEnumTest.class.getDeclaredField("STRING_FIELD"); | |
for (int i = 1; i <= 10000; i++) { | |
addEnum(TestEnum.class, "v_" + i); | |
TestEnum.values(); | |
injectField(arrayField, new int[] {i}); | |
getArrayField(); | |
injectField(customField, new Custom(i)); | |
getCustomField(); | |
injectField(stringField, Integer.toString(i)); | |
getStringField(); | |
} | |
// Run a few tests just to show it works OK. | |
TestEnum[] all = TestEnum.values(); | |
System.out.println(all[all.length - 1]); | |
System.out.println(getArrayField()[0]); | |
System.out.println(getCustomField()); | |
System.out.println(getStringField()); | |
// Correct result: | |
// v_10000 | |
// 10000 | |
// 10000 | |
// 10000 | |
// In fact, all four things would fuck up because of the jit optimization | |
} | |
public static int[] getArrayField() { | |
return ARRAY_FIELD.clone(); | |
} | |
public static Custom getCustomField() { | |
// if we clone, the death limit is much later; otherwise, it comes way earlier. | |
// compare this result to the string one's result | |
return CUSTOM_FIELD.clone(); | |
} | |
public static String getStringField() { | |
return STRING_FIELD; | |
} | |
public static final class Custom implements Cloneable { | |
private final int value; | |
public Custom(int value) { | |
this.value = value; | |
} | |
public int getValue() { | |
return value; | |
} | |
@Override | |
public int hashCode() { | |
return value; | |
} | |
@Override | |
public boolean equals(Object obj) { | |
return obj instanceof Custom && ((Custom) obj).value == value; | |
} | |
@Override | |
public String toString() { | |
return Integer.toString(value); | |
} | |
@Override | |
public Custom clone() { | |
try { | |
return (Custom) super.clone(); | |
} catch (CloneNotSupportedException ex) { | |
throw new Error(); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment