Skip to content
This repository was archived by the owner on Jan 5, 2023. It is now read-only.

Commit 302eb55

Browse files
authored
Merge pull request #245 from smowton/smowton/feature/missing-error-check-query-conservative
Add query searching for missing error checks on functions that return a (pointer, error) pair
2 parents 02920ab + 429a385 commit 302eb55

8 files changed

Lines changed: 444 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* New query "Missing error check" (`go/missing-error-check`) added. This checks for dangerous pointer dereferences when an accompanying error value returned from a call has not been checked.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
)
7+
8+
func user(input string) {
9+
10+
ptr, err := os.Open(input)
11+
// BAD: ptr is dereferenced before either it or `err` has been checked.
12+
fmt.Printf("Opened %v\n", *ptr)
13+
if err != nil {
14+
fmt.Printf("Bad input: %s\n", input)
15+
}
16+
17+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>When a function call returns two values, a pointer and a (subtype of) error, it is conventional to assume that the pointer
8+
might be nil until either the pointer or error value has been checked.</p>
9+
10+
<p>If the pointer is dereferenced without a check, an unexpected nil pointer dereference panic may occur.</p>
11+
</overview>
12+
<recommendation>
13+
14+
<p>Ensure that the returned pointer is either directly checked against nil, or the error value is checked before using
15+
the returned pointer.</p>
16+
17+
</recommendation>
18+
<example>
19+
20+
<p>In the example below, <code>user</code> dereferences <code>ptr</code> without checking either
21+
<code>ptr</code> or <code>err</code>. This might lead to a panic.</p>
22+
23+
<sample src="MissingErrorCheck.go" />
24+
25+
<p>The corrected version of <code>user</code> checks <code>err</code> before using <code>ptr</code>.</p>
26+
27+
<sample src="MissingErrorCheckGood.go" />
28+
29+
</example>
30+
<references>
31+
32+
<li>
33+
The Go Blog:
34+
<a href="https://blog.golang.org/error-handling-and-go">Error handling and Go</a>.
35+
</li>
36+
37+
</references>
38+
</qhelp>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* @name Missing error check
3+
* @description When a function returns a pointer alongside an error value, one should normally
4+
* assume that the pointer may be nil until either the pointer or error has been checked.
5+
* @kind problem
6+
* @problem.severity warning
7+
* @id go/missing-error-check
8+
* @tags reliability
9+
* correctness
10+
* logic
11+
* @precision high
12+
*/
13+
14+
import go
15+
16+
/**
17+
* Holds if `node` is a reference to the `nil` builtin constant.
18+
*/
19+
predicate isNil(DataFlow::Node node) { node = Builtin::nil().getARead() }
20+
21+
/**
22+
* Matches if `call` may return a nil pointer alongside an error value.
23+
*
24+
* This is both an over- and under-estimate: over in that we assume opaque functions may use this
25+
* convention, and under in that functions with bodies are only recognized if they use a literal
26+
* `nil` for the pointer return value at some return site.
27+
*/
28+
predicate calleeMayReturnNilWithError(DataFlow::CallNode call) {
29+
not exists(call.getACallee())
30+
or
31+
exists(FuncDef callee | callee = call.getACallee() |
32+
not exists(callee.getBody())
33+
or
34+
exists(IR::ReturnInstruction ret, DataFlow::Node ptrReturn, DataFlow::Node errReturn |
35+
callee = ret.getRoot() and
36+
ptrReturn = DataFlow::instructionNode(ret.getResult(0)) and
37+
errReturn = DataFlow::instructionNode(ret.getResult(1)) and
38+
isNil(ptrReturn) and
39+
not isNil(errReturn)
40+
)
41+
)
42+
}
43+
44+
/**
45+
* Matches if `type` is a pointer, slice or interface type, or an alias for such a type.
46+
*/
47+
predicate isDereferenceableType(Type maybePointer) {
48+
exists(Type t | t = maybePointer.getUnderlyingType() |
49+
t instanceof PointerType or t instanceof SliceType or t instanceof InterfaceType
50+
)
51+
}
52+
53+
/**
54+
* Matches if `instruction` checks `value`.
55+
*
56+
* We consider testing value for equality (against anything), passing it as a parameter to
57+
* a function call, switching on either its value or its type or casting it to constitute a
58+
* check.
59+
*/
60+
predicate checksValue(IR::Instruction instruction, DataFlow::SsaNode value) {
61+
exists(DataFlow::InstructionNode instNode | instNode.asInstruction() = instruction |
62+
instNode.(DataFlow::CallNode).getAnArgument() = value.getAUse() or
63+
instNode.(DataFlow::EqualityTestNode).getAnOperand() = value.getAUse()
64+
)
65+
or
66+
value.getAUse().asInstruction() = instruction and
67+
(
68+
exists(ExpressionSwitchStmt s | instruction.(IR::EvalInstruction).getExpr() = s.getExpr())
69+
or
70+
// This case accounts for both a type-switch or cast used to check `value`
71+
exists(TypeAssertExpr e | instruction.(IR::EvalInstruction).getExpr() = e.getExpr())
72+
)
73+
}
74+
75+
/**
76+
* Matches if `call` is a function returning (`ptr`, `err`) where `ptr` may be nil, and neither
77+
* `ptr` not `err` has been checked for validity as of `node`.
78+
*
79+
* This is initially true of any callsite that may call either an opaque function or a user-defined
80+
* function that may return (nil, error), and is true of any downstream control-flow node where a
81+
* check has not certainly been made against either `ptr` or `err`.
82+
*/
83+
predicate returnUncheckedAtNode(
84+
DataFlow::CallNode call, ControlFlow::Node node, DataFlow::SsaNode ptr, DataFlow::SsaNode err
85+
) {
86+
(
87+
// Base case: check that `ptr` and `err` have appropriate types, and that the callee may return
88+
// a nil pointer with an error.
89+
ptr.getAPredecessor() = call.getResult(0) and
90+
err.getAPredecessor() = call.getResult(1) and
91+
call.asInstruction() = node and
92+
isDereferenceableType(ptr.getType()) and
93+
err.getType().implements(Builtin::error().getType().getUnderlyingType()) and
94+
calleeMayReturnNilWithError(call)
95+
or
96+
// Recursive case: check that some predecessor is missing a check, and `node` does not itself
97+
// check either `ptr` or `err`.
98+
// localFlow is used to permit checks via either an SSA phi node or ordinary assignment.
99+
returnUncheckedAtNode(call, node.getAPredecessor(), ptr, err) and
100+
not exists(DataFlow::SsaNode checked |
101+
DataFlow::localFlow(ptr, checked) or DataFlow::localFlow(err, checked)
102+
|
103+
checksValue(node, checked)
104+
)
105+
)
106+
}
107+
108+
from
109+
DataFlow::CallNode call, DataFlow::SsaNode ptr, DataFlow::SsaNode err,
110+
DataFlow::PointerDereferenceNode deref, ControlFlow::Node derefNode
111+
where
112+
// `derefNode` is a control-flow node corresponding to `deref`
113+
deref.getOperand().asInstruction() = derefNode and
114+
// neither `ptr` nor `err`, the return values of `call`, have been checked as of `derefNode`
115+
returnUncheckedAtNode(call, derefNode, ptr, err) and
116+
// `deref` dereferences `ptr`
117+
deref.getOperand() = ptr.getAUse()
118+
select deref.getOperand(),
119+
ptr.getSourceVariable() + " may be nil here, because $@ may not have been checked.", err,
120+
err.getSourceVariable().toString()
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
)
7+
8+
func user(input string) {
9+
10+
ptr, err := os.Open(input)
11+
if err != nil {
12+
fmt.Printf("Bad input: %s\n", input)
13+
return
14+
}
15+
// GOOD: `err` has been checked before `ptr` is used
16+
fmt.Printf("Result was %v\n", *ptr)
17+
18+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
| tests.go:61:30:61:35 | result | result may be nil here, because $@ may not have been checked. | tests.go:59:10:59:12 | definition of err | err |
2+
| tests.go:243:27:243:32 | result | result may be nil here, because $@ may not have been checked. | tests.go:241:10:241:12 | definition of err | err |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
InconsistentCode/MissingErrorCheck.ql

0 commit comments

Comments
 (0)