Skip to content

Commit e87b391

Browse files
authored
Merge pull request github#14910 from alexet/incorrect-scanf
CPP: Add query for detecteing incorrect error checking for scanf
2 parents 4d430d5 + c883ce8 commit e87b391

File tree

10 files changed

+149
-4
lines changed

10 files changed

+149
-4
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
int i, j;
3+
4+
// BAD: The result is only checked against zero
5+
if (scanf("%d %d", &i, &j)) {
6+
use(i);
7+
use(j);
8+
}
9+
10+
// BAD: The result is only checked against zero
11+
if (scanf("%d %d", &i, &j) == 0) {
12+
i = 0;
13+
j = 0;
14+
}
15+
use(i);
16+
use(j);
17+
18+
if (scanf("%d %d", &i, &j) == 2) {
19+
// GOOD: the result is checked against 2
20+
}
21+
22+
// GOOD: the result is compared directly
23+
int r = scanf("%d %d", &i, &j);
24+
if (r < 2) {
25+
return;
26+
}
27+
if (r == 1) {
28+
j = 0;
29+
}
30+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
7+
<overview>
8+
<p>
9+
This query finds calls of <tt>scanf</tt>-like functions with
10+
improper return-value checking. Specifically, it flags uses of <code>scanf</code> where the return value is only checked against zero.
11+
</p>
12+
<p>
13+
Functions in the <tt>scanf</tt> family return either <tt>EOF</tt> (a negative value)
14+
in case of IO failure, or the number of items successfully read from the
15+
input. Consequently, a simple check that the return value is nonzero
16+
is not enough.
17+
</p>
18+
</overview>
19+
20+
<recommendation>
21+
<p>
22+
Ensure that all uses of <tt>scanf</tt> check the return value against the expected number of arguments
23+
rather than just against zero.
24+
</p>
25+
</recommendation>
26+
27+
<example>
28+
<p>The following examples show different ways of guarding a <tt>scanf</tt> output. In the BAD examples, the results are only checked against zero. In the GOOD examples, the results are checked against the expected number of matches instead.</p>
29+
<sample src="IncorrectCheckScanf.cpp" />
30+
</example>
31+
32+
<references>
33+
<li>SEI CERT C++ Coding Standard: <a href="https://wiki.sei.cmu.edu/confluence/display/cplusplus/ERR62-CPP.+Detect+errors+when+converting+a+string+to+a+number">ERR62-CPP. Detect errors when converting a string to a number</a>.</li>
34+
<li>SEI CERT C Coding Standard: <a href="https://wiki.sei.cmu.edu/confluence/display/c/ERR33-C.+Detect+and+handle+standard+library+errors">ERR33-C. Detect and handle standard library errors</a>.</li>
35+
<li>cppreference.com: <a href="https://en.cppreference.com/w/c/io/fscanf">scanf, fscanf, sscanf, scanf_s, fscanf_s, sscanf_s</a>.</li>
36+
</references>
37+
</qhelp>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @name Incorrect return-value check for a 'scanf'-like function
3+
* @description Failing to account for EOF in a call to a scanf-like function can lead to
4+
* undefined behavior.
5+
* @kind problem
6+
* @problem.severity warning
7+
* @security-severity 7.5
8+
* @precision high
9+
* @id cpp/incorrectly-checked-scanf
10+
* @tags security
11+
* correctness
12+
* external/cwe/cwe-253
13+
*/
14+
15+
import cpp
16+
import semmle.code.cpp.commons.Scanf
17+
import ScanfChecks
18+
19+
from ScanfFunctionCall call
20+
where incorrectlyCheckedScanf(call)
21+
select call, "The result of scanf is only checked against 0, but it can also return EOF."

cpp/ql/src/Critical/MissingCheckScanf.ql

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import semmle.code.cpp.controlflow.Guards
1919
import semmle.code.cpp.dataflow.new.DataFlow::DataFlow
2020
import semmle.code.cpp.ir.IR
2121
import semmle.code.cpp.ir.ValueNumbering
22+
import ScanfChecks
2223

