0

I'm trying to call CreateProcessAsUser to run a desktop application for a user from a service. (See Launching a process in user’s session from a service and CreateProcessAsUser from service and user security issues)

It basically works, but some applications fail to run properly. Ultimately after some debugging I found out that one C# application fails because Environment.GetFolderPath returns null for a special folder. I'm thinking that this could be because the invoked user's registry or some paths in it are not correctly loaded for the user profile.

In the REMARKS section of CreateProcessAsUserW there are a few hints what to consider when creating a process for a user. I'm already creating an environment block using CreateEnvironmentBlock (and the environment variables seem to work), but it also says:

CreateProcessAsUser does not load the specified user's profile into the HKEY_USERS registry key. Therefore, to access the information in the HKEY_CURRENT_USER registry key, you must load the user's profile information into HKEY_USERS with the LoadUserProfile function before calling CreateProcessAsUser. [...]

I have checked the documentation of LoadUserProfile where it says something similar:

Note that it is your responsibility to load the user's registry hive into the HKEY_USERS registry key with the LoadUserProfile function before you call CreateProcessAsUser. This is because CreateProcessAsUser does not load the specified user's profile into HKEY_USERS. This means that access to information in the HKEY_CURRENT_USER registry key may not produce results consistent with a normal interactive logon.

However I didn't find any example or further details how to load a user's registry hive into the HKEY_USERS registry key and the registry APIs I looked at (RegLoadKey looked most promising, but I don't think that's the correct one or how to use it with a my target process before CreateProcessAsUser) don't do what is described on LoadUserProfile. Furthermore all StackOverflow questions I looked at didn't mention this or only just mentioned on the side that they are using this function.

I thought maybe passing the profile could be part of STARTUPINFOEX but I also didn't find anything regarding "profile" or "registry" in the documentation there.

I'm really struggling trying to find out what to do with the LoadUserProfile result (struct PROFILEINFO) and how to use it with the CreateProcessAsUserW function. I would be glad if you could give me some hints what I actually want to do with that function and how I can use it with CreateProcessAsUserW given that I have already acquired a user token with WTSQueryUserToken.

This is my current C# code how I spawn the process (it also contains some piping code which I left out for readability):

public class Win32ConPTYTerminal
{
    public bool HasExited { get; private set; }

    private IntPtr hPC;
    private IntPtr hPipeIn = MyWin32.INVALID_HANDLE_VALUE;
    private IntPtr hPipeOut = MyWin32.INVALID_HANDLE_VALUE;

    private readonly TaskCompletionSource<int> tcs;
    private readonly SemaphoreSlim exitSemaphore = new SemaphoreSlim(1);
    private IntPtr hProcess;
    private int dwPid;

    public Win32ConPTYTerminal()
    {
        CreatePseudoConsoleAndPipes(out hPC, out hPipeIn, out hPipeOut);
        tcs = new TaskCompletionSource<int>();
    }

    private static unsafe int InitializeStatupInfo(
        ref MyWin32.STARTUPINFOEXW pInfo, IntPtr hPC)
    {
        pInfo.StartupInfo.cb = (uint)sizeof(MyWin32.STARTUPINFOEXW);
        pInfo.StartupInfo.dwFlags = MyWin32.StartFlags.STARTF_USESTDHANDLES;
        pInfo.StartupInfo.hStdInput = IntPtr.Zero;
        pInfo.StartupInfo.hStdOutput = IntPtr.Zero;
        pInfo.StartupInfo.hStdError = IntPtr.Zero;

        fixed (char* title = "Console Title")
            pInfo.StartupInfo.lpTitle = title;

        var attrListSize = IntPtr.Zero;
        MyWin32.InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0,
            ref attrListSize);

        pInfo.lpAttributeList = Marshal.AllocHGlobal(attrListSize.ToInt32());

        if (MyWin32.InitializeProcThreadAttributeList(
            pInfo.lpAttributeList, 1, 0, ref attrListSize))
        {
            if (!MyWin32.UpdateProcThreadAttribute(
                    pInfo.lpAttributeList,
                    0,
                    MyWin32.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
                    hPC,
                    new IntPtr(sizeof(IntPtr)),
                    IntPtr.Zero,
                    IntPtr.Zero
                ))
            {
                Marshal.FreeHGlobal(pInfo.lpAttributeList);
                return Marshal.GetLastWin32Error();
            }
            else
            {
                return 0;
            }
        }
        else
        {
            Marshal.FreeHGlobal(pInfo.lpAttributeList);
            return Marshal.GetLastWin32Error();
        }
    }

    private unsafe IntPtr ObtainUserToken(out string activeWinsta)
    {
        activeWinsta = @"Winsta0\default";

        var process = MyWin32.GetCurrentProcess();
        if (!MyWin32.OpenProcessToken(process,
            MyWin32.AccessMask.TOKEN_ADJUST_PRIVILEGES
            | MyWin32.AccessMask.TOKEN_QUERY
            | MyWin32.AccessMask.TOKEN_DUPLICATE
            | MyWin32.AccessMask.TOKEN_ASSIGN_PRIMARY,
            out IntPtr processToken))
        {
            if (MyWin32.OpenProcessToken(process,
                MyWin32.AccessMask.TOKEN_QUERY
                | MyWin32.AccessMask.TOKEN_DUPLICATE
                | MyWin32.AccessMask.TOKEN_ASSIGN_PRIMARY,
                out var fallbackToken))
            {
                PrintWarning("could not obtain high privilege token for UI",
                    win32: true);
                return fallbackToken;
            }
            throw new Win32Exception(
                Marshal.GetLastWin32Error(),
                "Unexpected win32 error code opening process token");
        }

        int sessionId = -1;
        if (MyWin32.WTSEnumerateSessionsW(MyWin32.WTS_CURRENT_SERVER_HANDLE, 0, 1, out IntPtr pSession, out int sessionCount))
        {
            var sessions = (MyWin32.WTS_SESSION_INFOW*)pSession;
            for (int i = 0; i < sessionCount; i++)
            {
                var session = sessions[i];
                if (session.State != MyWin32.WTS_CONNECTSTATE_CLASS.WTSActive)
                    continue;
                var winsta = Marshal.PtrToStringUni((IntPtr)session.pWinStationName);
                PrintTrace("Detected active session " + winsta);
                // activeWinsta = winsta;
                sessionId = session.SessionId;
            }
        }
        else
        {
            PrintWarning("WTSEnumerateSessionsW failed", win32: true);
        }

        if (sessionId == -1)
            sessionId = MyWin32.WTSGetActiveConsoleSessionId();

        if (sessionId == -1)
        {
            PrintWarning("no desktop user logged in to use",
                win32: true);
            return processToken;
        }

        var tokenPrivs = new MyWin32.TOKEN_PRIVILEGES();
        if (!MyWin32.LookupPrivilegeValueW(null, "SeTcbPrivilege",
            out var luid))
        {
            PrintWarning(
                "could not change to desktop user (LookupPrivilegeValue)",
                win32: true);
            return processToken;
        }

        tokenPrivs.PrivilegeCount = 1;
        tokenPrivs.Privileges.Luid = luid;
        tokenPrivs.Privileges.Attributes = MyWin32.SE_PRIVILEGE_ENABLED;

        if (!MyWin32.AdjustTokenPrivileges(processToken, false,
            ref tokenPrivs, 0, IntPtr.Zero, IntPtr.Zero))
        {
            PrintWarning(
                "could not change to desktop user (AdjustTokenPrivileges)",
                win32: true);
            return processToken;
        }

        try
        {
            if (!MyWin32.WTSQueryUserToken(sessionId, out var currentToken))
            {
                PrintWarning(
                    "could not change to desktop user on session " + sessionId + " (WTSQueryUserToken)",
                    win32: true);
                return processToken;
            }
            return currentToken;
        }
        finally
        {
            tokenPrivs.Privileges.Attributes =
                MyWin32.SE_PRIVILEGE_DISABLED;
            MyWin32.AdjustTokenPrivileges(processToken, false,
                ref tokenPrivs, 0, IntPtr.Zero, IntPtr.Zero);
        }
    }

    private unsafe string? TraceTokenInfo(IntPtr token)
    {
        if (!MyWin32.GetTokenInformation(token,
            MyWin32.TOKEN_INFORMATION_CLASS.TokenUser,
            IntPtr.Zero,
            0,
            out int returnLength))
        {
            var error = Marshal.GetLastWin32Error();
            if (error != MyWin32.ERROR_INSUFFICIENT_BUFFER)
            {
                PrintWarning(
                    "could not determine token user (GetTokenInformation)",
                    win32: true);
                return null;
            }
        }

        string username;
        var userInfoPtr = Marshal.AllocHGlobal(returnLength);
        try
        {
            if (!MyWin32.GetTokenInformation(token,
                MyWin32.TOKEN_INFORMATION_CLASS.TokenUser,
                userInfoPtr,
                returnLength,
                out int returnLength2))
            {
                PrintWarning(
                    "could not determine token user (GetTokenInformation)",
                    win32: true);
                return null;
            }

            var user = (MyWin32.TOKEN_USER*)userInfoPtr;
            var userSid = (*user).User.Sid;

            StringBuilder name = new StringBuilder(255);
            int nameLength = name.Capacity;
            StringBuilder domainName = new StringBuilder(255);
            int domainNameLength = domainName.Capacity;
            if (!MyWin32.LookupAccountSidW(
                null,
                userSid,
                name, ref nameLength,
                domainName, ref domainNameLength,
                out var peUse))
            {
                PrintWarning(
                    "could not determine token user (LookupAccountSidW)",
                    win32: true);
                return null;
            }

            username = name.ToString();
            PrintTrace("Running process with user " + username);
        }
        finally
        {
            Marshal.FreeHGlobal(userInfoPtr);
        }
        return username;
    }

    public void Start(ProcessStartInfo startInfo, CancellationToken cancel)
    {
        HasExited = false;

        var startupinfo = new MyWin32.STARTUPINFOEXW();
        var result = InitializeStatupInfo(ref startupinfo, hPC);
        if (result != 0)
            throw new Exception("Unexpected win32 error code " + result);
        var token = ObtainUserToken(out var winsta);
        var username = TraceTokenInfo(token);

        string environment = MakeEnvironment(token, startInfo.Environment);

        var cmdLine = new StringBuilder();
        cmdLine
            .Append(LocalProcessExecutor.EscapeShellParam(startInfo.FileName))
            .Append(' ')
            .Append(startInfo.Arguments);

        cancel.ThrowIfCancellationRequested();

        MyWin32.PROCESS_INFORMATION piClient;
        var withProfileInfo = new WithProfileInfo(token, username);
        if (!withProfileInfo.LoadedProfileInfo)
            PrintWarning("Failed to LoadUserProfile, registry will not work as expected", true);

        unsafe
        {
            fixed (char* winstaPtr = winsta)
            {
                startupinfo.StartupInfo.lpDesktop = winstaPtr;

                result = MyWin32.CreateProcessAsUserW(
                    token,
                    startInfo.FileName,
                    cmdLine,
                    IntPtr.Zero, // Process handle not inheritable
                    IntPtr.Zero, // Thread handle not inheritable
                    false, // Inherit handles
                    MyWin32.ProcessCreationFlags.EXTENDED_STARTUPINFO_PRESENT // use startupinfoex
                    | MyWin32.ProcessCreationFlags.CREATE_UNICODE_ENVIRONMENT // environment is wstring
                    // | MyWin32.ProcessCreationFlags.CREATE_NEW_PROCESS_GROUP // make this not a child (for maximum compatibility)
                    ,
                    environment,
                    startInfo.WorkingDirectory,
                    ref startupinfo.StartupInfo,
                    out piClient
                ) ? 0 : Marshal.GetLastWin32Error();
            }
        }

        if (result != 0)
        {
            Marshal.FreeHGlobal(startupinfo.lpAttributeList);

            withProfileInfo.Dispose();

            throw result switch
            {
                2 => new FileNotFoundException(
                    "Could not find file to execute",
                    startInfo.FileName
                ),
                3 => new FileNotFoundException(
                    "Could not find path to execute",
                    startInfo.FileName
                ),
                _ => new Win32Exception(result),
            };
        }

        dwPid = piClient.dwProcessId;
        hProcess = piClient.hProcess;

        cancel.Register(() =>
        {
            if (HasExited || hProcess == IntPtr.Zero)
                return;

            Kill();
        });

        Task.Run(async () =>
        {
            MyWin32.WaitForSingleObject(hProcess, -1);

            MyWin32.GetExitCodeProcess(hProcess, out var exitCode);
            HasExited = true;

            // wait a little bit for final log lines
            var exited = await exitSemaphore.WaitAsync(50)
                .ConfigureAwait(false);
            if (exited)
                exitSemaphore.Release();

            tcs.TrySetResult(exitCode);

            withProfileInfo.Dispose();

            MyWin32.CloseHandle(piClient.hThread);
            MyWin32.CloseHandle(piClient.hProcess);
            hProcess = IntPtr.Zero;
            MyWin32.DeleteProcThreadAttributeList(
                startupinfo.lpAttributeList);
            Marshal.FreeHGlobal(startupinfo.lpAttributeList);

            Dispose();
        });
    }

    private unsafe string MakeEnvironment(IntPtr token, IDictionary<string, string> envOverride)
    {
        StringBuilder environment = new StringBuilder();

        foreach (var kv in envOverride)
        {
            environment.Append(kv.Key)
                .Append('=')
                .Append(kv.Value)
                .Append('\0');
        }

        if (!MyWin32.CreateEnvironmentBlock(out var lpEnvironment, token, bInherit: false))
        {
            PrintWarning(
                "could not generate environment variables (CreateEnvironmentBlock)",
                win32: true);
            return environment.ToString();
        }

        var envPtr = (char*)lpEnvironment;
        int i = 0;
        while (i == 0 || !(envPtr[i] == '\0' && envPtr[i - 1] == '\0'))
        {
            int start = i;
            while (envPtr[i] != '\0' && envPtr[i] != '=')
            {
                i++;
            }
            // malformed check, needs =
            if (envPtr[i] == '\0')
                continue;

            var name = Marshal.PtrToStringUni((IntPtr)(envPtr + start), i - start);
            i++;
            start = i;

            while (envPtr[i] != '\0')
            {
                i++;
            }

            var value = Marshal.PtrToStringUni((IntPtr)(envPtr + start), i - start);
            i++;

            if (!envOverride.Keys.Contains(name))
            {
                environment.Append(name)
                    .Append('=')
                    .Append(value)
                    .Append('\0');
            }
        }

        if (!MyWin32.DestroyEnvironmentBlock(lpEnvironment))
        {
            PrintWarning(
                "failed to free environment block, leaking memory (DestroyEnvironmentBlock)",
                win32: true);
        }

        environment.Append('\0');
        return environment.ToString();
    }

    public Task<int> WaitForExit()
    {
        return tcs.Task;
    }

    public void Kill(int statusCode = -1)
    {
        if (HasExited || hProcess == IntPtr.Zero)
            return;

        MyWin32.EnumWindows((hwnd, _) => {
            MyWin32.GetWindowThreadProcessId(hwnd, out var pid);
            if (pid == dwPid)
            {
                MyWin32.PostMessageW(hwnd, MyWin32.Message.WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
            }
            return true;
        }, IntPtr.Zero);

        if (MyWin32.WaitForSingleObject(hProcess, 1000) != 0)
        {
            MyWin32.TerminateProcess(hProcess, statusCode);

            MyWin32.WaitForSingleObject(hProcess, 500);
        }
        else
        {
            statusCode = 0;
        }

        // wait a little bit for final log lines
        if (exitSemaphore.Wait(50))
            exitSemaphore.Release();

        // actual exit code is more accurate, give watching thread time to set it first
        Thread.Sleep(50);

        // if thread hung up, set manually
        tcs.TrySetResult(statusCode);
    }

    private bool disposed;
    private readonly object _lockObject = new object();

    public void Dispose()
    {
        lock (_lockObject)
        {
            if (disposed)
                return;
            disposed = true;
        }

        if (hPC != IntPtr.Zero)
            MyWin32.ClosePseudoConsole(hPC);
        hPC = IntPtr.Zero;

        DisposePipes();

        exitSemaphore.Dispose();
    }

    private void DisposePipes()
    {
        if (hPipeOut != MyWin32.INVALID_HANDLE_VALUE)
            MyWin32.CloseHandle(hPipeOut);
        hPipeOut = MyWin32.INVALID_HANDLE_VALUE;

        if (hPipeIn != MyWin32.INVALID_HANDLE_VALUE)
            MyWin32.CloseHandle(hPipeIn);
        hPipeIn = MyWin32.INVALID_HANDLE_VALUE;
    }
}

public unsafe class WithProfileInfo : IDisposable
{
    private MyWin32.PROFILEINFOW profileInfo;
    private IntPtr token;
    private char* username;
    public bool LoadedProfileInfo { get; }

    public WithProfileInfo(IntPtr token, string username)
    {
        this.token = token;
        this.username = (char*)Marshal.StringToHGlobalUni(username);

        profileInfo.dwSize = sizeof(MyWin32.PROFILEINFOW);
        profileInfo.lpUserName = this.username;

        if (!MyWin32.LoadUserProfileW(token, ref profileInfo))
        {
            LoadedProfileInfo = false;
        }
        else
        {
            LoadedProfileInfo = true;
        }
    }

    public void Dispose()
    {
        if (LoadedProfileInfo)
            MyWin32.UnloadUserProfile(token, profileInfo.hProfile);
        Marshal.FreeHGlobal((IntPtr)username);
    }
}

Then when I try to print out a bunch of known folders in that application (using SHGetSpecialFolderPathW) I get the following inside the service:

FOLDERID_LocalAppData:
(Win32 Error for above) SHGetKnownFolderPath: 5 - Access is denied.
FOLDERID_RoamingAppData:
(Win32 Error for above) SHGetKnownFolderPath: 5 - Access is denied.
FOLDERID_Desktop:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Documents:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Downloads:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Favorites:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Links: C:\Users\testuser\Links
FOLDERID_Music:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Pictures:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Programs:
(Win32 Error for above) SHGetKnownFolderPath: 5 - Access is denied.
FOLDERID_SavedGames: C:\Users\testuser\Saved Games
FOLDERID_Startup:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Templates:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Videos:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Fonts: C:\WINDOWS\Fonts
FOLDERID_ProgramData: C:\ProgramData
FOLDERID_CommonPrograms: C:\ProgramData\Microsoft\Windows\Start Menu\Programs
FOLDERID_CommonStartup: C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup
FOLDERID_CommonTemplates: C:\ProgramData\Microsoft\Windows\Templates
FOLDERID_PublicDesktop: C:\Users\Public\Desktop
FOLDERID_PublicDocuments: C:\Users\Public\Documents
FOLDERID_PublicDownloads: C:\Users\Public\Downloads
FOLDERID_PublicMusic: C:\Users\Public\Music
FOLDERID_PublicPictures: C:\Users\Public\Pictures
FOLDERID_PublicVideos: C:\Users\Public\Videos

while outside the service I get:

FOLDERID_LocalAppData: C:\Users\testuser\AppData\Local
FOLDERID_RoamingAppData: C:\Users\testuser\AppData\Roaming
FOLDERID_Desktop: C:\Users\testuser\Desktop
FOLDERID_Documents: C:\Users\testuser\Documents
FOLDERID_Downloads: C:\Users\testuser\Downloads
FOLDERID_Favorites: C:\Users\testuser\Favorites
FOLDERID_Links: C:\Users\testuser\Links
FOLDERID_Music: C:\Users\testuser\Music
FOLDERID_Pictures: C:\Users\testuser\Pictures
FOLDERID_Programs: C:\Users\testuser\AppData\Roaming\Microsoft\Windows\Start Menu\Programs
FOLDERID_SavedGames: C:\Users\testuser\Saved Games
FOLDERID_Startup: C:\Users\testuser\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
FOLDERID_Templates: C:\Users\testuser\AppData\Roaming\Microsoft\Windows\Templates
FOLDERID_Videos: C:\Users\testuser\Videos
FOLDERID_Fonts: C:\WINDOWS\Fonts
FOLDERID_ProgramData: C:\ProgramData
FOLDERID_CommonPrograms: C:\ProgramData\Microsoft\Windows\Start Menu\Programs
FOLDERID_CommonStartup: C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup
FOLDERID_CommonTemplates: C:\ProgramData\Microsoft\Windows\Templates
FOLDERID_PublicDesktop: C:\Users\Public\Desktop
FOLDERID_PublicDocuments: C:\Users\Public\Documents
FOLDERID_PublicDownloads: C:\Users\Public\Downloads
FOLDERID_PublicMusic: C:\Users\Public\Music
FOLDERID_PublicPictures: C:\Users\Public\Pictures
FOLDERID_PublicVideos: C:\Users\Public\Videos

I don't know why SHGetKnownFolderPath fails like this. (in C# it just returns null)

I found another question that seems to be having exactly the same problem, however I'm already calling LoadUserProfile, so the solution doesn't work for me: SHGetKnownFolderPath fails with E_ACCESSDENIED

WebFreak001
  • 2,415
  • 1
  • 15
  • 24
  • 1
    It is not obvious how you obtained the user token. Normally that requires LogonUser(), kill two birds by using the CreateProcessWithLogon() stone instead. Now it is simple with dwLogonFlags = LOGON_WITH_PROFILE. – Hans Passant Dec 17 '21 at 12:39
  • @HansPassant I'm basically using `WTSQueryUserToken(WTSGetActiveConsoleSessionId(), &token)` to get the user token for the currently logged in desktop user. (correction to my question: I used the SeTcpPrivilege on the handle returned by OpenProcessToken for GetCurrentProcess to query that token, I didn't give that privilege to the retrieved token) I don't have the logon credentials so I'm not using LogonUser right now. – WebFreak001 Dec 17 '21 at 12:52
  • https://github.com/JetBrains/runAs/blob/4d56ab016fc5888cba138ff056a2c406eee25f55/JetBrains.runAs/ProcessAsUser.cpp#L135 – Hans Passant Dec 17 '21 at 12:58
  • LoadUserProfileW function - and *Loads the specified user's profile.* this api load a user's registry hive into the HKEY_USERS registry key (or add internal reference count to already loaded). however if you get token from active session - profile already loaded. better look for concrete code instead description – RbMm Dec 17 '21 at 13:21
  • hm so the LoadUserProfileW just magically associates with the token then? I tried adding just a call to LoadUserProfileW before it and hoping that it would resolve things, but it didn't. I need to make a minimal reproduction case for this and add it to my question, though I might only get around to that by next year (2 weeks) The code right now isn't very pretty and it's all written in C#, which doesn't make it better because native Win32 interop with C# adds a lot of boilerplate. – WebFreak001 Dec 17 '21 at 13:36
  • 1
    *LoadUserProfileW just magically associates with the token* - it take token as argument. really it use token for query user Sid and load or reference profile. in your case - if you get token from user session - profile already loaded. you not need again load it. so error at all not here and you on wrong path – RbMm Dec 17 '21 at 13:43
  • ok I have cleaned up my C# code a little and included it, though it's not a runnable example right now. I will come up with more reproducible code later. Maybe you can already see something I'm doing wrong though. – WebFreak001 Dec 17 '21 at 13:45

1 Answers1

0

ok the issue was that I was passing the wrong environment variables to the process. I was correctly using CreateEnvironmentBlock, but was offering a way to override environment variables through my API.

My API to do that was using ProcessStartInfo, which defaults its Environment dictionary to the running process environment, (the system service environment) which then overrode the environment variables generated for the user.

This caused the known folders API SHGetKnownFolderPath to fail with error 2 (The system cannot find the file specified) or error 5 (Access is denied)

So not overriding the environment variables with the service user environment fixed the issue and now programs are running correctly.

WebFreak001
  • 2,415
  • 1
  • 15
  • 24