From 08132a36116d82d5367d10f717f183d75615c08d Mon Sep 17 00:00:00 2001 From: shuaiyuan Date: Mon, 22 Jun 2026 13:01:34 +0800 Subject: [PATCH] Fix wslg#1009: sanitize .lnk/.ico filename leaf An app whose Name= contains a Windows-reserved filename character (e.g. "Code::Blocks") produced an illegal .lnk path; IPersistFile::Save failed and the error was swallowed, so the app was silently missing from the Start Menu with no diagnostic. Add SanitizeFileName() and apply it to the filename leaves derived from appDesc (.lnk) and appId (.ico). appDesc is kept intact for SetDescription. Design: - Reserved chars (\ / : * ? " < > |) and control chars map to '_'. This is predictable and renders in any font, unlike look-alike glyph substitution. - Reserved DOS device names (CON, PRN, AUX, NUL, COM1..9, LPT1..9) are illegal as a filename base even with an extension (e.g. "NUL.txt"); Windows keys this on the token before the first '.', so prepend '_' to neutralize the base rather than relying on the hash suffix. - A legal name is left byte-for-byte unchanged (no suffix), so the common case is identical to today. - '_' substitution can map distinct names ("A:B" and "A/B") onto the same leaf; to avoid two apps' files overwriting each other, append a short stable FNV-1a hash of the app's unique id (~XXXXXXXX) whenever the name was altered. The hash is derived from appId, so it is unique per app and deterministic across syncs. The FileDB key remains appGroup\appId, so add/delete/sync bookkeeping is unaffected. Trailing space/dot (illegal on Win32) are trimmed. Co-Authored-By: Claude Opus 4.8 (1M context) --- WSLDVCPlugin/WSLDVCCallback.cpp | 28 ++++++++-- WSLDVCPlugin/utils.cpp | 95 +++++++++++++++++++++++++++++++++ WSLDVCPlugin/utils.h | 8 +++ 3 files changed, 126 insertions(+), 5 deletions(-) diff --git a/WSLDVCPlugin/WSLDVCCallback.cpp b/WSLDVCPlugin/WSLDVCCallback.cpp index b10132e6..b1536376 100644 --- a/WSLDVCPlugin/WSLDVCCallback.cpp +++ b/WSLDVCPlugin/WSLDVCCallback.cpp @@ -652,8 +652,17 @@ class WSLDVCCallback : } } } + // Sanitize the appId-derived leaf for the same reason as the .lnk + // leaf below: a reserved char (e.g. ':') yields an illegal path and + // CreateIconFile/IPersistFile::Save fails (wslg#1009). + WCHAR iconLeaf[MAX_PATH] = {}; + if (wcscpy_s(iconLeaf, ARRAYSIZE(iconLeaf), updateAppList.appId) != 0) + { + return E_FAIL; + } + SanitizeFileName(iconLeaf, ARRAYSIZE(iconLeaf), updateAppList.appId); if ((wcscat_s(iconPath, ARRAYSIZE(iconPath), L"\\") != 0) || - (wcscat_s(iconPath, ARRAYSIZE(iconPath), updateAppList.appId) != 0) || + (wcscat_s(iconPath, ARRAYSIZE(iconPath), iconLeaf) != 0) || (wcscat_s(iconPath, ARRAYSIZE(iconPath), L".ico") != 0)) { return E_FAIL; @@ -688,11 +697,20 @@ class WSLDVCCallback : } } } - // Use description to name link file since this is name shows up - // at StartMenu UI. SHSetLocalizedName can't be uses since this - // is not in resource. + // Use the description to name the link file since this is the name shown + // at the StartMenu UI. SHSetLocalizedName can't be used since this + // is not in a resource. + // Sanitize only the filename leaf (appDesc is kept intact for + // SetDescription); names like "Code::Blocks" otherwise produce an + // illegal .lnk path and the shortcut is silently dropped (wslg#1009). + WCHAR linkLeaf[MAX_PATH] = {}; + if (wcscpy_s(linkLeaf, ARRAYSIZE(linkLeaf), updateAppList.appDesc) != 0) + { + return E_FAIL; + } + SanitizeFileName(linkLeaf, ARRAYSIZE(linkLeaf), updateAppList.appId); if ((wcscat_s(linkPath, ARRAYSIZE(linkPath), L"\\") != 0) || - (wcscat_s(linkPath, ARRAYSIZE(linkPath), updateAppList.appDesc) != 0) || + (wcscat_s(linkPath, ARRAYSIZE(linkPath), linkLeaf) != 0) || (wcscat_s(linkPath, ARRAYSIZE(linkPath), L".lnk") != 0)) { return E_FAIL; diff --git a/WSLDVCPlugin/utils.cpp b/WSLDVCPlugin/utils.cpp index 9d73d3a3..2d100f17 100644 --- a/WSLDVCPlugin/utils.cpp +++ b/WSLDVCPlugin/utils.cpp @@ -17,6 +17,101 @@ void DebugPrint(const wchar_t* format, ...) } #endif // DBG_MESSAGE +// True if the filename base (the token before the first '.') is a reserved DOS +// device name. These are illegal as a filename even with an extension, e.g. +// "NUL.txt", and contain no reserved character, so they must be checked separately. +static bool IsReservedDeviceName(LPCWSTR name) +{ + size_t base = 0; + while (name[base] && name[base] != L'.') + ++base; + + if (base == 3 && + (_wcsnicmp(name, L"CON", 3) == 0 || _wcsnicmp(name, L"PRN", 3) == 0 || + _wcsnicmp(name, L"AUX", 3) == 0 || _wcsnicmp(name, L"NUL", 3) == 0)) + return true; + + if (base == 4 && + (_wcsnicmp(name, L"COM", 3) == 0 || _wcsnicmp(name, L"LPT", 3) == 0) && + name[3] >= L'1' && name[3] <= L'9') + return true; + + return false; +} + +_Use_decl_annotations_ +void SanitizeFileName(LPWSTR name, size_t cch, LPCWSTR uniqueId) +{ + // Replace characters that are illegal in a Windows filename component with '_'. + // The Start-Menu tile text is this filename (SetDescription only sets the + // tooltip), so the displayed name changes for such apps -- acceptable and + // predictable, and it renders in any font (unlike look-alike glyph substitution). + bool changed = false; + for (LPWSTR p = name; *p; ++p) + { + switch (*p) + { + case L'\\': case L'/': case L':': case L'*': + case L'?': case L'"': case L'<': case L'>': case L'|': + *p = L'_'; changed = true; break; + default: + if (*p < 0x20) { *p = L'_'; changed = true; } + break; + } + } + // Win32: a trailing space or dot in a filename component is illegal. + size_t n = wcslen(name); + while (n > 0 && (name[n - 1] == L' ' || name[n - 1] == L'.')) + { + name[--n] = L'\0'; + changed = true; + } + + // A reserved DOS device name (CON, PRN, AUX, NUL, COM1..9, LPT1..9) is illegal + // as a filename base on its own. Windows keys this on the token before the first + // '.', so a trailing hash suffix wouldn't help "NUL.txt" -- prepend '_' to + // neutralize the base instead. + if (IsReservedDeviceName(name)) + { + if (n + 1 < cch) + { + memmove(name + 1, name, (n + 1) * sizeof(WCHAR)); + name[0] = L'_'; + ++n; + } + changed = true; + } + + // If nothing was altered the leaf is already a valid, unambiguous filename -- + // keep it verbatim (the common case, byte-for-byte as before). + if (!changed) + return; + + // '_' substitution can map distinct names (e.g. "A:B" and "A/B") onto the same + // leaf, which would make two apps' .lnk/.ico files overwrite each other. Append + // a short, stable hash of the app's unique id so a sanitized leaf is always + // unique per app and deterministic across syncs. (wslg#1009) + UINT32 h = 2166136261u; // FNV-1a (32-bit) + for (LPCWSTR q = uniqueId; q && *q; ++q) + { + h ^= (UINT16)*q; + h *= 16777619u; + } + WCHAR suffix[10]; // "~" + 8 hex digits + NUL + swprintf_s(suffix, ARRAYSIZE(suffix), L"~%08X", h); + + // Ensure leaf + suffix fits in cch; truncate the leaf if needed. + size_t suffixLen = wcslen(suffix); + if (n + suffixLen + 1 > cch) + { + n = (cch > suffixLen + 1) ? (cch - suffixLen - 1) : 0; + name[n] = L'\0'; + while (n > 0 && (name[n - 1] == L' ' || name[n - 1] == L'.')) + name[--n] = L'\0'; + } + wcscat_s(name, cch, suffix); +} + _Use_decl_annotations_ BOOL IsDirectoryPresent(LPCWSTR lpszPath) { diff --git a/WSLDVCPlugin/utils.h b/WSLDVCPlugin/utils.h index b013ac57..b473a5d2 100644 --- a/WSLDVCPlugin/utils.h +++ b/WSLDVCPlugin/utils.h @@ -20,6 +20,14 @@ void DebugPrint(const wchar_t* format, ...); BOOL IsDirectoryPresent(_In_z_ LPCWSTR lpszPath); +// Make a string safe to use as a Windows filename component, in place: replace +// reserved/control characters with '_' and trim trailing space/dot. If anything +// was changed, append a short stable hash of uniqueId so distinct names that +// sanitize to the same leaf don't collide on disk. Used to derive a safe .lnk/.ico +// leaf from app names that may contain ':' '/' etc. (wslg#1009). +void +SanitizeFileName(_Inout_updates_z_(cch) LPWSTR name, size_t cch, _In_z_ LPCWSTR uniqueId); + HRESULT CreateShellLink(_In_z_ LPCWSTR lpszPathLink, _In_z_ LPCWSTR lpszPathObj,