|
| 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