[PATCH v2 0/5] MR9843: wtsapi32: Fix session id and desktop handling when using CreateProcessAsUserW with tokens from WTSQueryUserToken
This Merge request resolves [Wine-Bug #58588](https://bugs.winehq.org/show_bug.cgi?id=58588). The underlying problem was that services started on their own, non-interactive winstation, but within the current user's session. This is trivially not the case for system services on Windows: they run, by design, in other user contexts (e.g. NTAUTHORITY\SYSTEM). Whenever a service wants to start a process within another user's graphical session, it needs to set the session id to the correct value. This may be done using the WTS API. This MR implements this functionality in a (hopefully) wine-compatible way: 1. Patch wineserver so tokens can be transferred to other sessions 2. Implement NtSetInformationToken for TokenSessionId 3. Change the session of non-interactive services to session 0 (from the default session 1) in addition to setting their desktop to `__wineservice_winstation\Default` 4. Let CreateProcessAsUserW reset the desktop to the default value `__winsta0\Default` if the session of the to-be-created process is not a service session. This avoids implementing all user and session handling in-depth while still allowing the underlying functionality to work. A real-world user of this mechanism is the Dassault 3DEXPERIENCE launcher, which runs as a system service and wants to open interactive child processes within a user's scope. -- v2: kernelbase: Let CreateProcessInternalW overwrite the desktop if needed services: start services with session id 0 wtsapi32: Make WTSQueryUserToken return a token with correct session id ntdll: Implement NtSetInformationToken for TokenSessionId https://gitlab.winehq.org/wine/wine/-/merge_requests/9843
From: Katharina Bogad <katharina@hacked.xyz> Add a server protokol message to set session ids for process tokens. --- server/protocol.def | 5 +++++ server/token.c | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/server/protocol.def b/server/protocol.def index 0ac64297026..9f210a8504d 100644 --- a/server/protocol.def +++ b/server/protocol.def @@ -3550,6 +3550,11 @@ enum caret_state VARARG(acl,acl); /* default dacl to set */ @END +@REQ(set_token_session_id) + obj_handle_t handle; /* handle to the token */ + unsigned int session_id; /* session_id the token should become */ +@END + @REQ(set_security_object) obj_handle_t handle; /* handle to the object */ unsigned int security_info; /* which parts of security descriptor to set */ diff --git a/server/token.c b/server/token.c index 5ce7298211e..39da631b9b1 100644 --- a/server/token.c +++ b/server/token.c @@ -1602,6 +1602,18 @@ DECL_HANDLER(set_token_default_dacl) } } +DECL_HANDLER(set_token_session_id) +{ + struct token *token; + + if((token = (struct token *)get_handle_obj( current->process, req->handle, + TOKEN_ADJUST_DEFAULT, + &token_ops))) + { + token->session_id = req->session_id; + } +} + DECL_HANDLER(create_linked_token) { struct token *token, *linked; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9843
From: Katharina Bogad <katharina@hacked.xyz> --- dlls/ntdll/unix/security.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dlls/ntdll/unix/security.c b/dlls/ntdll/unix/security.c index 949af82f783..23c63bb5d26 100644 --- a/dlls/ntdll/unix/security.c +++ b/dlls/ntdll/unix/security.c @@ -712,7 +712,14 @@ NTSTATUS WINAPI NtSetInformationToken( HANDLE token, TOKEN_INFORMATION_CLASS cla ret = STATUS_ACCESS_VIOLATION; break; } - FIXME("TokenSessionId stub!\n"); + SERVER_START_REQ( set_token_session_id ) + { + req->handle = wine_server_obj_handle( token ); + /* rather annoyingly, winapi passeses the session_id via the void* argument. + * the tokens maximum length is a DWORD, so 32 bits, so this cast is safe. */ + req->session_id = (ULONG)((size_t)info & 0xFFFFFFFF); + } + SERVER_END_REQ; ret = STATUS_SUCCESS; break; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9843
From: Katharina Bogad <katharina@hacked.xyz> On Windows, WTSQueryUserToken called with a session_id returns a token that is in that user's current session and winstation. Since WINE lacks handling for multiple users, make WTSQueryUserToken return the duplicated token with the requested session_id set. --- dlls/wtsapi32/wtsapi32.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dlls/wtsapi32/wtsapi32.c b/dlls/wtsapi32/wtsapi32.c index 5a1a87f416e..852fdf80c6c 100644 --- a/dlls/wtsapi32/wtsapi32.c +++ b/dlls/wtsapi32/wtsapi32.c @@ -653,9 +653,13 @@ BOOL WINAPI WTSQueryUserToken(ULONG session_id, PHANDLE token) return FALSE; } - return DuplicateHandle(GetCurrentProcess(), GetCurrentProcessToken(), + if(!DuplicateHandle(GetCurrentProcess(), GetCurrentProcessToken(), GetCurrentProcess(), token, - 0, FALSE, DUPLICATE_SAME_ACCESS); + 0, FALSE, DUPLICATE_SAME_ACCESS)) + return FALSE; + + + return SetTokenInformation(token, TokenSessionId, &session_id, sizeof(ULONG)); } /************************************************************ -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9843
From: Katharina Bogad <katharina@hacked.xyz> Services run in a non-interactive session by default; this session is also not enumeratable in WTSEnumerateSessions. Previously, the first part was handled by overwriting the winstation to __wineservice_winstation\Default while leaving the session id to the default 1. Change the default service session to session id 0 when starting a service and fixup WTSEnumerateSessions to never return the service session id. --- dlls/wtsapi32/wtsapi32.c | 5 +++++ programs/services/services.c | 22 +++++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/dlls/wtsapi32/wtsapi32.c b/dlls/wtsapi32/wtsapi32.c index 852fdf80c6c..d04359b9083 100644 --- a/dlls/wtsapi32/wtsapi32.c +++ b/dlls/wtsapi32/wtsapi32.c @@ -364,6 +364,11 @@ BOOL WINAPI WTSEnumerateSessionsW(HANDLE server, DWORD reserved, DWORD version, WTSFreeMemory(*session_info); return FALSE; } + if ((*session_info)->SessionId == 0) + { + FIXME("replacing service session with default\n"); + (*session_info)->SessionId = 1 /* default_session_id, server/process.h */; + } *count = 1; (*session_info)->State = WTSActive; (*session_info)->pWinStationName = (WCHAR *)((char *)*session_info + sizeof(**session_info)); diff --git a/programs/services/services.c b/programs/services/services.c index dff3cd60040..eedbfdeac72 100644 --- a/programs/services/services.c +++ b/programs/services/services.c @@ -918,7 +918,7 @@ static DWORD service_start_process(struct service_entry *service_entry, struct p BOOL is_wow64 = FALSE; HANDLE token; WCHAR *path; - DWORD err; + DWORD err, service_session_id = 0; BOOL r; service_lock(service_entry); @@ -1028,7 +1028,10 @@ found: si.lpDesktop = (WCHAR *)L"__wineservice_winstation\\Default"; } - if (!environment && OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &token)) + r = DuplicateHandle(GetCurrentProcess(), GetCurrentProcessToken(), + GetCurrentProcess(), &token, 0, FALSE, DUPLICATE_SAME_ACCESS); + TRACE("DuplicateHandle %u %lu\n", r, GetLastError()); + if (!environment && r ) { WCHAR val[16]; CreateEnvironmentBlock(&environment, token, FALSE); @@ -1038,9 +1041,7 @@ found: UNICODE_STRING value; RtlInitUnicodeString( &value, val ); - RtlSetEnvironmentVariable( (WCHAR **)&environment, &name, &value ); } - CloseHandle(token); } service_entry->status.dwCurrentState = SERVICE_START_PENDING; @@ -1053,8 +1054,19 @@ found: process->use_count++; service_unlock(service_entry); - r = CreateProcessW(NULL, path, NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT | DETACHED_PROCESS, environment, NULL, &si, &pi); + r = SetTokenInformation(token, TokenSessionId, &service_session_id, sizeof(service_session_id)); + + if(!r) + { + err = GetLastError(); + process_terminate(process); + release_process(process); + return err; + } + + r = CreateProcessAsUserW(token, NULL, path, NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT | DETACHED_PROCESS, environment, NULL, &si, &pi); free(path); + CloseHandle(token); if (!r) { err = GetLastError(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9843
From: Katharina Bogad <katharina@hacked.xyz> Calling CreateProcessAsUserW with a token that has session_id != 0 set should result in the program being started on the token's assoiciated desktop. Currently, WINE doesn't handle multiple users and multiple winstations, so for the moment, if the associated token has a session_id != 0, overwrite the desktop to the default value. This allows serivces started with session_id 0 running non-interactively to get a session_id from wtsapi32 belonging to an interactive session and use CreateProcessAsUserW to open child processes in an interactive environment. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=58588 --- dlls/kernelbase/process.c | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/dlls/kernelbase/process.c b/dlls/kernelbase/process.c index 3656e40280d..d7cd32a4944 100644 --- a/dlls/kernelbase/process.c +++ b/dlls/kernelbase/process.c @@ -519,6 +519,11 @@ BOOL WINAPI DECLSPEC_HOTPATCH CreateProcessInternalW( HANDLE token, const WCHAR ULONG nt_flags = 0; USHORT machine = 0; NTSTATUS status; + DWORD dwSessionId = 0; + DWORD dwReturnLength; + STARTUPINFOW local_startup_info; + BOOL success; + /* Process the AppName and/or CmdLine to get module name and path */ @@ -541,6 +546,26 @@ BOOL WINAPI DECLSPEC_HOTPATCH CreateProcessInternalW( HANDLE token, const WCHAR app_name = name; } + /* Fixup startupinfo->lpDesktop if needed */ + + memcpy(&local_startup_info, startup_info, sizeof(STARTUPINFOW)); + if(token != NULL) + { + success = GetTokenInformation(token, TokenSessionId, &dwSessionId, sizeof(dwSessionId), &dwReturnLength); + + TRACE("success = %u, dwSessionId = %lu\n", success, dwSessionId); + + + if (success && dwSessionId != 0) + { + /* the created process should be attached to an interactive session. + * So we override the winstation for this process. */ + FIXME( "Child process will be started with default desktop\n"); + + local_startup_info.lpDesktop = (WCHAR*)L"winsta0\\default"; + } + } + /* Warn if unsupported features are used */ if (flags & (IDLE_PRIORITY_CLASS | HIGH_PRIORITY_CLASS | REALTIME_PRIORITY_CLASS | @@ -560,7 +585,7 @@ BOOL WINAPI DECLSPEC_HOTPATCH CreateProcessInternalW( HANDLE token, const WCHAR info->hThread = info->hProcess = 0; info->dwProcessId = info->dwThreadId = 0; - if (!(params = create_process_params( app_name, tidy_cmdline, cur_dir, env, flags, startup_info ))) + if (!(params = create_process_params( app_name, tidy_cmdline, cur_dir, env, flags, &local_startup_info ))) { status = STATUS_NO_MEMORY; goto done; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9843
After some discussion in IRC (thanks to Sir_Walrus and @nsivov ), I changed the place the token/winstation dance is happening to CreateProcessInternalW. Also, Sir_Walrus spotted absolutely correctly that directly editing the startup struct is a terrible idea, so I copy it to local stack first and edit it there. Also, we had some discussion in IRC where to best override the desktop. I chose CreateProcessXXX mainly because of convenience: it is the last place the startup struct is not serialized; after the call to create_process_params all parameters are serialized everywhere (struct with strings appended). Editing this structure is not trivial and I couldn't find a place in wine that already edits this structure. Conceptually, this should probably be done in wineserver; but unless the consensus is that the current place absolutely cannot stay, I'd rather avoid coding re-packing of the struct startup_info. If the code for this already exists and I just couldn't find it, please point me to it and I will update this MR accordingly. Also, please excuse the rather stupid CI failures and the according waste of CI time. I just noticed that my local build doesn't do -Werror. I'll reconfigure my local build and push my updates once I finished fixing the build. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9843#note_126310
participants (2)
-
Katharina Bogad -
Katharina Bogad (@hackathi)