Why would waveOutWrite() cause an exception in the debug heap?

5.8k views Asked by At

While researching this issue, I found multiple mentions of the following scenario online, invariably as unanswered questions on programming forums. I hope that posting this here will at least serve to document my findings.

First, the symptom: While running pretty standard code that uses waveOutWrite() to output PCM audio, I sometimes get this when running under the debugger:

 ntdll.dll!_DbgBreakPoint@0()   
 ntdll.dll!_RtlpBreakPointHeap@4()  + 0x28 bytes    
 ntdll.dll!_RtlpValidateHeapEntry@12()  + 0x113 bytes   
 ntdll.dll!_RtlDebugGetUserInfoHeap@20()  + 0x96 bytes  
 ntdll.dll!_RtlGetUserInfoHeap@20()  + 0x32743 bytes    
 kernel32.dll!_GlobalHandle@4()  + 0x3a bytes   
 wdmaud.drv!_waveCompleteHeader@4()  + 0x40 bytes   
 wdmaud.drv!_waveThread@4()  + 0x9c bytes   
 kernel32.dll!_BaseThreadStart@8()  + 0x37 bytes    

While the obvious suspect would be a heap corruption somewhere else in the code, I found out that that's not the case. Furthermore, I was able to reproduce this problem using the following code (this is part of a dialog based MFC application:)

void CwaveoutDlg::OnBnClickedButton1()
{
    WAVEFORMATEX wfx;
    wfx.nSamplesPerSec = 44100; /* sample rate */
    wfx.wBitsPerSample = 16; /* sample size */
    wfx.nChannels = 2;
    wfx.cbSize = 0; /* size of _extra_ info */
    wfx.wFormatTag = WAVE_FORMAT_PCM;
    wfx.nBlockAlign = (wfx.wBitsPerSample >> 3) * wfx.nChannels;
    wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;

    waveOutOpen(&hWaveOut, 
                WAVE_MAPPER, 
                &wfx,  
                (DWORD_PTR)m_hWnd, 
                0,
                CALLBACK_WINDOW );

    ZeroMemory(&header, sizeof(header));
    header.dwBufferLength = 4608;
    header.lpData = (LPSTR)GlobalLock(GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT, 4608));

    waveOutPrepareHeader(hWaveOut, &header, sizeof(header));
    waveOutWrite(hWaveOut, &header, sizeof(header));
}

afx_msg LRESULT CwaveoutDlg::OnWOMDone(WPARAM wParam, LPARAM lParam)
{
    HWAVEOUT dev = (HWAVEOUT)wParam;
    WAVEHDR *hdr = (WAVEHDR*)lParam;
    waveOutUnprepareHeader(dev, hdr, sizeof(WAVEHDR));
    GlobalFree(GlobalHandle(hdr->lpData));
    ZeroMemory(hdr, sizeof(*hdr));
    hdr->dwBufferLength = 4608;
    hdr->lpData = (LPSTR)GlobalLock(GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT, 4608));
    waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
    waveOutWrite(hWaveOut, hdr, sizeof(WAVEHDR));
    return 0;
 }

Before anyone comments on this, yes - the sample code plays back uninitialized memory. Don't try this with your speakers turned all the way up.

Some debugging revealed the following information: waveOutPrepareHeader() populates header.reserved with a pointer to what appears to be a structure containing at least two pointers as its first two members. The first pointer is set to NULL. After calling waveOutWrite(), this pointer is set to a pointer allocated on the global heap. In pseudo code, that would look something like this:

struct Undocumented { void *p1, *p2; } /* This might have more members */

MMRESULT waveOutPrepareHeader( handle, LPWAVEHDR hdr, ...) {
    hdr->reserved = (Undocumented*)calloc(sizeof(Undocumented));
    /* Do more stuff... */
}

MMRESULT waveOutWrite( handle, LPWAVEHDR hdr, ...) {

    /* The following assignment fails rarely, causing the problem: */
    hdr->reserved->p1 = malloc( /* chunk of private data */ );
    /* Probably more code to initiate playback */
}

Normally, the header is returned to the application by waveCompleteHeader(), a function internal to wdmaud.dll. waveCompleteHeader() tries to deallocate the pointer allocated by waveOutWrite() by calling GlobalHandle()/GlobalUnlock() and friends. Sometimes, GlobalHandle() bombs, as shown above.

Now, the reason that GlobalHandle() bombs is not due to a heap corruption, as I suspected at first - it's because waveOutWrite() returned without setting the first pointer in the internal structure to a valid pointer. I suspect that it frees the memory pointed to by that pointer before returning, but I haven't disassembled it yet.

This only appears to happen when the wave playback system is low on buffers, which is why I'm using a single header to reproduce this.

At this point I have a pretty good case against this being a bug in my application - after all, my application is not even running. Has anyone seen this before?

I'm seeing this on Windows XP SP2. The audio card is from SigmaTel, and the driver version is 5.10.0.4995.

Notes:

