Talos Vulnerability Report

TALOS-2024-2063

miniaudio ma_dr_flac__decode_samples__lpc out-of-bounds write vulnerability

March 4, 2025
CVE Number

CVE-2024-41147

SUMMARY

An out-of-bounds write vulnerability exists in the ma_dr_flac__decode_samples__lpc functionality of Miniaudio miniaudio v0.11.21. A specially crafted .flac file can lead to memory corruption. An attacker can provide a malicious file to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Miniaudio v0.11.21

PRODUCT URLS

Miniaudio - https://miniaud.io/index.html

CVSSv3 SCORE

7.7 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:H

CWE

CWE-122 - Heap-based Buffer Overflow

DETAILS

MiniAudio is a lightweight, single-file audio playback and capture library written in C. Designed for simplicity and ease of integration, MiniAudio supports a wide range of audio formats and multiple backends, making it cross-platform compatible with Windows, macOS, Linux, Android, and WebAssembly. It provides functionalities for both audio playback and recording, and includes basic audio effects like volume control and mixing.

There is a vulnerability in the ma_dr_flac__decode_samples__lpc function, due to a buffer overflow caused by a missing check of the allocation size.
A specially crafted FLAC file can lead to an out-of-bounds write which can result in a memory corruption when in playback mode, the application sends raw audio data to miniaudio which is then played back through the default playback device as defined by the operating system.

Trying to playback a malformed FLAC file, we end up in the following situation:

 free(): invalid pointer

Thread 1 received signal SIGABRT, Aborted.
__pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44

Investigating the backtrace of the entire stack show us the following :

#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3  0x000079f71204526e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x000079f7120288ff in __GI_abort () at ./stdlib/abort.c:79
#5  0x000079f7120297b6 in __libc_message_impl (fmt=fmt@entry=0x79f7121ce8d7 "%s\n") at ../sysdeps/posix/libc_fatal.c:132
#6  0x000079f7120a8fe5 in malloc_printerr (str=str@entry=0x79f7121cc672 "free(): invalid pointer") at ./malloc/malloc.c:5772
#7  0x000079f7120ab37c in _int_free (av=<optimized out>, p=<optimized out>, have_lock=0) at ./malloc/malloc.c:4507
#8  0x000079f7120add9e in __GI___libc_free (mem=0x5a5fa0f96740) at ./malloc/malloc.c:3398
#9  0x00005a5f9f959d72 in ma__free_default (p=0x5a5fa0f96740, pUserData=0x0) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:13359
#10 0x00005a5f9f9a6f92 in ma_free (p=0x5a5fa0f96740, pAllocationCallbacks=0x7ffc2fbafc80)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:57049
#11 0x00005a5f9f97214f in ma_device_uninit (pDevice=0x7ffc2fbb0150) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:42212
#12 0x00005a5f9fa1b320 in main (argc=2, argv=0x7ffc2fbb1148) at playback.c:58
#13 0x000079f71202a1ca in __libc_start_call_main (main=main@entry=0x5a5f9fa1b15d <main>, argc=argc@entry=2, argv=argv@entry=0x7ffc2fbb1148) at ../sysdeps/nptl/libc_start_call_main.h:58
#14 0x000079f71202a28b in __libc_start_main_impl (main=0x5a5f9fa1b15d <main>, argc=2, argv=0x7ffc2fbb1148, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, 
    stack_end=0x7ffc2fbb1138) at ../csu/libc-start.c:360
#15 0x00005a5f9f9587c5 in _start ()

The concern is this is happening when the ma_device_uninit is called and freeing object internally as indicated in#11 0x00005a5f9f97214f. Using heap command provided by the gdb plugin pwndbg will be usefull to investigate in the heap and the output correspond to the following when crash is happening

Allocated chunk | PREV_INUSE
Addr: 0x5a5fa0f94000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x5a5fa0f94290
Size: 0x80 (with flag bits: 0x81)

Allocated chunk | PREV_INUSE
Addr: 0x5a5fa0f94310
Size: 0x1e0 (with flag bits: 0x1e1)

Allocated chunk | PREV_INUSE
Addr: 0x5a5fa0f944f0
Size: 0x1010 (with flag bits: 0x1011)

Allocated chunk | PREV_INUSE
Addr: 0x5a5fa0f95500
Size: 0x1210 (with flag bits: 0x1211)

Allocated chunk
Addr: 0x5a5fa0f96710
Size: 0x00 (with flag bits: 0x00)

The pointer to be free at #10passed as parameter to function ma_free is derived from the pDevice pointer passed as argument in #11, below the part of ma_device_uninit responsible to the free around miniaudio.h:42212 indicated by the backtrace:

   42207 
   42208     if (pDevice->isOwnerOfContext) {
   42209         ma_allocation_callbacks allocationCallbacks = pDevice->pContext->allocationCallbacks;
   42210 
   42211         ma_context_uninit(pDevice->pContext);
 ► 42212         ma_free(pDevice->pContext, &allocationCallbacks);
   42213     }
   42214 
   42215     MA_ZERO_OBJECT(pDevice);
   42216 }
   42217 