2324
/** Holds if `n` reaches an argument to a call to a `scanf`-like function. */
2425
pragma[nomagic]
@@ -60,7 +61,9 @@ predicate isSink(ScanfFunctionCall call, int index, Node n, Expr input) {
6061
* argument that has not been previously initialized.
6162
*/
6263
predicate isRelevantScanfCall(ScanfFunctionCall call, int index, Expr output) {
63-
exists(Node n | fwdFlow0(n) and isSink(call, index, n, output))
64+
exists(Node n | fwdFlow0(n) and isSink(call, index, n, output)) and
65+
// Exclude results from incorrectky checked scanf query
66+
not incorrectlyCheckedScanf(call)
6467
}
6568

6669
/**

cpp/ql/src/Critical/ScanfChecks.qll

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
private import cpp
2+
private import semmle.code.cpp.commons.Scanf
3+
private import semmle.code.cpp.controlflow.IRGuards
4+
private import semmle.code.cpp.ir.ValueNumbering
5+
6+
private predicate exprInBooleanContext(Expr e) {
7+
exists(IRGuardCondition gc |
8+
exists(Instruction i, ConstantInstruction zero |
9+
zero.getValue() = "0" and
10+
i.getUnconvertedResultExpression() = e and
11+
gc.comparesEq(valueNumber(i).getAUse(), zero.getAUse(), 0, _, _)
12+
)
13+
or
14+
gc.getUnconvertedResultExpression() = e
15+
)
16+
}
17+
18+
private predicate isLinuxKernel() {
19+
// For the purpose of sscanf, we check the header guards for the files that it is defined in (which have changed)
20+
exists(Macro macro | macro.getName() in ["_LINUX_KERNEL_SPRINTF_H_", "_LINUX_KERNEL_H"])
21+
}
22+
23+
/**
24+
* Holds if `call` is a `scanf`-like call were the result is only checked against 0, but it can also return EOF.
25+
*/
26+
predicate incorrectlyCheckedScanf(ScanfFunctionCall call) {
27+
exprInBooleanContext(call) and
28+
not isLinuxKernel() // scanf in the linux kernel can't return EOF
29+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: newQuery
3+
---
4+
* The `cpp/incorrectly-checked-scanf` query has been added. This finds results where the return value of scanf is not checked correctly. Some of these were previously found by `cpp/missing-check-scanf` and will no longer be reported there.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
| test.cpp:162:7:162:11 | call to scanf | The result of scanf is only checked against 0, but it can also return EOF. |
2+
| test.cpp:171:7:171:11 | call to scanf | The result of scanf is only checked against 0, but it can also return EOF. |
3+
| test.cpp:204:7:204:11 | call to scanf | The result of scanf is only checked against 0, but it can also return EOF. |
4+
| test.cpp:436:7:436:11 | call to scanf | The result of scanf is only checked against 0, but it can also return EOF. |
5+
| test.cpp:443:11:443:15 | call to scanf | The result of scanf is only checked against 0, but it can also return EOF. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Critical/IncorrectCheckScanf.ql

cpp/ql/test/query-tests/Critical/MissingCheckScanf/MissingCheckScanf.expected

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
| test.cpp:98:7:98:8 | * ... | This variable is read, but may not have been written. It should be guarded by a check that the $@ returns at least 1. | test.cpp:97:3:97:7 | call to scanf | call to scanf |
66
| test.cpp:108:7:108:7 | i | This variable is read, but may not have been written. It should be guarded by a check that the $@ returns at least 1. | test.cpp:107:3:107:8 | call to fscanf | call to fscanf |
77
| test.cpp:115:7:115:7 | i | This variable is read, but may not have been written. It should be guarded by a check that the $@ returns at least 1. | test.cpp:114:3:114:8 | call to sscanf | call to sscanf |
8-
| test.cpp:164:8:164:8 | i | This variable is read, but may not have been written. It should be guarded by a check that the $@ returns at least 1. | test.cpp:162:7:162:11 | call to scanf | call to scanf |
9-
| test.cpp:173:8:173:8 | i | This variable is read, but may not have been written. It should be guarded by a check that the $@ returns at least 1. | test.cpp:171:7:171:11 | call to scanf | call to scanf |
10-
| test.cpp:205:8:205:8 | i | This variable is read, but may not have been written. It should be guarded by a check that the $@ returns at least 1. | test.cpp:204:7:204:11 | call to scanf | call to scanf |
118
| test.cpp:224:8:224:8 | j | This variable is read, but may not have been written. It should be guarded by a check that the $@ returns at least 2. | test.cpp:221:7:221:11 | call to scanf | call to scanf |
129
| test.cpp:248:9:248:9 | d | This variable is read, but may not have been written. It should be guarded by a check that the $@ returns at least 2. | test.cpp:246:25:246:29 | call to scanf | call to scanf |
1310
| test.cpp:252:9:252:9 | d | This variable is read, but may not have been written. It should be guarded by a check that the $@ returns at least 2. | test.cpp:250:14:250:18 | call to scanf | call to scanf |

cpp/ql/test/query-tests/Critical/MissingCheckScanf/test.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,21 @@ void scan_and_static_variable() {
429429
scanf("%d", &i);
430430
use(i); // GOOD: static variables are always 0-initialized
431431
}
432+
433+
void bad_check() {
434+
{
435+
int i = 0;
436+
if (scanf("%d", &i) != 0) {
437+
return;
438+
}
439+
use(i); // GOOD [FALSE POSITIVE]: Technically no security issue, but code is incorrect.
440+
}
441+
{
442+
int i = 0;
443+
int r = scanf("%d", &i);
444+
if (!r) {
445+
return;
446+
}
447+
use(i); // GOOD [FALSE POSITIVE]: Technically no security issue, but code is incorrect.
448+
}
449+
}

0 commit comments

Comments
 (0)