Home of The JavaSpecialists' Newsletter

272Hacking Enums Revisited

Author: Dr Heinz M. KabutzDate: 2019-09-07Java Version: 12Category: Tips and Tricks
 

Abstract: In a previous newsletter, we looked at how we could dynamically add new enums and also rewire affected switch statements. Due to improvements in Java security, our old approach needs to be updated for Java 12.

 

Welcome to the 272nd edition of The Java(tm) Specialists' Newsletter, sent from the beautiful Island of Crete. I popped in to Agile Crete yesterday, run by our fellow JCretan Yiorgos Saslis. After running it in Heraklion since 2014, they decided to relocate to the Orthodox Academy on my side of the island. It was great seeing how they operate and I've got some good ideas for how we can improve JCrete in 2020.

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

Hacking Enums Revisited

In 2008, I published a newsletter demonstrating how we could change static final fields by changing the modifier of the java.lang.reflect.Field instance. I have since seen this trick used by lots of Java gurus, but without giving credit. Perhaps this is a good thing. There is a good reason why we cannot change static final fields. It is exceedingly dangerous and the result is unpredictable. Not something I want to be remembered by.

Suggestion: before reading on, may I gently suggest that you have a look at Newsletter 161? This newsletter is an update to the previous article and won't make much sense without first understanding the old code.

Björn Kautler pointed out to me yesterday that my old EnumBuster did not work anymore. In earlier versions of Java, the class holding the SwitchMap was a named inner class. At some point this changed to an anonymous inner class. We can find all named classes using the Class.getDeclaredClasses() method, but there is no easy way to get all the anonymous inner classes. He sent me some code that scanned the classes field of ClassLoader. However, that relied on the switch statement having been executed at least once, something that I did not want to rely on in an updated EnumBuster.

Another interesting gotcha appears in Java 12. In the past, Java would happily deliver into our clutches meta-data of classes that perhaps it shouldn't have. JDK-8210522 now filters the following fields from us:

Reflection.*
AccessibleObject.*
Class.classLoader
ClassLoader.*
Constructor.*
Field.*
Method.*
Module.*
System.security

Don't bother trying to find the fields of Field.class or ClassLoader.class via reflection. At the moment only fields are filtered, but this may change to also include dangerous methods.

The following prints the fields of Field.class in Java 1.1 up to Java 11, but not in Java 12:

import java.lang.reflect.*;

public class Spielverderber {
  public static void main(String... args) {
    Field[] declaredFields = Field.class.getDeclaredFields();
    for (int i = 0; i < declaredFields.length; i++) {
      System.out.println(declaredFields[i]);
    }
  }
}

We can bypass this using VarHandles and MethodHandles.privateLookupIn():

import java.lang.invoke.*;
import java.lang.reflect.*;

public class HiddenFieldsRevealed {
  private static final Object greeting = "Hello world";

  public static void main(String... args)
      throws ReflectiveOperationException {
    VarHandle vh = MethodHandles.privateLookupIn(
        Field.class, MethodHandles.lookup()
    ).findVarHandle(Field.class, "modifiers", int.class);
    Field greetingField = HiddenFieldsRevealed.class
        .getDeclaredField("greeting");
    System.out.println("isFinal=" +
        Modifier.isFinal(greetingField.getModifiers()));
    vh.set(greetingField, 0);
    System.out.println("isFinal=" +
        Modifier.isFinal(greetingField.getModifiers()));
  }
}
  

This prints first isFinal=true and then isFinal=false.

However, on retrospection, I don't like this approach that much, even though I invented it. Seeing we are doing something unsafe, let's rather just use the sun.misc.Unsafe class. That seems a bit more "honest".

Here is an updated ReflectionHelper class that also has support for finding all anonymous inner classes, based on some sample code that Björn Kautler sent me. It recursively goes through the declared classes and other anonymous inner classes and tries to guess more. We assume (this might be a bad assumption) that anonymous inner classes always follow the pattern classname$[1..n]. Note that when we load the classes with Class.forName(), we use the thread context class loader and we do not initialize the classes. That can happen later - if necessary.

package eu.javaspecialists.reflection;

import sun.misc.*;

import java.lang.reflect.*;
import java.util.*;

