Fixing a simple memory leak in alsa_rs
2024-11-25
I recently wrote a Rust wrapper of
libltc by x42 to use in my master's thesis.
The fact that the original library uses only * mut
pointers made me realize
how much I like Rust's explicitness. In the C library, there is no guarantee
that a given pointer won't mutate the data it points to, and that is left to a
mix of reading the code, and making assumptions.
While using the library on my project, I tried to pay attention to whether the
memory was being freed correctly and I noticed several memory leaks. They did
not come, however from my humble bindings, but rather from cpal
- which I was
using to play the audio. The output of valgrind
was something like this:
(...)
==1297036== 1,512 bytes in 21 blocks are possibly lost in loss record 142 of 164
==1297036== at 0x484D953: calloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1297036== by 0x48B3D92: ??? (in /usr/lib/x86_64-linux-gnu/libasound.so.2.0.0)
==1297036== by 0x48C4C44: ??? (in /usr/lib/x86_64-linux-gnu/libasound.so.2.0.0)
==1297036== by 0x48C5289: ??? (in /usr/lib/x86_64-linux-gnu/libasound.so.2.0.0)
==1297036== by 0x48CFD84: snd_config_update_r (in /usr/lib/x86_64-linux-gnu/libasound.so.2.0.0)
==1297036== by 0x48D0447: snd_config_update_ref (in /usr/lib/x86_64-linux-gnu/libasound.so.2.0.0)
==1297036== by 0x48FB35F: snd_pcm_open (in /usr/lib/x86_64-linux-gnu/libasound.so.2.0.0)
==1297036== by 0x4E95A4: alsa::pcm::PCM::open (error.rs:21)
==1297036== by 0x4E94A0: alsa::pcm::PCM::new (pcm.rs:147)
==1297036== by 0x4D9F04: cpal::host::alsa::DeviceHandles::try_open (mod.rs:214)
==1297036== by 0x4DA099: cpal::host::alsa::DeviceHandles::take (mod.rs:233)
==1297036== by 0x4DA372: cpal::host::alsa::Device::build_stream_inner (mod.rs:251)
# Fixing the leak
The leak happens in libasound
(alsa's C library). The snd_pcm_open
function
allocates memory for a pulse code modulation (PCM) device.
- Who's fault is it? - me?,
cpal
?alsa_rs
?libasound
?... - Is this fixed already? How easy is it to fix?
- How did audio on Linux work again?
# Going straight to the source
I started by creating a program using alsa_rs
alone, removing cpal
from the
mix, I order to confirm that the leak was still there.
fn main() {
let _ = alsa::PCM::new("default", alsa::Direction::Capture, false).unwrap();
// PCM::drop is called by the borrow checker
}
Running valgrind --leak-check=full --show-leak-kinds=all ./alsa_leak_test
showed the same leak as before (and a bunch of others).
(...)
==625479== LEAK SUMMARY:
==625479== definitely lost: 1,560 bytes in 22 blocks
==625479== indirectly lost: 1,670 bytes in 12 blocks
==625479== possibly lost: 76,900 bytes in 2,360 blocks
==625479== still reachable: 37,113 bytes in 68 blocks
==625479== suppressed: 0 bytes in 0 blocks
==625479== Reachable blocks (those to which a pointer was found) are not shown.
==625479== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==625479==
==625479== ERROR SUMMARY: 114 errors from 114 contexts (suppressed: 0 from 0)
So the problem is still there, confirming that the leak is not cpal's
responsibility. Great. Now let's figure out if the leak is an alsa_rs
issue
or an alsa
issue.
I replicated my example code to C, and linked to the alsa library on my system.
#include <alsa/asoundlib.h>
#include <stdio.h>
int main() {
snd_pcm_t *handle;
int err;
err = snd_pcm_open(&handle, "default", SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
err = snd_pcm_close(handle);
return 0;
}
Still there. Same exact leak. It was at this point when I realized, snd_pcm_open
is actually allocating memory for a global configuration struct. This struct is
not freed on snd_pcm_close
, but rather on snd_config_update_free_global
.
This is a naming issue snd_pcm_open
does not communicate that it is
allocating memory for a global. The docs probably mention something about it
(but who reads docs right?).
Adding a function call to snd_config_update_free_global
after snd_pcm_close
seems to do something. The output of valgrind now looks like this:
(...)
==647830== 17,172 (16,616 direct, 556 indirect) bytes in 1 blocks are definitely lost in loss record 38 of 38
==647830== at 0x484D953: calloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==647830== by 0x510ECE8: ???
==647830== by 0x5129096: ???
==647830== by 0x510F818: ???
==647830== by 0x508967B: ???
==647830== by 0x48CB6CD: ??? (in /usr/lib/x86_64-linux-gnu/libasound.so.2.0.0)
==647830== by 0x48CBD6D: ??? (in /usr/lib/x86_64-linux-gnu/libasound.so.2.0.0)
==647830== by 0x48FB37E: snd_pcm_open (in /usr/lib/x86_64-linux-gnu/libasound.so.2.0.0)
==647830== by 0x1091E0: main (a.c:7)
==647830==
==647830== LEAK SUMMARY:
==647830== definitely lost: 18,185 bytes in 24 blocks
==647830== indirectly lost: 2,226 bytes in 21 blocks
==647830== possibly lost: 0 bytes in 0 blocks
==647830== still reachable: 16,224 bytes in 43 blocks
==647830== suppressed: 0 bytes in 0 blocks
==647830==
==647830== For lists of detected and suppressed errors, rerun with: -s
==647830== ERROR SUMMARY: 11 errors from 11 contexts (suppressed: 0 from 0)
Great. So that's 114 errors down to 11. It's slightly misleading that the
amount of definitely lost
bytes is actually larger than before, but possibly lost
is now 0, which tells me that probably the memory is being freed
correctly better than before.
But just to be sure, I wanted to build the latest version of libasound
from
source and see if there had been any changes, and if the "leftover leaks" were
still there.
#
Building libasound
from source
I am not very familiar with the C build process, so I started by cloning the
alsa-lib repository. My system
(Ubuntu 24) is using libasound 1.2.11
while the latest version at the moment is
1.2.13
.
I figured out that the project is meant to be built with GNU autoconf
so I
generated the configure
script and ran it. I also added a flag to enable
pipewire
support, as I am using pipewire
on my system.
autoreconf -i
./configure --prefix=$HOME/local/alsa-lib-1.2.13 --enable-pipewire
make
make install
In order to link to the newly built library, I set
LD_LIBRARY_PATH=$(HOME)/local/alsa-lib-1.2.13/lib:$$LD_LIBRARY_PATH
before
compiling. It is useful to double check with ldd
that the binary is linked
against the right library. Running the same C code as before, I got the same
valgrind
result as before
# Final steps
For the last 11 errors
in valgrind
I eventually got the idea to try opening
a device different from "Default". The devices can be listed with aplay -L
, I
tried to choose a device as raw as possible.
hw:CARD=Generic,DEV=9
HD-Audio Generic, HDMI 3
Direct hardware device without any conversions
Turns out, when using this device, all the rest of the leaks are gone. This
confirms that the solution above is correct, and that the "leftover leaks" are
caused by pipewire
, pipewire-pulse
or any of it's siblings. At this point I
had already spent more time than I wanted on this, so I decided to leave it
there for now.
I would eventually like to see if upgrading the rest of the audio libraries to the latest version will fix this problem.
🦆