Looking into the type of pDevice indicate a ma_device * with a member, the first one as a pointer named pContext :

pwndbg> p pDevice
$11 = (ma_device *) 0x7ffc2fbb0150
pwndbg> ptype ma_device
type = struct ma_device {
    ma_context *pContext;
    ma_device_type type;
    ma_uint32 sampleRate;
    ma_atomic_device_state state;
    ma_device_data_proc onData;
    ma_device_notification_proc onNotification;
    ma_stop_proc onStop;
    void *pUserData;
    ma_mutex startStopLock;
    ma_event wakeupEvent;
    ma_event startEvent;
    ma_event stopEvent;
    ma_thread thread;
    ma_result workResult;
    ma_bool8 isOwnerOfContext;
    ma_bool8 noPreSilencedOutputBuffer;

pwndbg> p pDevice
$12 = (ma_device *) 0x7ffc2fbb0150
pwndbg> p pDevice->pContext
$13 = (ma_context *) 0x5a5fa0f96740

A heap chunk is defined by first a value corresponding to the size plus three bits used for allocation mechanism. The plugin pwndbg with gdb can be really helpfull to understand heap allocation mechanism . For example a quick allocation like following :

  62217     /* For now we're just allocating the decoder backend on the heap. */
  62218     pFlac = (ma_flac*)ma_malloc(sizeof(*pFlac), pAllocationCallbacks);

when looking into the pointer returned corresponding to pFlac the value is

pwndbg> p pFlac 
$22 = (ma_flac *) 0x5a5fa0f942a0

Now as this is a 64bits, heap chunk are 16bytes aligned so to get the metadata we can use for example malloc_chunk cmd and here :

pwndbg> malloc_chunk 0x5a5fa0f942a0-16
Allocated chunk | PREV_INUSE
Addr: 0x5a5fa0f94290
Size: 0x80 (with flag bits: 0x81)

which is the same as inspecting the first 16 bytes previous the pointer :

pwndbg> x/8xg 0x5a5fa0f942a0-16
0x5a5fa0f94290:	0x0000000000000000	0x0000000000000081
0x5a5fa0f942a0:	0x0000000000000000	0x0000000000000000

You see the adress of the user data with previous metadata. When there is not a lot of allocation the heap command confirm that point too :

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x5a5fa0f94000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x5a5fa0f94290
Size: 0x80 (with flag bits: 0x81)

Top chunk | PREV_INUSE
Addr: 0x5a5fa0f94310
Size: 0x20cf0 (with flag bits: 0x20cf1)

The second heap chunk here is the chunk corresponding to our example. Now coming back to our issue it’s clear that if the pointer is allocated and modified somewhere we can use an hardware breakpoint at pDevice->pContext-8 to see when size and flags are overwritten. To do so the rr tooling came in place to record the trace and then playing with reverse-continue command from the crash may lead us to the culprit of it. Doing so, let put a hardware breakpoint on our address :

pwndbg> watch *0x5a5fa0f96738
Hardware watchpoint 15: *0x5a5fa0f96738

with reverse-continue command we land into :

Continuing.
shared memfd open() failed: Function not implemented
shared memfd open() failed: Function not implemented
free(): invalid pointer
shared memfd open() failed: Function not implemented
shared memfd open() failed: Function not implemented
shared memfd open() failed: Function not implemented
shared memfd open() failed: Function not implemented
[New Thread 14633.14634]
[Switching to Thread 14633.14634]

Thread 5 hit Hardware watchpoint 15: *0x5a5fa0f96738

Old value = 0
New value = 1025
0x00005a5f9f9f8ba8 in ma_dr_flac__decode_samples__lpc (bs=0x5a5fa0f95640, blockSize=1, bitsPerSample=5, lpcOrder=32 ' ', pDecodedSamples=0x5a5fa0f966c0)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:84907
84907	        pDecodedSamples[i] = sample;

In file: /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:84907
   84902     for (i = 0; i < lpcOrder; ++i) {
   84903         ma_int32 sample;
   84904         if (!ma_dr_flac__read_int32(bs, bitsPerSample, &sample)) {
   84905             return MA_FALSE;
   84906         }
 ► 84907         pDecodedSamples[i] = sample;

this is the place where the value corresponding to our pointer size is overwritten but something is weird, we see here Old value = 0. In fact as we’re going backward we’re to read it the other way, meaning previous value = 1025 and now new value = 0 So we know now the heap corruption is happening during this instruction at 84907 pDecodedSamples[i] = sample corresponding to the function ma_dr_flac__decode_samples__lpc and the table pointer corresponding to pDecodedSamples passed as an arugment. We need to investigate how this pointer is built in order to understand where the vulnerability is. We can first look again at backtrace which is a quite large but we can stop at #2 0x00005a5f9f9fcab9 below as we don’t see further any more parameter of the pDecodedSamplesOut so have a look at miniaudio.h:85271

pwndbg> bt

#0  ma_dr_flac__decode_samples__lpc (bs=0x5a5fa0f95640, blockSize=1, bitsPerSample=5, lpcOrder=32 ' ', pDecodedSamples=0x5a5fa0f966c0)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:84902
#1  0x00005a5f9f9fc622 in ma_dr_flac__decode_subframe (bs=0x5a5fa0f95640, frame=0x5a5fa0f95558, subframeIndex=0, pDecodedSamplesOut=0x5a5fa0f966c0)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:85169
#2  0x00005a5f9f9fcab9 in ma_dr_flac__decode_flac_frame (pFlac=0x5a5fa0f95510) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:85271
#3  0x00005a5f9f9fdfcb in ma_dr_flac__read_and_decode_next_flac_frame (pFlac=0x5a5fa0f95510)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:85334
#4  0x00005a5f9fa0acde in ma_dr_flac_read_pcm_frames_f32 (pFlac=0x5a5fa0f95510, framesToRead=900, pBufferOut=0x5a5fa0fbc760)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:89377
#5  0x00005a5f9f9ae21c in ma_flac_read_pcm_frames (pFlac=0x5a5fa0f942a0, pFramesOut=0x5a5fa0fbc760, frameCount=900, pFramesRead=0x79f7093fc9b8)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:62002
#6  0x00005a5f9f9adbb7 in ma_flac_ds_read (pDataSource=0x5a5fa0f942a0, pFramesOut=0x5a5fa0fbc760, frameCount=900, pFramesRead=0x79f7093fc9b8)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:61731
#7  0x00005a5f9f9a753d in ma_data_source_read_pcm_frames_within_range (pDataSource=0x5a5fa0f942a0, pFramesOut=0x5a5fa0fbc760, frameCount=900, pFramesRead=0x79f7093fca50)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:57220
#8  0x00005a5f9f9a77ce in ma_data_source_read_pcm_frames (pDataSource=0x5a5fa0f942a0, pFramesOut=0x5a5fa0fbc760, frameCount=900, pFramesRead=0x79f7093fcae0)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:57335
#9  0x00005a5f9f9b2470 in ma_decoder_read_pcm_frames (pDecoder=0x7ffc2fbaff20, pFramesOut=0x5a5fa0fbc760, frameCount=900, pFramesRead=0x0)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:65094
#10 0x00005a5f9fa1b158 in data_callback (pDevice=0x7ffc2fbb0150, pOutput=0x5a5fa0fbc760, pInput=0x0, frameCount=900) at playback.c:14
#11 0x00005a5f9f95d8bb in ma_device__on_data_inner (pDevice=0x7ffc2fbb0150, pFramesOut=0x5a5fa0fbc760, pFramesIn=0x0, frameCount=900)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:18690
#12 0x00005a5f9f95dbfb in ma_device__on_data (pDevice=0x7ffc2fbb0150, pFramesOut=0x79f7093fed20, pFramesIn=0x0, frameCount=900)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:18771
#13 0x00005a5f9f95df74 in ma_device__handle_data_callback (pDevice=0x7ffc2fbb0150, pFramesOut=0x79f7093fed20, pFramesIn=0x0, frameCount=900)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:18823
#14 0x00005a5f9f95e55a in ma_device__read_frames_from_client (pDevice=0x7ffc2fbb0150, frameCount=900, pFramesOut=0x79f709600040)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:18926
#15 0x00005a5f9f972b55 in ma_device_handle_backend_data_callback (pDevice=0x7ffc2fbb0150, pOutput=0x79f709600040, pInput=0x0, frameCount=900)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:42531
#16 0x00005a5f9f96813a in ma_device_write_to_stream__pulse (pDevice=0x7ffc2fbb0150, pStream=0x5a5fa0fb87c0, pFramesProcessed=0x0)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:30145
#17 0x00005a5f9f96a182 in ma_device_start__pulse (pDevice=0x7ffc2fbb0150) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:30758
#18 0x00005a5f9f96e81a in ma_worker_thread (pData=0x7ffc2fbb0150) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:40888
#19 0x00005a5f9f95ad11 in ma_thread_entry_proxy (pData=0x5a5fa0fbbe00) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:16508
#20 0x000079f71209ca94 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:447
#21 0x000079f712129a34 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:100

Using two times the up command lead us to the context of our interesting call : With its minimalist design, MiniAudio is ideal for developers seeking a straightforward solution for audio management in their applications.

85266	    channelCount = ma_dr_flac__get_channel_count_from_channel_assignment(pFlac->currentFLACFrame.header.channelAssignment);
85267	    if (channelCount != (int)pFlac->channels) {
85268	        return MA_ERROR;
85269	    }
85270	    for (i = 0; i < channelCount; ++i) {
85271	        if (!ma_dr_flac__decode_subframe(&pFlac->bs, &pFlac->currentFLACFrame, i, pFlac->pDecodedSamples + (pFlac->currentFLACFrame.header.blockSizeInPCMFrames * i))) {
85272	            return MA_ERROR;
85273	        }
85274	    }
85275	    paddingSizeInBits = (ma_uint8)(MA_DR_FLAC_CACHE_L1_BITS_REMAINING(&pFlac->bs) & 7);

here we have to read :

pFlac->pDecodedSamples + (pFlac->currentFLACFrame.header.blockSizeInPCMFrames * i) is corresponding to our pDecodedSamplesOut

Looking at pFlac type again :

pwndbg> ptype pFlac

type = struct {
    ma_dr_flac_meta_proc onMeta;
    void *pUserDataMD;
    ma_allocation_callbacks allocationCallbacks;
    ma_uint32 sampleRate;
    ma_uint8 channels;
    ma_uint8 bitsPerSample;
    ma_uint16 maxBlockSizeInPCMFrames;
    ma_uint64 totalPCMFrameCount;
    ma_dr_flac_container container;
    ma_uint32 seekpointCount;
    ma_dr_flac_frame currentFLACFrame;
    ma_uint64 currentPCMFrame;
    ma_uint64 firstFLACFramePosInBytes;
    ma_dr_flac__memory_stream memoryStream;
    ma_int32 *pDecodedSamples;
    ma_dr_flac_seekpoint *pSeekpoints;
    void *_oggbs;
    ma_bool32 _noSeekTableSeek : 1;
    ma_bool32 _noBinarySearchSeek : 1;
    ma_bool32 _noBruteForceSeek : 1;
    ma_dr_flac_bs bs;
    ma_uint8 pExtraData[1];
} *

We can observe that our pDecodedSamples is a ma_int32 * type, like a table of ma_int32 Let see what is the value of the for loop range from 0 to channelCount at 85270:

pwndbg> p channelCount 
$33 = 1

pwndbg> p i
$34 = 0

and the current index is 0 so the index of the table is also null, e.g Flac->currentFLACFrame.header.blockSizeInPCMFrames * i is null. We can read the code like inside the for loop like flac__decode_subframe(&pFlac->bs, &pFlac->currentFLACFrame, i, pFlac->pDecodedSamples)

Again we can investigate heap pointer by inspecting variables :

pwndbg> p pFlac
$35 = (ma_dr_flac *) 0x5a5fa0f95510

pwndbg> p pFlac->pDecodedSamples
$36 = (ma_int32 *) 0x5a5fa0f966c0

Again let play with hardware breakpoint and backward tracing to lead us to the following point :

pwndbg> bt

#0  0x000079f71211d779 in __brk_call (addr=0x5a5fa0fb5000) at ../sysdeps/unix/sysv/linux/brk_call.h:24
#1  __brk (addr=0x5a5fa0fb5000) at ../sysdeps/unix/sysv/linux/brk.c:37
#2  0x000079f712126b54 in __GI___sbrk (increment=increment@entry=135168) at ./misc/sbrk.c:74
#3  0x000079f7120aaa16 in __glibc_morecore (increment=increment@entry=135168) at ./malloc/morecore.c:29
#4  0x000079f7120acbe7 in sysmalloc (av=0x79f712203ac0 <main_arena>, nb=656) at ./malloc/malloc.c:2709
#5  _int_malloc (av=av@entry=0x79f712203ac0 <main_arena>, bytes=bytes@entry=640) at ./malloc/malloc.c:4481
#6  0x000079f7120ace37 in tcache_init () at ./malloc/malloc.c:3252
#7  0x000079f7120ad766 in tcache_init () at ./malloc/malloc.c:3248
#8  __GI___libc_malloc (bytes=120) at ./malloc/malloc.c:3313
#9  0x00005a5f9f959d23 in ma__malloc_default (sz=120, pUserData=0x0) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:13347
#10 0x00005a5f9f9a6e20 in ma_malloc (sz=120, pAllocationCallbacks=0x7ffc2fbb0110) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:57009
#11 0x00005a5f9f9ae5d4 in ma_decoding_backend_init_file__flac (pUserData=0x0, pFilePath=0x7ffc2fbb2eb7 "./sample1_mod.flac", pConfig=0x7ffc2fbafb00, 
    pAllocationCallbacks=0x7ffc2fbb0110, ppBackend=0x7ffc2fbafaf8) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:62218
#12 0x00005a5f9f9ac557 in ma_decoder_init_from_file__internal (pVTable=0x5a5f9fa41980 <g_ma_decoding_backend_vtable_flac>, pVTableUserData=0x0, 
    pFilePath=0x7ffc2fbb2eb7 "./sample1_mod.flac", pConfig=0x7ffc2fbafc10, pDecoder=0x7ffc2fbaff20)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:60820
#13 0x00005a5f9f9ae81a in ma_decoder_init_flac_from_file__internal (pFilePath=0x7ffc2fbb2eb7 "./sample1_mod.flac", pConfig=0x7ffc2fbafc10, pDecoder=0x7ffc2fbaff20)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:62308
#14 0x00005a5f9f9b1b6c in ma_decoder_init_file (pFilePath=0x7ffc2fbb2eb7 "./sample1_mod.flac", pConfig=0x0, pDecoder=0x7ffc2fbaff20)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:64821
#15 0x00005a5f9fa1b1d8 in main (argc=2, argv=0x7ffc2fbb1148) at playback.c:31
#16 0x000079f71202a1ca in __libc_start_call_main (main=main@entry=0x5a5f9fa1b15d <main>, argc=argc@entry=2, argv=argv@entry=0x7ffc2fbb1148) at ../sysdeps/nptl/libc_start_call_main.h:58
#17 0x000079f71202a28b in __libc_start_main_impl (main=0x5a5f9fa1b15d <main>, argc=2, argv=0x7ffc2fbb1148, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, 
    stack_end=0x7ffc2fbb1138) at ../csu/libc-start.c:360
#18 0x00005a5f9f9587c5 in _start ()

In this case the pointer is never touched, so we need to investigate when the address corresponding to the field of the struct is set , by looking first as it address and add another hardware breakpoint:

pwndbg> p &(pFlac->pDecodedSamples)
$62 = (ma_int32 **) 0x5a5fa0f95620

pwndbg> reverse-continue 
Continuing.

Thread 1 hit Hardware watchpoint 23: *0x5a5fa0f95620

Old value = -1594267968
New value = 0
0x00005a5f9fa03549 in ma_dr_flac_open_with_metadata_private (onRead=0x5a5f9fa03b01 <ma_dr_flac__on_read_stdio>, onSeek=0x5a5f9fa03b34 <ma_dr_flac__on_seek_stdio>, onMeta=0x0, 
    container=ma_dr_flac_container_unknown, pUserData=0x5a5fa0f94320, pUserDataMD=0x5a5fa0f94320, pAllocationCallbacks=0x7ffc2fbb0110)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:87022
87022	    pFlac->pDecodedSamples = (ma_int32*)ma_dr_flac_align((size_t)pFlac->pExtraData, MA_DR_FLAC_MAX_SIMD_VECTOR_SIZE);

The ma_dr_flac_align is a #defined instruction as follow :

#define ma_dr_flac_align(x, a)                              ((((x) + (a) - 1) / (a)) * (a))
#define MA_DR_FLAC_MAX_SIMD_VECTOR_SIZE                     64

This is a macro to aligns a given value up to the nearest mulitple of MA_DR_FLAC_MAX_SIMD_VECTOR_SIZE, e.g 64 meaning that as we got pExtraData one byte length indicates the chunk size must be 64 bytes larger at least. So we need to figure now how pFlac size is computed.

pwndbg> p pFlac
$75 = (ma_dr_flac *) 0x5a5fa0f95510

pwndbg> malloc_chunk 0x5a5fa0f95510-16
Allocated chunk | PREV_INUSE
Addr: 0x5a5fa0f95500
Size: 0x1210 (with flag bits: 0x1211)

Again using an hardware breakpoint on the address corresponding to the size of pFlac, land us to the following backtrack :

pwndbg> bt
#0  0x000079f7120ac506 in _int_malloc (av=av@entry=0x79f712203ac0 <main_arena>, bytes=4616) at ./malloc/malloc.c:4454
#1  0x000079f7120ad7e2 in __GI___libc_malloc (bytes=<optimized out>) at ./malloc/malloc.c:3328
#2  0x00005a5f9f959d23 in ma__malloc_default (sz=4616, pUserData=0x0) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:13347
#3  0x00005a5f9f9ff664 in ma_dr_flac__malloc_from_callbacks (sz=4616, pAllocationCallbacks=0x7ffc2fbae780)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:85846
#4  0x00005a5f9fa034aa in ma_dr_flac_open_with_metadata_private (onRead=0x5a5f9fa03b01 <ma_dr_flac__on_read_stdio>, onSeek=0x5a5f9fa03b34 <ma_dr_flac__on_seek_stdio>, onMeta=0x0, 
    container=ma_dr_flac_container_unknown, pUserData=0x5a5fa0f94320, pUserDataMD=0x5a5fa0f94320, pAllocationCallbacks=0x7ffc2fbb0110)
#5  0x00005a5f9fa042ed in ma_dr_flac_open (onRead=0x5a5f9fa03b01 <ma_dr_flac__on_read_stdio>, onSeek=0x5a5f9fa03b34 <ma_dr_flac__on_seek_stdio>, pUserData=0x5a5fa0f94320, 
    pAllocationCallbacks=0x7ffc2fbb0110) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:87261
#6  0x00005a5f9fa03c10 in ma_dr_flac_open_file (pFileName=0x7ffc2fbb2eb7 "./sample1_mod.flac", pAllocationCallbacks=0x7ffc2fbb0110)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:87116
#7  0x00005a5f9f9ae007 in ma_flac_init_file (pFilePath=0x7ffc2fbb2eb7 "./sample1_mod.flac", pConfig=0x7ffc2fbafb00, pAllocationCallbacks=0x7ffc2fbb0110, pFlac=0x5a5fa0f942a0)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:61879
#8  0x00005a5f9f9ae5fe in ma_decoding_backend_init_file__flac (pUserData=0x0, pFilePath=0x7ffc2fbb2eb7 "./sample1_mod.flac", pConfig=0x7ffc2fbafb00, 
    pAllocationCallbacks=0x7ffc2fbb0110, ppBackend=0x7ffc2fbafaf8) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:62223
#9  0x00005a5f9f9ac557 in ma_decoder_init_from_file__internal (pVTable=0x5a5f9fa41980 <g_ma_decoding_backend_vtable_flac>, pVTableUserData=0x0, 
    pFilePath=0x7ffc2fbb2eb7 "./sample1_mod.flac", pConfig=0x7ffc2fbafc10, pDecoder=0x7ffc2fbaff20)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:60820
#10 0x00005a5f9f9ae81a in ma_decoder_init_flac_from_file__internal (pFilePath=0x7ffc2fbb2eb7 "./sample1_mod.flac", pConfig=0x7ffc2fbafc10, pDecoder=0x7ffc2fbaff20)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:62308
#11 0x00005a5f9f9b1b6c in ma_decoder_init_file (pFilePath=0x7ffc2fbb2eb7 "./sample1_mod.flac", pConfig=0x0, pDecoder=0x7ffc2fbaff20)
    at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:64821
#12 0x00005a5f9fa1b1d8 in main (argc=2, argv=0x7ffc2fbb1148) at playback.c:31
#13 0x000079f71202a1ca in __libc_start_call_main (main=main@entry=0x5a5f9fa1b15d <main>, argc=argc@entry=2, argv=argv@entry=0x7ffc2fbb1148) at ../sysdeps/nptl/libc_start_call_main.h:58
#14 0x000079f71202a28b in __libc_start_main_impl (main=0x5a5f9fa1b15d <main>, argc=2, argv=0x7ffc2fbb1148, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, 
    stack_end=0x7ffc2fbb1138) at ../csu/libc-start.c:360
#15 0x00005a5f9f9587c5 in _start ()

corresponding to #4 0x00005a5f9fa034aa

In file: /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:87013
   87008         #endif
   87009             return NULL;
   87010         }
   87011         allocationSize += seekpointCount * sizeof(ma_dr_flac_seekpoint);
   87012     }
 ► 87013     pFlac = (ma_dr_flac*)ma_dr_flac__malloc_from_callbacks(allocationSize, &allocationCallbacks);
   87014     if (pFlac == NULL) {
   87015     #ifndef MA_DR_FLAC_NO_OGG
   87016         ma_dr_flac__free_from_callbacks(pOggbs, &allocationCallbacks);
   87017     #endif
   87018         return NULL;

Below now the part of the interesting source code function :

 LINE86935 static ma_dr_flac* ma_dr_flac_open_with_metadata_private(ma_dr_flac_read_proc onRead, ma_dr_flac_seek_proc onSeek, ma_dr_flac_meta_proc onMeta, ma_dr_flac_container container, void* pUserData, void* pUserDataMD, const ma_allocation_callbacks* pAllocationCallbacks)
LINE86936 {
LINE86937     ma_dr_flac_init_info init;
LINE86938     ma_uint32 allocationSize;
LINE86939     ma_uint32 wholeSIMDVectorCountPerChannel;
LINE86940     ma_uint32 decodedSamplesAllocationSize;
LINE86941 #ifndef MA_DR_FLAC_NO_OGG
LINE86942     ma_dr_flac_oggbs* pOggbs = NULL;
LINE86943 #endif
LINE86944     ma_uint64 firstFramePos;
LINE86945     ma_uint64 seektablePos;
LINE86946     ma_uint32 seekpointCount;
LINE86947     ma_allocation_callbacks allocationCallbacks;
LINE86948     ma_dr_flac* pFlac;
LINE86949     ma_dr_flac__init_cpu_caps();
LINE86950     if (!ma_dr_flac__init_private(&init, onRead, onSeek, onMeta, container, pUserData, pUserDataMD)) {
LINE86951         return NULL;
LINE86952     }
LINE86953     if (pAllocationCallbacks != NULL) {
LINE86954         allocationCallbacks = *pAllocationCallbacks;
LINE86955         if (allocationCallbacks.onFree == NULL || (allocationCallbacks.onMalloc == NULL && allocationCallbacks.onRealloc == NULL)) {
LINE86956             return NULL;
LINE86957         }
LINE86958     } else {
LINE86959         allocationCallbacks.pUserData = NULL;
LINE86960         allocationCallbacks.onMalloc  = ma_dr_flac__malloc_default;
LINE86961         allocationCallbacks.onRealloc = ma_dr_flac__realloc_default;
LINE86962         allocationCallbacks.onFree    = ma_dr_flac__free_default;
LINE86963     }
LINE86964     allocationSize = sizeof(ma_dr_flac);
LINE86965     if ((init.maxBlockSizeInPCMFrames % (MA_DR_FLAC_MAX_SIMD_VECTOR_SIZE / sizeof(ma_int32))) == 0) {
LINE86966         wholeSIMDVectorCountPerChannel = (init.maxBlockSizeInPCMFrames / (MA_DR_FLAC_MAX_SIMD_VECTOR_SIZE / sizeof(ma_int32)));
LINE86967     } else {
LINE86968         wholeSIMDVectorCountPerChannel = (init.maxBlockSizeInPCMFrames / (MA_DR_FLAC_MAX_SIMD_VECTOR_SIZE / sizeof(ma_int32))) + 1;
LINE86969     }
LINE86970     decodedSamplesAllocationSize = wholeSIMDVectorCountPerChannel * MA_DR_FLAC_MAX_SIMD_VECTOR_SIZE * init.channels;
LINE86971     allocationSize += decodedSamplesAllocationSize;
LINE86972     allocationSize += MA_DR_FLAC_MAX_SIMD_VECTOR_SIZE;
LINE86973 #ifndef MA_DR_FLAC_NO_OGG
LINE86974     if (init.container == ma_dr_flac_container_ogg) {
LINE86975         allocationSize += sizeof(ma_dr_flac_oggbs);
LINE86976         pOggbs = (ma_dr_flac_oggbs*)ma_dr_flac__malloc_from_callbacks(sizeof(*pOggbs), &allocationCallbacks);
LINE86977         if (pOggbs == NULL) {
LINE86978             return NULL;
LINE86979         }
LINE86980         MA_DR_FLAC_ZERO_MEMORY(pOggbs, sizeof(*pOggbs));
LINE86981         pOggbs->onRead = onRead;
LINE86982         pOggbs->onSeek = onSeek;
LINE86983         pOggbs->pUserData = pUserData;
LINE86984         pOggbs->currentBytePos = init.oggFirstBytePos;
LINE86985         pOggbs->firstBytePos = init.oggFirstBytePos;
LINE86986         pOggbs->serialNumber = init.oggSerial;
LINE86987         pOggbs->bosPageHeader = init.oggBosHeader;
LINE86988         pOggbs->bytesRemainingInPage = 0;
LINE86989     }
LINE86990 #endif
LINE86991     firstFramePos  = 42;
LINE86992     seektablePos   = 0;
LINE86993     seekpointCount = 0;
LINE86994     if (init.hasMetadataBlocks) {
LINE86995         ma_dr_flac_read_proc onReadOverride = onRead;
LINE86996         ma_dr_flac_seek_proc onSeekOverride = onSeek;
LINE86997         void* pUserDataOverride = pUserData;
LINE86998 #ifndef MA_DR_FLAC_NO_OGG
LINE86999         if (init.container == ma_dr_flac_container_ogg) {
LINE87000             onReadOverride = ma_dr_flac__on_read_ogg;
LINE87001             onSeekOverride = ma_dr_flac__on_seek_ogg;
LINE87002             pUserDataOverride = (void*)pOggbs;
LINE87003         }
LINE87004 #endif
LINE87005         if (!ma_dr_flac__read_and_decode_metadata(onReadOverride, onSeekOverride, onMeta, pUserDataOverride, pUserDataMD, &firstFramePos, &seektablePos, &seekpointCount, &allocationCallbacks)) {
LINE87006         #ifndef MA_DR_FLAC_NO_OGG
LINE87007             ma_dr_flac__free_from_callbacks(pOggbs, &allocationCallbacks);
LINE87008         #endif
LINE87009             return NULL;
LINE87010         }
LINE87011         allocationSize += seekpointCount * sizeof(ma_dr_flac_seekpoint);
LINE87012     }
LINE87013     pFlac = (ma_dr_flac*)ma_dr_flac__malloc_from_callbacks(allocationSize, &allocationCallbacks);
LINE87014     if (pFlac == NULL) {
LINE87015     #ifndef MA_DR_FLAC_NO_OGG
LINE87016         ma_dr_flac__free_from_callbacks(pOggbs, &allocationCallbacks);
LINE87017     #endif
LINE87018         return NULL;
LINE87019     }
LINE87020     

The allocationSize is computed first at LINE86964 which is at minimum the size of the struct ma_dr_flac, then added to it one value computed through the variable decodedSamplesAllocationSize at LINE86970. The decodedSamplesAllocationSize is directly computed from fields extracted and derived from the malformed file throught init.maxBlockSizeInPCMFrames and init.channels from LINE86965 to LINE86970 MA_DR_FLAC_MAX_SIMD_VECTOR_SIZEis also added to the allocationSize at LINE86972

So the pFlac pointer chunk size is effectively added some extra length based on file details and the variable inside the structure pDecodedSamples is pointed to further the &pFlac->pExtraData+64 but this is only the pFlac pointer which is added block multiple of 64, it’s missing in some case extra space, the difference between the offset inside the struct from pDecodedSamples to pExtraData

if we put also a hardware breakpoint on how is lpcOrder computed we’ll land into the function :

85103             pSubframe->subframeType = MA_DR_FLAC_SUBFRAME_LPC;
 ► 85104             pSubframe->lpcOrder = (ma_uint8)(type & 0x1F) + 1;

from the function ma_dr_flac__read_subframe_header

LINE85086 static ma_bool32 ma_dr_flac__read_subframe_header(ma_dr_flac_bs* bs, ma_dr_flac_subframe* pSubframe)
LINE85087 {
LINE85088     ma_uint8 header;
LINE85089     int type;
LINE85090     if (!ma_dr_flac__read_uint8(bs, 8, &header)) {
LINE85091         return MA_FALSE;
LINE85092     }
LINE85093     if ((header & 0x80) != 0) {
LINE85094         return MA_FALSE;
LINE85095     }
LINE85096     type = (header & 0x7E) >> 1;
LINE85097     if (type == 0) {
LINE85098         pSubframe->subframeType = MA_DR_FLAC_SUBFRAME_CONSTANT;
LINE85099     } else if (type == 1) {
LINE85100         pSubframe->subframeType = MA_DR_FLAC_SUBFRAME_VERBATIM;
LINE85101     } else {
LINE85102         if ((type & 0x20) != 0) {
LINE85103             pSubframe->subframeType = MA_DR_FLAC_SUBFRAME_LPC;
LINE85104             pSubframe->lpcOrder = (ma_uint8)(type & 0x1F) + 1;
LINE85105 	[....]
LINE85128 }

So pSubframe->lpcOrder is derived from type at LINE85104 which is read from header at LINE85096. header is read from the file at LINE85090

Now if we come back to the loop at 84902 controlled by lpcOrder, the value of it is at 0x00005a5f9f9f8ba8 equal to 32, so we’ll need a pDecodedSamples at least a size of : ma_int32 * 32 = 128 which can be less when playing with value controlled from the field. The miss of check of the allocated left size memory for the pointer pFlac->pDecodedSamples at 87022 is enabling an heap overflow at line84907causing a memory corruption and might be turned be an attacker into remote code execution.

Crash Information

free(): invalid pointer
[Thread 0x7fffef2006c0 (LWP 13228) exited]

Thread 1 "playback2" received signal SIGABRT, Aborted.
__pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
warning: 44	./nptl/pthread_kill.c: Aucun fichier ou dossier de ce type


#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3  0x00007ffff7c4526e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007ffff7c288ff in __GI_abort () at ./stdlib/abort.c:79
#5  0x00007ffff7c297b6 in __libc_message_impl (fmt=fmt@entry=0x7ffff7dce8d7 "%s\n") at ../sysdeps/posix/libc_fatal.c:132
#6  0x00007ffff7ca8fe5 in malloc_printerr (str=str@entry=0x7ffff7dcc672 "free(): invalid pointer") at ./malloc/malloc.c:5772
#7  0x00007ffff7cab37c in _int_free (av=<optimized out>, p=<optimized out>, have_lock=0) at ./malloc/malloc.c:4507
#8  0x00007ffff7cadd9e in __GI___libc_free (mem=0x555555643740) at ./malloc/malloc.c:3398
#9  0x0000555555558d72 in ma__free_default (p=0x555555643740, pUserData=0x0) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:13359
#10 0x00005555555a5f92 in ma_free (p=0x555555643740, pAllocationCallbacks=0x7fffffffc680) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:57049
#11 0x000055555557114f in ma_device_uninit (pDevice=0x7fffffffcb50) at /home/manu/Documents/work/miniaudio/tests/corpus/miniaudio_verified_crashes/src/miniaudio.h:42212
#12 0x000055555561a320 in main (argc=2, argv=0x7fffffffdb48) at playback.c:58
#13 0x00007ffff7c2a1ca in __libc_start_call_main (main=main@entry=0x55555561a15d <main>, argc=argc@entry=2, argv=argv@entry=0x7fffffffdb48) at ../sysdeps/nptl/libc_start_call_main.h:58
#14 0x00007ffff7c2a28b in __libc_start_main_impl (main=0x55555561a15d <main>, argc=2, argv=0x7fffffffdb48, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, 
    stack_end=0x7fffffffdb38) at ../csu/libc-start.c:360
#15 0x00005555555577c5 in _start ()
TIMELINE

2024-08-29 - Initial Vendor Contact
2024-09-04 - Vendor Disclosure
2025-02-24 - Vendor Patch Release
2025-03-04 - Public Release

Credit

Discovered by a member of Cisco Talos.