Files
codeql/csharp/ql/src/API Abuse/DisposeNotCalledOnException.ql

150 lines
4.7 KiB
Plaintext

/**
* @name Dispose may not be called if an exception is thrown during execution
* @description Methods that create objects of type 'IDisposable' should call 'Dispose()' on those
* objects, even during exceptional circumstances, otherwise unmanaged resources may
* not be released.
* @kind problem
* @problem.severity warning
* @precision medium
* @id cs/dispose-not-called-on-throw
* @tags quality
* reliability
* error-handling
* performance
* external/cwe/cwe-404
* external/cwe/cwe-459
* external/cwe/cwe-460
*/
import csharp
import Dispose
import semmle.code.csharp.frameworks.System
private class DisposeCall extends MethodCall {
DisposeCall() { this.getTarget() instanceof DisposeMethod }
}
pragma[nomagic]
private predicate isDisposedAccess(AssignableRead ar) {
exists(AssignableDefinition def, UsingStmt us |
ar = def.getAFirstRead() and
def.getTargetAccess() = us.getAVariableDeclExpr().getAccess()
)
or
exists(AssignableRead mid |
isDisposedAccess(mid) and
ar = mid.getANextRead()
)
}
private predicate localFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
DataFlow::localFlowStep(nodeFrom, nodeTo) and
not isDisposedAccess(nodeTo.asExpr())
}
private predicate reachesDisposeCall(DisposeCall disposeCall, DataFlow::Node node) {
localFlowStep(node, DataFlow::exprNode(disposeCall.getQualifier()))
or
exists(DataFlow::Node mid | reachesDisposeCall(disposeCall, mid) | localFlowStep(node, mid))
}
/**
* Holds if `disposeCall` disposes the object created by `disposableCreation`.
*/
predicate disposeReachableFromDisposableCreation(DisposeCall disposeCall, Expr disposableCreation) {
// The qualifier of the Dispose call flows from something that introduced a disposable into scope
(
disposableCreation instanceof LocalScopeDisposableCreation or
disposableCreation instanceof MethodCall
) and
reachesDisposeCall(disposeCall, DataFlow::exprNode(disposableCreation))
}
/**
* Holds if control flow element is tried against throwing an exception of type
* `ec`.
*/
pragma[noinline]
predicate isTriedAgainstException(ControlFlowElement cfe, ExceptionClass ec) {
(cfe instanceof ThrowElement or cfe instanceof MethodCall) and
exists(TryStmt ts |
ts.getATriedElement() = cfe and
exists(ts.getAnExceptionHandler(ec))
)
}
ControlFlowElement getACatchOrFinallyClauseChild() {
exists(TryStmt ts | result = ts.getACatchClause() or result = ts.getFinally())
or
result = getACatchOrFinallyClauseChild().getAChild()
}
private predicate candidate(DisposeCall disposeCall, Call call, Expr disposableCreation) {
disposeReachableFromDisposableCreation(disposeCall, disposableCreation) and
// The dispose call is not, itself, within a dispose method.
not disposeCall.getEnclosingCallable() instanceof DisposeMethod and
// Dispose call not within a finally or catch block
not getACatchOrFinallyClauseChild() = disposeCall and
// At least one method call exists between the allocation and disposal that could throw
disposableCreation.getAReachableElement() = call and
call.getAReachableElement() = disposeCall
}
private class RelevantMethod extends Method {
RelevantMethod() {
exists(Call call |
candidate(_, call, _) and
this = call.getARuntimeTarget()
)
or
exists(RelevantMethod other | other.calls(this))
}
pragma[noinline]
private RelevantMethod callsNoTry() {
exists(MethodCall mc |
result = mc.getARuntimeTarget() and
not isTriedAgainstException(mc, _) and
mc.getEnclosingCallable() = this
)
}
pragma[noinline]
private RelevantMethod callsInTry(MethodCall mc) {
result = mc.getARuntimeTarget() and
isTriedAgainstException(mc, _) and
mc.getEnclosingCallable() = this
}
/**
* Gets an exception type that may be thrown during the execution of this method.
* Assumes any exception may be thrown by library types.
*/
Class getAThrownException() {
this.fromLibrary() and
result instanceof SystemExceptionClass
or
exists(ControlFlowElement cfe |
result = cfe.(ThrowElement).getExpr().getType() and
cfe.getEnclosingCallable() = this
or
result = this.callsInTry(cfe).getAThrownException()
|
not isTriedAgainstException(cfe, result)
)
or
result = this.callsNoTry().getAThrownException()
}
}
class MethodCallThatMayThrow extends MethodCall {
MethodCallThatMayThrow() {
exists(this.getARuntimeTarget().(RelevantMethod).getAThrownException())
}
}
from DisposeCall disposeCall, Expr disposableCreation, MethodCallThatMayThrow callThatThrows
where candidate(disposeCall, callThatThrows, disposableCreation)
select disposeCall, "Dispose missed if exception is thrown by $@.", callThatThrows,
callThatThrows.toString()