public class ReflectionHelper {
  public static Class<?>[] getAnonymousClasses(Class<?> clazz) {
    Collection<Class<?>> classes = new ArrayList<>();
    for (int i = 1; ; i++) {
      try {
        classes.add(Class.forName(
            clazz.getName() + "$" + i,
            false, // do not initialize
            Thread.currentThread().getContextClassLoader()));
      } catch (ClassNotFoundException e) {
        break;
      }
    }
    for (Class<?> inner : clazz.getDeclaredClasses()) {
      Collections.addAll(classes, getAnonymousClasses(inner));
    }
    for (Class<?> anon : new ArrayList<>(classes)) {
      Collections.addAll(classes, getAnonymousClasses(anon));
    }
    return classes.toArray(new Class<?>[0]);
  }

  private final static Unsafe UNSAFE;

  static {
    try {
      Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
      theUnsafe.setAccessible(true);
      UNSAFE = (Unsafe) theUnsafe.get(null);
    } catch (ReflectiveOperationException e) {
      throw new IllegalStateException(e);
    }
  }

  /**
   * Super dangerous and might have unintended side effects.  Use
   * only for testing, and then with circumspection.
   *
   * @param field
   * @param value
   * @throws ReflectiveOperationException
   */
  public static void setStaticFinalField(Field field,
                                         Object value)
      throws ReflectiveOperationException {
    if (!Modifier.isStatic(field.getModifiers())
        || !Modifier.isFinal(field.getModifiers()))
      throw new IllegalArgumentException(
          "field should be final static");
    Object fieldBase = UNSAFE.staticFieldBase(field);
    long fieldOffset = UNSAFE.staticFieldOffset(field);
    UNSAFE.putObject(fieldBase, fieldOffset, value);
  }

  public static <E extends Enum<E>> E makeEnum(
      Class<E> enumType, String name)
      throws ReflectiveOperationException {
    E e = (E) UNSAFE.allocateInstance(enumType);
    Field nameField = Enum.class.getDeclaredField("name");
    nameField.setAccessible(true);
    nameField.set(e, name);
    return e;
  }
}
  

The setStaticFinalField() and makeEnum() methods simply call the relevant methods in sun.misc.Unsafe. Yes, it is horrible and dangerous and has no place in real code. We use it only for testing purposes.

Our EnumBuster from Newsletter 161 is also updated. It works correctly from Java 7 up to Java 12. Java 13-ea also seems to work. For a detailed explanation of EnumBuster, please refer to our earlier article.

package eu.javaspecialists.reflection;

import java.lang.reflect.*;
import java.util.*;

import static eu.javaspecialists.reflection.ReflectionHelper.*;

public class EnumBuster<E extends Enum<E>> {
  private final Class<E> clazz;
  private final Collection<Field> switchFields;
  private final Deque<Memento> undoStack = new ArrayDeque<>();

  /**
   * Construct an EnumBuster for the given enum class and keep
   * the switch statements of the classes specified in
   * switchUsers in sync with the enum values.
   */
  public EnumBuster(Class<E> clazz, Class<?>... switchUsers) {
    this.clazz = clazz;
    switchFields = findRelatedSwitchFields(switchUsers);
  }

  /**
   * Make a new enum instance, without adding it to the values
   * array and using the default ordinal of 0.
   */
  public E make(String value) throws ReflectiveOperationException {
    return makeEnum(clazz, value);
  }

  /**
   * This method adds the given enum into the array
   * inside the enum class.  If the enum already
   * contains that particular value, then the value
   * is overwritten with our enum.  Otherwise it is
   * added at the end of the array.
   * <p>
   * In addition, if there is a constant field in the
   * enum class pointing to an enum with our value,
   * then we replace that with our enum instance.
   * <p>
   * The ordinal is either set to the existing position
   * or to the last value.
   * <p>
   * Warning: This should probably never be called,
   * since it can cause permanent changes to the enum
   * values.  Use only in extreme conditions.
   *
   * @param e the enum to add
   */
  public void addByValue(E e) throws ReflectiveOperationException {
    undoStack.push(new Memento());
    Field valuesField = findValuesField();

    // we get the current Enum[]
    E[] values = values();
    for (int i = 0; i < values.length; i++) {
      E value = values[i];
      if (value.name().equals(e.name())) {
        setOrdinal(e, value.ordinal());
        values[i] = e;
        replaceConstant(e);
        return;
      }
    }

    // we did not find it in the existing array, thus
    // append it to the array
    E[] newValues = Arrays.copyOf(values, values.length + 1);
    newValues[newValues.length - 1] = e;
    setStaticFinalField(valuesField, newValues);

    int ordinal = newValues.length - 1;
    setOrdinal(e, ordinal);
    addSwitchCase();
  }

