Live data from Hacker News

Breaking java.lang.String

wouter.coekaerts.be

21–30 of 206 posts

Re: Breaking java.lang.String

#21
post #7

Every time, without fail, somebody shows a bug about a piece of code that we take for granted (In this case, the String class) the bug is related to concurrent modifications. Concurrency is so hard that even OpenJDK developers can't prevent these kind of bugs

Is not that OpenJDK developers can't prevent these, but there's a forbidding cost for doing so.

The simplest "safe" way of doing this involves defensively copying the input argument. However, the `compress` function will likely make yet another smaller copy, making the constructor very allocation and CPU intensive.

In fact, due to the fixed array size in Java, all thread-safe implementations must either allocate two arrays to hold the two possible encodings, which guarantees one piece of garbage, or iterating the input array twice.

For such a core class like String, this is probably unacceptable cost. And the constructor is not documented to be thread-safe, so no one should expect it to.

In reality, there are much more impactful data structures to abuse in Java.

Re: Breaking java.lang.String

#22
post #16

Out of interest, how should this be handled? Is this a bug in Java which should be fixed (looks like that to me)? My understanding was Java generally doesn't do "you did an undefined behaviour, so it's your fault", except for specifically marked very low-level interfaces.

Java definitely does "you wrote thread dangerous code, so it's your fault" for APIs not marked as being thread safe.

This is yet another way that running untrusted code inside the same JVM is a terrible mess. There's a lot of JVM state that gets "locked in" on first use (e.g. ) and a malicious bit of code could corrupt a LOT of shared data (like the post's mentioned string internment zone) even if you sanitize all of your inputs and outputs.

I wonder if you could do something nasty with this bug from inside an IntelliJ plugin...

Re: Breaking java.lang.String

#24
post #12
post #4

Earlier quoted context omitted.

Mutexes etc ... exist in Java.

What about Rust’s borrow checker (affine types) enforces the use of mutexes (or other sync prims) here?

Why would it need to? Rust's borrow checker makes it a compile-time error to share a mutable array between threads. No need for run time synchronization.

Re: Breaking java.lang.String

#25
post #20

Is this actually a bug? The default assumption in Java is that types are not thread-safe unless otherwise specified. Attempting to use types in a way that exceeds their documented thread safety has always been allowed to leave your program in an inconsistent state.

That's true, but in the case of Strings in particular they are generally considered to be thread-safe by virtue of being immutable (and the Javadocs themselves say this in many places). Concurrently modifying the input character array may seem like willful abuse in this case, but I suppose there might be some carelessly written code out there which does it and the post shows how it would create some weird and very ha…

As the article points out, the only thing the Javadoc guarantees is the subsequent modifications of the character array have no effect. It says nothing about concurrent modifications.

The type whose thread safety is in question here is not actually String, but char[]. I'm not going to say it is always wrong to share char[] between threads (as a primitive array of something other than double and long, the Java Spec does make some guarentees about char[]), however it is almost always wrong to be sharing char[] between threads.

If you want a language that protects you from this type of mistake, you simply need something more advanced than Java.

This isn't even the biggest hole in Java's safety. Java has a type system, which is at least a nominal claim to type safety. Yet, you can do things like:

    Integer[] foo = new Integer[1];
    Object[] bar = foo;
    bar[0] = new Object()
And the compiler will let you (although the runtime won't).

Its even worse with generics:

        List foo = new ArrayList();
        List bar = foo;
        bar.add(new Object());
        System.out.println(foo.get(0));
        Integer x = foo.get(0);
Will compile, and won't even through an exception until the very last line, past the point where you have retrieved and used a non Integer from a List

In fairness to java on the last one, it is at least a warning, and a deliberate compromise to support backwards compatability when they introduced generics.

Re: Breaking java.lang.String

#26
Calling this a "bug in java.lang.String" is silly. The same "bug" exists for all functions that take mutable objects. If you take a map and lookup two different keys, yep, that's a "bug".

The bug is the other piece of code that introduces the data race in the first place. You can argue the case for languages like Rust with it's borrow system, or others that use linear types or something along those lines, to eliminate the possibility of this happening, but it's quite misleading to say that the innocent user of a mutable object is the source of a bug. You may as well say there's a bug in `printf("Hello, World!\n");` in C because you could have another thread writing random values to random memory, running `while(1) { *((unsigned char*)(void*)rand()) = rand(); }`

Re: Breaking java.lang.String

#27

Is this actually a bug? The default assumption in Java is that types are not thread-safe unless otherwise specified. Attempting to use types in a way that exceeds their documented thread safety has always been allowed to leave your program in an inconsistent state.

For locally generated strings, it's not a concern. For String.intern, this is actually a very serious bug that should be treated as a security vulnerability.

Re: Breaking java.lang.String

#28
post #9
post #6

Earlier quoted context omitted.

Right, but in rust, not using one is a compile time error. In Java (as you can see by the article), not using one is a silent bug at runtime.

This is a heavily optimized system library - you don’t use mutexes here. Rust wouldn’t help here, if mutexes would be fine, they would have been used. Especially that this is the result of C++ and Java code simultaneously. Hell, it’s probably one area where rust’s benefits are a “hard sell” — you would have to constantly be in unsafe rust manipulating pointers manually as the compiler can’t reason statically about wh…

99% of the time, the calling code trivially owns the array. If you are in a situation where the compiler cannot figure that out, then you need to deal with it regardless of what String does, because the exact same problem exists by the caller itself having a reference to the object.

Re: Breaking java.lang.String

#29

Is this actually a bug? The default assumption in Java is that types are not thread-safe unless otherwise specified. Attempting to use types in a way that exceeds their documented thread safety has always been allowed to leave your program in an inconsistent state.

For locally generated strings, it's not a concern. For String.intern, this is actually a very serious bug that should be treated as a security vulnerability.

At best it is only a security vulnerability if you are running different trust domains within your process. That is something Java supports, but most Java code does not attempt to take advantage of it. Most Java programs have 100% of there code being allowed to use reflection to simply hack the program however they please, including corrupting the intern pool.

But still, for the few people who actually trust Java's isolation features and use it, possible bypasses are a concern. Having said that, I don't see how you can turn this into an attack on String.intern.

The guarentees about interning are as follows:

> When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

> It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.

If you pollute the pool with a "broken" String, all that would happen is that that String would not be returned in place of a logically equivalent correct String, and vice-versa.

Re: Breaking java.lang.String

#30
post #5
post #2

This is exactly why java needs frozen arrays [1]. The safe thing to do is freeze the array before doing anything with it. Then, you can rely on COW to copy to the array if someone is modifying it concurrently with you reading it. In the general case, you'd have fast string creation and in the tricky case you simply pay the clone cost as a penalty for being dumb. [1] https://openjdk.org/jeps/8261007#:~:text=How%20do%2…

Java does have immutable collections. It's just not an explicit type. Lots of common ways to instantiate arrays (i.e. Arrays.asList) generate immutable lists

Arrays.asList doesn’t generate an immutable list, prevent writes to the array, or prevent modifying the array via the List interface.

https://docs.oracle.com/en/java/javase/17/docs/api/java.base......)

Post reply on HN