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

[MWB] Fix helper process termination issue in service mode #36892

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

YDKK
Copy link

@YDKK YDKK commented Jan 15, 2025

Summary of the Pull Request

This PR addresses an issue reported in #30259, where running MWB (Mouse Without Borders) in service mode causes the helper process to remain terminated at various points, resulting in functionalities such as clipboard sharing becoming non-functional.

Detailed Description of the Pull Request / Additional comments

When MWB is launched in service mode, events such as desktop switches (e.g., UAC prompts) cause the helper process to be killed and subsequently restarted. To restart the helper process within the user's desktop session, the CreateProcessInInputDesktopSession method uses APIs such as WTSQueryUserToken, which require privileges and the LocalSystem account to function properly.
Link to the relevant code:

internal static int CreateProcessInInputDesktopSession(string commandLine, string arg, string desktop, short wShowWindow, bool lowIntegrity = false)
// As user who runs explorer.exe
{
if (!Program.User.Contains("system", StringComparison.InvariantCultureIgnoreCase))
{
ProcessStartInfo s = new(commandLine, arg);
s.WindowStyle = wShowWindow != 0 ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden;
Process p = Process.Start(s);
return p == null ? 0 : p.Id;
}
string commandLineWithArg = commandLine + " " + arg;
int lastError;
int dwSessionId;
IntPtr hUserToken = IntPtr.Zero, hUserTokenDup = IntPtr.Zero;
Logger.LogDebug("CreateProcessInInputDesktopSession called, launching " + commandLineWithArg + " on " + desktop);
try
{
dwSessionId = Process.GetCurrentProcess().SessionId;
// Get the user token used by DuplicateTokenEx
lastError = (int)NativeMethods.WTSQueryUserToken((uint)dwSessionId, ref hUserToken);
NativeMethods.STARTUPINFO si = default;
si.cb = Marshal.SizeOf(si);
si.lpDesktop = "winsta0\\" + desktop;
si.wShowWindow = wShowWindow;
NativeMethods.SECURITY_ATTRIBUTES sa = default;
sa.Length = Marshal.SizeOf(sa);
if (!NativeMethods.DuplicateTokenEx(hUserToken, NativeMethods.MAXIMUM_ALLOWED, ref sa, (int)NativeMethods.SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int)NativeMethods.TOKEN_TYPE.TokenPrimary, ref hUserTokenDup))
{
lastError = Marshal.GetLastWin32Error();
Logger.Log(string.Format(CultureInfo.CurrentCulture, "DuplicateTokenEx error: {0} Token does not have the privilege.", lastError));
_ = NativeMethods.CloseHandle(hUserToken);
return 0;
}
if (lowIntegrity)
{
NativeMethods.TOKEN_MANDATORY_LABEL tIL;
// Low
string sIntegritySid = "S-1-16-4096";
bool rv = NativeMethods.ConvertStringSidToSid(sIntegritySid, out IntPtr pIntegritySid);
if (!rv)
{
Logger.Log("ConvertStringSidToSid failed");
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return 0;
}
tIL.Label.Attributes = NativeMethods.SE_GROUP_INTEGRITY;
tIL.Label.Sid = pIntegritySid;
rv = NativeMethods.SetTokenInformation(hUserTokenDup, NativeMethods.TOKEN_INFORMATION_CLASS.TokenIntegrityLevel, ref tIL, (uint)Marshal.SizeOf(tIL) + (uint)IntPtr.Size);
if (!rv)
{
Logger.Log("SetTokenInformation failed");
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return 0;
}
}
uint dwCreationFlags = NativeMethods.NORMAL_PRIORITY_CLASS | NativeMethods.CREATE_NEW_CONSOLE;
IntPtr pEnv = IntPtr.Zero;
if (NativeMethods.CreateEnvironmentBlock(ref pEnv, hUserTokenDup, true))
{
dwCreationFlags |= NativeMethods.CREATE_UNICODE_ENVIRONMENT;
}
else
{
pEnv = IntPtr.Zero;
}
_ = NativeMethods.CreateProcessAsUser(
hUserTokenDup, // client's access token
null, // file to execute
commandLineWithArg, // command line
ref sa, // pointer to process SECURITY_ATTRIBUTES
ref sa, // pointer to thread SECURITY_ATTRIBUTES
false, // handles are not inheritable
(int)dwCreationFlags, // creation flags
pEnv, // pointer to new environment block
null, // name of current directory
ref si, // pointer to STARTUPINFO structure
out NativeMethods.PROCESS_INFORMATION pi); // receives information about new process
// GetLastError should be 0
int iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();
Logger.LogDebug("CreateProcessAsUser returned " + iResultOfCreateProcessAsUser.ToString(CultureInfo.CurrentCulture));
// Close handles task
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return (iResultOfCreateProcessAsUser == 0) ? (int)pi.dwProcessId : 0;
}
catch (Exception e)
{
Logger.Log(e);
return 0;
}
}

However, in the scenario where this issue occurs, the main thread is impersonated with user-level permissions, causing these API calls to fail. As a result, the helper process remains terminated, and features like clipboard sharing stop working.

Through debugging, I found that the execution permissions of the main thread are impersonated (and not reverted) after a delegate is invoked via DoSomethingInUIThread from other threads (e.g., InputCallback Thread). The root cause of this behavior is unknown at this time, but I personally feel like it's due to a framework-side issue rather than an implementation issue.

After experimenting with several approaches, I discovered that suppressing the flow of the execution context in the calling thread (using ExecutionContext.SuppressFlow();) before invoking DoSomethingInUIThread prevents this issue. Given that suppressing the execution context flow from non-main threads does not appear to pose any problems in the current MWB implementation, this PR applies this fix.

Validation Steps Performed

  1. Launch MWB in service mode and move the mouse cursor.
  2. Trigger desktop switch events such as displaying a UAC prompt or pressing Ctrl+Alt+Delete.
  3. After returning to the original desktop, confirm that the helper process is restarted and that clipboard sharing functionality continues to operate as expected.

@YDKK
Copy link
Author

YDKK commented Jan 15, 2025

@microsoft-github-policy-service agree

@YDKK YDKK marked this pull request as ready for review January 15, 2025 17:17
@YDKK YDKK force-pushed the feature/fix-helper-process-exit branch from 5639d15 to ce41453 Compare January 18, 2025 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant