Closed CodeTako closed 2 years ago
I can also add that by commenting out the mem::forget call in properties.rs/push I can make the mem-leak in this example stop:
/// Adds a property to the collection.
pub fn push(&mut self, prop: Property) -> Result<()> {
let rc = unsafe { ffi::MQTTProperties_add(&mut self.cprops, &prop.cprop) };
if rc == 0 {
// mem::forget(prop);
Ok(())
}
else {
Err(rc.into())
}
}
However, unsure if this is resulting in a potential double free. Reading through the Paho C documentation I don't see an obvious way that Rust implementation is calling the C library incorrectly.
Is that a reliable way to measure memory usage on Linux? (I'm honestly not sure). I know Linux has a lazy recapture mechanism at the OS level, and have been tricked before by it, thinking there was a leak in an app when there really was not. I'm not sure if what it reports for the process does the same thing.
A much better thing to do would be to run the app with valgrind. That would be a more definitive way to detect a memory leak. I can try it as well.
But I will say, that if anything in the library is leaking memory, it would probably be the Properties!
Confirmed leak with valgrind:
$ cargo valgrind run --bin paho_bug
warning: /home/carter/open_source/paho.mqtt.rust/paho-mqtt-sys/Cargo.toml: unused manifest key: package.package
Compiling paho_bug v0.1.0 (/home/carter/open_source/paho_bug)
warning: unused import: `properties`
--> paho_bug/src/main.rs:1:17
|
1 | use paho_mqtt::{properties, Properties};
| ^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: unused variable: `i`
--> paho_bug/src/main.rs:11:9
|
11 | for i in 0..1_000 {
| ^ help: if this is intentional, prefix it with an underscore: `_i`
|
= note: `#[warn(unused_variables)]` on by default
warning: unused variable: `e`
--> paho_bug/src/main.rs:28:17
|
28 | Err(e) => return,
| ^ help: if this is intentional, prefix it with an underscore: `_e`
warning: `paho_bug` (bin "paho_bug") generated 3 warnings
Finished dev [unoptimized + debuginfo] target(s) in 0.56s
Running `/home/carter/.cargo/bin/cargo-valgrind target/debug/paho_bug`
Done
Error leaked 1000 B in 1000 blocks
Info at malloc
at alloc::alloc::alloc (alloc.rs:87)
at alloc::alloc::Global::alloc_impl (status.rs:240)
at <alloc::alloc::Global as core::alloc::Allocator>::allocate (alloc.rs:229)
at alloc::alloc::exchange_malloc (alloc.rs:318)
at paho_bug::surprise_mem_increase (status.rs:240)
at paho_bug::main (main.rs:14)
at core::ops::function::FnOnce::call_once (function.rs:227)
at std::sys_common::backtrace::__rust_begin_short_backtrace (backtrace.rs:122)
at std::rt::lang_start::{{closure}} (rt.rs:145)
at call_once<(), (dyn core::ops::function::Fn<(), Output=i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe)> (function.rs:259)
at do_call<&(dyn core::ops::function::Fn<(), Output=i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe), i32> (panicking.rs:492)
at try<i32, &(dyn core::ops::function::Fn<(), Output=i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe)> (panicking.rs:456)
at catch_unwind<&(dyn core::ops::function::Fn<(), Output=i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe), i32> (panic.rs:137)
at {closure#2} (rt.rs:128)
at do_call<std::rt::lang_start_internal::{closure_env#2}, isize> (panicking.rs:492)
at try<isize, std::rt::lang_start_internal::{closure_env#2}> (panicking.rs:456)
at catch_unwind<std::rt::lang_start_internal::{closure_env#2}, isize> (panic.rs:137)
at std::rt::lang_start_internal (rt.rs:128)
at std::rt::lang_start (status.rs:241)
Summary Leaked 1000 B total
Changed the above example script to run 1000 times, leaked exactly 1000 bytes.
Re-running with the mem::forget line in propertis.rs:push commented out valgrind reports no memory leak:
open_source (master) $ cargo valgrind run --bin paho_bug
warning: /home/carter/open_source/paho.mqtt.rust/paho-mqtt-sys/Cargo.toml: unused manifest key: package.package
Compiling paho-mqtt v0.10.0 (/home/carter/open_source/paho.mqtt.rust)
Compiling paho_bug v0.1.0 (/home/carter/open_source/paho_bug)
warning: unused import: `properties`
--> paho_bug/src/main.rs:1:17
|
1 | use paho_mqtt::{properties, Properties};
| ^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: unused variable: `i`
--> paho_bug/src/main.rs:11:9
|
11 | for i in 0..1_000 {
| ^ help: if this is intentional, prefix it with an underscore: `_i`
|
= note: `#[warn(unused_variables)]` on by default
warning: unused variable: `e`
--> paho_bug/src/main.rs:28:17
|
28 | Err(e) => return,
| ^ help: if this is intentional, prefix it with an underscore: `_e`
warning: `paho_bug` (bin "paho_bug") generated 3 warnings
Finished dev [unoptimized + debuginfo] target(s) in 0.88s
Running `/home/carter/.cargo/bin/cargo-valgrind target/debug/paho_bug`
Done
Again I'm not sure if this has other negative effects, but that is definitely the memory that isn't being cleaned up by the C lib.
OK. Nice. Thanks. I'll mark it as a bug, and get it fixed, assuming it's in the Rust lib and not the C one. If that's the case, I'll push the bug report upstream. I know there's active work on the C lib right now, and the timing would be good (if it hasn't been fixed already).
As for commenting out the mem::forget()
, my initial thought is that's probably not the correct solution. Just like you are guessing, I assume I put it there because the C lib is supposed to clean up the memory.
Hmm... I think I see it....
You're completely correct on both parts:
mem::forget()
is a problemSimply commenting it out should cause a double-free problem (specifically for string and binary data)
The C MQTTProperty
type is a struct which could be holding heap memory if the type is a binary (array), string, or string pair. When adding the property to a C MQTTProperties
collection, the collection does a shallow copy of the struct and takes ownership of the heap memory.
There are two possible problems with the Rust wrapper implementation:
CString
and giving it's heap memory to the C lib to free. That worked when I wrote it, but perhaps something has changed and now there's some alignment or allocation issue causing it to free less data than was allocated? Or maybe I just got lucky.I think the proper fix is two-fold:
datadup()
looks good, but needs to be renamed and made public (not static).Properties::push()
, and making sure Property::drop()
ignores NULL pointers.OK. The Rust CString
documentation specifically says not to transfer ownership of it's raw memory to C code to free. It will result in memory leaks. So I just got lucky when I originally wrote and tested it.
The best thing would be to add some helper code to the C lib.
Sorry. I'm going around in circles. Apparently the C lib is not taking ownership of the heap memory. It's making a copy using datadup()
.
In that case, yes; just getting rid of that mem::forget()
would fix the leak.
Thank you for diving in, and the super quick response! Honestly, neither I nor @CodeTako know much about C <-> Rust interfacing, but we've been heavily using this lib and deeply appreciate the help!
Would love to help move this issue along? Submit a PR with a test and removal of mem:forget()?
No worries. Thanks to you both. This seems trivial enough to get a quick fix in, and it may be worth pushing a point release sooner than later. I'm partially through fixing some build issues, but it may take some time to get that out. And I'm just about to release some v5 production code and I don't want a memory leak in it!
The whole properties
module was an experiment to see if it would be better to manage C memory directly from Rust code. I believe the answer is, "no". The other Rust structs, like connect_options
, ssl_options
use a pinned cache of heap data consisting of CString
values and things like that, with the C structs pointing into that memory. The CString
values themselves take care of the memory management, without need for manual drop()
and forget()
code. But that creates self-referential data structs, and care must be made for moving values. Overall though, doing it that way seems cleaner. I should probably switch it over.
Thanks for the responsiveness! The memory interfacing definitely sounds tricky to handle.
There are memory issues when using Properties with a message. The memory for the properties doesn't seem to get properly freed when the struct is dropped.
Here's an example that shows the undesired behavior with both paho_mqtt v0.9.1 and v0.10:
Example output:
Using: cargo 1.58.0 (f01b232bc 2022-01-19) rustc 1.58.1 (db9d1b20b 2022-01-20) Ubuntu 20.04.4 LTS