-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rust: Query for uncontrolled allocation size #19171
base: main
Are you sure you want to change the base?
Changes from all commits
ae555f2
9409cd6
03f94de
e49c1af
64aa4e8
addc1d3
cdd5cb0
f7d3a51
6a5a100
fb22d55
f96b00a
44b26e5
8e7e162
a5883b1
c993938
6ad7a95
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
/** | ||
* Provides classes and predicates for reasoning about uncontrolled allocation | ||
* size vulnerabilities. | ||
*/ | ||
|
||
import rust | ||
private import codeql.rust.Concepts | ||
private import codeql.rust.dataflow.DataFlow | ||
private import codeql.rust.dataflow.FlowSink | ||
private import codeql.rust.controlflow.ControlFlowGraph as Cfg | ||
private import codeql.rust.controlflow.CfgNodes as CfgNodes | ||
|
||
/** | ||
* Provides default sources, sinks and barriers for detecting uncontrolled | ||
* allocation size vulnerabilities, as well as extension points for adding your own. | ||
*/ | ||
module UncontrolledAllocationSize { | ||
/** | ||
* A data flow sink for uncontrolled allocation size vulnerabilities. | ||
*/ | ||
abstract class Sink extends QuerySink::Range { | ||
override string getSinkType() { result = "UncontrolledAllocationSize" } | ||
} | ||
|
||
/** | ||
* A barrier for uncontrolled allocation size vulnerabilities. | ||
*/ | ||
abstract class Barrier extends DataFlow::Node { } | ||
|
||
/** | ||
* A sink for uncontrolled allocation size from model data. | ||
*/ | ||
private class ModelsAsDataSink extends Sink { | ||
ModelsAsDataSink() { sinkNode(this, ["alloc-size", "alloc-layout"]) } | ||
} | ||
|
||
/** | ||
* A barrier for uncontrolled allocation size that is an upper bound check / guard. | ||
*/ | ||
private class UpperBoundCheckBarrier extends Barrier { | ||
UpperBoundCheckBarrier() { | ||
this = DataFlow::BarrierGuard<isUpperBoundCheck/3>::getABarrierNode() | ||
} | ||
} | ||
|
||
/** | ||
* Gets the operand on the "greater" (or "greater-or-equal") side | ||
* of this relational expression, that is, the side that is larger | ||
* if the overall expression evaluates to `true`; for example on | ||
* `x <= 20` this is the `20`, and on `y > 0` it is `y`. | ||
*/ | ||
private Expr getGreaterOperand(BinaryExpr op) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If these could potentially be useful in other queries we could add them as member predicates to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I have plans to do that, but I think we should add a |
||
op.getOperatorName() = ["<", "<="] and | ||
result = op.getRhs() | ||
or | ||
op.getOperatorName() = [">", ">="] and | ||
result = op.getLhs() | ||
} | ||
|
||
/** | ||
* Gets the operand on the "lesser" (or "lesser-or-equal") side | ||
* of this relational expression, that is, the side that is smaller | ||
* if the overall expression evaluates to `true`; for example on | ||
* `x <= 20` this is `x`, and on `y > 0` it is the `0`. | ||
*/ | ||
private Expr getLesserOperand(BinaryExpr op) { | ||
op.getOperatorName() = ["<", "<="] and | ||
result = op.getLhs() | ||
or | ||
op.getOperatorName() = [">", ">="] and | ||
result = op.getRhs() | ||
} | ||
|
||
/** | ||
* Holds if comparison `g` having result `branch` indicates an upper bound for the sub-expression | ||
* `node`. For example when the comparison `x < 10` is true, we have an upper bound for `x`. | ||
*/ | ||
private predicate isUpperBoundCheck(CfgNodes::AstCfgNode g, Cfg::CfgNode node, boolean branch) { | ||
exists(BinaryExpr cmp | g = cmp.getACfgNode() | | ||
node = getLesserOperand(cmp).getACfgNode() and | ||
branch = true | ||
or | ||
node = getGreaterOperand(cmp).getACfgNode() and | ||
branch = false | ||
or | ||
cmp.getOperatorName() = "==" and | ||
[cmp.getLhs(), cmp.getRhs()].getACfgNode() = node and | ||
branch = true | ||
or | ||
cmp.getOperatorName() = "!=" and | ||
[cmp.getLhs(), cmp.getRhs()].getACfgNode() = node and | ||
branch = false | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<!DOCTYPE qhelp PUBLIC | ||
"-//Semmle//qhelp//EN" | ||
"qhelp.dtd"> | ||
<qhelp> | ||
<overview> | ||
|
||
<p>Allocating memory with a size based on user input may allow arbitrary amounts of memory to be | ||
allocated, leading to a crash or denial of service incident.</p> | ||
|
||
<p>If the user input is multiplied by a constant, such as the size of a type, the result may | ||
overflow. In a build with the <code>--release</code> flag Rust performs two's complement wrapping, | ||
with the result that less memory may be allocated than expected. This can lead to buffer overflow | ||
incidents.</p> | ||
|
||
</overview> | ||
<recommendation> | ||
|
||
<p>Implement a guard to limit the amount of memory that is allocated, and reject the request if | ||
the guard is not met. Ensure that any multiplications in the calculation cannot overflow, either | ||
by guarding their inputs, or using a multiplication routine such as <code>checked_mul</code> that | ||
does not wrap around.</p> | ||
|
||
</recommendation> | ||
<example> | ||
|
||
<p>In the following example, an arbitrary amount of memory is allocated based on user input. In | ||
addition, due to the multiplication operation the result may overflow if a very large value is | ||
provided, leading to less memory being allocated than other parts of the program expect.</p> | ||
<sample src="UncontrolledAllocationSizeBad.rs" /> | ||
|
||
<p>In the fixed example, the user input is checked against a maximum value. If the check fails an | ||
error is returned, and both the multiplication and alloaction do not take place.</p> | ||
<sample src="UncontrolledAllocationSizeGood.rs" /> | ||
|
||
</example> | ||
<references> | ||
|
||
<li>The Rust Programming Language: <a href="https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-overflow">Data Types - Integer Overflow</a>.</li> | ||
|
||
</references> | ||
</qhelp> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/** | ||
* @name Uncontrolled allocation size | ||
* @description Allocating memory with a size controlled by an external user can result in | ||
* arbitrary amounts of memory being allocated. | ||
* @kind path-problem | ||
* @problem.severity recommendation | ||
* @security-severity 7.5 | ||
* @precision high | ||
* @id rust/uncontrolled-allocation-size | ||
* @tags reliability | ||
* security | ||
* external/cwe/cwe-770 | ||
* external/cwe/cwe-789 | ||
*/ | ||
|
||
import rust | ||
import codeql.rust.Concepts | ||
import codeql.rust.dataflow.DataFlow | ||
import codeql.rust.dataflow.TaintTracking | ||
import codeql.rust.dataflow.internal.DataFlowImpl | ||
import codeql.rust.security.UncontrolledAllocationSizeExtensions | ||
|
||
/** | ||
* A taint-tracking configuration for uncontrolled allocation size vulnerabilities. | ||
*/ | ||
module UncontrolledAllocationConfig implements DataFlow::ConfigSig { | ||
import UncontrolledAllocationSize | ||
|
||
predicate isSource(DataFlow::Node source) { source instanceof ActiveThreatModelSource } | ||
|
||
predicate isSink(DataFlow::Node sink) { sink instanceof Sink } | ||
|
||
predicate isBarrier(DataFlow::Node barrier) { barrier instanceof Barrier } | ||
} | ||
|
||
module UncontrolledAllocationFlow = TaintTracking::Global<UncontrolledAllocationConfig>; | ||
|
||
import UncontrolledAllocationFlow::PathGraph | ||
|
||
from UncontrolledAllocationFlow::PathNode source, UncontrolledAllocationFlow::PathNode sink | ||
where UncontrolledAllocationFlow::flowPath(source, sink) | ||
select sink.getNode(), source, sink, | ||
"This allocation size is derived from a $@ and could allocate arbitrary amounts of memory.", | ||
source.getNode(), "user-provided value" | ||
Comment on lines
+40
to
+44
Check warning Code scanning / CodeQL Consistent alert message Warning
The rust/uncontrolled-allocation-size query does not have the same alert message as go.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it's almost identical to the CPP version though. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
|
||
fn allocate_buffer(user_input: String) -> Result<*mut u8, Error> { | ||
let num_bytes = user_input.parse::<usize>()? * std::mem::size_of::<u64>(); | ||
|
||
let layout = std::alloc::Layout::from_size_align(num_bytes, 1).unwrap(); | ||
unsafe { | ||
let buffer = std::alloc::alloc(layout); // BAD: uncontrolled allocation size | ||
|
||
Ok(buffer) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
|
||
const BUFFER_LIMIT: usize = 10 * 1024; | ||
|
||
fn allocate_buffer(user_input: String) -> Result<*mut u8, Error> { | ||
let size = user_input.parse::<usize>()?; | ||
if size > BUFFER_LIMIT { | ||
return Err("Size exceeds limit".into()); | ||
} | ||
let num_bytes = size * std::mem::size_of::<u64>(); | ||
|
||
let layout = std::alloc::Layout::from_size_align(num_bytes, 1).unwrap(); | ||
unsafe { | ||
let buffer = std::alloc::alloc(layout); // GOOD | ||
|
||
Ok(buffer) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the difference between
alloc-size
andalloc-layout
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
alloc-size
is an integer that controls the size of an allocation, whereasalloc-layout
is aLayout
that controls an allocation. ALayout
is basically a glorified(size, alignment)
pair, the result of a call such asstd::alloc::Layout::array::<u32>(v)
.The current design has taint flow through
Layout
constructors, with sinks where the allocation is actually made. The query doesn't care which kind of sink, but I figured it might be better to tag them separately in case some future query does care about the difference (perhaps because it cares about alignments rather than sizes???).An alternative and slightly simpler design would be to have an
alloc-size
sink directly in theLayout
constructors, rather than a summary, and drop thealloc-layout
sinks (effectively assuming that you would only create aLayout
because you intend to do an allocation).Another alternative design would be to have taint/data flow to the
size
field of theLayout
and havealloc-size
sinks on thesize
field of the allocations that use a layout.