1

I tried to change the klass pointer of an object to point to a different class which has an identical setup. To be precise, for my test I used a copy of the original class with a modified toString() method, just to print out something else.

Assuming that the JVM orders the attributes in the same way in memory objects of the two classes should look identical.

So in my test I obtained the klass pointer from an object of the new class and set in on an object of the old, original class. After calling toString() I saw the new output as expected.

When I did this in a loop, however, the JVM crashed. I tried to create new Test() objects and modified the klass pointer to point to Test2 like this (note: 64bit compressed OOP):

int test2KlassIdentifier = unsafe.getInt(test2Obj, 8L);
unsafe.putInt(testObj, 8L, test2KlassIdentifier);

After creating hundreds of thousands of objects I got a core dump:

#  Internal Error (C:\ojdkbuild\lookaside\java-1.8.0-openjdk\hotspot\src\share\vm\opto\memnode.cpp:906), pid=27120, tid=0x0000000000009374
#  assert(!(adr_type->isa_oopptr() && adr_type->offset() == oopDesc::klass_offset_in_bytes())) failed: use LoadKlassNode instead

I then reduced the number to only create 100.000 --> no core dump until I created a bunch of new Object()s afterwards.

So my feeling is that it is a GC related problem and that my change messes up something internally. I would like to understand, however, how my "patched" object is different from a newly created object of type Test2

Haasip Satang
  • 457
  • 3
  • 17
  • 4
    I’d never expect this to work reliably. There might be plenty of places, like optimized code, where the actual class is already implied without reading the object header again. You’d better use Instrumentation to replace the `toString` method implementation if that’s your actual goal… – Holger May 15 '18 at 11:50
  • Figured out that it actually works in JDK, just not in OpenJDK. Even with millions of executions I didn't get a crash. Any idea for how I could try to write a sample to force a scenario where it does not work? – Haasip Satang May 15 '18 at 17:27
  • Ok, ignore my last question. Just thought about inlining. The `toString` I used to test was to big to be inlined in my small sample. When I changed it to just return Test1 and Test2 it was inlined and really gave unreliable results. Understood. Thanks! – Haasip Satang May 15 '18 at 17:43

1 Answers1

4

Do not try to fool JVM. Such experiments are almost always doomed to failure.

In this particular case JIT compiler rejects a 'regular' load operation at offset #8, since it assumes that only LoadKlassNode is allowed to read at klass_offset. But there are many other reasons why such tricks may crash JVM.

Even if the trick works sometimes in interpreted code, it is likely to fail after JIT compilation, since the notion of object's class is a lot more than just a reference in object header. The machine code generated for one particular class becomes invalid if you try to call it on a different instance: think of abolute addresses in the instruction stream etc.

Also mind that classes may have incompatible states when you change the header, e.g. they have different states of constant pool cache and possibly other structures that JVM fills in lazily.

apangin
  • 92,924
  • 10
  • 193
  • 247
  • Actually I figured that I only get the error with openJDK in fastDebug mode. JDK seems to work fine, even with millions of executions (so JIT compilation has kicked in already). Regarding your answer, I fear I don't get the 2nd part completely. Even after JIT kicked in to my understanding this only affects the classes or better said methods. The object representation on heap is not affected by this and will stay as is, won't it? So I assumed that during execution the klass pointer was used to get to the class and with it a referece to the methods (JIT compiled or not). – Haasip Satang May 15 '18 at 17:17
  • About the different class states; let's assume I ensure the classes I set in the klass pointer are loaded using the same `ClassLoader`, are prepared and initialized. Also let's ignore static values for now. Everything else that is lazily filled could still be lazily filled again, couldn't it? How could that conflict or be affected with any object on heap? – Haasip Satang May 15 '18 at 17:23
  • 1
    @HaasipSatang One random example: imagine that bulk biased lock revocation has just happened on class `B`, and then you change an instance of `A` to become class `B`, while the instance still has biased lock pattern set in its mark word. – apangin May 16 '18 at 17:35
  • 1
    Another example: there is an instruction `new A()` in the bytecode, then you change an instance class to `B` and after that OSR compilation happens. JIT finds an instance of `B` on the stack while from the bytecode stream it expects an instance of `A`, and this triggers an assertion failure inside compiler. – apangin May 16 '18 at 17:42
  • I don't mean this will definitely happen - in a lucky case you may never see any problems, but I just wanted to say there are many theoretical possibilities to break things. – apangin May 16 '18 at 17:46
  • Understood. Thanks a lot! As mentioned in the comments above (under my question) I already managed to create a simple use cases that proves that what I was trying is not reliable; simple inlining scenario. Shame. Was hoping that this could be used to address a problem with slow class redefinitions I have (as mentioned here: https://stackoverflow.com/questions/47909176/workaround-for-slow-redefine-retransform ). – Haasip Satang May 17 '18 at 09:43