fix(browser): real-profile detects the Windows 11 25H2 default browser

Windows 11 25H2 Settings records the default browser under
UrlAssociations\https\UserChoiceLatest\ProgId and stops updating the legacy
UserChoice key, which UCPD.sys also write-protects. Reading only the legacy
ProgId made real-profile browsing refuse a correctly configured Edge/Chrome
default ("not a supported Chromium browser"), with WinError 2 when the key
exists without a value, or picked a stale browser when it still held one.

Resolve the effective ProgId via AssocQueryStringW(ASSOCSTR_PROGID) first,
then fall back to UserChoiceLatest\ProgId, then the legacy UserChoice value.
Classification (channels fail closed) is unchanged.

Salvages #106971 (Shell API lookup); the UserChoiceLatest fallback follows
#108064 and the reports on #107854.

Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>
This commit is contained in:
Teknium
2026-10-07 01:25:25 -07:00
parent 3e5b43e0bb
commit 5419d869bb
2 changed files with 82 additions and 114 deletions

View File

@@ -233,89 +233,53 @@ def _classify_default(value: str, channels, table, match) -> str | None:
return next((browser for frag, browser in table if match(value, frag)), None)
_WINDOWS_HTTPS_ASSOC = r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https"
def _windows_shell_progid(scheme: str) -> str | None:
"""Return the effective ProgId for a URL scheme via the Windows Shell API."""
"""Effective ProgId for a URL scheme as the Windows Shell resolves it."""
try:
import ctypes
from ctypes import wintypes
# ASSOCSTR_PROGID from shlwapi.h.
assocstr_progid = 20
assoc_query = ctypes.WinDLL(
"Shlwapi.dll",
use_last_error=True,
).AssocQueryStringW
assoc_query.argtypes = [
wintypes.DWORD,
wintypes.DWORD,
wintypes.LPCWSTR,
wintypes.LPCWSTR,
wintypes.LPWSTR,
ctypes.POINTER(wintypes.DWORD),
]
assoc_query.restype = ctypes.c_long
length = wintypes.DWORD(0)
# First call obtains the required output buffer size.
assoc_query(
0,
assocstr_progid,
scheme,
None,
None,
ctypes.byref(length),
)
if not length.value:
query = ctypes.WinDLL("shlwapi", use_last_error=True).AssocQueryStringW
query.argtypes = [wintypes.DWORD, wintypes.DWORD, wintypes.LPCWSTR, wintypes.LPCWSTR,
wintypes.LPWSTR, ctypes.POINTER(wintypes.DWORD)]
query.restype = ctypes.c_long
buf = ctypes.create_unicode_buffer(256)
size = wintypes.DWORD(len(buf))
# 20 = ASSOCSTR_PROGID. ASSOCSTR_EXECUTABLE can fail (0x80070483) on 25H2.
if query(0, 20, scheme, None, buf, ctypes.byref(size)) != 0:
return None
buffer = ctypes.create_unicode_buffer(length.value)
result = assoc_query(
0,
assocstr_progid,
scheme,
None,
buffer,
ctypes.byref(length),
)
return buffer.value if result == 0 else None
except (AttributeError, OSError):
return buf.value or None
except (AttributeError, OSError): # non-Windows host
return None
def _detect_default_windows() -> str | None:
# Prefer the effective association reported by the Windows Shell.
# Modern Windows builds may no longer expose it through the legacy
# UserChoice\ProgId registry value.
prog_id = _windows_shell_progid("https")
# Compatibility fallback for older Windows versions/environments.
if not prog_id:
def _windows_registry_progid() -> str | None:
"""Registry fallback. Windows 11 25H2 Settings writes only ``UserChoiceLatest\\ProgId``;
the legacy ``UserChoice`` key can be stale, missing, or present without a value."""
try:
import winreg # type: ignore
except ImportError:
return None
for sub in (r"UserChoiceLatest\ProgId", "UserChoice"):
try:
import winreg # type: ignore
with winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\Shell\Associations"
r"\UrlAssociations\https\UserChoice",
) as key:
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, rf"{_WINDOWS_HTTPS_ASSOC}\{sub}") as key:
prog_id, _ = winreg.QueryValueEx(key, "ProgId")
except OSError:
continue
if prog_id:
return str(prog_id)
return None
except (ImportError, OSError):
return None
return _classify_default(
str(prog_id or "").lower(),
_WINDOWS_CHANNEL_PROGIDS,
_WINDOWS_PROGID_MAP,
str.startswith,
)
def _detect_default_windows() -> str | None:
prog_id = _windows_shell_progid("https") or _windows_registry_progid()
if not prog_id:
return None
return _classify_default(prog_id.lower(), _WINDOWS_CHANNEL_PROGIDS,
_WINDOWS_PROGID_MAP, str.startswith)
def _run_stdout(argv: list[str]) -> str | None:

View File

@@ -59,53 +59,57 @@ class TestLaunchServicesHttpsHandler:
assert bc._launchservices_https_handler(dump) == "com.microsoft.edgemac"
class _FakeKey(str):
def __enter__(self):
return self
def __exit__(self, *_exc):
return False
class _FakeWinreg:
"""``winreg`` stand-in: ``values`` maps an https association subkey to its ProgId value;
a subkey mapped to None exists but has no ProgId (``QueryValueEx`` raises WinError 2)."""
HKEY_CURRENT_USER = 0
def __init__(self, values):
self.values = values
def OpenKey(self, _root, path):
sub = path.split("\\https\\", 1)[1]
if sub not in self.values:
raise FileNotFoundError(path)
return _FakeKey(sub)
def QueryValueEx(self, sub, _name):
if self.values[sub] is None:
raise FileNotFoundError(2, "The system cannot find the file specified")
return self.values[sub], 1
def CloseKey(self, _key):
pass
class TestDetectDefaultWindows:
@pytest.mark.parametrize(
"prog_id,expected",
[
("ChromeHTML", "chrome"),
("ChromeHTML.123", "chrome"),
("MSEdgeHTM", "edge"),
("BraveHTML", "brave"),
("BraveOHTML", "brave-origin"),
("ChromiumHTM", "chromium"),
("ChromeBHTML", bc.UNSUPPORTED_CHANNEL),
("FirefoxURL", None),
],
)
def test_shell_progid_maps_to_browser(self, prog_id, expected):
with patch.object(bc, "_windows_shell_progid", return_value=prog_id):
assert bc._detect_default_windows() == expected
def test_shell_progid_takes_precedence_over_legacy_registry(self):
with patch.object(bc, "_windows_shell_progid", return_value="ChromeHTML"):
def test_shell_association_wins_over_stale_legacy_userchoice(self):
stale = _FakeWinreg({"UserChoice": "VivaldiHTM.ABC"})
with patch.object(bc, "_windows_shell_progid", return_value="ChromeHTML", create=True), \
patch.dict("sys.modules", {"winreg": stale}):
assert bc._detect_default_windows() == "chrome"
with patch.object(bc, "_windows_shell_progid", return_value="ChromeBHTML", create=True):
assert bc._detect_default_windows() == bc.UNSUPPORTED_CHANNEL
def test_shell_miss_falls_back_to_legacy_userchoice(self):
class FakeKey:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
return False
class FakeWinreg:
HKEY_CURRENT_USER = object()
@staticmethod
def OpenKey(*args, **kwargs):
return FakeKey()
@staticmethod
def QueryValueEx(key, name):
assert name == "ProgId"
return "MSEdgeHTM", 1
with (
patch.object(bc, "_windows_shell_progid", return_value=None),
patch.dict("sys.modules", {"winreg": FakeWinreg()}),
):
assert bc._detect_default_windows() == "edge"
@pytest.mark.parametrize("values,expected", [
# 25H2: Settings writes only UserChoiceLatest\ProgId; legacy key exists without a value.
({"UserChoiceLatest\\ProgId": "MSEdgeHTM", "UserChoice": None}, "edge"),
({"UserChoiceLatest\\ProgId": "ChromeHTML", "UserChoice": "MSEdgeHTM"}, "chrome"),
({"UserChoice": "BraveHTML"}, "brave"), # pre-25H2
({"UserChoice": "FirefoxURL-308046B0AF4A39CB"}, None),
])
def test_registry_fallback_prefers_userchoicelatest(self, values, expected):
with patch.object(bc, "_windows_shell_progid", return_value=None, create=True), \
patch.dict("sys.modules", {"winreg": _FakeWinreg(values)}):
assert bc._detect_default_windows() == expected
class TestDetectDefaultDarwin: