@@ -466,27 +466,6 @@ namespace
466466 return PathJoin (GetProductRegistryPath (), GetMajorVersionString (), strPath).TrimEnd (" \\ " );
467467 }
468468
469- bool IsTemporaryUpdateLaunchPath (const SString& strLaunchPath)
470- {
471- if (strLaunchPath.empty ())
472- return false ;
473-
474- if (!strLaunchPath.ContainsI (" \\ upcache\\ " ))
475- return false ;
476-
477- return ExtractFilename (strLaunchPath).ContainsI (" _tmp_" );
478- }
479-
480- bool IsUsableMtasaInstallRoot (const SString& strPath)
481- {
482- if (strPath.empty ())
483- return false ;
484-
485- return FileExists (PathJoin (strPath, " Multi Theft Auto.exe" )) || FileExists (PathJoin (strPath, " Multi Theft Auto_d.exe" )) ||
486- FileExists (PathJoin (strPath, " mta" , " core.dll" )) || FileExists (PathJoin (strPath, " MTA" , " core.dll" )) ||
487- FileExists (PathJoin (strPath, " mta" , " core_d.dll" )) || FileExists (PathJoin (strPath, " MTA" , " core_d.dll" ));
488- }
489-
490469 bool HasDistinct64BitRegistryView ()
491470 {
492471#if defined(_WIN64)
@@ -580,6 +559,62 @@ namespace
580559 static_cast <DWORD>((wstrValue.length () + 1 ) * sizeof (wchar_t )));
581560 RegCloseKey (hKey);
582561 }
562+
563+ // Read Last Run Location from a specific registry view. viewFlag should be one of
564+ // KEY_WOW64_64KEY, KEY_WOW64_32KEY, or 0 (no view override). Returns empty string on any failure.
565+ // Oversized values are rejected without allocation so a malformed registry entry on the U01
566+ // recovery path cannot turn into a large allocation; the same 1 MB cap is used by the generic
567+ // ReadRegistryStringValue helper in SharedUtil.Misc.hpp.
568+ SString ReadInstallRootRegistryView (REGSAM viewFlag)
569+ {
570+ constexpr DWORD kMaxRegistryValueBytes = 1024u * 1024u ;
571+
572+ const WString wstrSubKey = FromUTF8 (MakeCurrentVersionRegistryPath (" " ));
573+ const WString wstrValueName = FromUTF8 (" Last Run Location" );
574+
575+ HKEY hKey = nullptr ;
576+ if (RegOpenKeyExW (HKEY_LOCAL_MACHINE, wstrSubKey.c_str (), 0 , KEY_READ | viewFlag, &hKey) != ERROR_SUCCESS || !hKey)
577+ return " " ;
578+
579+ DWORD valueType = REG_SZ;
580+ DWORD valueSize = 0 ;
581+ LONG result = RegQueryValueExW (hKey, wstrValueName.c_str (), nullptr , &valueType, nullptr , &valueSize);
582+ if (result != ERROR_SUCCESS || (valueType != REG_SZ && valueType != REG_EXPAND_SZ) || valueSize == 0 || valueSize > kMaxRegistryValueBytes )
583+ {
584+ RegCloseKey (hKey);
585+ return " " ;
586+ }
587+
588+ std::vector<wchar_t > buffer ((valueSize / sizeof (wchar_t )) + 1u , L' \0 ' );
589+ result = RegQueryValueExW (hKey, wstrValueName.c_str (), nullptr , &valueType, reinterpret_cast <LPBYTE>(buffer.data ()), &valueSize);
590+ RegCloseKey (hKey);
591+
592+ if (result != ERROR_SUCCESS || (valueType != REG_SZ && valueType != REG_EXPAND_SZ) || valueSize > kMaxRegistryValueBytes )
593+ return " " ;
594+
595+ if (valueSize >= sizeof (wchar_t ))
596+ buffer[(valueSize / sizeof (wchar_t )) - 1u ] = L' \0 ' ;
597+ else
598+ buffer[0 ] = L' \0 ' ;
599+
600+ // Expand environment variable references for REG_EXPAND_SZ values so callers see a real
601+ // filesystem path. Without this, a value like "%ProgramFiles%\..." would fail validation.
602+ // The expansion is bounded by the same 1 MB cap so a value containing a self-referential or
603+ // explosive expansion cannot trigger a large allocation here.
604+ if (valueType == REG_EXPAND_SZ)
605+ {
606+ constexpr DWORD kMaxExpandChars = kMaxRegistryValueBytes / sizeof (wchar_t );
607+ const DWORD expandedChars = ExpandEnvironmentStringsW (buffer.data (), nullptr , 0 );
608+ if (expandedChars > 0 && expandedChars <= kMaxExpandChars )
609+ {
610+ std::vector<wchar_t > expanded (expandedChars, L' \0 ' );
611+ if (ExpandEnvironmentStringsW (buffer.data (), expanded.data (), expandedChars))
612+ return ToUTF8 (expanded.data ());
613+ }
614+ }
615+
616+ return ToUTF8 (buffer.data ());
617+ }
583618}
584619
585620SString GetInstallPathForLauncher ()
@@ -588,15 +623,63 @@ SString GetInstallPathForLauncher()
588623 if (!IsTemporaryUpdateLaunchPath (strLaunchPath))
589624 return strLaunchPath;
590625
591- const SString strSavedInstallPath = GetRegistryValue (" " , " Last Run Location" );
592- if (IsUsableMtasaInstallRoot (strSavedInstallPath) && !IsTemporaryUpdateLaunchPath (strSavedInstallPath))
593- return strSavedInstallPath;
626+ // Prefer the resolved base dir over the registry when one was already established. In the
627+ // far-update Process C, CInstallManager applies SetMTASABaseDirOverride from the sequencer
628+ // carried INSTALL_ROOT and runs ::SetMTASAPathSource(true) before any consumer reaches here,
629+ // so g_strMTASAPath holds the validated real install root that Process B was updating. A stale
630+ // or differently-installed registry "Last Run Location" must not win over that carried root and
631+ // aim consumers at a different MTA install. Read g_strMTASAPath directly rather than calling
632+ // GetMTASAPath(), because GetMTASAPath() lazy-inits via SetMTASAPathSource(false), which calls
633+ // back into GetInstallPathForLauncher() and would recurse for any temp launcher whose g_strMTASAPath
634+ // has not yet been populated by the far-update sequencer path. Empty string falls through to the
635+ // per-view registry loop below, preserving the temp-launcher-without-far-update fallback.
636+ if (!g_strMTASAPath.empty () && IsUsableMtasaInstallRoot (g_strMTASAPath) && !IsTemporaryUpdateLaunchPath (g_strMTASAPath) &&
637+ !g_strMTASAPath.CompareI (strLaunchPath))
638+ return g_strMTASAPath;
639+
640+ // Read each registry view independently so a stale value in one view cannot shadow a usable value in another.
641+ REGSAM viewFlags[3 ] = {0 , 0 , 0 };
642+ int viewCount = 0 ;
643+ #if defined(KEY_WOW64_64KEY)
644+ viewFlags[viewCount++] = KEY_WOW64_64KEY;
645+ #endif
646+ #if defined(KEY_WOW64_32KEY)
647+ viewFlags[viewCount++] = KEY_WOW64_32KEY;
648+ #endif
649+ viewFlags[viewCount++] = 0 ;
650+
651+ for (int i = 0 ; i < viewCount; ++i)
652+ {
653+ const SString strSavedInstallPath = ReadInstallRootRegistryView (viewFlags[i]);
654+ if (IsUsableMtasaInstallRoot (strSavedInstallPath) && !IsTemporaryUpdateLaunchPath (strSavedInstallPath))
655+ return strSavedInstallPath;
656+ }
594657
595- const SString strSavedInstallPath64 = GetRegistryValue64 (" " , " Last Run Location" );
596- if (IsUsableMtasaInstallRoot (strSavedInstallPath64) && !IsTemporaryUpdateLaunchPath (strSavedInstallPath64))
597- return strSavedInstallPath64;
658+ // No usable non-temp install root resolved. Returning empty forces callers to use their own fallbacks
659+ // (such as a base-dir override carried forward from the parent launcher) instead of treating a temp
660+ // update directory as the real install location.
661+ return SString ();
662+ }
598663
599- return strLaunchPath;
664+ bool IsUsableMtasaInstallRoot (const SString& strPath)
665+ {
666+ if (strPath.empty ())
667+ return false ;
668+
669+ return FileExists (PathJoin (strPath, " Multi Theft Auto.exe" )) || FileExists (PathJoin (strPath, " Multi Theft Auto_d.exe" )) ||
670+ FileExists (PathJoin (strPath, " mta" , " core.dll" )) || FileExists (PathJoin (strPath, " MTA" , " core.dll" )) ||
671+ FileExists (PathJoin (strPath, " mta" , " core_d.dll" )) || FileExists (PathJoin (strPath, " MTA" , " core_d.dll" ));
672+ }
673+
674+ bool IsTemporaryUpdateLaunchPath (const SString& strLaunchPath)
675+ {
676+ if (strLaunchPath.empty ())
677+ return false ;
678+
679+ if (!strLaunchPath.ContainsI (" \\ upcache\\ " ))
680+ return false ;
681+
682+ return ExtractFilename (strLaunchPath).ContainsI (" _tmp_" );
600683}
601684
602685void SetMTASAPathSource (bool bReadFromRegistry)
@@ -612,6 +695,14 @@ void SetMTASAPathSource(bool bReadFromRegistry)
612695 SString strLaunchPath = GetLaunchPath ();
613696 SString strInstallPath = GetInstallPathForLauncher ();
614697
698+ // GetInstallPathForLauncher returns empty when running from a temp update directory and no
699+ // usable non-temp install root could be resolved. In that case the safest local fallback is
700+ // the launch path itself, which preserves the prior behavior for non-update launches without
701+ // contaminating the registry from the auto-update flow (Process C now goes through the far
702+ // branch with a base-dir override carried forward by CInstallManager).
703+ if (strInstallPath.empty ())
704+ strInstallPath = strLaunchPath;
705+
615706 if (!strInstallPath.CompareI (strLaunchPath))
616707 {
617708 AddReportLog (1063 , SString (" SetMTASAPathSource: preserving install path '%s' for temp launcher '%s'" , strInstallPath.c_str (),
@@ -620,6 +711,17 @@ void SetMTASAPathSource(bool bReadFromRegistry)
620711 return ;
621712 }
622713
714+ // Refuse to write a temp update extraction directory into the install-location registry
715+ // values. A temp launcher started without the far-update sequencer state would otherwise
716+ // contaminate Last Run Location with an upcache\_*_tmp_* path that gets deleted later by
717+ // CleanDownloadCache, leaving a stale registry pointer that produces U01 on the next launch.
718+ if (IsTemporaryUpdateLaunchPath (strLaunchPath))
719+ {
720+ AddReportLog (1063 , SString (" SetMTASAPathSource: refusing to record temp launch path '%s' in registry" , strLaunchPath.c_str ()));
721+ g_strMTASAPath = strLaunchPath;
722+ return ;
723+ }
724+
623725 SString strHash = " -" ;
624726 {
625727 MD5 md5;
@@ -1292,6 +1394,20 @@ void UpdateMTAVersionApplicationSetting(bool bQuiet)
12921394 bFreeModule = hModule != NULL ;
12931395 }
12941396
1397+ // GetInstallPathForLauncher returns empty when running from a temp update directory and no
1398+ // usable non-temp install root could be resolved. GetMTASAPath consults SetMTASABaseDirOverride
1399+ // (which the install manager populates from the sequencer-carried INSTALL_ROOT in the far branch),
1400+ // so this attempt covers the recovered far-update case before falling back to the launch dir.
1401+ if (!hModule)
1402+ {
1403+ const SString strBaseDirPath = GetMTASAPath ();
1404+ if (IsUsableMtasaInstallRoot (strBaseDirPath) && !strBaseDirPath.CompareI (strInstallPath))
1405+ {
1406+ hModule = LoadVersionModule (strBaseDirPath, dwLastError);
1407+ bFreeModule = hModule != NULL ;
1408+ }
1409+ }
1410+
12951411 if (!hModule)
12961412 {
12971413 hModule = LoadVersionModule (GetLaunchPath (), dwLastError);
0 commit comments