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

Fix unsoundess in Resource Leak Checker related to owning fields and EnsuresCalledMethods #4869

Merged
merged 25 commits into from
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a8b7ac6
test case
kelloggm Jul 26, 2021
1257d46
another test case
kelloggm Jul 26, 2021
b773b14
remove class file
kelloggm Jul 26, 2021
6caab86
fix false negative in RLC related to owning fields and EnsuresCalledM…
kelloggm Jul 28, 2021
0b0a9f0
Merge branch 'master' of github.com:typetools/checker-framework into …
kelloggm Jul 28, 2021
6875cc9
Merge branch 'master' of github.com:typetools/checker-framework into …
kelloggm Jul 29, 2021
06c632c
test for circular field reasoning
kelloggm Jul 29, 2021
04b08cf
Merge branch 'master' of github.com:typetools/checker-framework into …
kelloggm Jul 29, 2021
82a0c64
stopping for the day
kelloggm Jul 30, 2021
a75c74d
go back to the version that uses just the exceptional exit store
kelloggm Jul 30, 2021
a19ea5d
WIP
msridhar Aug 5, 2021
27f4945
something basic working
msridhar Aug 5, 2021
4bbe5f1
docs
msridhar Aug 5, 2021
76c94db
fix CI failures
msridhar Aug 5, 2021
459bf6c
Merge branch 'master' of github.com:typetools/checker-framework into …
kelloggm Aug 6, 2021
b3ee4d9
Merge branch 'dataflow-ignore-exception-types' of github.com:msridhar…
kelloggm Aug 6, 2021
568677a
handle runtime exceptions
kelloggm Aug 6, 2021
69ecee5
remove unrelated changes
kelloggm Aug 6, 2021
27adf1e
the rest of the unrelated changes
kelloggm Aug 6, 2021
40d0294
fix docs
kelloggm Aug 6, 2021
a3642ed
Merge branch 'master' of github.com:typetools/checker-framework into …
kelloggm Aug 9, 2021
ad1da5c
add a passing test
kelloggm Aug 9, 2021
225b902
merge
kelloggm Aug 16, 2021
e906243
revert change that manu's PR used to include
kelloggm Aug 16, 2021
a00cbe9
extract out a helper method
kelloggm Aug 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.checkerframework.checker.calledmethods;

import com.sun.tools.javac.code.Type;
import java.util.HashSet;
import java.util.Set;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.framework.flow.CFAnalysis;
Expand All @@ -20,21 +22,31 @@ public class CalledMethodsAnalysis extends CFAnalysis {
protected CalledMethodsAnalysis(
BaseTypeChecker checker, CalledMethodsAnnotatedTypeFactory factory) {
super(checker, factory);
// Use the Nullness Checker.
ignoredExceptionTypes.add("java.lang.NullPointerException");
// Ignore run-time errors, which cannot be predicted statically. Doing so is unsound
// in the sense that they could still occur - e.g., the program could run out of memory -
// but if they did, the checker's results would be useless anyway.
ignoredExceptionTypes.add("java.lang.Error");
ignoredExceptionTypes.add("java.lang.RuntimeException");
Comment on lines +30 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding these exception types in this PR? Do the tests not pass without it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests do not pass without it.

In particular, we get these three false positives:

    3 unexpected diagnostics were found
    CheckFields.java:68: error: (destructor.exceptional.postcondition)
    CreatesMustCallForOverride2.java:38: error: (destructor.exceptional.postcondition)
    CreatesMustCallForOverride2.java:56: error: (destructor.exceptional.postcondition)

For reference, here's the relevant lines of CheckFields:

    @EnsuresCalledMethods(
        value = {"this.finalOwningFoo", "this.owningFoo"},
        methods = {"a"})
    void b() {
      this.finalOwningFoo.a();
      this.finalOwningFoo.c();
      this.owningFoo.a();
    }
  }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this lands we may want to think about another manual update like #4892

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that's really necessary - that run-time errors don't invalidate CalledMethods types follows from the idea that annotations (and therefore types) specify normal behavior, and a run-time exception is definitely not normal behavior.

}

/**
* Ignore exceptional control flow due to {@code NullPointerException}s, as this checker assumes
* the Nullness Checker will be used to prevent such exceptions.
* The fully-qualified names of the exception types that are ignored by this checker when
* computing dataflow stores.
*/
private final Set<String> ignoredExceptionTypes = new HashSet<>();

