🔒 Section 13 · Question #19

Can You Change String Value via Reflection

However, mutating strings via reflection causes catastrophic application-wide side effects:


🟢 Junior Level

30-Second Summary

  • In Modern Java (Java 17+, LTS 21): NO, blocked by default. Starting with Java 16 (JEP 396) and Java 17 (JEP 403), the JVM enforces Strong Encapsulation of JDK Internals. Calling field.setAccessible(true) on String.value immediately throws a fatal InaccessibleObjectException.
  • In Legacy Java (Java 8 and earlier): Technically yes. Using the Reflection API, developers could access the private value field, suppress access checks via setAccessible(true), and overwrite the backing array elements in place.

However, mutating strings via reflection causes catastrophic application-wide side effects:

  1. Global String Pool Corruption: Mutating "Hello" changes the literal globally across all classes and frameworks in the entire JVM.
  2. HashMap and HashSet Destruction: The cached hash field is not updated; map keys become permanently unlocatable, triggering memory leaks.
  3. Security Invariant Destruction: File paths, user logins, and authentication tokens are silently falsified.

Historical Anti-Pattern: Java 8 Reflection Hack

public class StringReflectionHackJava8 {
    public static void main(String[] args) throws Exception {
        String greeting = "Hello";

        // Access private field 'value' (char[] in Java 8)
        Field valueField = String.class.getDeclaredField("value");
        valueField.setAccessible(true);

        char[] chars = (char[]) valueField.get(greeting);
        chars[0] = 'J'; // Replaces 'H' with 'J'

        System.out.println(greeting); // Prints "Jello"!
        System.out.println("Hello");  // 💥 ALSO prints "Jello" due to String Pool sharing!
    }
}

🟡 Middle Level

Evolution of JVM Defenses: Strong Encapsulation (Java 9 – 17+)

With the Java Platform Module System (Project Jigsaw) in Java 9, Oracle systematically restricted reflective access to internal JDK fields:

  • Java 9–15: Emitted runtime warning messages (“An illegal reflective access operation has occurred”).
  • Java 16 (JEP 396): Encapsulation enabled by default.
  • Java 17 (JEP 403): The escape hatch --illegal-access=permit was permanently removed.

Attempting reflection on String.value in Java 17 / 21:

Field field = String.class.getDeclaredField("value");
field.setAccessible(true); // 💥 THIEVES ARE STOPPED HERE!

The JVM aborts the operation immediately:

java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module

The Only JVM Escape Hatch: --add-opens

To bypass encapsulation in Java 17+, you must pass explicit JVM startup flags:

java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar

[!CAUTION] Utilizing --add-opens java.base/java.lang in production environments introduces severe security vulnerabilities and violates enterprise compliance and security standards.


Internal Representation: Java 8 vs. Java 9+ (Compact Strings)

  • Java 8:
    private final char[] value; // UTF-16, 2 bytes per char
    
  • Java 9+ (JEP 254 Compact Strings):
    @Stable
    private final byte[] value; // 1 byte per char for LATIN1, 2 bytes for UTF16
    private final byte coder;   // 0 = LATIN1, 1 = UTF16
    

    In modern Java, mutating backing data requires manipulating raw byte[] arrays while accounting for the active encoding coder.


🔴 Senior Level

JIT C2 Compiler: Constant Folding and Memory Desynchronization

Even if an attacker circumvents module encapsulation via JVM flags or low-level off-heap writes, HotSpot C2 compiler optimizations introduce Undefined Behavior:

  1. Constant Folding:
    • HotSpot C2 treats string constants as absolute compile-time invariants.
    • String literal references are directly inlined into machine instructions or CPU registers during compilation.
  2. Memory Desynchronization:
    • The heap byte[] array may be modified in physical RAM to "HACKED".
    • However, compiled native JIT methods continue reading the original constant "ORIGINAL" directly from CPU registers or the code constant segment.
    • As a result, different threads and execution paths observe contradictory, desynchronized values for the exact same object reference.

