Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Windows: Changed thread_event_target_callback's WM_DESTROY to WM_NCDESTROY #1780

Merged
merged 4 commits into from
Dec 10, 2020
Merged

Conversation

VZout
Copy link
Contributor

@VZout VZout commented Nov 29, 2020

Our application was not exiting gracefully but crashed on platform_impl/windows/event_loop.rs:1927.
WM_DESTROY destroys subclass_input but WM_NCDESTOY is send after WM_DESTROY (See MS docs) which causes subclass_input.event_loop_runner.clone(); to fail.

Examples don't repro, are the they just lucky that the memory didn't change?
Creating a minimal test case from our project would be quite a lot of work.

  • Tested on all platforms changed
  • Compilation warnings were addressed
  • cargo fmt has been run on this branch
  • cargo doc builds successfully
  • Added an entry to CHANGELOG.md if knowledge of this change could be valuable to users
  • Updated documentation to reflect any user-facing changes, including notes of platform-specific behavior
  • Created or updated an example program if it would help users understand this functionality
  • Updated feature matrix, if new features were added or implemented

@VZout VZout marked this pull request as ready for review November 29, 2020 17:26
@VZout
Copy link
Contributor Author

VZout commented Nov 29, 2020

Actually seems exactly the same as #1745 but thread_event_target_callback instead of public_window_callback

@VZout VZout changed the title Changed wm_destroy to wm_ncdestroy Windows: Changed thread_event_target_callback's WM_DESTROY to WM_NCDESTROY Nov 29, 2020
@@ -1939,7 +1939,7 @@ unsafe extern "system" fn thread_event_target_callback<T: 'static>(
// the closure to catch_unwind directly so that the match body indendation wouldn't change and
// the git blame and history would be preserved.
let callback = || match msg {
winuser::WM_DESTROY => {
winuser::WM_NCDESTROY => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation says WM_DESTROY is for destruction of the main window, while WM_NCDESTROY is for child windows.

Looking at the docs, it seems like winit should be using WM_DESTROY, what makes you believe that WM_NCDESTROY is the better choice?

Copy link
Contributor Author

@VZout VZout Nov 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because WM_NCDESTROY is also emitted if there are no child windows I believe. Another solution would be returning immediatly if a WM_NCDESTROY event is received and still use WM_DESTROY.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But WM_NCDESTROY being emitted doesn't seem sufficient as a reason to use it? That just seems like a coincidental fix.

Copy link
Member

@maroider maroider Nov 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm (obviously) not @VZout, but...

I think you've misunderstood the docs.
When a window receives WM_DESTROY, three things are promised:

  1. The window is being destroyed.
  2. Any child windows still exist (but will soon be destroyed).
  3. (implicitly) A WM_NCDESTROY message will be dispatched soon.

For WM_NCDESTROY, the following should hold true:

  1. Any child windows are now destroyed
  2. MSDN "This message frees any memory internally allocated for the window."

This would suggest that the window can't receive any further messages after WM_NCDESTROY since Windows has presumably invalidated the window's handle, message queue, and other associated state (see this SO answer).

EDIT: Yikes, didn't see the activity after looking into this (I apparently started writing this minutes before @VZout answered)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your answer was more detailed anyway ♥

CHANGELOG.md Outdated
@@ -1,5 +1,6 @@
# Unreleased

- On Windows, fix applications not exiting gracefully.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth mentioning the use-after-free (and perhaps how window destruction triggers it) here.

Copy link
Contributor Author

@VZout VZout Nov 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:
"On Windows, fix applications not exiting gracefully due to thread_event_target_callback accessing corrupted memory."
or a more detailed version:
"On Windows, fix applications not exiting gracefully due to WM_DESTROY events destroying subclass_input but WM_NCDESTOY is send after WM_DESTROY where the former uses subclass_input."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed it to my first suggestion :)

Copy link
Contributor

@chrisduerr chrisduerr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find it questionable that memory is freed based on an event which hopefully is the last one received, that seems very unsafe to me.

But I have no knowledge of the Windows backend and do not plan to change that.

@maroider
Copy link
Member

maroider commented Dec 2, 2020

It is unfortunate that this isn't documented all that well, but it's probably safe to do it this way, provided that no other funny buisness occurs (read: trying to destroy a window after it has received WM_DESTROY).

@chrisduerr
Copy link
Contributor

it's probably safe to do it this way,

I'd say "probable safe" is not good enough personally.

As I've stated previously I'm not familiar with the Windows winit internals, but this error seems to be triggered by trying to handle events after we've already decided that the lifetime was over. Would it not be possible to store this information internally and prevent further events from being processed even if they would come in?

@maroider
Copy link
Member

maroider commented Dec 3, 2020

It should be possible to replace the window's "window procedure" (event handler function) with a no-op function after the window receives WM_NCDESTROY, which would make it safe to deallocate subclass_input regardless of what messages are received afterwards.

WM_NCDESTROY => {
    SetWindowLongW(hwnd, GWL_WNDPROC, no_op_window_callback);
    // deallocate `subclass_input`
}
extern "system" fn no_op_window_callback( /* callback args */ ) {
    0
}

I'll have to look closer at sublcass_input's lifetime before I feel confident in a solution, tough, as I'm not too familiar with the assumptions that the event_loop module makes internally.

@msiglreith msiglreith mentioned this pull request Dec 9, 2020
Copy link
Member

@msiglreith msiglreith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it's fine to move forward with this change - more sophisticated cleanup can be done as followup as well. This would include removing the subclass, which is supposed to be done in WM_NCDESTROY anyway due to reverse ordering.

@msiglreith msiglreith merged commit 6f70fd9 into rust-windowing:master Dec 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

4 participants