Skip to content

Commit 96305c6

Browse files
authored
Add byte-short-bitwise-op-could-become-negative-int.ql
1 parent cd53910 commit 96305c6

File tree

1 file changed

+92
-0
lines changed

1 file changed

+92
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Finds bitwise operations which could unintentionally convert a `byte` or `short` to
3+
* a negative `int`. Bitwise operations perform implicit widening conversion to `int`
4+
* and preserve the sign of the original value. Therefore if the `byte` or `short` is
5+
* negative it could turn the result negative as well, even though the `byte` or `short`
6+
* was intended to be treated as unsigned.
7+
*
8+
* To avoid this, explicitly treat the value as unsigned and convert it to `int` before
9+
* performing other bitwise operations by doing `& 0xFF` (for byte) respectively `& 0xFFFF`
10+
* (for short).
11+
*
12+
* For example, consider this code snippet which is supposed to read a 16-bit integer:
13+
* ```java
14+
* // BAD: Does not treat bytes as unsigned
15+
* int i = bytes[0] | bytes[1] << 8;
16+
* ```
17+
* If one of the bytes is > 127, then the complete result erroneously becomes negative.
18+
*
19+
* To solve this, treat the bytes as unsigned using `& 0xFF`:
20+
* ```java
21+
* int i = (bytes[0] & 0xFF) | ((bytes[1] & 0xFF) << 8);
22+
* ```
23+
*
24+
* @kind problem
25+
* @id TODO
26+
*/
27+
28+
// TODO: Maybe also consider `int` being ORed with `long`?
29+
30+
import java
31+
32+
class SignedSmallerThanIntType extends Type {
33+
SignedSmallerThanIntType() { unbox(this).hasName(["byte", "short"]) }
34+
}
35+
36+
PrimitiveType unbox(Type t) { result = t or result = t.(BoxedType).getPrimitiveType() }
37+
38+
class SignPreservingShiftExpr extends BinaryExpr {
39+
SignPreservingShiftExpr() {
40+
this instanceof RightShiftExpr
41+
or
42+
this instanceof UnsignedRightShiftExpr
43+
or
44+
this instanceof LeftShiftExpr and
45+
// And does not explicitly overwrite sign bit by shifting value into it
46+
not this.getRightOperand().(CompileTimeConstantExpr).getIntValue() >= 24
47+
}
48+
}
49+
50+
from BinaryExpr bitwiseExpr, Expr bitwiseOperand
51+
where
52+
bitwiseOperand.getType() instanceof SignedSmallerThanIntType and
53+
// Only cover the bitwise operations where the implicit sign conversion has an effect (and is likely undesired)
54+
(
55+
bitwiseExpr.(OrBitwiseExpr).getAnOperand() = bitwiseOperand or
56+
bitwiseExpr.(SignPreservingShiftExpr).getLeftOperand() = bitwiseOperand
57+
) and
58+
// Ignore if parent discards implicit sign bits again
59+
not bitwiseExpr.getParent() instanceof AndBitwiseExpr and
60+
not exists(CastExpr castExpr, int targetByteSize, int opByteSize |
61+
castExpr = bitwiseExpr.getParent+() and
62+
targetByteSize = castExpr.getType().(IntegralType).getByteSize() and
63+
opByteSize = bitwiseOperand.getType().(IntegralType).getByteSize()
64+
|
65+
// Target type is <= op type, e.g. casting `short` to `byte`
66+
targetByteSize <= opByteSize
67+
or
68+
// Target type is potentially larger, but op was shifted explicitly into sign bit
69+
bitwiseExpr.(LeftShiftExpr).getRightOperand().(CompileTimeConstantExpr).getIntValue() / 8 >=
70+
targetByteSize - opByteSize
71+
) and
72+
// Ignore if op is a constant expr, then the bitwise operation is likely intentional
73+
not bitwiseOperand instanceof CompileTimeConstantExpr and
74+
// Ignore patterns like `a | CONST == CONST` (or !=); for example Guava uses that to check for all
75+
// bits being 0 ignoring those matched by the bitmask
76+
// Explicitly require a CompileTimeConstantExpr here; for example Netty has multiple checks like
77+
// `a[0] | (a[1] << 8) == CONST` where non-const values are ORed; those don't actually seem safe
78+
not exists(CompileTimeConstantExpr maskOp |
79+
maskOp = bitwiseExpr.(OrBitwiseExpr).getAnOperand() and
80+
maskOp != bitwiseOperand and
81+
bitwiseExpr.getParent() instanceof EqualityTest
82+
) and
83+
// Ignore patterns like `a >> ... == CONST` (or !=)
84+
not exists(EqualityTest eqTest |
85+
eqTest.getAnOperand() instanceof CompileTimeConstantExpr and
86+
bitwiseExpr = eqTest.getAnOperand() and
87+
(
88+
bitwiseExpr instanceof RightShiftExpr or
89+
bitwiseExpr instanceof UnsignedRightShiftExpr
90+
)
91+
)
92+
select bitwiseExpr, "Implicitly converts $@ to a signed int", bitwiseOperand, "this expr"

0 commit comments

Comments
 (0)