Skip to content
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

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion rust/ql/lib/codeql/rust/frameworks/libc.model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,12 @@ extensions:
pack: codeql/rust-all
extensible: sourceModel
data:
# Alloc
- ["repo:https://github.com/rust-lang/libc:libc", "::free", "Argument[0]", "pointer-invalidate", "manual"]
- addsTo:
pack: codeql/rust-all
extensible: sinkModel
data:
- ["repo:https://github.com/rust-lang/libc:libc", "::malloc", "Argument[0]", "alloc-size", "manual"]
- ["repo:https://github.com/rust-lang/libc:libc", "::aligned_alloc", "Argument[1]", "alloc-size", "manual"]
- ["repo:https://github.com/rust-lang/libc:libc", "::calloc", "Argument[0,1]", "alloc-size", "manual"]
- ["repo:https://github.com/rust-lang/libc:libc", "::realloc", "Argument[1]", "alloc-size", "manual"]
32 changes: 26 additions & 6 deletions rust/ql/lib/codeql/rust/frameworks/stdlib/lang-alloc.model.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,30 @@
extensions:
- addsTo:
pack: codeql/rust-all
extensible: sourceModel
data:
# Alloc
- ["lang:alloc", "crate::alloc::dealloc", "Argument[0]", "pointer-invalidate", "manual"]
- addsTo:
pack: codeql/rust-all
extensible: sinkModel
data:
# Alloc
- ["lang:alloc", "crate::alloc::alloc", "Argument[0]", "alloc-layout", "manual"]
- ["lang:alloc", "crate::alloc::alloc_zeroed", "Argument[0]", "alloc-layout", "manual"]
- ["lang:alloc", "crate::alloc::realloc", "Argument[2]", "alloc-size", "manual"]
- ["lang:std", "<crate::alloc::System as crate::alloc::global::GlobalAlloc>::alloc", "Argument[0]", "alloc-layout", "manual"]
- ["lang:std", "<crate::alloc::System as crate::alloc::global::GlobalAlloc>::alloc_zeroed", "Argument[0]", "alloc-layout", "manual"]
- ["lang:std", "<crate::alloc::System as crate::alloc::Allocator>::allocate", "Argument[0]", "alloc-layout", "manual"]
- ["lang:std", "<crate::alloc::System as crate::alloc::Allocator>::allocate_zeroed", "Argument[0]", "alloc-layout", "manual"]
- ["lang:std", "<crate::alloc::System as crate::alloc::Allocator>::grow", "Argument[2]", "alloc-layout", "manual"]
- ["lang:std", "<crate::alloc::System as crate::alloc::Allocator>::grow_zeroed", "Argument[2]", "alloc-layout", "manual"]
- ["lang:alloc", "<crate::alloc::Global as crate::alloc::global::GlobalAlloc>::alloc", "Argument[0]", "alloc-layout", "manual"]
- ["lang:alloc", "<crate::alloc::Global as crate::alloc::global::GlobalAlloc>::alloc_zeroed", "Argument[0]", "alloc-layout", "manual"]
- ["lang:alloc", "<crate::alloc::Global as crate::alloc::Allocator>::allocate", "Argument[0]", "alloc-layout", "manual"]
- ["lang:alloc", "<crate::alloc::Global as crate::alloc::Allocator>::allocate_zeroed", "Argument[0]", "alloc-layout", "manual"]
- ["lang:alloc", "<crate::alloc::Global as crate::alloc::Allocator>::grow", "Argument[2]", "alloc-layout", "manual"]
- ["lang:alloc", "<crate::alloc::Global as crate::alloc::Allocator>::grow_zeroed", "Argument[2]", "alloc-layout", "manual"]
- addsTo:
pack: codeql/rust-all
extensible: summaryModel
Expand All @@ -9,9 +35,3 @@ extensions:
- ["lang:alloc", "<crate::string::String>::as_str", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["lang:alloc", "<crate::string::String>::as_bytes", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["lang:alloc", "<_ as crate::string::ToString>::to_string", "Argument[self]", "ReturnValue", "taint", "manual"]
- addsTo:
pack: codeql/rust-all
extensible: sourceModel
data:
# Alloc
- ["lang:alloc", "crate::alloc::dealloc", "Argument[0]", "pointer-invalidate", "manual"]
15 changes: 15 additions & 0 deletions rust/ql/lib/codeql/rust/frameworks/stdlib/lang-core.model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ extensions:
- ["lang:core", "<crate::slice::iter::Iter as crate::iter::traits::iterator::Iterator>::collect", "Argument[self].Element", "ReturnValue.Element", "value", "manual"]
- ["lang:core", "<crate::slice::iter::Iter as crate::iter::traits::iterator::Iterator>::map", "Argument[self].Element", "Argument[0].Parameter[0]", "value", "manual"]
- ["lang:core", "<crate::slice::iter::Iter as crate::iter::traits::iterator::Iterator>::for_each", "Argument[self].Element", "Argument[0].Parameter[0]", "value", "manual"]
# Layout
- ["lang:core", "<crate::alloc::layout::Layout>::from_size_align", "Argument[0]", "ReturnValue.Field[crate::result::Result::Ok(0)]", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::from_size_align_unchecked", "Argument[0]", "ReturnValue", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::array", "Argument[0]", "ReturnValue.Field[crate::result::Result::Ok(0)]", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::repeat", "Argument[self]", "ReturnValue.Field[crate::result::Result::Ok(0)].Field[0]", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::repeat", "Argument[0]", "ReturnValue.Field[crate::result::Result::Ok(0)].Field[0]", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::repeat_packed", "Argument[self]", "ReturnValue.Field[crate::result::Result::Ok(0)]", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::repeat_packed", "Argument[0]", "ReturnValue.Field[crate::result::Result::Ok(0)]", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::extend", "Argument[self]", "ReturnValue.Field[crate::result::Result::Ok(0)].Field[0]", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::extend", "Argument[0]", "ReturnValue.Field[crate::result::Result::Ok(0)].Field[0]", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::extend_packed", "Argument[self]", "ReturnValue.Field[crate::result::Result::Ok(0)]", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::extend_packed", "Argument[0]", "ReturnValue.Field[crate::result::Result::Ok(0)]", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::align_to", "Argument[self]", "ReturnValue.Field[crate::result::Result::Ok(0)]", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::pad_to_align", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["lang:core", "<crate::alloc::layout::Layout>::size", "Argument[self]", "ReturnValue", "taint", "manual"]
# Ptr
- ["lang:core", "crate::ptr::read", "Argument[0].Reference", "ReturnValue", "value", "manual"]
- ["lang:core", "crate::ptr::read_unaligned", "Argument[0].Reference", "ReturnValue", "value", "manual"]
Expand Down
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"]) }
Copy link
Contributor

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 and alloc-layout?

Copy link
Contributor Author

@geoffw0 geoffw0 Apr 4, 2025

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, whereas alloc-layout is a Layout that controls an allocation. A Layout is basically a glorified (size, alignment) pair, the result of a call such as std::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 the Layout constructors, rather than a summary, and drop the alloc-layout sinks (effectively assuming that you would only create a Layout because you intend to do an allocation).

Another alternative design would be to have taint/data flow to the size field of the Layout and have alloc-size sinks on the size field of the allocations that use a layout.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! That clears things up for me. I think there is something to be said for the last approach, but for now we should probably just stick to the current approach 👍

}

/**
* 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 BinaryExpr?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 ComparisonOperation class and possibly clean up the existing BinaryExpr hierarchy. So it deserves a PR of its own.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes a lot of sense and I completely agree that BinaryExpr could be handled better. Would it even make sense to have a class for every operator type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, though I think getting the class hierarchy right is the priority. I've created an internal issue tracking this.

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 a denial-of-service (DoS) attack.</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 than expected may be allocated. 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. This may lead to less memory being allocated than expected by other parts of the program.</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 allocation 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>
45 changes: 45 additions & 0 deletions rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* @name Uncontrolled allocation size
* @description Allocating memory with a size controlled by an external user can result in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description LGTM but perhaps (feel free to ignore) we should mention why it is bad to allocate arbitrary amounts of memory (DoS)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated (using the wording from the .qhelp).

* arbitrary amounts of memory being allocated, leading to a crash or a
* denial-of-service (DoS) attack.
* @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"
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)
}
}
1 change: 1 addition & 0 deletions rust/ql/src/queries/summary/Stats.qll
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ private import codeql.rust.security.CleartextLoggingExtensions
private import codeql.rust.security.CleartextTransmissionExtensions
private import codeql.rust.security.SqlInjectionExtensions
private import codeql.rust.security.TaintedPathExtensions
private import codeql.rust.security.UncontrolledAllocationSizeExtensions
private import codeql.rust.security.WeakSensitiveDataHashingExtensions

/**
Expand Down
Loading