smartalloc/lib.rs
1//! [](https://img.shields.io/badge/no-std-red)
2//! [](https://crates.io/crates/smartalloc)
3//! [](https://docs.rs/smartalloc)
4//! [](https://github.com/ehsanmok/smartalloc-rs)
5//!
6//! <br>
7//!
8//! This crate provides a `no_std` idiomatic Rust binding to [smartalloc](https://www.fourmilab.ch/smartall/) used for
9//! **detecting orphaned buffer allocation** which is a type of heap memory leak that the program has lost all access to it.
10//! The primary usecase is as a *debugging* tool when writing **unsafe** code where normal Rust static checks are not available.
11//! It is best used along side [SANs](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/sanitizer.html) where SANs
12//! alone are either unable to detect or their outputs are cumbersome to work through.
13//! To get the best experience, `RUSTFLAGS=-Zsanitizer=leak` is used and is included in `.cargo/config.toml`.
14//!
15//! <br>
16//!
17//! ## Usage
18//!
19//! ```ini
20//! [dev-dependencies]
21//! smartalloc = "0.2"
22//! ```
23//!
24//! In fact, with `#![cfg(debug_assertions)]` the crate does **not** compile in the `--release` mode so preventing from any accidental usage.
25//! The crate **requires nightly** Rust toolchain (MSRV 1.65).
26//!
27//! <br>
28//!
29//! ## Example
30//!
31//! During debugging, configure the `SmartAlloc` as the global allocator. Then include `sm_dump(true)` at the end of an unsafe code block.
32//! Here is the [examples/orphan.rs](https://github.com/ehsanmok/smartalloc-rs/blob/main/examples/orphan.rs)
33//!
34//! ```no_run
35//! use core::alloc::{GlobalAlloc, Layout};
36//!
37//! use smartalloc::{sm_dump, SmartAlloc};
38//!
39//! #[global_allocator]
40//! static GLOBAL: SmartAlloc = SmartAlloc;
41//!
42//! fn main() {
43//! unsafe {
44//! let alloc = SmartAlloc;
45//! let layout = Layout::from_size_align(8, 8).unwrap();
46//! alloc.alloc(layout); // orphaned memory leak as it's pointer is lost
47//! // and there's no alloc.dealloc(ptr, layout)
48//! sm_dump(true);
49//! }
50//! }
51//! ```
52//!
53//! which outputs
54//!
55//! ```txt
56//! Orphaned buffer: 8 bytes allocated at line 12 of examples/orphan.rs
57//! ```
58//!
59//! *Note* that the detector also throws
60//!
61//! ```txt
62//! Orphaned buffer: 5 bytes allocated at line 5 of examples/orphan.rs
63//! Orphaned buffer: 48 bytes allocated at line 5 of examples/orphan.rs
64//! ```
65//!
66//! which refers to the `#[global_allocator]` itself and can be ignored.
67//!
68//! <br>
69//!
70//! ## Features
71//!
72//! The detector can be turned off using `sm_static(true)` and turned back on `sm_static(false)` to wrap cases where allocation
73//! is done through std or safe cases such as [examples/native.rs](https://github.com/ehsanmok/smartalloc-rs/blob/main/examples/native.rs).
74//! For more details, checkout the original [docs](https://www.fourmilab.ch/smartall/).
75//!
76//! ## Aren't SANs alone supposed to detect such errors?
77//!
78//! Neither of the `leak/address/memory` [sanitizers](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/sanitizer.html) are sufficient and can detect such errors *easily*.
79//! In fact, running
80//!
81//! ```txt
82//! RUSTFLAGS="-Zsanitizer=leak" cargo +nightly run --example undetected
83//! // OR
84//! RUSTFLAGS="-Zsanitizer=address" cargo +nightly run --example undetected
85//! ```
86//!
87//! for [examples/undetected.rs](https://github.com/ehsanmok/smartalloc-rs/blob/main/examples/undetected.rs) which is
88//!
89//! ```no_run
90//! unsafe {
91//! let alloc = SmartAlloc;
92//! let layout = Layout::from_size_align(8, 8).unwrap();
93//! alloc.alloc(layout);
94//! }
95//! ```
96//!
97//! with no `sm_dump(true)` at the end, does not show anything, mainly because we specified
98//!
99//! ```ini
100//! [profile.dev]
101//! opt-level = 0
102//! ```
103//!
104//! for the SmartAlloc to work with introspection as opposed to what has been advised to include (at least `opt-level=1`)
105//! [here](https://github.com/japaric/rust-san#unrealiable-leaksanitizer)
106//! to cirvumvent such a limitation but when is done the context gets destroyed. Also
107//!
108//! ```txt
109//! RUSTFLAGS="-Zsanitizer=memory -Zsanitizer-memory-track-origins" cargo +nightly run --example undetected
110//! ```
111//!
112//! cannot compile and it throws unhelpful messages
113//!
114//! ```txt
115//! error: failed to run custom build command for `libc v0.2.132`
116//!
117//! Caused by:
118//! process didn't exit successfully: `/home/workspace/smartalloc-rs/target/debug/build/libc-02d4e594eff5723f/build-script-build` (exit status: 1)
119//! --- stdout
120//! cargo:rerun-if-changed=build.rs
121//!
122//! --- stderr
123//! ==186416==WARNING: MemorySanitizer: use-of-uninitialized-value
124//! #0 0x56367729226c (/home/workspace/smartalloc-rs/target/debug/build/libc-02d4e594eff5723f/build-script-build+0x7a26c) (BuildId: ff090caba1904387acf3f0fecb58801c6fa5caed)
125//! #1 0x56367728e95d (/home/workspace/smartalloc-rs/target/debug/build/libc-02d4e594eff5723f/build-script-build+0x7695d) (BuildId: ff090caba1904387acf3f0fecb58801c6fa5caed)
126//! ...
127//! Uninitialized value was created by an allocation of '_2' in the stack frame of function '_ZN18build_script_build19rustc_minor_nightly17hfbf53e202478a57bE'
128//! #0 0x563677291e70 (/home/workspace/smartalloc-rs/target/debug/build/libc-02d4e594eff5723f/build-script-build+0x79e70) (BuildId: ff090caba1904387acf3f0fecb58801c6fa5caed)
129//!
130//! SUMMARY: MemorySanitizer: use-of-uninitialized-value (/home/workspace/smartalloc-rs/target/debug/build/libc-02d4e594eff5723f/build-script-build+0x7a26c) (BuildId: ff090caba1904387acf3f0fecb58801c6fa5caed)
131//! Exiting
132//! ```
133//!
134//! so it needs more work!
135//!
136//! <br>
137//!
138//! ## Known issue
139//!
140//! [smartalloc-sys/csrc/smartall.c](https://github.com/ehsanmok/smartalloc-rs/blob/main/smartalloc-sys/csrc/smartall.c)
141//! writes into the passed filename pointer tracked by `#[track_caller]` (which is immutable)
142//! which is an UB that could result into displaying more garbage after the filename in its report using this binding.
143
144#![no_std]
145#![crate_type = "lib"]
146#![cfg(debug_assertions)]
147
148extern crate smartalloc_sys as ffi;
149
150use ffi::{c_char, c_int, c_void};
151
152use core::alloc::{GlobalAlloc, Layout};
153use core::panic::Location;
154
155/// Prints orphaned buffers when enabled `true` with
156/// the number of allocated bytes and the line location in source code
157pub fn sm_dump(enable: bool) {
158 unsafe { ffi::sm_dump(enable.into()) }
159}
160
161/// Orphaned buffer detection can be disabled (for such
162/// items as buffers allocated during initialisation) by
163/// calling `sm_static(true)`. Normal orphaned buffer
164/// detection can be re-enabled with `sm_static(false)`. Note
165/// that all the other safeguards still apply to buffers
166/// allocated when `sm_static(true)` mode is in effect.
167pub fn sm_static(enable: bool) {
168 unsafe { ffi::sm_static(enable.into()) }
169}
170
171/// SmartAlloc allocator which needs to be use with `#[global_allocator]`
172pub struct SmartAlloc;
173
174unsafe impl GlobalAlloc for SmartAlloc {
175 #[track_caller]
176 #[inline]
177 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
178 let caller_loc = Location::caller();
179 let fname = caller_loc.file();
180 let lineno = caller_loc.line();
181 ffi::sm_malloc(
182 // this is an unfortunate UB making the output contain some garbage
183 // and seems no other way to make it safe as
184 // `smartall.c` writes into the passed ptr and `fname` is behind a shared pointer
185 // and also neither of `core::cell::Cell` or `alloc::string::String.as_mut_ptr`
186 // can guarentee UTF-8 validity concernes!
187 fname.as_ptr() as *const _ as *mut c_char,
188 lineno as c_int,
189 layout.size(),
190 ) as *mut u8
191 }
192
193 #[inline]
194 unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
195 ffi::sm_free(ptr as *mut c_void);
196 }
197
198 #[track_caller]
199 #[inline]
200 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
201 let caller_loc = Location::caller();
202 let fname = caller_loc.file();
203 let lineno = caller_loc.line();
204 ffi::sm_calloc(
205 // this is an unfortunate UB making the output contain some garbage
206 // and seems no other way to make it safe as
207 // `smartall.c` writes into the passed ptr and `fname` is behind a shared pointer
208 // and also neither of `core::cell::Cell` or `alloc::string::String.as_mut_ptr`
209 // can guarentee UTF-8 validity concernes!
210 fname.as_ptr() as *const _ as *mut c_char,
211 lineno as c_int,
212 1,
213 layout.size(),
214 ) as *mut u8
215 }
216
217 #[track_caller]
218 #[inline]
219 unsafe fn realloc(&self, ptr: *mut u8, _layout: Layout, new_size: usize) -> *mut u8 {
220 let caller_loc = Location::caller();
221 let fname = caller_loc.file();
222 let lineno = caller_loc.line();
223 ffi::sm_realloc(
224 // this is an unfortunate UB making the output contain some garbage
225 // and seems no other way to make it safe as
226 // `smartall.c` writes into the passed ptr and `fname` is behind a shared pointer
227 // and also neither of `core::cell::Cell` or `alloc::string::String.as_mut_ptr`
228 // can guarentee UTF-8 validity concernes!
229 fname.as_ptr() as *const _ as *mut c_char,
230 lineno as c_int,
231 ptr as *mut c_void,
232 new_size,
233 ) as *mut u8
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn free_alloc() {
243 unsafe {
244 let layout = Layout::from_size_align(8, 8).unwrap();
245 let alloc = SmartAlloc;
246 let ptr = alloc.alloc(layout);
247 alloc.dealloc(ptr, layout);
248 }
249 sm_dump(true);
250 }
251
252 #[test]
253 fn free_big_alloc() {
254 unsafe {
255 let layout = Layout::from_size_align(1 << 20, 32).unwrap();
256 let alloc = SmartAlloc;
257 let ptr = alloc.alloc(layout);
258 alloc.dealloc(ptr, layout);
259 sm_dump(true)
260 }
261 }
262
263 #[test]
264 fn free_zero_alloc() {
265 unsafe {
266 let layout = Layout::from_size_align(8, 8).unwrap();
267 let alloc = SmartAlloc;
268 let ptr = alloc.alloc_zeroed(layout);
269 alloc.dealloc(ptr, layout);
270 sm_dump(true)
271 }
272 }
273
274 #[test]
275 fn free_zero_big_alloc() {
276 unsafe {
277 let layout = Layout::from_size_align(1 << 20, 32).unwrap();
278 let alloc = SmartAlloc;
279 let ptr = alloc.alloc_zeroed(layout);
280 alloc.dealloc(ptr, layout);
281 sm_dump(true)
282 }
283 }
284
285 #[test]
286 fn free_realloc() {
287 unsafe {
288 let layout = Layout::from_size_align(8, 8).unwrap();
289 let alloc = SmartAlloc;
290 let ptr = alloc.alloc(layout);
291 let ptr = alloc.realloc(ptr, layout, 16);
292 alloc.dealloc(ptr, layout);
293 sm_dump(true)
294 }
295 }
296
297 #[test]
298 fn free_big_realloc() {
299 unsafe {
300 let layout = Layout::from_size_align(1 << 20, 32).unwrap();
301 let alloc = SmartAlloc;
302 let ptr = alloc.alloc(layout);
303 let ptr = alloc.realloc(ptr, layout, 2 << 20);
304 alloc.dealloc(ptr, layout);
305 sm_dump(true)
306 }
307 }
308}