0

Inherited a MFC based application that hosts both windowed and windowless ActiveX controls. In this application the user can design SCADA pages on which he/she is able to create and delete these AX controls dynamically. The controls are mostly simple widgets like buttons, readouts, lines, circles etc.

Since upgrading to a newer version of Visual Studio (2017) the program now crashes when deleting AX controls. Originally it was VC6.

More specifically, most crashes occur when you are attempting to delete a windowless AX control. If those AX controls are hosted in another host app say, Internet Explorer, then the crash does not occur.

It can also crash when the program is quitting, because then all the AX controls on the page are also deleted one by one.

Even more specifically, the order in which objects are created matters. If you delete a windowless object and the next object in focus is going to be a windowed object, you're safe: there is no crash. I think the "route events here" (focus) framework code then selects the windowed object as a valid target for window messages and everything will be fine from then on. If however the new object is also windowless, you are out of luck.

Note that we create all the controls using the WS_DISABLED flag, otherwise the button widget for instance will start reacting to mouse clicks when the user is sizing and moving around the widget.

When the GPF occurs after deleting a windowless AX control, the debugger breaks in and stops in the OLE container file occcont.cpp, function COleControlContainer::HandleWindowlessMessage. At that point it wants to call m_pSiteFocus->m_pWindowlessObject->OnWindowMessage.

This is in the context of the "container" window that hosts all the AX controls. A Window message is about to be routed to the currently focused AX control.

But here the incorrect destination is selected, because m_pWindowlessObject is 0xdddddddd (which means freed memory).

It looks to me like that site focus object should no longer be selected because all its content has been destroyed along with the deleted AX control!

Funny thing is that the delete action of the control itself does not cause any (direct) problems. Indirectly the "post delete" window messages now want to be delivered to an invalid OLE control container. And even when the program is shutting down, the program can still crash because of an earlier delete AX control action.

BTW: The AX control gets created with a CWnd::CreateControl and gets deleted again with a call to CWnd::DestroyWindow().

As a workaround, just after that destroy window call, I force the offending pointer to a null pointer:

COleControlContainer * pContainer = m_wndContainer.GetControlContainer();
const COleControlContainer * pFreedMemory(reinterpret_cast<COleControlContainer*>(0xdddddddd));
if (pContainer != nullptr && pContainer != pFreedMemory) {
    // After we delete the control, the library wants to focus another control.
    // But if there is no more control or the next control is also windowless/disabled
    // it will bump into a stale pointer (0xdddddddd). The code does check for a null pointer,
    // so we force a null pointer.
    pContainer->m_pSiteFocus = 0;
}

Now the program does not crash anymore, but this is ugly!

So the question is: am I doing something wrong, am I facing a bug in the framework or perhaps even a bug in the windowless AX control?

Update #1:

Found this code fragment in MFC's occsite.cpp

BOOL COleControlSite::DestroyControl()
...
    //VBBUG: VB controls will crash if IOleObject::Close is called on them
    //  when they have focus (and unfortunately, deactivating them does not
    //  always move the focus).  To work around this problem, we always hide
    //  the control before closing it.
    ShowWindow(SW_HIDE);
...

Apparently that's a workaround for a similar (if not the same) problem.

Update #2:

I found that for AX controls their destructor wasn't even called. Then indeed found a reference counted interface pointer that wasn't properly handled. Just as iinspectable predicted in the comments!

E. van Putten
  • 615
  • 7
  • 17
  • 1
    Like all COM objects, ActiveX controls are reference counted, and it sounds like your objects' reference counts go bad, one way or another. If those ActiveX controls can successfully be hosted in another host, it stands to reason that the bug is in your host. This is often caused by developers, that didn't understand basic COM rules, and manifests itself in code such as `m_pObj->Release() // Release superfluous reference to prevent mem leak`. Not much help I can offer. You're going to have to debug through the entire life cycle of a control and look for (subtle) bugs. – IInspectable Aug 08 '17 at 17:13
  • Producing a [mcve] is also a good exercise, if only to understand, how this thing *should* work. – IInspectable Aug 08 '17 at 17:13
  • Thanks for your comments. I'm definitely going to build that example. Meanwhile, I have also seen that the windowless controls do not implement stock property `DISPID_ENABLED`. I suspect that this has something to do with the site focus. During creation of the windowless control in the AX container I get an exception (`COleException`) because of the missing property. When I add the stock property, the exception goes away. – E. van Putten Aug 23 '17 at 12:13
  • That's an awkward comment in *occsite.cpp* indeed. Disabling a control doesn't ever automatically move input focus to another control (see [Never leave focus on a disabled control](https://blogs.msdn.microsoft.com/oldnewthing/20040804-00/?p=38243)). If this workaround is indeed necessary, a more reliable implementation would first move input focus to the next control, as explained in the blog post. – IInspectable Dec 15 '17 at 10:42
  • I did something like that before, but then the risk was that the next control would have the same problem. And if it was the last one standing, then there was no other control left anymore to focus. And it would crash again. – E. van Putten Dec 15 '17 at 10:49
  • 1
    The next control won't have the same issue, because upon destruction, it runs the same logic (i.e. it moves the input focus to the next control prior to destroying the control). To find out, whether this is indeed the last control, query the control with focus after moving the input focus forward. If it is the same window, remove input focus by sending `WM_NEXTDLGCTL` with `nullptr` and `TRUE` as the arguments. – IInspectable Dec 15 '17 at 11:13

0 Answers0