Wine currently runs all processes with an elevated administrator token. As described in bug 40613, some applications refuse to be run with such a token.
This patch set addresses the situation by creating all processes with an unelevated adminstrator token by default. When a process attempts to elevate itself through one of several mechanisms—which would show a UAC prompt on Windows—we instead silently elevate the process, as if the user had granted access through the UAC prompt.
This works for almost all applications. I have found only one application which didn't get the memo, and actually asks the user to right click and run as administrator, namely PaintTool SAI. Fortunately, 063a377df4f, combined with the shell32 patch in this series, allows a Wine user to elevate that process by opening explorer.exe, right-clicking, and selecting Run as Administrator, just like on Windows.
This patch series has been in Wine-Staging for about three years. This was partly to try to find and fix all the different creative ways that applications tried to elevate themselves, but mostly because I needed to find a solution for PaintTool SAI, and never quite got the time to implement run-as-administrator in shell32.
From: Zebediah Figura z.figura12@gmail.com
Like all other verbs, the actual command line template is specified in the registry. The elevation seems to be hardcoded into shell32 for this specific verb.
The Foobar2000 installer requires administrator privileges, and elevates itself in this way.
Based on a patch by Michael Müller.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=50727 --- dlls/shell32/shlexec.c | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/dlls/shell32/shlexec.c b/dlls/shell32/shlexec.c index eb9ca4a06df..f0dbb984103 100644 --- a/dlls/shell32/shlexec.c +++ b/dlls/shell32/shlexec.c @@ -293,6 +293,21 @@ static HRESULT SHELL_GetPathFromIDListForExecuteW(LPCITEMIDLIST pidl, LPWSTR psz return hr; }
+static HANDLE get_admin_token(void) +{ + TOKEN_ELEVATION_TYPE type; + TOKEN_LINKED_TOKEN linked; + DWORD size; + + if (!GetTokenInformation(GetCurrentThreadEffectiveToken(), TokenElevationType, &type, sizeof(type), &size) + || type == TokenElevationTypeFull) + return NULL; + + if (!GetTokenInformation(GetCurrentThreadEffectiveToken(), TokenLinkedToken, &linked, sizeof(linked), &size)) + return NULL; + return linked.LinkedToken; +} + /************************************************************************* * SHELL_ExecuteW [Internal] * @@ -306,6 +321,7 @@ static UINT_PTR SHELL_ExecuteW(const WCHAR *lpCmd, WCHAR *env, BOOL shWait, UINT gcdret = 0; WCHAR curdir[MAX_PATH]; DWORD dwCreationFlags; + HANDLE token = NULL;
TRACE("Execute %s from directory %s\n", debugstr_w(lpCmd), debugstr_w(psei->lpDirectory));
@@ -327,8 +343,12 @@ static UINT_PTR SHELL_ExecuteW(const WCHAR *lpCmd, WCHAR *env, BOOL shWait, dwCreationFlags = CREATE_UNICODE_ENVIRONMENT; if (!(psei->fMask & SEE_MASK_NO_CONSOLE)) dwCreationFlags |= CREATE_NEW_CONSOLE; - if (CreateProcessW(NULL, (LPWSTR)lpCmd, NULL, NULL, FALSE, dwCreationFlags, env, - NULL, &startup, &info)) + + if (psei->lpVerb && !wcsicmp(psei->lpVerb, L"runas")) + token = get_admin_token(); + + if (CreateProcessAsUserW(token, NULL, (LPWSTR)lpCmd, NULL, NULL, FALSE, + dwCreationFlags, env, NULL, &startup, &info)) { /* Give 30 seconds to the app to come up, if desired. Probably only needed when starting app immediately before making a DDE connection. */ @@ -348,6 +368,8 @@ static UINT_PTR SHELL_ExecuteW(const WCHAR *lpCmd, WCHAR *env, BOOL shWait, retval = ERROR_BAD_FORMAT; }
+ CloseHandle(token); + TRACE("returning %Iu\n", retval);
psei_out->hInstApp = (HINSTANCE)retval;
From: Zebediah Figura z.figura12@gmail.com
This signifies that UAC is active.
Foobar2000 checks this value, and won't even try to elevate itself otherwise.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=50727 --- loader/wine.inf.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/loader/wine.inf.in b/loader/wine.inf.in index 35644cbd285..af40ca0260b 100644 --- a/loader/wine.inf.in +++ b/loader/wine.inf.in @@ -374,7 +374,7 @@ HKLM,%CurrentVersion%\Explorer\DriveIcons,,16 HKLM,%CurrentVersion%\Explorer\KindMap,,16 HKLM,%CurrentVersion%\Group Policy,,16 HKLM,%CurrentVersion%\Installer,"InstallerLocation",,"%11%" -HKLM,%CurrentVersion%\Policies\System,"EnableLUA",0x10003,0 +HKLM,%CurrentVersion%\Policies\System,"EnableLUA",0x10001,1 HKLM,%CurrentVersion%\PreviewHandlers,,16 HKLM,%CurrentVersion%\Run,,16 HKLM,%CurrentVersion%\Setup,"BootDir",,"%30%"
From: Zebediah Figura z.figura12@gmail.com
Dragon Naturally Speaking 12.5 manually validates that the custom action server is elevated.
One might imagine that the right approach here is to add a manifest to msiexec; however, msiexec does not always trigger a UAC prompt on Windows.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=51143 --- dlls/msi/custom.c | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-)
diff --git a/dlls/msi/custom.c b/dlls/msi/custom.c index d1e064f5b65..12a7c3c3676 100644 --- a/dlls/msi/custom.c +++ b/dlls/msi/custom.c @@ -573,12 +573,28 @@ UINT CDECL __wine_msi_call_dll_function(DWORD client_pid, const GUID *guid) return r; }
+static HANDLE get_admin_token(void) +{ + TOKEN_ELEVATION_TYPE type; + TOKEN_LINKED_TOKEN linked; + DWORD size; + + if (!GetTokenInformation(GetCurrentThreadEffectiveToken(), TokenElevationType, &type, sizeof(type), &size) + || type == TokenElevationTypeFull) + return NULL; + + if (!GetTokenInformation(GetCurrentThreadEffectiveToken(), TokenLinkedToken, &linked, sizeof(linked), &size)) + return NULL; + return linked.LinkedToken; +} + static DWORD custom_start_server(MSIPACKAGE *package, DWORD arch) { WCHAR path[MAX_PATH], cmdline[MAX_PATH + 23]; PROCESS_INFORMATION pi = {0}; STARTUPINFOW si = {0}; WCHAR buffer[24]; + HANDLE token; void *cookie; HANDLE pipe;
@@ -600,14 +616,18 @@ static DWORD custom_start_server(MSIPACKAGE *package, DWORD arch) lstrcatW(path, L"\msiexec.exe"); swprintf(cmdline, ARRAY_SIZE(cmdline), L"%s -Embedding %d", path, GetCurrentProcessId());
+ token = get_admin_token(); + if (is_wow64 && arch == SCS_64BIT_BINARY) { Wow64DisableWow64FsRedirection(&cookie); - CreateProcessW(path, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); + CreateProcessAsUserW(token, path, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); Wow64RevertWow64FsRedirection(cookie); } else - CreateProcessW(path, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); + CreateProcessAsUserW(token, path, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); + + if (token) CloseHandle(token);
CloseHandle(pi.hThread);
From: Zebediah Figura z.figura12@gmail.com
Rufus 3.13 Portable requires administrator privileges, and uses a manifest to elevate itself.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=51000 --- dlls/kernelbase/process.c | 57 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-)
diff --git a/dlls/kernelbase/process.c b/dlls/kernelbase/process.c index 775512e7f0d..aec00edb345 100644 --- a/dlls/kernelbase/process.c +++ b/dlls/kernelbase/process.c @@ -28,6 +28,7 @@ #include "winnls.h" #include "wincontypes.h" #include "winternl.h" +#include "winuser.h"
#include "kernelbase.h" #include "wine/debug.h" @@ -431,6 +432,54 @@ BOOL WINAPI DECLSPEC_HOTPATCH CloseHandle( HANDLE handle ) }
+static BOOL image_needs_elevation( const WCHAR *path ) +{ + ACTIVATION_CONTEXT_RUN_LEVEL_INFORMATION run_level; + BOOL ret = FALSE; + HANDLE handle; + ACTCTXW ctx; + + ctx.cbSize = sizeof(ctx); + ctx.dwFlags = ACTCTX_FLAG_RESOURCE_NAME_VALID; + ctx.lpSource = path; + ctx.lpResourceName = (const WCHAR *)CREATEPROCESS_MANIFEST_RESOURCE_ID; + + if (RtlCreateActivationContext( &handle, &ctx )) return FALSE; + + if (!RtlQueryInformationActivationContext( 0, handle, NULL, RunlevelInformationInActivationContext, + &run_level, sizeof(run_level), NULL )) + { + TRACE( "image requested run level %#x\n", run_level.RunLevel ); + if (run_level.RunLevel == ACTCTX_RUN_LEVEL_HIGHEST_AVAILABLE + || run_level.RunLevel == ACTCTX_RUN_LEVEL_REQUIRE_ADMIN) + ret = TRUE; + } + RtlReleaseActivationContext( handle ); + + return ret; +} + + +static HANDLE get_elevated_token(void) +{ + TOKEN_ELEVATION_TYPE type; + TOKEN_LINKED_TOKEN linked; + NTSTATUS status; + + if ((status = NtQueryInformationToken( GetCurrentThreadEffectiveToken(), + TokenElevationType, &type, sizeof(type), NULL ))) + return NULL; + + if (type == TokenElevationTypeFull) return NULL; + + if ((status = NtQueryInformationToken( GetCurrentThreadEffectiveToken(), + TokenLinkedToken, &linked, sizeof(linked), NULL ))) + return NULL; + + return linked.LinkedToken; +} + + /********************************************************************** * CreateProcessAsUserA (kernelbase.@) */ @@ -517,7 +566,7 @@ BOOL WINAPI DECLSPEC_HOTPATCH CreateProcessInternalW( HANDLE token, const WCHAR WCHAR *p, *tidy_cmdline = cmd_line; RTL_USER_PROCESS_PARAMETERS *params = NULL; RTL_USER_PROCESS_INFORMATION rtl_info; - HANDLE parent = 0, debug = 0; + HANDLE parent = 0, debug = 0, elevated_token = NULL; ULONG nt_flags = 0; USHORT machine = 0; NTSTATUS status; @@ -629,6 +678,9 @@ BOOL WINAPI DECLSPEC_HOTPATCH CreateProcessInternalW( HANDLE token, const WCHAR if (flags & CREATE_BREAKAWAY_FROM_JOB) nt_flags |= PROCESS_CREATE_FLAGS_BREAKAWAY; if (flags & CREATE_SUSPENDED) nt_flags |= PROCESS_CREATE_FLAGS_SUSPENDED;
+ if (!token && image_needs_elevation( params->ImagePathName.Buffer )) + token = elevated_token = get_elevated_token(); + status = create_nt_process( token, debug, process_attr, thread_attr, nt_flags, params, &rtl_info, parent, machine, handle_list, job_list ); switch (status) @@ -670,7 +722,8 @@ BOOL WINAPI DECLSPEC_HOTPATCH CreateProcessInternalW( HANDLE token, const WCHAR TRACE( "started process pid %04lx tid %04lx\n", info->dwProcessId, info->dwThreadId ); }
- done: +done: + if (elevated_token) NtClose( elevated_token ); RtlDestroyProcessParameters( params ); if (tidy_cmdline != cmd_line) HeapFree( GetProcessHeap(), 0, tidy_cmdline ); return set_ntstatus( status );
From: Zebediah Figura z.figura12@gmail.com
This is an ntdll port of the previous commit, although no known application depends on this behaviour from ntdll. --- dlls/ntdll/process.c | 81 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 5 deletions(-)
diff --git a/dlls/ntdll/process.c b/dlls/ntdll/process.c index 102e0c930a9..6a6abed6fe8 100644 --- a/dlls/ntdll/process.c +++ b/dlls/ntdll/process.c @@ -37,6 +37,11 @@ #include "wine/exception.h"
+/* we don't want to include winuser.h */ +#define CREATEPROCESS_MANIFEST_RESOURCE_ID ((ULONG_PTR)1) + +WINE_DEFAULT_DEBUG_CHANNEL(process); + /****************************************************************************** * RtlGetCurrentPeb [NTDLL.@] * @@ -89,6 +94,63 @@ NTSTATUS WINAPI RtlWow64EnableFsRedirectionEx( ULONG disable, ULONG *old_value ) }
+static BOOL image_needs_elevation( const UNICODE_STRING *path ) +{ + ACTIVATION_CONTEXT_RUN_LEVEL_INFORMATION run_level; + UNICODE_STRING path0; + BOOL ret = FALSE; + HANDLE handle; + ACTCTXW ctx; + + if (RtlDuplicateUnicodeString( 1, path, &path0 )) + return FALSE; + + ctx.cbSize = sizeof(ctx); + ctx.dwFlags = ACTCTX_FLAG_RESOURCE_NAME_VALID; + ctx.lpSource = path0.Buffer; + ctx.lpResourceName = (const WCHAR *)CREATEPROCESS_MANIFEST_RESOURCE_ID; + + if (RtlCreateActivationContext( &handle, &ctx )) + { + RtlFreeUnicodeString( &path0 ); + return FALSE; + } + + if (!RtlQueryInformationActivationContext( 0, handle, NULL, RunlevelInformationInActivationContext, + &run_level, sizeof(run_level), NULL )) + { + TRACE( "image requested run level %#x\n", run_level.RunLevel ); + if (run_level.RunLevel == ACTCTX_RUN_LEVEL_HIGHEST_AVAILABLE + || run_level.RunLevel == ACTCTX_RUN_LEVEL_REQUIRE_ADMIN) + ret = TRUE; + } + RtlReleaseActivationContext( handle ); + RtlFreeUnicodeString( &path0 ); + return ret; +} + + +static HANDLE get_elevated_token(void) +{ + TOKEN_ELEVATION_TYPE type; + TOKEN_LINKED_TOKEN linked; + NTSTATUS status; + + if ((status = NtQueryInformationToken( GetCurrentThreadEffectiveToken(), + TokenElevationType, &type, sizeof(type), NULL ))) + return NULL; + + if (type == TokenElevationTypeFull) return NULL; + + + if ((status = NtQueryInformationToken( GetCurrentThreadEffectiveToken(), + TokenLinkedToken, &linked, sizeof(linked), NULL ))) + return NULL; + + return linked.LinkedToken; +} + + /********************************************************************** * RtlWow64GetCurrentMachine (NTDLL.@) */ @@ -462,8 +524,15 @@ NTSTATUS WINAPI RtlCreateUserProcess( UNICODE_STRING *path, ULONG attributes, PS_CREATE_INFO create_info; ULONG_PTR buffer[offsetof( PS_ATTRIBUTE_LIST, Attributes[6] ) / sizeof(ULONG_PTR)]; PS_ATTRIBUTE_LIST *attr = (PS_ATTRIBUTE_LIST *)buffer; + HANDLE elevated_token = NULL; + NTSTATUS status; UINT pos = 0;
+ /* It's not clear whether we should use path or ¶ms->ImagePathName here, + * but Roblox Player tries to pass an empty string for the latter. */ + if (!token && image_needs_elevation( path )) + token = elevated_token = get_elevated_token(); + RtlNormalizeProcessParams( params );
attr->Attributes[pos].Attribute = PS_ATTRIBUTE_IMAGE_NAME; @@ -510,11 +579,13 @@ NTSTATUS WINAPI RtlCreateUserProcess( UNICODE_STRING *path, ULONG attributes, InitializeObjectAttributes( &process_attr, NULL, 0, NULL, process_descr ); InitializeObjectAttributes( &thread_attr, NULL, 0, NULL, thread_descr );
- return NtCreateUserProcess( &info->Process, &info->Thread, PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS, - &process_attr, &thread_attr, - inherit ? PROCESS_CREATE_FLAGS_INHERIT_HANDLES : 0, - THREAD_CREATE_FLAGS_CREATE_SUSPENDED, params, - &create_info, attr ); + status = NtCreateUserProcess( &info->Process, &info->Thread, PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS, + &process_attr, &thread_attr, + inherit ? PROCESS_CREATE_FLAGS_INHERIT_HANDLES : 0, + THREAD_CREATE_FLAGS_CREATE_SUSPENDED, params, &create_info, attr ); + + if (elevated_token) NtClose( elevated_token ); + return status; }
/***********************************************************************
From: Zebediah Figura z.figura12@gmail.com
Normally, when an application with a manifest is run, ntdll will notice and create the new process with an administrator token. However, if the process is launched directly from the loader, this code is never run. By launching all processes via start.exe, we make sure that the process is created through RtlCreateUserProcess() and thus elevated if necessary.
The alternative to this patch would be to elevate the child from within ntdll.so or the server. However, either one would require parsing the manifest there, which would require copying a lot of code. This seems like a simpler approach, with no actual disadvantage. --- dlls/ntdll/unix/env.c | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-)
diff --git a/dlls/ntdll/unix/env.c b/dlls/ntdll/unix/env.c index ad9ab0dc220..427c577f2c9 100644 --- a/dlls/ntdll/unix/env.c +++ b/dlls/ntdll/unix/env.c @@ -1924,6 +1924,7 @@ static void init_peb( RTL_USER_PROCESS_PARAMETERS *params, void *module ) */ static RTL_USER_PROCESS_PARAMETERS *build_initial_params( void **module ) { + static const char *args[] = { "start.exe", "/exec" }; static const WCHAR valueW[] = {'1',0}; static const WCHAR pathW[] = {'P','A','T','H'}; RTL_USER_PROCESS_PARAMETERS *params = NULL; @@ -1952,29 +1953,8 @@ static RTL_USER_PROCESS_PARAMETERS *build_initial_params( void **module ) add_registry_environment( &env, &env_pos, &env_size ); env[env_pos++] = 0;
- status = load_main_exe( NULL, main_argv[1], curdir, 0, &image, module ); - if (!status) - { - char *loader; - - if (main_image_info.ImageCharacteristics & IMAGE_FILE_DLL) status = STATUS_INVALID_IMAGE_FORMAT; - /* if we have to use a different loader, fall back to start.exe */ - if ((loader = get_alternate_wineloader( main_image_info.Machine ))) - { - free( loader ); - status = STATUS_INVALID_IMAGE_FORMAT; - } - } - - if (status) /* try launching it through start.exe */ - { - static const char *args[] = { "start.exe", "/exec" }; - free( image ); - if (*module) NtUnmapViewOfSection( GetCurrentProcess(), *module ); - load_start_exe( &image, module ); - prepend_argv( args, 2 ); - } - else rebuild_argv(); + load_start_exe( &image, module ); + prepend_argv( args, 2 );
main_wargv = build_wargv( get_dos_path( image )); cmdline = build_command_line( main_wargv );
From: Zebediah Figura z.figura12@gmail.com
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=40613 --- server/process.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server/process.c b/server/process.c index 3651696f505..e5dcff74794 100644 --- a/server/process.c +++ b/server/process.c @@ -710,7 +710,7 @@ struct process *create_process( int fd, struct process *parent, unsigned int fla if (!parent) { process->handles = alloc_handle_table( process, 0 ); - process->token = token_create_admin( TRUE, -1, TokenElevationTypeFull, default_session_id ); + process->token = token_create_admin( TRUE, -1, TokenElevationTypeLimited, default_session_id ); process->affinity = ~0; } else
Hi,
It looks like your patch introduced the new failures shown below. Please investigate and fix them before resubmitting your patch. If they are not new, fixing them anyway would help a lot. Otherwise please ask for the known failures list to be updated.
The tests also ran into some preexisting test failures. If you know how to fix them that would be helpful. See the TestBot job for the details:
The full results can be found at: https://testbot.winehq.org/JobDetails.pl?Key=143258
Your paranoid android.
=== debian11b (64 bit WoW report) ===
advapi32: security.c:8505: Test failed: got error 1 security.c:8510: Test failed: got error 1
faultrep: faultrep.c:97: Test failed: AddERExcludedApplicationA should have failed, got 1
sc.exe: sc.c:200: Test failed: "sc create" tests need elevated permissions
How does this patch interact with wine tests that require admin privileges? Some tests are skipped when not running elevated.
On Tue Feb 20 11:15:02 2024 +0000, Jinoh Kang wrote:
How does this patch interact with wine tests that require admin privileges? Some tests are skipped when not running elevated.
For example, this patch introduces the following test skip message:
``` shlexec.c:2309: Tests skipped: No admin privileges, skipping runas test. ```
Unconditional `start.exe` will break debugging/profiling workflow that assume that wine will load the main exe in-process. You'll have to either tweak follow-fork-mode or attach to existing wine process. Is this inconvenience worth it?
Hans Leidekker (@hans) commented about dlls/kernelbase/process.c:
ret = TRUE;
- }
- RtlReleaseActivationContext( handle );
- return ret;
+}
+static HANDLE get_elevated_token(void) +{
- TOKEN_ELEVATION_TYPE type;
- TOKEN_LINKED_TOKEN linked;
- NTSTATUS status;
- if ((status = NtQueryInformationToken( GetCurrentThreadEffectiveToken(),
TokenElevationType, &type, sizeof(type), NULL )))
'status' is unused here and in ntdll.
On Tue Feb 20 11:17:50 2024 +0000, Jinoh Kang wrote:
For example, this patch introduces the following test skip message:
shlexec.c:2309: Tests skipped: No admin privileges, skipping runas test.
I'll add a patch to winetest to elevate itself when running under Wine.
On Tue Feb 20 14:27:42 2024 +0000, Hans Leidekker wrote:
'status' is unused here and in ntdll.
Oops, good catch.
On Tue Feb 20 11:20:32 2024 +0000, Jinoh Kang wrote:
Unconditional `start.exe` will break debugging/profiling workflow that assume that wine will load the main exe in-process. You'll have to either tweak follow-fork-mode or attach to existing wine process. Is this inconvenience worth it?
I would assert that functionality is worth a minor inconvenience to debugging workflows, yes. Especially for something as rare as using a debugger. For that matter I've always had to use follow-fork every time I run a debugger anyway.
On Tue Feb 20 18:35:27 2024 +0000, Zebediah Figura wrote:
I would assert that functionality is worth a minor inconvenience to debugging workflows, yes. Especially for something as rare as using a debugger. For that matter I've always had to use follow-fork every time I run a debugger anyway.
Fwiw I use debuggers a lot and it's been extremely useful for finding bugs.
I've also been putting a lot of effort on trying to improve the experience of using Gdb with Wine, so IMO it would be better if we could do this differently.
Using follow-fork-mode is especially annoying as there's plenty of processes spawned on startup, and whatever start.exe does, the main process is *not* going to be just one fork ahead.
I've also been putting a lot of effort on trying to improve the experience of using Gdb with Wine, so IMO it would be better if we could do this differently.
How else do you propose we do this?
On Tue Feb 20 18:55:53 2024 +0000, Zebediah Figura wrote:
I've also been putting a lot of effort on trying to improve the
experience of using Gdb with Wine, so IMO it would be better if we could do this differently. How else do you propose we do this?
I don't know, I am only replying to the dismissal of debugging as an interesting use case. Debugger not working well is not a fatality, assuming we don't make it even harder to fix.
On Tue Feb 20 19:01:20 2024 +0000, Rémi Bernon wrote:
I don't know, I am only replying to the dismissal of debugging as an interesting use case. Debugger not working well is not a fatality, assuming we don't make it even harder to fix.
IMO now the case when you can start the debugee process directly without intermediate process is a relatively rare luck (well, maybe not that rare overall if games are not concerned, but there are great amount of cases when you can't). How do you do it now (I only attach debuggers to running processes)? Maybe there is some generic way to improve this instead of caring about first process start case, like env var to wait for debugger attach early?