Skip to content

Rust: Query for uncontrolled allocation size #19171

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

Merged
merged 22 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ae555f2
Rust: Add a test for uncontrolled allocation size.
geoffw0 Feb 5, 2025
9409cd6
Rust: Prototype query.
geoffw0 Feb 5, 2025
03f94de
Rust: Add models.
geoffw0 Feb 5, 2025
e49c1af
Rust: Add a few missing models.
geoffw0 Mar 31, 2025
64aa4e8
Rust: Ensure that the sinks for this query appear in metrics.
geoffw0 Mar 25, 2025
addc1d3
Rust: Add qhelp, examples, and tests of examples.
geoffw0 Mar 25, 2025
cdd5cb0
Rust: More test cases for bounds / guards.
geoffw0 Mar 31, 2025
f7d3a51
Rust: Implement barrier guard.
geoffw0 Mar 28, 2025
6a5a100
Rust: Refine the barrier guard.
geoffw0 Mar 31, 2025
fb22d55
Rust: Remove duplicate models.
geoffw0 Mar 31, 2025
f96b00a
Update rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSiz…
geoffw0 Apr 4, 2025
44b26e5
Rust: Change the test copy of the example as well.
geoffw0 Apr 4, 2025
8e7e162
Merge branch 'main' into badalloc
geoffw0 Apr 4, 2025
a5883b1
Rust: Accept test changes (due to added models?).
geoffw0 Apr 4, 2025
c993938
Rust: Turn on PrettyPrintModels for RegexInjection so we hopefully do…
geoffw0 Apr 4, 2025
6ad7a95
Merge branch 'main' into badalloc
geoffw0 Apr 4, 2025
893e423
Merge branch 'main' into badalloc
geoffw0 Apr 7, 2025
dad8585
Apply suggestions from code review
geoffw0 Apr 7, 2025
41f54d8
Rust: Tweak query description.
geoffw0 Apr 7, 2025
e2f63db
Merge branch 'main' into badalloc
geoffw0 Apr 8, 2025
10ad578
Rust: Try a different toolchain version to fix the test in CI?
geoffw0 Apr 8, 2025
c821f27
Merge branch 'main' into badalloc
geoffw0 Apr 14, 2025
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: 9 additions & 0 deletions rust/ql/lib/codeql/rust/frameworks/libc.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
extensions:
- 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"]
21 changes: 21 additions & 0 deletions rust/ql/lib/codeql/rust/frameworks/stdlib/lang-alloc.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
extensions:
- 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"]
17 changes: 16 additions & 1 deletion rust/ql/lib/codeql/rust/frameworks/stdlib/lang-core.model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,22 @@ 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"]
# ptr
# 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"]
- ["lang:core", "crate::ptr::read_volatile", "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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it correct that if we had a guards library for Rust, then we could've used that here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This uses the barrier guard functionality in DataFlow.qll that was added here. It seems to be sufficient.

Which guards library are you thinking of?

I wonder if you're thinking of range analysis actually - that would provide an actual value for the upper bound, which we don't really need here, but it would probably also improve accuracy just by covering less common cases better (like v < 2 * v in the test).

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here: #19535

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"
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 @@ -15,6 +15,7 @@ private import codeql.rust.Concepts
private import codeql.rust.security.CleartextLoggingExtensions
private import codeql.rust.security.SqlInjectionExtensions
private import codeql.rust.security.WeakSensitiveDataHashingExtensions
private import codeql.rust.security.UncontrolledAllocationSizeExtensions
private import codeql.rust.security.regex.RegexInjectionExtensions

/**
Expand Down
2 changes: 1 addition & 1 deletion rust/ql/test/query-tests/diagnostics/SummaryStats.expected
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
| Macro calls - resolved | 8 |
| Macro calls - total | 9 |
| Macro calls - unresolved | 1 |
| Taint edges - number of edges | 1674 |
| Taint edges - number of edges | 1677 |
| Taint reach - nodes tainted | 0 |
| Taint reach - per million nodes | 0 |
| Taint sinks - cryptographic operations | 0 |
Expand Down
Loading