  /**
   * We delete the enum from the values array and set the
   * constant pointer to null.
   *
   * @param e the enum to delete from the type.
   * @return true if the enum was found and deleted;
   * false otherwise
   */
  public boolean deleteByValue(E e)
      throws ReflectiveOperationException {
    if (e == null) throw new NullPointerException();
    undoStack.push(new Memento());
    // we get the current E[]
    E[] values = values();
    for (int i = 0; i < values.length; i++) {
      E value = values[i];
      if (value.name().equals(e.name())) {
        E[] newValues = Arrays.copyOf(values, values.length - 1);
        System.arraycopy(values, i + 1, newValues, i,
            values.length - i - 1);
        for (int j = i; j < newValues.length; j++) {
          setOrdinal(newValues[j], j);
        }
        Field valuesField = findValuesField();
        setStaticFinalField(valuesField, newValues);
        removeSwitchCase(i);
        blankOutConstant(e);
        return true;
      }
    }
    return false;
  }

  /**
   * Undo the state right back to the beginning when the
   * EnumBuster was created.
   */
  public void restore() throws ReflectiveOperationException {
    while (undo()) {
      //
    }
  }

  /**
   * Undo the previous operation.
   */
  public boolean undo() throws ReflectiveOperationException {
    Memento memento = undoStack.poll();
    if (memento == null) return false;
    memento.undo();
    return true;
  }


  /**
   * The only time we ever add a new enum is at the end.
   * Thus all we need to do is expand the switch map arrays
   * by one empty slot.
   */
  private void addSwitchCase() throws ReflectiveOperationException {
    for (Field switchField : switchFields) {
      int[] switches = (int[]) switchField.get(null);
      switches = Arrays.copyOf(switches, switches.length + 1);
      setStaticFinalField(switchField, switches);
    }
  }

  private void replaceConstant(E e)
      throws ReflectiveOperationException {
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
      if (field.getName().equals(e.name())) {
        setStaticFinalField(field, e);
      }
    }
  }

  private void blankOutConstant(E e)
      throws ReflectiveOperationException {
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
      if (field.getName().equals(e.name())) {
        setStaticFinalField(field, null);
      }
    }
  }

  private void setOrdinal(E e, int ordinal)
      throws NoSuchFieldException, IllegalAccessException {
    Field ordinalField = Enum.class.getDeclaredField("ordinal");
    ordinalField.setAccessible(true);
    ordinalField.set(e, ordinal);
  }

  /**
   * Method to find the values field, set it to be accessible,
   * and return it.
   *
   * @return the values array field for the enum.
   * @throws NoSuchFieldException if the field could not be found
   */
  private Field findValuesField() throws NoSuchFieldException {
    // first we find the static final array that holds
    // the values in the enum class
    Field valuesField = clazz.getDeclaredField("$VALUES");
    // we mark it to be public
    valuesField.setAccessible(true);
    return valuesField;
  }

  private Collection<Field> findRelatedSwitchFields(
      Class<?>[] switchUsers) {
    Collection<Field> result = new ArrayList<>();
    for (Class<?> switchUser : switchUsers) {
      Class<?>[] clazzes = getAnonymousClasses(switchUser);
      for (Class<?> suspect : clazzes) {
        Field[] fields = suspect.getDeclaredFields();
        for (Field field : fields) {
          if (field.getName().startsWith("$SwitchMap$" +
              clazz.getName().replace(".", "$"))) {
            field.setAccessible(true);
            result.add(field);
          }
        }
      }
    }
    return result;
  }

  private void removeSwitchCase(int ordinal)
      throws ReflectiveOperationException {
    for (Field switchField : switchFields) {
      int[] switches = (int[]) switchField.get(null);
      int[] newSwitches = Arrays.copyOf(
          switches, switches.length - 1);
      System.arraycopy(switches, ordinal + 1, newSwitches,
          ordinal, switches.length - ordinal - 1);
      setStaticFinalField(switchField, newSwitches);
    }
  }

  private E[] values() throws ReflectiveOperationException {
    return (E[]) findValuesField().get(null);
  }

  private class Memento {
    private final E[] values;
    private final Map<Field, int[]> savedSwitchFieldValues =
        new HashMap<>();

    private Memento() throws ReflectiveOperationException {
      values = values().clone();
      for (Field switchField : switchFields) {
        int[] switchArray = (int[]) switchField.get(null);
        savedSwitchFieldValues.put(switchField,
            switchArray.clone());
      }
    }

    private void undo() throws ReflectiveOperationException {
      Field valuesField = findValuesField();
      setStaticFinalField(valuesField, values);

      for (int i = 0; i < values.length; i++) {
        setOrdinal(values[i], i);
      }

      // reset all of the constants defined inside the enum
      Map<String, E> valuesMap = new HashMap<>();
      for (E e : values) {
        valuesMap.put(e.name(), e);
      }
      Field[] constantEnumFields = clazz.getDeclaredFields();
      for (Field constantEnumField : constantEnumFields) {
        E en = valuesMap.get(constantEnumField.getName());
        if (en != null) {
          setStaticFinalField(constantEnumField, en);
        }
      }

      for (Map.Entry<Field, int[]> entry :
          savedSwitchFieldValues.entrySet()) {
        Field field = entry.getKey();
        int[] mappings = entry.getValue();
        setStaticFinalField(field, mappings);
      }
    }
  }
}
  

