Skip to content

Commit c64eb8b

Browse files
committed
feat: add 'no alloc' to stop functions from allocating on the heap
1 parent 51e9bdd commit c64eb8b

File tree

12 files changed

+541
-3
lines changed

12 files changed

+541
-3
lines changed

cargo_pup_lint_config/src/function_lint/builder.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ impl<'a> FunctionConstraintBuilder<'a> {
129129
self
130130
}
131131

132+
/// Require that the function does not perform heap allocations
133+
pub fn no_allocation(mut self) -> Self {
134+
self.add_rule_internal(FunctionRule::NoAllocation(self.current_severity));
135+
self
136+
}
137+
132138
/// Create a new MaxLength rule with the current severity
133139
pub fn create_max_length_rule(&self, length: usize) -> FunctionRule {
134140
FunctionRule::MaxLength(length, self.current_severity)

cargo_pup_lint_config/src/function_lint/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ pub enum FunctionRule {
6262
ResultErrorMustImplementError(Severity),
6363
/// Enforces that a function matching the selector must not exist at all
6464
MustNotExist(Severity),
65+
/// Enforces that a function must not perform heap allocations
66+
NoAllocation(Severity),
6567
}
6668

6769
// Helper methods for FunctionRule

cargo_pup_lint_impl/src/lints/function_lint/lint.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ use rustc_lint::{LateContext, LateLintPass, LintStore};
1111
use rustc_middle::ty::TyKind;
1212
use rustc_session::impl_lint_pass;
1313
use rustc_span::BytePos;
14+
use std::collections::HashMap;
15+
use std::sync::Mutex;
16+
17+
use super::no_allocation::detect_allocation_in_mir;
1418

1519
// Helper: retrieve the concrete Self type of the impl the method belongs to, if any
1620
fn get_self_type<'tcx>(
@@ -26,6 +30,8 @@ pub struct FunctionLint {
2630
name: String,
2731
matches: FunctionMatch,
2832
function_rules: Vec<FunctionRule>,
33+
// Cache for allocation detection to avoid re-analyzing the same functions
34+
allocation_cache: Mutex<HashMap<rustc_hir::def_id::DefId, bool>>,
2935
}
3036

3137
impl FunctionLint {
@@ -36,6 +42,7 @@ impl FunctionLint {
3642
name: f.name.clone(),
3743
matches: f.matches.clone(),
3844
function_rules: f.rules.clone(),
45+
allocation_cache: Mutex::new(HashMap::new()),
3946
})
4047
} else {
4148
panic!("Expected a Function lint configuration")
@@ -245,6 +252,7 @@ impl ArchitectureLintRule for FunctionLint {
245252
name: name.clone(),
246253
matches: matches.clone(),
247254
function_rules: function_rules.clone(),
255+
allocation_cache: Mutex::new(HashMap::new()),
248256
})
249257
});
250258
}
@@ -351,6 +359,28 @@ impl<'tcx> LateLintPass<'tcx> for FunctionLint {
351359
"Remove this function to satisfy the architectural rule",
352360
);
353361
}
362+
FunctionRule::NoAllocation(severity) => {
363+
if ctx.tcx.is_mir_available(fn_def_id) {
364+
let mir = ctx.tcx.optimized_mir(fn_def_id);
365+
366+
if let Some(violation) = detect_allocation_in_mir(
367+
ctx.tcx,
368+
mir,
369+
fn_def_id,
370+
&mut self.allocation_cache.lock().unwrap(),
371+
) {
372+
span_lint_and_help(
373+
ctx,
374+
FUNCTION_LINT::get_by_severity(*severity),
375+
self.name().as_str(),
376+
violation.span,
377+
format!("Function allocates heap memory: {}", violation.reason),
378+
None,
379+
"Remove heap allocations to satisfy the NoAllocation rule",
380+
);
381+
}
382+
}
383+
}
354384
}
355385
}
356386
}
@@ -454,6 +484,28 @@ impl<'tcx> LateLintPass<'tcx> for FunctionLint {
454484
"Remove this function to satisfy the architectural rule",
455485
);
456486
}
487+
FunctionRule::NoAllocation(severity) => {
488+
if ctx.tcx.is_mir_available(fn_def_id) {
489+
let mir = ctx.tcx.optimized_mir(fn_def_id);
490+
491+
if let Some(violation) = detect_allocation_in_mir(
492+
ctx.tcx,
493+
mir,
494+
fn_def_id,
495+
&mut self.allocation_cache.lock().unwrap(),
496+
) {
497+
span_lint_and_help(
498+
ctx,
499+
FUNCTION_LINT::get_by_severity(*severity),
500+
self.name().as_str(),
501+
violation.span,
502+
format!("Function allocates heap memory: {}", violation.reason),
503+
None,
504+
"Remove heap allocations to satisfy the NoAllocation rule",
505+
);
506+
}
507+
}
508+
}
457509
}
458510
}
459511
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// This product includes software developed at Datadog (https://www.datadoghq.com/) Copyright 2024 Datadog, Inc.
22

33
mod lint;
4+
mod no_allocation;
45

56
pub use lint::FunctionLint;
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// This product includes software developed at Datadog (https://www.datadoghq.com/) Copyright 2024 Datadog, Inc.
2+
3+
use rustc_hir::def_id::DefId;
4+
use rustc_middle::mir::{Body, TerminatorKind};
5+
use rustc_middle::ty::TyCtxt;
6+
use rustc_span::Span;
7+
use std::collections::HashMap;
8+
9+
/// Represents a violation of the no-allocation rule
10+
#[derive(Debug)]
11+
pub struct AllocationViolation {
12+
pub span: Span,
13+
pub reason: String,
14+
}
15+
16+
/// Detects heap allocations in a function's MIR
17+
pub fn detect_allocation_in_mir<'tcx>(
18+
tcx: TyCtxt<'tcx>,
19+
mir: &Body<'tcx>,
20+
_fn_def_id: DefId,
21+
cache: &mut HashMap<DefId, bool>,
22+
) -> Option<AllocationViolation> {
23+
// Iterate through basic blocks
24+
for (_bb, bb_data) in mir.basic_blocks.iter_enumerated() {
25+
// Check terminator for calls
26+
if let Some(terminator) = &bb_data.terminator
27+
&& let TerminatorKind::Call { func, .. } = &terminator.kind
28+
{
29+
// Extract function DefId using const_fn_def
30+
if let Some((callee_def_id, _generics)) = func.const_fn_def() {
31+
let path = tcx.def_path_str(callee_def_id);
32+
33+
// Check if it's a known allocating function
34+
if is_allocating_function(&path) {
35+
return Some(AllocationViolation {
36+
span: terminator.source_info.span,
37+
reason: format!("calls allocating function: {path}"),
38+
});
39+
}
40+
41+
// Check transitively (with cycle detection)
42+
if should_analyze_transitively(tcx, callee_def_id)
43+
&& function_allocates(tcx, callee_def_id, cache)
44+
{
45+
return Some(AllocationViolation {
46+
span: terminator.source_info.span,
47+
reason: format!("calls function that allocates: {path}"),
48+
});
49+
}
50+
}
51+
}
52+
}
53+
54+
None
55+
}
56+
57+
/// Checks if a function path corresponds to a known allocating function
58+
fn is_allocating_function(path: &str) -> bool {
59+
// Direct allocation functions - these are the low-level allocators
60+
if path.contains("alloc::alloc::")
61+
&& (path.contains("::alloc")
62+
|| path.contains("::allocate")
63+
|| path.contains("::exchange_malloc")
64+
|| path.contains("::box_free"))
65+
{
66+
return true;
67+
}
68+
69+
// Box allocations - check for various Box patterns
70+
if (path.contains("::Box::") || path.contains("::Box::<")) && path.contains("::new") {
71+
return true;
72+
}
73+
74+
// Vec allocations and operations that may allocate
75+
if (path.contains("::Vec::") || path.contains("::Vec::<"))
76+
&& (path.contains("::new")
77+
|| path.contains("::with_capacity")
78+
|| path.contains("::push")
79+
|| path.contains("::insert")
80+
|| path.contains("::extend")
81+
|| path.contains("::append")
82+
|| path.contains("::resize")
83+
|| path.contains("::from_elem"))
84+
{
85+
return true;
86+
}
87+
88+
// String allocations
89+
if path.contains("::String::")
90+
&& (path.contains("::new")
91+
|| path.contains("::from")
92+
|| path.contains("::from_utf8")
93+
|| path.contains("::from_utf16")
94+
|| path.contains("::push_str")
95+
|| path.contains("::push")
96+
|| path.contains("::insert")
97+
|| path.contains("::insert_str"))
98+
{
99+
return true;
100+
}
101+
102+
// Format macro and related
103+
if path.contains("::format") || path.contains("fmt::format") {
104+
return true;
105+
}
106+
107+
// Rc and Arc
108+
if (path.contains("::Rc::")
109+
|| path.contains("::Rc::<")
110+
|| path.contains("::Arc::")
111+
|| path.contains("::Arc::<"))
112+
&& (path.contains("::new") || path.contains("::clone"))
113+
{
114+
return true;
115+
}
116+
117+
// Collection types - broader matching
118+
if (path.contains("HashMap")
119+
|| path.contains("BTreeMap")
120+
|| path.contains("HashSet")
121+
|| path.contains("BTreeSet")
122+
|| path.contains("VecDeque")
123+
|| path.contains("LinkedList")
124+
|| path.contains("BinaryHeap"))
125+
&& (path.contains(">::new")
126+
|| path.contains(">::with_capacity")
127+
|| path.contains(">::insert")
128+
|| path.contains(">::push"))
129+
{
130+
return true;
131+
}
132+
133+
// to_string, to_owned methods - these allocate
134+
if path.contains("::to_string") || path.contains("::to_owned") {
135+
return true;
136+
}
137+
138+
// RawVec - internal vec allocator
139+
if path.contains("RawVec") && (path.contains("::new") || path.contains("::allocate")) {
140+
return true;
141+
}
142+
143+
false
144+
}
145+
146+
/// Determines if we should recursively analyze a function
147+
fn should_analyze_transitively(tcx: TyCtxt<'_>, def_id: DefId) -> bool {
148+
// Only analyze functions in the local crate
149+
// External crates are harder to analyze and may not have MIR available
150+
def_id.krate == rustc_hir::def_id::LOCAL_CRATE && tcx.is_mir_available(def_id)
151+
}
152+
153+
/// Recursively checks if a function allocates, with memoization
154+
fn function_allocates<'tcx>(
155+
tcx: TyCtxt<'tcx>,
156+
def_id: DefId,
157+
cache: &mut HashMap<DefId, bool>,
158+
) -> bool {
159+
// Check cache
160+
if let Some(&result) = cache.get(&def_id) {
161+
return result;
162+
}
163+
164+
// Mark as false initially (cycle detection)
165+
cache.insert(def_id, false);
166+
167+
// Try to get MIR
168+
if !tcx.is_mir_available(def_id) {
169+
// Conservative: assume external functions don't allocate
170+
// This prevents false positives for standard library functions
171+
return false;
172+
}
173+
174+
let mir = tcx.optimized_mir(def_id);
175+
let allocates = detect_allocation_in_mir(tcx, mir, def_id, cache).is_some();
176+
177+
// Update cache with actual result
178+
cache.insert(def_id, allocates);
179+
allocates
180+
}

