2

I wish that I had authored this, but I only came across it. It has a line of code that reads System.out.println("meep"); and it prints, maddeningly, Nutn.

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

public class SomethingFun {
  public static void main(String[] args) throws java.lang.Exception {
    doSomething("meep");
    System.out.println("meep");
  }

  public static void doSomething(String s) throws java.lang.Exception {
    Field privateStringField = String.class.getDeclaredField("value");
    privateStringField.setAccessible(true);
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);

    // This line is where the trouble is:
    modifiersField.setInt(privateStringField, privateStringField.getModifiers() & ~Modifier.FINAL);

    privateStringField.set(s, new char[] {'N', 'u', 't', 'n'});
  }
}

I've gathered a bit about the reflective classes - I suspect that it is messing around with the interning somehow. Can someone shed some light on what's going on here?

In particular, it is the line modifiersField.setInt(privateStringField, privateStringField.getModifiers() & ~Modifier.FINAL); that is so perplexing.

Ben I.
  • 1,065
  • 1
  • 13
  • 29

1 Answers1

8

Yes, this is very dark voodoo.

"meep" is an interned String (by the compiler, it resides in the String literal pool), so any two "meep" in your program (created by literals) will point at the same instance.

And then reflection is used to change the backing char[] of that String instance to something else.

In order to do that, you have to make private fields accessible. A SecurityManager could be used to prevent this.

They are also making the char[] non-final. Not sure why that is necessary. You can change even final fields using reflection.

Note that this String might be quite broken now:

  • Strings cache their hashCode on first access. If that does not match the value, it won't work in hash tables anymore, and equals may also be non-functional now.
  • As a result, maybe new String("meep").intern() != "meep" now
  • In older versions of the JVM, String had length and offset properties against shared arrays. If you don't update those as well, you might be in trouble (here the length of the replacement is the same at least).

Finally note, that if you are going to break JDK classes, String is a particularily dangerous one. A lot of code (and security reasoning) depends on strings being immutable.

Community
  • 1
  • 1
Thilo
  • 257,207
  • 101
  • 511
  • 656
  • Thanks! It's really the line `modifiersField.setInt(privateStringField, privateStringField.getModifiers() & ~Modifier.FINAL);` that is so perplexing. I modified the question a little bit to reflect this. Any ideas about what's going on there? – Ben I. Dec 05 '14 at 03:55
  • That removes the `final` from `private final char[] value`. But for me at least the program works the same without the three lines about `modifiersField`. – Thilo Dec 05 '14 at 04:00
  • 1
    I wouldn’t expect that `modifier` field to do more than reporting these bits to applications using Reflection; the JVM surely maintains the original modifier bits at a different place so the verifier will still reject code trying to write to that field directly (without Reflection). And right, you don’t need to modify these bits; once you did `setAccessible(true)` you can modify `final` instance fields (not `final static` fields). That feature exists to support customized Deserialization. – Holger Dec 12 '14 at 17:40