The sun.misc.Unsafe Attack and Its Sunset (JEP 471)

Historically, reflection blocks could be bypassed via direct memory offsets in sun.misc.Unsafe:

Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);

long offset = unsafe.objectFieldOffset(String.class.getDeclaredField("value"));
byte[] value = (byte[]) unsafe.getObject(str, offset);
unsafe.putByte(value, 16L, (byte) 'X'); // Direct physical RAM write

Modern JVM Trajectory:

  • In modern OpenJDK releases, objectFieldOffset on fields in java.base throws an exception.
  • In Java 23/24 (JEP 471), memory access methods in sun.misc.Unsafe are deprecated for removal, directing developers toward the safe Foreign Function & Memory API (JEP 454).

4 Tricky Questions

1. If string literal "secret" is modified via reflection under Java 8, what will subsequent System.out.println("secret") statements output?

Answer: Subsequent System.out.println("secret") calls will print the mutated text (e.g., "pecret")! The literal "secret" is loaded into the String Constant Pool once during class loading. All code locations referencing "secret" point to that exact single instance in heap memory. Mutating its internal character array modifies the string globally throughout the entire JVM.


2. Why does map.get(key) fail to find entries in a HashMap after reflection modifies the string key’s value, even if searched with the exact same key reference?

Answer: String caches its hash code in private int hash.

  1. When the key is first inserted, hashCode() is calculated and stored in hash, determining the bucket index.
  2. Mutating the backing value array via reflection does not recalculate or clear the hash field.
  3. When searching with a new string containing the modified text, its newly computed hash code routes to a different bucket, returning null.
  4. When searching using the mutated instance itself or original text, equals() compares actual array bytes, which fail equality checks against keys in the bucket. The entry becomes unretrievable, creating a permanent memory leak.

3. Can InaccessibleObjectException be bypassed in Java 17 using a custom SecurityManager?

Answer: No, it cannot!

  1. Strong encapsulation of modules (JEP 403) is enforced at the JVM runtime layer in AccessibleObject.checkCanSetAccessible() prior to any SecurityManager evaluation.
  2. Starting with Java 17 (JEP 411), SecurityManager is deprecated for removal and disabled by default. The only mechanism to permit access is the JVM command-line flag --add-opens.

4. How does the HotSpot C2 compiler behave when a method returns "status: " + "OK" if the literal "OK" was modified in memory via Unsafe?

Answer: The method will continue to return the original text "status: OK"! "status: " and "OK" are compile-time constants. The javac compiler performs constant folding during compilation and emits a single, pre-merged "status: OK" entry into the class Constant Pool. It possesses its own independent byte[] array in the String Pool, completely unaffected by mutations to the separate "OK" instance.


🎯 Interview Cheat Sheet

Summary: Can You Change String Value via Reflection?

  • Java 17+: NO, blocked by default with InaccessibleObjectException.
  • How to Bypass: Requires JVM startup flag --add-opens java.base/java.lang=ALL-UNNAMED.
  • Java 8: Technically possible via setAccessible(true) on value field.

Consequences of String Mutation

Impact Area Architectural Consequence
String Pool Alters all identical string literals globally across the JVM
HashMap / HashSet Cached hash desynchronizes from characters; entries become unlocatable
Security TOCTOU security validation breaches and unauthorized access
HotSpot C2 JIT Desynchronization between CPU register constant-folded values and Heap memory

Red Flags to Avoid

  • ❌ “In modern Java 17/21, you can freely modify String fields using reflection.” (Demonstrates ignorance of JEP 396/403 Strong Encapsulation).
  • ❌ “Mutating a String via reflection only impacts that local variable.” (Literals are shared via the String Pool; changes impact the entire JVM).
  • ❌ “HashMap will continue to work normally because the reference is identical.” (Hash codes and equals() byte comparisons will diverge, losing map entries).