/**
* Ignore exceptional control flow due to ignored exception types.
*
* @param exceptionType exception type
* @return {@code true} if {@code exceptionType} is {@code NullPointerException}, {@code false}
* otherwise
* @return {@code true} if {@code exceptionType} is a member of {@link #ignoredExceptionTypes},
* {@code false} otherwise
*/
@Override
protected boolean isIgnoredExceptionType(TypeMirror exceptionType) {
return ((Type) exceptionType)
.tsym
.getQualifiedName()
.contentEquals("java.lang.NullPointerException");
return ignoredExceptionTypes.contains(
((Type) exceptionType).tsym.getQualifiedName().toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import com.sun.source.tree.VariableTree;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import org.checkerframework.checker.calledmethods.CalledMethodsVisitor;
import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods;
import org.checkerframework.checker.mustcall.CreatesMustCallForElementSupplier;
Expand All @@ -16,6 +18,10 @@
import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor;
import org.checkerframework.checker.mustcall.qual.Owning;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.dataflow.expression.JavaExpression;
import org.checkerframework.framework.flow.CFAbstractStore;
import org.checkerframework.framework.flow.CFAbstractValue;
import org.checkerframework.framework.type.QualifierHierarchy;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.TreeUtils;
Expand Down Expand Up @@ -80,6 +86,66 @@ public Void visitMethod(MethodTree node, Void p) {
return super.visitMethod(node, p);
}

// Overwritten to check that destructors (i.e. methods responsible for resolving
// the must-call obligations of owning fields) enforce a stronger version of
// @EnsuresCalledMethods: that the claimed @CalledMethods annotation is true on
// both exceptional and regular exits, not just on regular exits.
@Override
protected void checkPostcondition(
MethodTree methodTree, AnnotationMirror annotation, JavaExpression expression) {
super.checkPostcondition(methodTree, annotation, expression);
// Only check if the required annotation is a CalledMethods annotation (implying
// the method was annotated with @EnsuresCalledMethods).
if (!AnnotationUtils.areSameByName(
annotation, "org.checkerframework.checker.calledmethods.qual.CalledMethods")) {
return;
}
// This method might be a destructor that is responsible for resolving the must-call
// obligations of one or more owning fields of the containing class.
ExecutableElement elt = TreeUtils.elementFromDeclaration(methodTree);
TypeElement containingClass = ElementUtils.enclosingTypeElement(elt);
MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory =
rlTypeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class);
AnnotationMirror mcAnno =
mustCallAnnotatedTypeFactory
.getAnnotatedType(containingClass)
.getAnnotationInHierarchy(mustCallAnnotatedTypeFactory.TOP);
List<String> mcValues =
AnnotationUtils.getElementValueArray(
mcAnno, mustCallAnnotatedTypeFactory.getMustCallValueElement(), String.class);
String methodName = elt.getSimpleName().toString();
if (!mcValues.contains(methodName)) {
// Not a destructor, just a method with an ECM annotation. No further checking to do.
return;
}
CFAbstractStore<?, ?> exitStore = atypeFactory.getExceptionalExitStore(methodTree);
if (exitStore == null) {
// If there is no exceptional exitStore, then the method cannot throw an exception and there
// is no need to check anything else.
} else {
CFAbstractValue<?> value = exitStore.getValue(expression);
AnnotationMirror inferredAnno = null;
if (value != null) {
QualifierHierarchy hierarchy = atypeFactory.getQualifierHierarchy();
Set<AnnotationMirror> annos = value.getAnnotations();
inferredAnno = hierarchy.findAnnotationInSameHierarchy(annos, annotation);
}
if (!checkContract(expression, annotation, inferredAnno, exitStore)) {
String inferredAnnoStr =
inferredAnno == null
? "no information about " + expression.toString()
: inferredAnno.toString();
checker.reportError(
methodTree,
"destructor.exceptional.postcondition",
methodTree.getName(),
expression.toString(),
inferredAnnoStr,
annotation);
}
}
}

/**
* Returns the {@link CreatesMustCallFor#value} element/argument of the given @CreatesMustCallFor
* annotation, or "this" if there is none.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ missing.creates.mustcall.for=This method re-assigns the non-final, owning field
incompatible.creates.mustcall.for=This method re-assigns the non-final, owning field %s.%s, but its @CreatesMustCallFor annotation targets %s.
reset.not.owning=Calling this method resets the must-call obligations of the expression %s, which is non-owning. Either annotate its declaration with an @Owning annotation, extract it into a local variable, or write a corresponding @CreatesMustCallFor annotation on the method that encloses this statement.
creates.mustcall.for.override.invalid=Method %s cannot override method %s, which defines fewer @CreatesMustCallFor targets.\nfound: %s\nrequired: %s
destructor.exceptional.postcondition=Method %s must resolve the must-call obligations of the owning field %s along all paths, including exceptional paths. On an exceptional path, the @EnsuresCalledMethods annotation was not satisfied.\nFound: %s\nRequired: %s
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class ReplicaInputStreams implements Closeable {
@EnsuresCalledMethods(
value = {"this.in1", "this.in2"},
methods = {"close"})
// :: error: destructor.exceptional.postcondition
public void close() throws IOException {
in1.close();
in2.close();
Expand Down
28 changes: 28 additions & 0 deletions checker/tests/resourceleak/ReplicaInputStreams.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// A test case for https://github.com/typetools/checker-framework/issues/4838.

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods;
import org.checkerframework.checker.mustcall.qual.Owning;

class ReplicaInputStreams implements Closeable {

private final @Owning InputStream in1;
private final @Owning InputStream in2;

public ReplicaInputStreams(@Owning InputStream i1, @Owning InputStream i2) {
this.in1 = i1;
this.in2 = i2;
}

@Override
@EnsuresCalledMethods(
value = {"this.in1", "this.in2"},
methods = {"close"})
// :: error: destructor.exceptional.postcondition
public void close() throws IOException {
in1.close();
in2.close();
}
}
31 changes: 31 additions & 0 deletions checker/tests/resourceleak/ReplicaInputStreams2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// A test case for https://github.com/typetools/checker-framework/issues/4838.
// This variant uses a try-finally in the destructor, so it is correct.

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods;
import org.checkerframework.checker.mustcall.qual.Owning;

class ReplicaInputStreams2 implements Closeable {

private final @Owning InputStream in1;
private final @Owning InputStream in2;

public ReplicaInputStreams2(@Owning InputStream i1, @Owning InputStream i2) {
this.in1 = i1;
this.in2 = i2;
}

@Override
@EnsuresCalledMethods(
value = {"this.in1", "this.in2"},
methods = {"close"})
public void close() throws IOException {
try {
in1.close();
} finally {
in2.close();
}
}
}
35 changes: 35 additions & 0 deletions checker/tests/resourceleak/TwoResourcesECM.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// A test case for https://github.com/typetools/checker-framework/issues/4838.
//
// This test that shows that no unsoundess occurs when a single close() method is responsible
// for closing two resources.

import java.io.IOException;
import java.net.Socket;
import org.checkerframework.checker.calledmethods.qual.*;
import org.checkerframework.checker.mustcall.qual.*;

@MustCall("dispose") class TwoResourcesECM {
@Owning Socket s1, s2;

// The contracts.postcondition error below is thrown because s1 is not final,
// and therefore might theoretically be side-effected by the call to s2.close()
// even on the non-exceptional path. See ReplicaInputStreams.java for a variant
// of this test where such an error is not issued. Because this method can leak
// along both regular and exceptional exits, both errors are issued.
@EnsuresCalledMethods(
value = {"this.s1", "this.s2"},
methods = {"close"})
// :: error: contracts.postcondition :: error: destructor.exceptional.postcondition
public void dispose() throws IOException {
s1.close();
s2.close();
}

static void test1(TwoResourcesECM obj) {
try {
obj.dispose();
} catch (IOException ioe) {

}
}
}
7 changes: 6 additions & 1 deletion docs/manual/resource-leak-checker.tex
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,12 @@
destructor-like method \<d()> must satisfy the field's must-call obligation
(and this fact must be expressed via a \<@EnsuresCalledMethods> annotation on \<d()>),
and the enclosing class must have a \<@MustCall("d")> obligation to
ensure the destructor is called.
ensure the destructor is called. In addition to the \<@EnsuresCalledMethods> annotation,
which guarantees that the field(s) it references have their must-call obligations satisfied
on non-exceptional paths, the Resource Leak Checker requires those fields to have their must-call
obligations satisfied on all paths in (only) the destructor, and will issue a \<destructor.exceptional.postcondition>
error if they are not satisfied. Resolve this error by ensuring that the required methods are called
on all exceptional paths.

\textbf{Non-final, non-static owning fields} usually require one or more \<@CreatesMustCallFor> annotations
when they might be re-assigned. See Section~\ref{resource-leak-createsmustcallfor} for
Expand Down