Live data from Hacker News

Breaking java.lang.String

wouter.coekaerts.be

161–170 of 206 posts

Re: Breaking java.lang.String

#161
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…

I would love to have this in Java, hopefully this JEP makes it!

Re: Breaking java.lang.String

#162

Earlier quoted context omitted.

In safe Rust, that is. For unsafe Rust, I don't know exactly which bets are off but it's more than none.

In unsafe rust this is a concurrent modification of an object with shared references, which is an UB.

Unless everyone is just holding pointers

Re: Breaking java.lang.String

#163
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…

I would love to have this in Java, hopefully this JEP makes it!

As would I. There are a ton of places where the JVM is defensively copying arrays. It often comes up (for me in my work) as a performance problem.

A real common example of this is `Enum#values`.

Ideally (IMO) this applies some aggressive COW operations. So perhaps internal to the enum you have a frozen array of the values and for "values()" you return something like `VALUES.unfreeze()` which points to a transparent unfrozen array. On a write action, you'd copy the array but in the general case you'd simply read from the frozen array until someone does something dumb.

You could take it a step further and simply expose the `values` field or add a new "frozenValues" method to not break existing code. In either case, you'd end up with faster performance because the JVM isn't copying the internal array needlessly.

Re: Breaking java.lang.String

#164
post #157

It is possible to fix this String constructor implementation without creating a defensive copy of the input array or having a TOCTOU vulnerability. // Change this implementation to a loop. public String(char[] value) { while (true) { byte[] temp = StringUTF16.compress(value); if (temp != null) { this.value = temp; this.coder = LATIN1; break; } temp = StringUTF16.toBytes(value); if (temp != null) { this.value = temp;…

An unconditional loop with no guarantee of forward progress may loop indefinitely, and hence, is not a sensible solution to the problem.

It only loops if you modify the string in certain ways partway through the loop. Is that a significant problem? As soon as you stop your indefinite loop of race-condition writes, this loop is guaranteed to finish.

Re: Breaking java.lang.String

#165
post #148
post #109

Earlier quoted context omitted.

Sound Rust code would either make functions touching the shared memory marked unsafe, or would do a defensive copy out of shared memory.

That safe layer around unsafe still has no way to validate the consistency of the data.

It can't proactively validate the data while it's in the shared memory.

If you do your validation during accesses it's fine. If you copy the data out of the shared memory it's fine.

Or you could use a mutex to protect the data between validation and use.

If you're worried about another process editing the memory without taking the mutex, that's equivalent to worrying about other unsafe code editing the memory without taking the mutex. The solution is the same in both place: don't share memory with completely arbitrary code. When people compare languages and techniques, they (rightfully) assume you're not doing that.

Re: Breaking java.lang.String

#166
post #111

Earlier quoted context omitted.

I didn't think it was a bug either till I got to the "Spooky action at a distance" section. The fact that the following code: "hello world".startsWith("hello") can return false in any circumstance, is a bug in the language. The fact that some other code in an entirely unrelated part of the codebase can intern a broken string and thus break string-comparisons for the entire codebase, is mind boggling. Fortunately, I d…

> After a defensive copy of the byte-array is made, just before returning the newly constructed string object, verify once again that the chosen coder matches the string contents. Allocating new String objects might very well be the most frequent memory allocation operation in the JVM. IMO it would be a mistake to do anything to slow this down to protect against this weird instantiation behavior, unless it implies a…

The language maintainers have already decided to slow down string-construction by calling StringUTF16.compress(value) which checks if the input can be converted into LATIN1 and if so, creating a LATIN1 byte-array representation from the ground up.

Compared to this, I wager that the incremental cost is small to verify that the coder matches the content.

Also, depending on the implementation of string-interning and StringUTF16.compress, it's possible that the verification step is only needed for non-LATIN1 strings. Which according to the JEP is only a small minority of strings seen.

If there is a cheaper solution, I'm certainly all for it. I'm just spitballing ideas here. But I don't think it is acceptable to allow "hello world".startsWith("hello") to return false.

Re: Breaking java.lang.String

#167
post #17

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.

I can only think of a couple of ways to fix this, none of them ideal from a performance perspective: - Make a defensive copy of the passed-in character array, which would be immediately discarded when it is encoded to bytes. This basically sucks given how often String creation happens in a typical Java program. - Dispense with the whole use of the coder to check for non-equality of Strings, and insist on a character-…

Comparing strings is extremely common, sure.

Is comparing two strings of the same length but different encodings all that common?

Re: Breaking java.lang.String

#168

Earlier quoted context omitted.

An unconditional loop with no guarantee of forward progress may loop indefinitely, and hence, is not a sensible solution to the problem.

It only loops if you modify the string in certain ways partway through the loop. Is that a significant problem? As soon as you stop your indefinite loop of race-condition writes, this loop is guaranteed to finish.

Well it's trading "bad code can populate the program's string intern table with invalid string objects" for "bad code can instantly deadlock the program", which is not much of an upgrade. And wouldn't you need to do this in every function that uses more than 2 or more related mutable objects 1 time each, or uses 1 mutable object more than 1 time? Do you know of any systems that work like this?

This is basically a very poor man's version of software transactional memory. Noticing this is one step on the road to realizing that shared memory concurrency needs cooperative synchronization (and locks are just one way to achieve that), and most important of all, you should strictly limit the number of functions that need to synchronize at all, by strictly limiting the number of shared data objects.

I think the article OP and many in the comments here have taken the wrong lessons from this. I think the real lessons are:

1. In a program containing data races, one cannot assume objects obey their stated invariants.

2. Therefore, security/correctness in a shared memory concurrent system cannot be achieved if there is untrusted/unverified code (i.e., code that may introduce data races).

3. Regardless, it may pay dividends to try harder to learn from 1 and do better at validating input, especially in silently pervasively used shared state (e.g., the string intern table). Unfortunately, I think this will always be best effort.

Re: Breaking java.lang.String

#169
post #151
post #128

Earlier quoted context omitted.

Yes, the contract of String::equals: The result is true if and only if the argument is not null and is a String object that represents the same sequence of characters as this object. The article constructs two String objects representing the same character sequence, for which however equals() returns false, in violation of the above-quoted contract.

So they should add that mutating arguments during object construction may lead to not-so-successfully constructed (invalid) object. __Mission failed successfully__

No. The job of a constructor is to establish the class’s invariants for the new instance, or else fail with an exception. This bug in the String class fails to do so.

Re: Breaking java.lang.String

#170
post #128

Earlier quoted context omitted.

Yes, the contract of String::equals: The result is true if and only if the argument is not null and is a String object that represents the same sequence of characters as this object. The article constructs two String objects representing the same character sequence, for which however equals() returns false, in violation of the above-quoted contract.

Functions have an assumed pre-requirement not explicitly spelled out in every single javadoc, that the program does not have a data race. May as well ask for every javadoc to explicitly include a pre-requirement that no cosmic rays flip bits of memory. But to be serious again, I'm sure if you look at Array or whatever type is involved in char[], you'll find that it is explicitly marked as not thread-safe.

> Functions have an assumed pre-requirement not explicitly spelled out in every single javadoc, that the program does not have a data race.

Maybe in your programs, not in mine. I want my classes’ instances to fulfill their contract once it has been constructed. If it can’t do so, construction should fail with an exception.

Note also that for example the OpenJDK’s String::hashCode implementation is formally not data-race free (racy single-check idiom), so your assumption as stated doesn’t hold.

Post reply on HN