Human and HumanState remain the same as before:

public class Human {
  public void sing(HumanState state) {
    switch (state) {
      case HAPPY:
        singHappySong();
        break;
      case SAD:
        singDirge();
        break;
      default:
        new IllegalStateException("Invalid State: " + state);
    }
  }

  private void singHappySong() {
    System.out.println("When you're happy and you know it ...");
  }

  private void singDirge() {
    System.out.println("Don't cry for me Argentina, ...");
  }
}
  
public enum HumanState {
  HAPPY, SAD
}
  

Our HumanTest has a few changes:

import eu.javaspecialists.reflection.*;
import org.junit.*;

import java.util.*;

import static org.junit.Assert.*;

public class HumanTest {
  @Test
  public void testSingingAddingEnum()
      throws ReflectiveOperationException {
    EnumBuster<HumanState> buster =
        new EnumBuster<>(HumanState.class, Human.class);
    try {
      Human heinz = new Human();
      heinz.sing(HumanState.HAPPY);
      heinz.sing(HumanState.SAD);

      HumanState MELLOW = buster.make("MELLOW");
      buster.addByValue(MELLOW);
      System.out.println(Arrays.toString(HumanState.values()));

      try {
        heinz.sing(MELLOW);
        fail("Should have caused an IllegalStateException");
      } catch (IllegalStateException success) { }
    } finally {
      System.out.println("Restoring HumanState");
      buster.restore();
      System.out.println(Arrays.toString(HumanState.values()));
    }
  }
}
  

Here is also a slightly modernized version of EnumSwitchTest:

import eu.javaspecialists.reflection.*;
import org.junit.*;
import org.junit.runner.*;

import static org.junit.Assert.*;

public class EnumSwitchTest {
  @Test
  public void testSingingDeletingEnum()
      throws ReflectiveOperationException {
    EnumBuster<HumanState> buster = new EnumBuster<>(
        HumanState.class, EnumSwitchTest.class);
    try {
      for (HumanState state : HumanState.values()) {
        switch (state) {
          case HAPPY:
          case SAD:
            break;
          default:
            fail("Unknown state");
        }
      }

      buster.deleteByValue(HumanState.HAPPY);
      for (HumanState state : HumanState.values()) {
        switch (state) {
          case SAD:
            break;
          case HAPPY:
          default:
            fail("Unknown state");
        }
      }

      buster.undo();
      buster.deleteByValue(HumanState.SAD);
      for (HumanState state : HumanState.values()) {
        switch (state) {
          case HAPPY:
            break;
          case SAD:
          default:
            fail("Unknown state");
        }
      }

      buster.deleteByValue(HumanState.HAPPY);
      for (HumanState state : HumanState.values()) {
        switch (state) {
          case HAPPY:
          case SAD:
          default:
            fail("Unknown state");
        }
      }
    } finally {
      buster.restore();
    }
  }
}
  

That's it. I've known for a long time that my old approach was broken, but needed a kick up my rear end to update my code. Thanks Björn :-)

Kind regards

Heinz

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack 2019

Superpack 2019 Our entire Java Specialists Training in one huge bundle more...
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...

Java Emergency?

If your system is down, we will review it for 15 minutes and give you our findings for just 1 € without any obligation.