Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check that PROBE_FIELD_NAME is not null when using it. #771

Closed
wants to merge 2 commits into from

Conversation

LaurentTho3
Copy link
Contributor

Hello,

This is a fix for #770, which also seems to solve #746.

It looks like in some configurations, it is possible that we try to access the coverage probes field ($$pitCoverageProbes) before it is initialised in the static initialiser (). This leads to a NullPointerException.

Here, instead of initialising the field in the static initialiser, we check if it is null every time we access it and initialise it if needed. All the tests are passing.

@hcoles
Copy link
Owner

hcoles commented Jun 6, 2020

Hi @LaurentTho3 thanks for looking at this.

Under most circumstances this sort of approach would not be acceptable as it is not thread safe, however in this case I think the worst consequence would be that we would loose some coverage as the array would be overwritten. That would be an improvement on erroring with a NullPointerException.

If you have time to investigate can you see if you can figure out why is not called for #770 and #746? A solution that robustly ensured this happened would be preferable.

@LaurentTho3
Copy link
Contributor Author

Hello @hcoles ,

Sorry, I did not think about the thread-safety aspect of things.

I do not completely understand the situation, but from what I understand, the problem is not that clinit is not called, but that it is not called first. This post seems to be related: https://stackoverflow.com/q/11419519 .

The problem would then occur when a class has a static member of its own type I think. Looking at the (instrumented) bytecode generated for com/example/NestedClassTest$MyEnum$1 in #770 would confirm this:

Bytecode // class version 52.0 (52) // access flags 0x4030 final enum com/example/NestedClassTest$MyEnum$1 extends com/example/NestedClassTest$MyEnum {

OUTERCLASS com/example/NestedClassTest$MyEnum null
// access flags 0x4009
public static enum INNERCLASS com/example/NestedClassTest$MyEnum com/example/NestedClassTest MyEnum
// access flags 0x4008
static enum INNERCLASS com/example/NestedClassTest$MyEnum$1 null null
// access flags 0x1008
static synthetic INNERCLASS com/example/NestedClassTest$1 null null