test_app/expected_output

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,5 +295,68 @@ error: Function 'process_item' is forbidden by lint rule
295295
= help: Remove this function to satisfy the architectural rule
296296
= note: Applied by cargo-pup rule 'async_functions_forbidden'.
297297

298-
warning: `test_app` (bin "test_app") generated 20 warnings
299-
error: could not compile `test_app` (bin "test_app") due to 6 previous errors; 20 warnings emitted
298+
warning: Function allocates heap memory: calls allocating function: std::boxed::Box::<T>::new
299+
--> src/no_allocation.rs:12:5
300+
|
301+
12 | Box::new(42)
302+
| ^^^^^^^^^^^^
303+
|
304+
= help: Remove heap allocations to satisfy the NoAllocation rule
305+
= note: Applied by cargo-pup rule 'no_allocation_check'.
306+
307+
warning: Function allocates heap memory: calls allocating function: std::vec::Vec::<T>::new
308+
--> src/no_allocation.rs:17:5
309+
|
310+
17 | Vec::new()
311+
| ^^^^^^^^^^
312+
|
313+
= help: Remove heap allocations to satisfy the NoAllocation rule
314+
= note: Applied by cargo-pup rule 'no_allocation_check'.
315+
316+
warning: Function allocates heap memory: calls allocating function: std::string::ToString::to_string
317+
--> src/no_allocation.rs:22:5
318+
|
319+
22 | x.to_string()
320+
| ^^^^^^^^^^^^^
321+
|
322+
= help: Remove heap allocations to satisfy the NoAllocation rule
323+
= note: Applied by cargo-pup rule 'no_allocation_check'.
324+
325+
warning: Function allocates heap memory: calls function that allocates: no_allocation::helper_that_allocates
326+
--> src/no_allocation.rs:27:5
327+
|
328+
27 | helper_that_allocates(x)
329+
| ^^^^^^^^^^^^^^^^^^^^^^^^
330+
|
331+
= help: Remove heap allocations to satisfy the NoAllocation rule
332+
= note: Applied by cargo-pup rule 'no_allocation_check'.
333+
334+
warning: Function allocates heap memory: calls allocating function: std::vec::Vec::<T>::new
335+
--> src/no_allocation.rs:32:5
336+
|
337+
32 | Vec::new()
338+
| ^^^^^^^^^^
339+
|
340+
= help: Remove heap allocations to satisfy the NoAllocation rule
341+
= note: Applied by cargo-pup rule 'no_allocation_check'.
342+
343+
warning: Function allocates heap memory: calls function that allocates: no_allocation::deep_helper
344+
--> src/no_allocation.rs:36:5
345+
|
346+
36 | deep_helper()
347+
| ^^^^^^^^^^^^^
348+
|
349+
= help: Remove heap allocations to satisfy the NoAllocation rule
350+
= note: Applied by cargo-pup rule 'no_allocation_check'.
351+
352+
warning: Function allocates heap memory: calls function that allocates: no_allocation::middle_helper
353+
--> src/no_allocation.rs:41:5
354+
|
355+
41 | middle_helper()
356+
| ^^^^^^^^^^^^^^^
357+
|
358+
= help: Remove heap allocations to satisfy the NoAllocation rule
359+
= note: Applied by cargo-pup rule 'no_allocation_check'.
360+
361+
warning: `test_app` (bin "test_app") generated 27 warnings
362+
error: could not compile `test_app` (bin "test_app") due to 6 previous errors; 27 warnings emitted

test_app/pup.ron

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,5 +129,12 @@
129129
MustNotExist(Error),
130130
],
131131
)),
132+
Function((
133+
name: "no_allocation_check",
134+
matches: InModule("^test_app::no_allocation$"),
135+
rules: [
136+
NoAllocation(Warn),
137+
],
138+
)),
132139
],
133140
)

test_app/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod result_error;
1414
mod trait_impl;
1515
mod builder_style;
1616
mod async_functions;
17+
mod no_allocation;
1718

1819
fn main() {
1920
println!("Hello, world!");

0 commit comments

Comments
 (0)