@@ -48,17 +48,30 @@ function powershellValue(expression: string): string {
4848 "-NoLogo" ,
4949 "-NoProfile" ,
5050 "-NonInteractive" ,
51- "-Command" ,
52- expression ,
51+ "-EncodedCommand" ,
52+ Buffer . from (
53+ "$ErrorActionPreference = 'Stop'; [Console]::OutputEncoding = [System.Text.Encoding]::UTF8;\n" + expression ,
54+ "utf16le" ,
55+ ) . toString ( "base64" ) ,
5356 ] , {
5457 stdin : "ignore" ,
5558 stdout : "pipe" ,
5659 stderr : "pipe" ,
60+ timeout : 10_000 ,
5761 } ) ;
5862 } catch ( cause ) {
5963 refuse ( "Windows effective-account lookup could not start." , cause ) ;
6064 }
61- if ( result . exitCode !== 0 ) refuse ( "Windows effective-account lookup failed." ) ;
65+ if ( result . exitCode !== 0 ) {
66+ // Only this fixed diagnostic grammar may cross the subprocess boundary.
67+ // Compiler messages and native errors can contain paths or account details.
68+ const diagnostic = new TextDecoder ( ) . decode ( result . stderr ) . match (
69+ / ^ C C X _ I D E N T I T Y _ F A I L U R E : ( c o m p i l e | t o k e n - e n v i r o n m e n t | r e g i s t e r e d - f o l d e r ) : ( [ A - Z a - z 0 - 9 _ . ] { 1 , 80 } ) : ( [ 0 - 9 A - F ] { 8 } ) \r ? $ / m,
70+ ) ;
71+ refuse ( diagnostic
72+ ? `Windows effective-account lookup failed (${ diagnostic [ 1 ] } , ${ diagnostic [ 2 ] } , HRESULT 0x${ diagnostic [ 3 ] } ).`
73+ : `Windows effective-account lookup failed (exit ${ Number . isSafeInteger ( result . exitCode ) ? result . exitCode : "unknown" } ).` ) ;
74+ }
6275 const value = new TextDecoder ( ) . decode ( result . stdout ) . trim ( ) ;
6376 if ( ! value ) refuse ( "Windows effective-account lookup returned an empty value." ) ;
6477 return value ;
@@ -143,12 +156,94 @@ function resolvePosixRuntimeRoot(uid: number): string {
143156
144157function resolveWindowsRuntimeRoot ( identity : Extract < UserIdentity , { platform : "win32" } > ) : string {
145158 if ( ! SID_PATTERN . test ( identity . sid ) ) refuse ( "The coordinator identity contains an invalid SID." ) ;
146- const localAppData = powershellValue (
147- "[Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)" ,
148- ) ;
159+ // Shell folder expansion can retain the launcher's ambient USERPROFILE.
160+ // Expand the raw registered location against the OS token's environment,
161+ // without consulting or mutating this process's environment.
162+ const localAppData = powershellValue ( String . raw `
163+ $stage = 'compile'
164+ try {
165+ Add-Type -TypeDefinition @'
166+ using System;
167+ using System.Collections.Generic;
168+ using System.IO;
169+ using System.Runtime.InteropServices;
170+ using System.Security.Principal;
171+ using System.Text.RegularExpressions;
172+ using Microsoft.Win32;
173+ public static class CodexCommanderKnownFolders {
174+ public static string Stage = "token-environment";
175+ [DllImport("userenv.dll", SetLastError = true, ExactSpelling = true)]
176+ [return: MarshalAs(UnmanagedType.Bool)]
177+ private static extern bool CreateEnvironmentBlock(out IntPtr block, IntPtr token, [MarshalAs(UnmanagedType.Bool)] bool inherit);
178+ [DllImport("userenv.dll", ExactSpelling = true)]
179+ [return: MarshalAs(UnmanagedType.Bool)]
180+ private static extern bool DestroyEnvironmentBlock(IntPtr block);
181+ public static string LocalAppData(string expectedSid) {
182+ using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) {
183+ if (!String.Equals(identity.User.Value, expectedSid, StringComparison.OrdinalIgnoreCase))
184+ throw new InvalidOperationException("Effective account changed during environment resolution.");
185+ IntPtr block = IntPtr.Zero;
186+ try {
187+ if (!CreateEnvironmentBlock(out block, identity.Token, false))
188+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
189+ var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
190+ IntPtr cursor = block;
191+ while (Marshal.ReadInt16(cursor) != 0) {
192+ string entry = Marshal.PtrToStringUni(cursor);
193+ int separator = entry.IndexOf('=');
194+ if (separator > 0) values[entry.Substring(0, separator)] = entry.Substring(separator + 1);
195+ cursor = IntPtr.Add(cursor, checked((entry.Length + 1) * 2));
196+ }
197+ Stage = "registered-folder";
198+ string path;
199+ bool expand;
200+ using (RegistryKey users = RegistryKey.OpenBaseKey(RegistryHive.Users, RegistryView.Registry64))
201+ using (RegistryKey key = users.OpenSubKey(identity.User.Value + @"\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders")) {
202+ if (key == null) throw new InvalidOperationException("Registered folder key is unavailable.");
203+ var kind = key.GetValueKind("Local AppData");
204+ if (kind != RegistryValueKind.String && kind != RegistryValueKind.ExpandString)
205+ throw new InvalidOperationException("Registered folder value is not a string.");
206+ expand = kind == RegistryValueKind.ExpandString;
207+ path = key.GetValue("Local AppData", null, RegistryValueOptions.DoNotExpandEnvironmentNames) as string;
208+ }
209+ if (String.IsNullOrWhiteSpace(path)) throw new InvalidOperationException("Registered folder is empty.");
210+ var seen = new HashSet<string>(StringComparer.Ordinal);
211+ for (int depth = 0; expand && path.IndexOf('%') >= 0; depth++) {
212+ if (depth >= 16 || path.Length > 32767 || !seen.Add(path))
213+ throw new InvalidOperationException("Registered folder expansion did not converge.");
214+ bool replaced = false;
215+ path = Regex.Replace(path, @"%([^%]+)%", match => {
216+ string value;
217+ if (!values.TryGetValue(match.Groups[1].Value, out value))
218+ throw new InvalidOperationException("Registered folder references an unavailable variable.");
219+ replaced = true;
220+ return value;
221+ });
222+ if (!replaced) throw new InvalidOperationException("Registered folder has an unresolved variable.");
223+ }
224+ string root = Path.GetPathRoot(path);
225+ if (path.Length > 32767 || path.IndexOf('\0') >= 0 || String.IsNullOrEmpty(root) || root.Length < 3 || root.EndsWith(":"))
226+ throw new InvalidOperationException("Registered folder is not an absolute path.");
227+ return Path.GetFullPath(path);
228+ } finally {
229+ if (block != IntPtr.Zero) DestroyEnvironmentBlock(block);
230+ }
231+ }
232+ }
233+ }
234+ '@
235+ $stage = 'token-environment'
236+ [CodexCommanderKnownFolders]::LocalAppData('${ identity . sid } ')
237+ } catch {
238+ if ($stage -ne 'compile') { $stage = [CodexCommanderKnownFolders]::Stage }
239+ $failure = $_.Exception.GetBaseException()
240+ [Console]::Error.WriteLine('CCX_IDENTITY_FAILURE:' + $stage + ':' + $failure.GetType().FullName + ':' + $failure.HResult.ToString('X8'))
241+ exit 1
242+ }
243+ ` ) ;
149244 if ( ! isAbsolute ( localAppData ) ) refuse ( "Windows LocalAppData resolution returned a relative path." ) ;
150245
151- // The SID and known- folder values come from the effective token/.NET OS APIs ,
246+ // The SID and registered folder values come from the effective token/registry ,
152247 // never USERPROFILE or LOCALAPPDATA. WP11 adds descriptor/reparse/ACL checks at
153248 // the stable-database open boundary where those checks can cover SQLite too.
154249 const root = resolve ( localAppData , "CodexCommander" , "Runtime" , "v1" , identity . sid . toUpperCase ( ) ) ;
0 commit comments