[PATCH v6 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. -- v6: shcore/tests: Add tests for Set/GetCurrentProcessExplicitAppUserModelID. shcore: Implement Set/GetCurrentProcessExplicitAppUserModelID. include: Add identity string 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, PACKAGE_RELATIVE_APPLICATION_ID, and all PACKAGE_* length constants from the Windows SDK. APPLICATION_USER_MODEL_ID and PACKAGE_RELATIVE_APPLICATION_ID lengths include space for the NULL terminator. PACKAGE_* lengths do not. Signed-off-by: Jan Robert Gerigk <Robert-Gerigk@online.de> --- include/appmodel.h | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/include/appmodel.h b/include/appmodel.h index 8c219e8080a..fba40d3f2ed 100644 --- a/include/appmodel.h +++ b/include/appmodel.h @@ -18,6 +18,46 @@ #ifndef _APPMODEL_H_ #define _APPMODEL_H_ +/* Application User Model ID (space included for NULL terminator) */ +#define APPLICATION_USER_MODEL_ID_MIN_LENGTH 21 +#define APPLICATION_USER_MODEL_ID_MAX_LENGTH 130 + +/* Package architecture (no space for NULL terminator) */ +#define PACKAGE_ARCHITECTURE_MIN_LENGTH 3 +#define PACKAGE_ARCHITECTURE_MAX_LENGTH 7 + +/* Package family name (no space for NULL terminator) */ +#define PACKAGE_FAMILY_NAME_MIN_LENGTH 17 +#define PACKAGE_FAMILY_NAME_MAX_LENGTH 64 + +/* Package full name (no space for NULL terminator) */ +#define PACKAGE_FULL_NAME_MIN_LENGTH 30 +#define PACKAGE_FULL_NAME_MAX_LENGTH 127 + +/* Package name (no space for NULL terminator) */ +#define PACKAGE_NAME_MIN_LENGTH 3 +#define PACKAGE_NAME_MAX_LENGTH 50 + +/* Package publisher ID (no space for NULL terminator) */ +#define PACKAGE_PUBLISHERID_MIN_LENGTH 13 +#define PACKAGE_PUBLISHERID_MAX_LENGTH 13 + +/* Package publisher (no space for NULL terminator) */ +#define PACKAGE_PUBLISHER_MIN_LENGTH 4 +#define PACKAGE_PUBLISHER_MAX_LENGTH 8192 + +/* Package relative application ID (space included for NULL terminator) */ +#define PACKAGE_RELATIVE_APPLICATION_ID_MIN_LENGTH 2 +#define PACKAGE_RELATIVE_APPLICATION_ID_MAX_LENGTH 65 + +/* Package resource ID (no space for NULL terminator) */ +#define PACKAGE_RESOURCEID_MIN_LENGTH 0 +#define PACKAGE_RESOURCEID_MAX_LENGTH 30 + +/* Package version (no space for NULL terminator) */ +#define PACKAGE_VERSION_MIN_LENGTH 7 +#define PACKAGE_VERSION_MAX_LENGTH 23 + #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 and strings at or 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 | 51 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/dlls/shcore/main.c b/dlls/shcore/main.c index f04853cb9f5..5811298987a 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,61 @@ 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) + 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); + TRACE("%p\n", appid); + + if (!appid) return E_INVALIDARG; + *appid = NULL; - return E_NOTIMPL; + + 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); + } + 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 max length validation (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 | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 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..df99a6180df 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,62 @@ 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; + } + + /* String at APPLICATION_USER_MODEL_ID_MAX_LENGTH (including NULL) should fail */ + { + WCHAR long_id[APPLICATION_USER_MODEL_ID_MAX_LENGTH + 10]; + memset(long_id, 'A', sizeof(long_id)); + + /* Exactly max length (130 chars = 129 + NULL) — should fail */ + long_id[APPLICATION_USER_MODEL_ID_MAX_LENGTH] = 0; + hr = pSetCurrentProcessExplicitAppUserModelID(long_id); + ok(hr == E_INVALIDARG, "Got hr %#lx for too-long string.\n", hr); + + /* One less (129 chars = 128 + NULL) — should succeed */ + long_id[APPLICATION_USER_MODEL_ID_MAX_LENGTH - 1] = 0; + hr = pSetCurrentProcessExplicitAppUserModelID(long_id); + ok(hr == S_OK, "Got hr %#lx for max-1 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 +851,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:19:03 2026 +0000, Tim Clem wrote:
Oh, looks like it's supposed to be min 21 and max 130: https://learn.microsoft.com/en-us/windows/win32/appxpkg/identity-constants Updated with your feedback:
- Fixed APPLICATION_USER_MODEL_ID_MAX_LENGTH to 130 and MIN_LENGTH to 21 - Added all other identity constants from the same page to appmodel.h - Max length check now uses \>= 130 (string length including NULL terminator) - Dropped empty string rejection since native does not enforce minimum length - Fixed code quality probleme -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10565#note_135534
On Wed Apr 8 19:02:47 2026 +0000, Jan Robert Gerigk wrote:
Updated with your feedback: - Fixed APPLICATION_USER_MODEL_ID_MAX_LENGTH to 130 and MIN_LENGTH to 21 - Added all other identity constants from the same page to appmodel.h - Max length check now uses \>= 130 (string length including NULL terminator) - Dropped empty string rejection since native does not enforce minimum length - Fixed code quality probleme Huh, E_INVALIDARG for APPLICATION_USER_MODEL_ID_MAX_LENGTH - 1 on native. I'm honestly not sure what's up with that.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10565#note_135536
On Wed Apr 8 19:21:01 2026 +0000, Tim Clem wrote:
Huh, E_INVALIDARG for APPLICATION_USER_MODEL_ID_MAX_LENGTH - 1 on native. I'm honestly not sure what's up with that. The documentation says MAX_LENGTH = 130 with "space included for NULL terminator", which I initially read as 129 chars + NULL = 130. But native rejects 129 and accepts 128.
Is the correct interpretation that the constant includes additional padding beyond the NULL terminator (128 chars + NULL + 1 reserved = 130)? Or is the documentation simply off by one? I am really not sure on this point, I have no idea how to read that right. Anyway, back to 128? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10565#note_135546
On Wed Apr 8 19:41:00 2026 +0000, Jan Robert Gerigk wrote:
The documentation says MAX_LENGTH = 130 with "space included for NULL terminator", which I initially read as 129 chars + NULL = 130. But native rejects 129 and accepts 128. Is the correct interpretation that the constant includes additional padding beyond the NULL terminator (128 chars + NULL + 1 reserved = 130)? Or is the documentation simply off by one? I am really not sure on this point, I have no idea how to read that right. Anyway, back to 128? Well.. we definitely want the constants in the header to match Windows... Honestly maybe it's just not worth worrying about the length check. It's a very unlikely scenario that it would matter anyway; it's not like that string is going to be dynamically generated. Sorry this turned out to be so complicated; I assumed it'd just be a simple check.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10565#note_135553
On Wed Apr 8 19:57:21 2026 +0000, Tim Clem wrote:
Well.. we definitely want the constants in the header to match Windows... Honestly maybe it's just not worth worrying about the length check. It's a very unlikely scenario that it would matter anyway; it's not like that string is going to be dynamically generated. Sorry this turned out to be so complicated; I assumed it'd just be a simple check. I will keep the length check at \> 128 since it matches the CI results. Pushing the update now with a new code quality fix too.
And thanks for your patience and Feedback, I only started looking into Wine a few months ago as part of a project to get KNX ETS (a building automation software) running on Linux, and I have only been contributing upstream for a few weeks now. My understanding of Wines internals is still very surface-level, so I really appreciate the feedback. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10565#note_135556
participants (3)
-
Jan Robert Gerigk (@RgSg86) -
Robert Gerigk -
Tim Clem (@tclem)