When Strings Are Not Immutable In Java

Give this code shot and see what happens:


public class MutableStrings {
    private static void toUpperCase(String str) {
        try {
            Field f = String.class.getDeclaredField("value");
            f.setAccessible(true);
            f.set(str, str.toUpperCase().toCharArray());
        } catch ( Exception e ) {
            // yes, I'm eating an exception! (cuz the above turns out to be a *horrible* idea, so why not compound it with another bad practice?)
        }
    }

    public static void main(String... args) {
        final String greeting = "Howdy";
        toUpperCase(greeting);
        System.out.println(greeting);
        System.out.println("Howdy");
    }
}

You should get the very unexpected output of:


HOWDY
HOWDY

What just happened??


So, I was having fun the other day with reflection in one of the Java classes I teach at Neumont University. Based on some cool stuff I learned when I got GSSP certified two years ago, I proposed that strings could possibly be mutable if Java was run without the security manager, making setAccessible(true) available. Sounds like it's worth a try:






public final class MutableStrings {
    public static void toUpperCase(String str) {
        try {
            Field f = String.class.getDeclaredField("value");
            f.setAccessible(true);
            f.set(str, str.toUpperCase().toCharArray());
        } catch ( Exception e ) {
            // yes, I'm eating an exception! (cuz the above turns out to be a *horrible* idea, so why not compound it with another bad practice?)
        }
    }
}

With this handy method, I can now pretend that strings are, indeed mutable.

Instead of


public final class Mainer {
    @Test
    public void testStringToUpperCase() {
        String greeting = "Hello";
        greeting = greeting.toUpperCase();
        Assert.assertEquals("HELLO", greeting);
    }
}


Now, I can do


    @Test
    public void testStringToUpperCase() {
        String greeting = "Hello";
        MutableStrings.toUpperCase(greeting); // notice the lack of assignment on the left
        Assert.assertEquals("HELLO", greeting);
    }
}

But, WAIT, there's more!


Because Java uses a String pool, two separate references set to identical string literals can point to the same reference, e.g.


    @Test
    public void testStringPoolEquals() {
        String greeting = "Hello";
        String salutation = "Hello";
        Assert.assertTrue(greeting == salutation);
    }
}

Because greeting and salutation point to the same reference in memory, if I change the value property in String reflectively, I get the following behavior:


    @Test
    public void testStringToUpperCaseStringPoolCorruption() {
        String greeting = "Hello";
        MutableStrings.toUpperCase(greeting);
        String salutation = "Hello";
        Assert.assertEquals("HELLO", greeting); // okay, makes sense
        Assert.assertEquals("HELLO", salutation); // wait... WHAT??
    }

Woah, what just happened?? Why did salutation get changed as well? Well, now we've come full circle: Strings are pooled in memory in Java. When I do "String variableName = string-literal;", it will search the string pool for that string and fashion a reference pointer to that object in memory instead of creating a new one. When we edit the internal value directly, everyone that points to that same object will get the modified value instead.

Less effective...


Obviously, this is a terrible idea. :) Imagine the chaos of something like string comparison failing because someone reflectively changed the underlying char[] array of a string-pooled public static final variable. It also seems like it could be exploited somehow, tricking an application into rendering malicious information to an end user, like in the case where a hacker gets arbitrary code to run on the server.

Still, it's fun to amaze your friends!

Josh Cummings

"I love to teach, as a painter loves to paint, as a singer loves to sing, as a musician loves to play" - William Lyon Phelps

1 comment:

  1. this demos the weirdness nicely.

    System.out.println("Howdy".equals("HOWDY")); // prints true

    ReplyDelete