// access flags 0x1019
public final static synthetic [Z $$pitCoverageProbes

// access flags 0x1019
public final static synthetic I $$pitCoverageProbeSize = 5

// access flags 0x0
(Ljava/lang/String;I)V
GETSTATIC com/example/NestedClassTest$MyEnum$1.$$pitCoverageProbes : [Z
IFNONNULL L0
ICONST_3
GETSTATIC com/example/NestedClassTest$MyEnum$1.$$pitCoverageProbeSize : I
INVOKESTATIC sun/pitest/CodeCoverageStore.getOrRegisterClassProbes (II)[Z
PUTSTATIC com/example/NestedClassTest$MyEnum$1.$$pitCoverageProbes : [Z
L0
FRAME FULL [U java/lang/String I] []
GETSTATIC com/example/NestedClassTest$MyEnum$1.$$pitCoverageProbes : [Z
DUP
ICONST_0
ICONST_1
BASTORE
ASTORE 3
ALOAD 3
ICONST_1
ICONST_1
BASTORE
ALOAD 0
ALOAD 1
ILOAD 2
ACONST_NULL
INVOKESPECIAL com/example/NestedClassTest$MyEnum. (Ljava/lang/String;ILcom/example/NestedClassTest$1;)V
ALOAD 3
ICONST_2
ICONST_1
BASTORE
RETURN
MAXSTACK = 4
MAXLOCALS = 4

// access flags 0x8
static ()V
GETSTATIC com/example/NestedClassTest$MyEnum$1.$$pitCoverageProbes : [Z
IFNONNULL L0
ICONST_3
ICONST_4
INVOKESTATIC sun/pitest/CodeCoverageStore.getOrRegisterClassProbes (II)[Z
PUTSTATIC com/example/NestedClassTest$MyEnum$1.$$pitCoverageProbes : [Z
L0
FRAME FULL [] []
RETURN
MAXSTACK = 2
MAXLOCALS = 0
}

Here static enum INNERCLASS com/example/NestedClassTest$MyEnum$1 null null would seem to be the culprit.

Similarly, looking at the Type class which was one of the classes giving errors in #746 (https://github.com/apache/commons-bcel/blob/rel/commons-bcel-6.4.0/src/main/java/org/apache/bcel/generic/Type.java) we see the following static member: public static final Type UNKNOWN = new Type(Const.T_UNKNOWN, "<unknown object>") { };

There might be other cases where this problem might occur, but I do not know which ones.

A solution might be to somehow ensure that clinit comes first as it seems that the initialisers are run "in textual order", but that might create other problems. Otherwise, I do not see another workaround for this problem, although I will keep thinking about it.

@hcoles
Copy link
Owner

hcoles commented Jun 6, 2020

I'm now wondering if there is a hole in the static initalizer approach when classes are loaded by reflection with initalize = false passed in.

Perhaps your lazy initialization route is the right way to go, but we'd need to handle the thread safety concern. I don't think a synchronized block on every probe retrieval would be acceptable, but we could possibly follow the double checked locking pattern.

https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

This would require that the field was volatile though, and I have a memory of that being an issue. There are at least some scenarios where I think the jvm spec says that it can't be (interfaces?).

Might be that we need different strategies for different types of class.

@LaurentTho3
Copy link
Contributor Author

LaurentTho3 commented Jun 6, 2020

Loading classes with reflection and initialize = false would cause the same problem I guess, yes.

The java specs say that "It is a compile-time error if a final variable is also declared volatile." (https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.3.1.4), and also that "Every field declaration in the body of an interface is implicitly public, static, and final" (https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.2), so it looks like volatile and interfaces would not mix. Could we make the field not final as we are generating synthetic bytecode?

I have started playing a bit with the double checked locking pattern. I use the following code for the synchronised code but it fails in MutationCoverageReportSystemTest.shouldMutateClassesSuppliedToAlternateClassPath, I will try to think of a better way.

if ((access & Opcodes.ACC_STATIC) != 0) { //method is static
     this.mv.visitLdcInsn(Type.getObjectType(className)); //class
   } else {
     this.mv.visitVarInsn(ALOAD,0); //this
   }
this.mv.visitInsn(MONITORENTER);

@LaurentTho3
Copy link
Contributor Author

LaurentTho3 commented Jun 9, 2020

Reading up on lazy initialisation, and I was wondering if the on demand holder idiom https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom could be a solution? At least it does not require volatile, but maybe breaks other things I did not think about.
Also, not sure how doable/easy it would be to implement in PIT...

@hcoles
Copy link
Owner

hcoles commented Jun 10, 2020

Think what is required is a two strategy approach.

For interfaces things should work exactly as they are with constructor initialization and final fields.

For classes + enums the probe field should be volatile and use double checked locking.

I don't see why you'd need to lookup the type, something like

if (probes == null) {
  probes = initProbes();
}

private synchronized boolean[] initProbes() {
  if (probes != null) {
   return probes;
  }
  return new probes[howevermany];
}

Should work.

@LaurentTho3
Copy link
Contributor Author

Tried my hand at the double locking but it fails in PitMojoIT.shouldWorkWithGWTMockito because of the following error: java.lang.ClassNotFoundException: boolean[] at java.base/java.lang.Class.forName0(Native Method) thrown in sample.MyWidgetTest.

I really do not get why this error is popping up from the changes I made, and it seems to not be happening on all jdk versions. Maybe I am missing something obvious?

@hcoles
Copy link
Owner

hcoles commented Jun 14, 2020

Not clear on what is happening here.

The jdk version dependent behaviour I do understand - gtwmockito doesn't work with all versions, so the test only runs for those it is supported for.

Had a bit of a hack locally with your branch.

The method init probes method ought to be marked private and synthetic (so that reflection frameworks don't pick it up). Changing this didn't make gwtmockito any happier.

On a hunch I hacked it so that the probes were always initialized in the static initialiser for classes as well as interfaces. This did work, although i don't really understand why.

So that seems to be a working solution, but given the lazy initialisation will only be used in corner cases it doesn't feel great.

An alternative would be to dig into why the initializer wasn't called in the two issues that resulted in null pointers, and add whatever code is required for those scenarios (plus check whether class.forname with initialize false passed in does cause and issue).

hcoles pushed a commit that referenced this pull request Dec 30, 2020
@hcoles
Copy link
Owner

hcoles commented Dec 31, 2020

@LaurentTho3 Finally understand what's going on here. The issue is that parent classes are always initilialized before their children. So given something like

public class ParentChildInitializationTest {
    @Test
    public void test() {
        new TesteeChild();
    }
}

class TesteeParent {
    static TesteeChild child = new TesteeChild();
}

class TesteeChild extends TesteeParent {
    final static Object f = "hello";

    TesteeChild() {
        System.out.println(f);
    }
}

You get output like

null
hello

so the array probe field in child classes like this are not initialized when their init method is called from the parent static initializer.

Knowing this, I think the appropriate fix is your initial null check, but with the static initializer left in place. In 99% of cases the probe will be created as normal, for the corner case it will be created by the init call from the parent static initializer.

I've put this together in #850.

Thanks for your work on this, I suspect it has been blowing up in a lot of codebases without being reported.

@hcoles hcoles closed this Dec 31, 2020
hcoles pushed a commit that referenced this pull request Dec 31, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants