vmrob
vmrob

Reputation: 3046

Heap corruption during signal handling

I have a multi-threaded Windows server that I'm working on and found that after a certain set of criteria occur while I close the program via control-c, it crashes. If my server receives packets from the client and then I use control-c, it crashes. If I start my server up, let it wait for packets for any period of time and then use control-c, it exits properly.

What's weird about it though, is that all of my threads are reporting that they're exiting with status 0 even when the program does throw an exception (unless that's normal).

First-chance exception at 0x75A16DA7 (kernel32.dll) in server.exe: 0x40010005: Control-C.
HEAP[server.exe]: HEAP: Free Heap block 96a818 modified at 96a908 after it was freed
server.exe has triggered a breakpoint.
The thread 0xc34 has exited with code 0 (0x0).
The thread 0x1c64 has exited with code 0 (0x0).
The thread 0xdbc has exited with code 0 (0x0).
The thread 0x117c has exited with code 0 (0x0).
The thread 0x1444 has exited with code 0 (0x0).
The thread 0x1d60 has exited with code 0 (0x0).
The thread 0x798 has exited with code 0 (0x0).
The thread 0x700 has exited with code 0 (0x0).
The thread 0x1bbc has exited with code 0 (0x0).
The thread 0x1b74 has exited with code 0 (0x0).
The program '[7528] server.exe' has exited with code 0 (0x0).

The part of code that seems to be the cause of this problem:

void handleSignal(int sig) {
    std::unique_lock<std::mutex> lock(signalMutex); // <-- comment out and it doesn't crash
    signaled = true;
    _receivedSignal = sig;
    signalHandlerCondition.notify_one(); // <-- comment out and it doesn't crash
}

The mutex and condition variables are both globals:

std::mutex signalMutex;
std::condition_variable signalHandlerCondition;

I have a dedicated signal handling thread that attempts to shut down the server gracefully when it is notified by that event.

void run() {
    while (gContinueRunning && _continueRunning) {
        std::unique_lock<std::mutex> lock(signalMutex);
        signalHandlerCondition.wait(lock);
        if (signaled) {
            gContinueRunning = false;
            signaled = false;
            Server::stop();
        }
    }
}

Granted, when I comment out the offending lines, the program doesn't respond to signals at all. I could have a wait_for so that I don't have to notify the signal handling loop that it has a new signal, but I don't think that's the best way to go.

I did read something from MSDN about signals:

When a CTRL+C interrupt occurs, Win32 operating systems generate a new thread to specifically handle that interrupt.

Because signal-handler routines are usually called asynchronously when an interrupt occurs, your signal-handler function may get control when a run-time operation is incomplete and in an unknown state.

I'm honestly not sure if that applies in this case. If it does, would that imply that my mutex may or may not exist when the signal handler is called?

So then, what's the best way to approach signals? What problem am I running into right here?


Edit: Just to clear a few things up:

void start() {
    _receivedSignal = 0;
    _continueRunning = true;
    // start thread
    std::thread signalHandlerThread(run);
    _signalHandlerThread = std::move(signalHandlerThread);

    // register signals
    signal(SIGABRT, SignalHandler::handleSignal);
    signal(SIGTERM, SignalHandler::handleSignal);
    signal(SIGINT,  SignalHandler::handleSignal);
}

Even after removing the mutex, it looks like the program progresses a little farther -- though only until main finishes.

msvcr110d.dll!operator delete(void * pUserData) Line 52 C++
server.exe!std::_Ref_count<User>::_Destroy() Line 161   C++
server.exe!std::_Ref_count_base::_Decref() Line 120 C++
server.exe!std::_Ptr_base<User>::_Decref() Line 347 C++
server.exe!std::shared_ptr<User>::~shared_ptr<User>() Line 624  C++
server.exe!std::pair<unsigned int const ,std::shared_ptr<User> >::~pair<unsigned int const ,std::shared_ptr<User> >()   C++
server.exe!std::pair<unsigned int const ,std::shared_ptr<User> >::`scalar deleting destructor'(unsigned int)    C++
server.exe!std::allocator<std::_Tree_node<std::pair<unsigned int const ,std::shared_ptr<User> >,void *> >::destroy<std::pair<unsigned int const ,std::shared_ptr<User> > >(std::pair<unsigned int const ,std::shared_ptr<User> > * _Ptr) Line 624   C++
server.exe!std::allocator_traits<std::allocator<std::_Tree_node<std::pair<unsigned int const ,std::shared_ptr<User> >,void *> > >::destroy<std::pair<unsigned int const ,std::shared_ptr<User> > >(std::allocator<std::_Tree_node<std::pair<unsigned int const ,std::shared_ptr<User> >,void *> > & _Al, std::pair<unsigned int const ,std::shared_ptr<User> > * _Ptr) Line 758 C++
server.exe!std::_Wrap_alloc<std::allocator<std::_Tree_node<std::pair<unsigned int const ,std::shared_ptr<User> >,void *> > >::destroy<std::pair<unsigned int const ,std::shared_ptr<User> > >(std::pair<unsigned int const ,std::shared_ptr<User> > * _Ptr) Line 909    C++
server.exe!std::_Tree<std::_Tmap_traits<unsigned int,std::shared_ptr<User>,std::less<unsigned int>,std::allocator<std::pair<unsigned int const ,std::shared_ptr<User> > >,0> >::_Erase(std::_Tree_node<std::pair<unsigned int const ,std::shared_ptr<User> >,void *> * _Rootnode) Line 2069 C++
server.exe!std::_Tree<std::_Tmap_traits<unsigned int,std::shared_ptr<User>,std::less<unsigned int>,std::allocator<std::pair<unsigned int const ,std::shared_ptr<User> > >,0> >::clear() Line 1538   C++
server.exe!std::_Tree<std::_Tmap_traits<unsigned int,std::shared_ptr<User>,std::less<unsigned int>,std::allocator<std::pair<unsigned int const ,std::shared_ptr<User> > >,0> >::erase(std::_Tree_const_iterator<std::_Tree_val<std::_Tree_simple_types<std::pair<unsigned int const ,std::shared_ptr<User> > > > > _First, std::_Tree_const_iterator<std::_Tree_val<std::_Tree_simple_types<std::pair<unsigned int const ,std::shared_ptr<User> > > > > _Last) Line 1512    C++
server.exe!std::_Tree<std::_Tmap_traits<unsigned int,std::shared_ptr<User>,std::less<unsigned int>,std::allocator<std::pair<unsigned int const ,std::shared_ptr<User> > >,0> >::_Tidy() Line 2216   C++
server.exe!std::_Tree<std::_Tmap_traits<unsigned int,std::shared_ptr<User>,std::less<unsigned int>,std::allocator<std::pair<unsigned int const ,std::shared_ptr<User> > >,0> >::~_Tree<std::_Tmap_traits<unsigned int,std::shared_ptr<User>,std::less<unsigned int>,std::allocator<std::pair<unsigned int const ,std::shared_ptr<User> > >,0> >() Line 1190 C++
server.exe!std::map<unsigned int,std::shared_ptr<User>,std::less<unsigned int>,std::allocator<std::pair<unsigned int const ,std::shared_ptr<User> > > >::~map<unsigned int,std::shared_ptr<User>,std::less<unsigned int>,std::allocator<std::pair<unsigned int const ,std::shared_ptr<User> > > >() C++
server.exe!`dynamic atexit destructor for 'User::_usersListBySession''()    C++
msvcr110d.dll!doexit(int code, int quick, int retcaller) Line 584   C
msvcr110d.dll!exit(int code) Line 394   C
server.exe!__tmainCRTStartup() Line 549 C
server.exe!mainCRTStartup() Line 377    C

It looks like all of the other threads are gone. I suppose I probably made a mistake elsewhere.

Thanks for clearing up the signal function safety though.


Edit 2: Looks like an unrelated shared pointer is causing me trouble! I'm glad to see something good came out of this though.

Edit 3: A completely unrelated problem was causing the crashes. All is good in the world now though.

Upvotes: 4

Views: 1648

Answers (1)

Sean Cline
Sean Cline

Reputation: 7209

I suspect that this is occurring because your debugger is handling the Ctrl-C event.

This MSDN article has the following to say:

If a console process is being debugged and CTRL+C signals have not been disabled, the system generates a DBG_CONTROL_C exception. This exception is raised only for the benefit of the debugger, and an application should never use an exception handler to deal with it. If the debugger handles the exception, an application will not notice the CTRL+C, with one exception: alertable waits will terminate. If the debugger passes the exception on unhandled, CTRL+C is passed to the console process and treated as a signal, as previously discussed.

You can set up your event filters to "output - not handled" to allow your application to handle this. I've attached a screenshow of how to set this up in WinDbg. Visual Studio lists this under "Win32 Exceptions".

WinDBG Event Filters

Edit: Also, I should add that trying to lock a mutex in an event handler is considered bad practice. In the event that the mutex is already acquired when the signal handler gets invoked, it will cause a deadlock as the application cannot resume until the signal handler has completed and the signal handler cannot complete until the mutex has been acquired. While this is unlikely in your use case, a CTRL-C during a spurious wake-up or 2 CTRL-C's back to back could cause a deadlock.

Upvotes: 2

Related Questions