To prevent confusion in the future, I'd like to point out that the answer suggesting that the problem lies with the use of malloc()/free() to manage the buffers being played is simply wrong. You'll note that I changed the code above to reflect the suggestion, to prevent more people from making the same mistake - it doesn't make a difference. The buffer being freed by waveCompleteHeader() is not the one containing the PCM data, the responsibility to free the PCM buffer lies with the application, and there's no requirement that it be allocated in any specific way.

Also, I make sure that none of the waveOut API calls I use fail.

I'm currently assuming that this is either a bug in Windows, or in the audio driver. Dissenting opinions are always welcome.

9

There are 9 answers

7
Stefan On BEST ANSWER
3
AShelly On

Now, the reason that GlobalHandle() bombs is not due to a heap corruption, as I suspected at first - it's because waveOutWrite() returned without setting the first pointer in the internal structure to a valid pointer. I suspect that it frees the memory pointed to by that pointer before returning, but I haven't disassembled it yet.

I can reproduce this with your code on my system. I see something similar to what Johannes reported. After the call to WaveOutWrite, hdr->reserved normally holds a pointer to allocated memory (which appears to contain the wave out device name in unicode, among other things).

But occasionally, after returning from WaveOutWrite(), the byte pointed to by hdr->reserved is set to 0. This is normally the least significant byte of that pointer. The rest of the bytes in hdr->reserved are ok, and the block of memory that it normally points to is still allocated and uncorrupted.

It probably is being clobbered by another thread - I can catch the change with a conditional breakpoint immediately after the call to WaveOutWrite(). And the system debug breakpoint is occurring in another thread, not the message handler.

However, I can't cause the system debug breakpoint to occur if I use a callback function instead of the windows messsage pump. (fdwOpen = CALLBACK_FUNCTION in WaveOutOpen() ) When I do it this way, my OnWOMDone handler is called by a different thread - possibly the one that's otherwise responsible for the corruption.

So I think there is a bug, either in windows or the driver, but I think you can work around by handling WOM_DONE with a callback function instead of the windows message pump.

1
Adam Rosenfield On

It may be helpful to look at the source code for Wine, although it's possible that Wine has fixed whatever bug there is, and it's also possible Wine has other bugs in it. The relevant files are dlls/winmm/winmm.c, dlls/winmm/lolvldrv.c, and possibly others. Good luck!

2
Ana Betts On

Use Application Verifier to figure out what's going on, if you do something suspicious, it will catch it much earlier.

3
pps On

What about the fact that you are not allowed to call winmm functions from within callback? MSDN does not mention such restrictions about window messages, but usage of window messages is similar to callback function. Possibly, internally it's implemented as a callback function from the driver and that callback does SendMessage. Internally, waveout has to maintain linked list of headers that were written using waveOutWrite; So, I guess that:

hdr->reserved = (Undocumented*)calloc(sizeof(Undocumented));

sets previous/next pointers of the linked list or something like this. If you write more buffers, then if you check the pointers and if any of them point to one another then my guess is most likely correct.

Multiple sources on the web mention that you don't need to unprepare/prepare same headers repeatedly. If you comment out Prepare/unprepare header in the original example then it appears to work fine without any problems.

0
Vadim Galkin On

I solved the problem by polling the sound playback and delays:

WAVEHDR header = { buffer, sizeof(buffer), 0, 0, 0, 0, 0, 0 };
waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
waveOutWrite(hWaveOut, &header, sizeof(WAVEHDR));
/*
* wait a while for the block to play then start trying
* to unprepare the header. this will fail until the block has
* played.
*/
while (waveOutUnprepareHeader(hWaveOut,&header,sizeof(WAVEHDR)) == WAVERR_STILLPLAYING) 
Sleep(100);
waveOutClose(hWaveOut);

Playing Audio in Windows using waveOut Interface

4
Stu Mackellar On

The first thing that I'd do would be to check the return values from the waveOutX functions. If any of them fail - which isn't unreasonable given the scenario you describe - and you carry on regardless then it isn't surprising that things start to go wrong. My guess would be that waveOutWrite is returning MMSYSERR_NOMEM at some point.

2
dmazzoni On

Not sure about this particular problem, but have you considered using a higher-level, cross-platform audio library? There are a lot of quirks with Windows audio programming, and these libraries can save you a lot of headaches.

Examples include PortAudio, RtAudio, and SDL.

3
AudioBubble On

I'm seeing the same problem and have done some analysis myself:

waveOutWrite() allocates (i.e. GlobalAlloc) a pointer to a heap area of 354 bytes and correctly stores it in the data area pointed to by header.reserved.

But when this heap area is to be freed again (in waveCompleteHeader(), according to your analysis; I don't have the symbols for wdmaud.drv myself), the least significant byte of the pointer has been set to zero, thus invalidating the pointer (while the heap is not corrupted yet). In other words, what happens is something like:

  • (BYTE *) (header.reserved) = 0

So I disagree with your statements in one point: waveOutWrite() stores a valid pointer first; the pointer only becomes corrupted later from another thread. Probably that's the same thread (mxdmessage) that later tries to free this heap area, but I did not yet find the point where the zero byte is stored.

This does not happen very often, and the same heap area (same address) has successfully been allocated and deallocated before. I'm quite convinced that this is a bug somewhere in the system code.