[PATCH v5 0/3] MR10565: shcore: Implement Set/GetCurrentProcessExplicitAppUserModelID.
Wine Bug: https://bugs.winehq.org/show_bug.cgi?id=59625 Set/GetCurrentProcessExplicitAppUserModelID were stubs. Set returned S_OK but discarded the value, Get always returned NULL with E_NOTIMPL. This broke .NET WPF applications that set an AppUserModelID during startup and later retrieve it, causing "String argument cannot be null or empty" crashes. Implement both using a process-wide variable protected by a critical section. Callers receive CoTaskMemAlloc'd copies per the API contract. -- v5: shcore/tests: Add tests for Set/GetCurrentProcessExplicitAppUserModelID. shcore: Implement Set/GetCurrentProcessExplicitAppUserModelID. include: Add APPLICATION_USER_MODEL_ID length constants to appmodel.h. https://gitlab.winehq.org/wine/wine/-/merge_requests/10565
From: Robert Gerigk <Robert-Gerigk@online.de> Add APPLICATION_USER_MODEL_ID_MIN_LENGTH and APPLICATION_USER_MODEL_ID_MAX_LENGTH defines matching the Windows SDK. Signed-off-by: Jan Robert Gerigk <Robert-Gerigk@online.de> --- include/appmodel.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/appmodel.h b/include/appmodel.h index 8c219e8080a..53049b461f7 100644 --- a/include/appmodel.h +++ b/include/appmodel.h @@ -18,6 +18,9 @@ #ifndef _APPMODEL_H_ #define _APPMODEL_H_ +#define APPLICATION_USER_MODEL_ID_MIN_LENGTH 1 +#define APPLICATION_USER_MODEL_ID_MAX_LENGTH 128 + #if defined(__cplusplus) extern "C" { #endif -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10565
From: Robert Gerigk <Robert-Gerigk@online.de> Wine's shcore.dll had Set/GetCurrentProcessExplicitAppUserModelID as stubs. SetCurrentProcessExplicitAppUserModelID returned S_OK but discarded the value, and GetCurrentProcessExplicitAppUserModelID always returned NULL with E_NOTIMPL. This broke applications that set an AppUserModelID and later retrieve it, causing "String argument cannot be null or empty" crashes in .NET WPF applications. Implement both functions using a process-wide static variable protected by a critical section. SetCurrentProcessExplicitAppUserModelID validates the input (rejects NULL, empty strings, and strings exceeding APPLICATION_USER_MODEL_ID_MAX_LENGTH) and stores a CoTaskMemAlloc'd copy. GetCurrentProcessExplicitAppUserModelID returns a CoTaskMemAlloc'd copy to the caller. Signed-off-by: Jan Robert Gerigk <Robert-Gerigk@online.de> --- dlls/shcore/main.c | 55 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/dlls/shcore/main.c b/dlls/shcore/main.c index f04853cb9f5..cc5411ce143 100644 --- a/dlls/shcore/main.c +++ b/dlls/shcore/main.c @@ -30,6 +30,7 @@ #include "featurestagingapi.h" #include "shellscalingapi.h" #include "shcore.h" +#include "appmodel.h" #define WINSHLWAPI #include "shlwapi.h" @@ -249,17 +250,63 @@ HRESULT WINAPI IUnknown_SetSite(IUnknown *obj, IUnknown *site) return hr; } +static WCHAR *explicit_app_user_model_id; +static CRITICAL_SECTION appid_cs; +static CRITICAL_SECTION_DEBUG appid_cs_debug = +{ + 0, 0, &appid_cs, + { &appid_cs_debug.ProcessLocksList, &appid_cs_debug.ProcessLocksList }, + 0, 0, { (DWORD_PTR)(__FILE__ ": appid_cs") } +}; +static CRITICAL_SECTION appid_cs = { &appid_cs_debug, -1, 0, 0, 0, 0 }; + HRESULT WINAPI SetCurrentProcessExplicitAppUserModelID(const WCHAR *appid) { - FIXME("%s: stub\n", debugstr_w(appid)); + WCHAR *new_id = NULL; + DWORD len; + + TRACE("%s\n", debugstr_w(appid)); + + if (!appid || !appid[0]) + return E_INVALIDARG; + + len = lstrlenW(appid); + if (len > APPLICATION_USER_MODEL_ID_MAX_LENGTH) + return E_INVALIDARG; + + new_id = CoTaskMemAlloc((len + 1) * sizeof(WCHAR)); + if (!new_id) return E_OUTOFMEMORY; + memcpy(new_id, appid, (len + 1) * sizeof(WCHAR)); + + EnterCriticalSection(&appid_cs); + CoTaskMemFree(explicit_app_user_model_id); + explicit_app_user_model_id = new_id; + LeaveCriticalSection(&appid_cs); + return S_OK; } HRESULT WINAPI GetCurrentProcessExplicitAppUserModelID(const WCHAR **appid) { - FIXME("%p: stub\n", appid); - *appid = NULL; - return E_NOTIMPL; + TRACE("%p\n", appid); + + if (!appid) return E_INVALIDARG; + + EnterCriticalSection(&appid_cs); + if (explicit_app_user_model_id) + { + DWORD len = (lstrlenW(explicit_app_user_model_id) + 1) * sizeof(WCHAR); + *appid = CoTaskMemAlloc(len); + if (*appid) + memcpy((WCHAR *)*appid, explicit_app_user_model_id, len); + } + else + { + *appid = NULL; + } + LeaveCriticalSection(&appid_cs); + + return *appid ? S_OK : E_FAIL; } /************************************************************************* -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10565
From: Robert Gerigk <Robert-Gerigk@online.de> Test input validation (empty string, APPLICATION_USER_MODEL_ID_MAX_LENGTH), set/get round-trip, and ID update behavior. Signed-off-by: Jan Robert Gerigk <Robert-Gerigk@online.de> --- dlls/shcore/tests/Makefile.in | 2 +- dlls/shcore/tests/shcore.c | 65 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/dlls/shcore/tests/Makefile.in b/dlls/shcore/tests/Makefile.in index 2b7eaa65686..d40498fda15 100644 --- a/dlls/shcore/tests/Makefile.in +++ b/dlls/shcore/tests/Makefile.in @@ -1,5 +1,5 @@ TESTDLL = shcore.dll -IMPORTS = advapi32 +IMPORTS = advapi32 ole32 SOURCES = \ shcore.c diff --git a/dlls/shcore/tests/shcore.c b/dlls/shcore/tests/shcore.c index 539c2e5c1bc..09f837d68b0 100644 --- a/dlls/shcore/tests/shcore.c +++ b/dlls/shcore/tests/shcore.c @@ -23,10 +23,14 @@ #include <windows.h> #include "initguid.h" #include "objidl.h" +#include "objbase.h" #include "shlwapi.h" +#include "appmodel.h" #include "wine/test.h" +static HRESULT (WINAPI *pSetCurrentProcessExplicitAppUserModelID)(const WCHAR *); +static HRESULT (WINAPI *pGetCurrentProcessExplicitAppUserModelID)(const WCHAR **); static HRESULT (WINAPI *pGetProcessReference)(IUnknown **); static void (WINAPI *pSetProcessReference)(IUnknown *); static HRESULT (WINAPI *pSHGetInstanceExplorer)(IUnknown **); @@ -62,6 +66,8 @@ static const char * initial_buffer ="0123456789"; static void init(HMODULE hshcore) { #define X(f) p##f = (void*)GetProcAddress(hshcore, #f) + X(SetCurrentProcessExplicitAppUserModelID); + X(GetCurrentProcessExplicitAppUserModelID); X(GetProcessReference); X(SetProcessReference); X(SHUnicodeToAnsi); @@ -777,6 +783,64 @@ static void test_stream_size(void) DeleteFileA(filename); } +static void test_AppUserModelID(void) +{ + const WCHAR *appid = NULL; + HRESULT hr; + + if (!pSetCurrentProcessExplicitAppUserModelID || !pGetCurrentProcessExplicitAppUserModelID) + { + win_skip("AppUserModelID functions not available.\n"); + return; + } + + /* Empty string should fail */ + hr = pSetCurrentProcessExplicitAppUserModelID(L""); + ok(hr == E_INVALIDARG, "Got hr %#lx.\n", hr); + + /* String exceeding APPLICATION_USER_MODEL_ID_MAX_LENGTH should fail */ + { + WCHAR long_id[APPLICATION_USER_MODEL_ID_MAX_LENGTH + 10]; + memset(long_id, 'A', sizeof(long_id)); + long_id[APPLICATION_USER_MODEL_ID_MAX_LENGTH + 1] = 0; + hr = pSetCurrentProcessExplicitAppUserModelID(long_id); + ok(hr == E_INVALIDARG, "Got hr %#lx for too-long string.\n", hr); + + /* Exactly max length should succeed */ + long_id[APPLICATION_USER_MODEL_ID_MAX_LENGTH] = 0; + hr = pSetCurrentProcessExplicitAppUserModelID(long_id); + ok(hr == S_OK, "Got hr %#lx for max-length string.\n", hr); + } + + /* Set a valid ID */ + hr = pSetCurrentProcessExplicitAppUserModelID(L"Wine.Test.AppId"); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + + /* Get should return the ID we set */ + appid = NULL; + hr = pGetCurrentProcessExplicitAppUserModelID(&appid); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + ok(appid != NULL, "Expected non-NULL appid.\n"); + if (appid) + { + ok(!lstrcmpW(appid, L"Wine.Test.AppId"), "Got %s.\n", wine_dbgstr_w(appid)); + CoTaskMemFree((void *)appid); + } + + /* Set a different ID */ + hr = pSetCurrentProcessExplicitAppUserModelID(L"Wine.Test.AppId2"); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + + appid = NULL; + hr = pGetCurrentProcessExplicitAppUserModelID(&appid); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + if (appid) + { + ok(!lstrcmpW(appid, L"Wine.Test.AppId2"), "Got %s.\n", wine_dbgstr_w(appid)); + CoTaskMemFree((void *)appid); + } +} + START_TEST(shcore) { HMODULE hshcore = LoadLibraryA("shcore.dll"); @@ -789,6 +853,7 @@ START_TEST(shcore) init(hshcore); + test_AppUserModelID(); test_process_reference(); test_SHUnicodeToAnsi(); test_SHAnsiToUnicode(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10565
On Wed Apr 8 17:16:55 2026 +0000, Tim Clem wrote:
New stuff looks good to me, ~~but I do think you should use the constant APPLICATION_USER_MODEL_ID_MAX_LENGTH rather than a hardcoded 128. It's in appmodel.h~~ Scratch that, I see that that's not upstream. Perhaps worth adding defines for APPLICATION_USER_MODEL_ID_MAX_LENGTH and APPLICATION_USER_MODEL_ID_MIN_LENGTH to appmodel.h? Quick question on the Windows CI results:
Set(L"") and Set(129-char string) both returned S_OK on Win10 21H2 instead of E_INVALIDARG. Should I keep the validation or drop it to match native behavior? I also have a fix ready for the code quality issue (uninitialized \*appid when CoTaskMemAlloc fails). Will push it once I got info on the validation behavior. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10565#note_135524
On Wed Apr 8 17:16:55 2026 +0000, Jan Robert Gerigk wrote:
Quick question on the Windows CI results: Set(L"") and Set(129-char string) both returned S_OK on Win10 21H2 instead of E_INVALIDARG. Should I keep the validation or drop it to match native behavior? I also have a fix ready for the code quality issue (uninitialized \*appid when CoTaskMemAlloc fails). Will push it once I got info on the validation behavior. Oh, looks like it's supposed to be min 21 and max 130: https://learn.microsoft.com/en-us/windows/win32/appxpkg/identity-constants
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10565#note_135525
participants (3)
-
Jan Robert Gerigk (@RgSg86) -
Robert Gerigk -
Tim Clem (@tclem)