Search code examples
c#.netwindows-servicesnamed-pipesaccess-rules

NamedPipeClientStream cannot connect without UAC


I'm writing an executable to send data to an open process (a Unity game) through a NamedPipe as to "simulate" deeplinks coming from a browser using a custom URI Scheme. This means both the pipe Client and the pipe Server will only ever run on the local user for IPC.

The game starts a NamedPipeServerStream and awaits until a Client connects and sends data:

var pipeSecurity = new PipeSecurity();
SecurityIdentifier everyone = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
pipeSecurity.AddAccessRule(new PipeAccessRule(everyone, PipeAccessRights.Read, AccessControlType.Allow));
                
pipe = new NamedPipeServerStream(PIPE_NAME, PipeDirection.InOut, 1, 
            PipeTransmissionMode.Message, PipeOptions.Asynchronous, 
            8192, 8192, pipeSecurity);

try
{
    await pipe.WaitForConnectionAsync(cancellationTokenSource.Token);
}

My Deeplink.exe (a WinExe built with Rider) creates a NamedPipeClientStream when opened and tries to connect:

using NamedPipeClientStream pipeClient = new(".", PIPE_NAME, 
            PipeDirection.InOut, PipeOptions.Asynchronous);
try
{
    pipeClient.Connect(PIPE_TIMEOUT);
}

This works correctly only when Deeplink.exe is Ran as Admin. If not, pipeClient.Connect throws an UnauthorizedAccessException: Access to the path is denied. I can't have my app requesting UAC every time a deeplink is used! From the public mono repo, this seems to match the UnauthorizedAccess_IODenied_NoPathName string, but I have checked and the PIPE_NAMEs are the correct.

At first I thought this was due to Deeplink.exe being placed under the C:/Program Files folder, next to where the Unity program is, but no, the issue persists even when trying to run it from the desktop. I have tried to use PipeAccessRights.FullAccess instead, but no luck there either.

Seeing as the UAC is the only difference between it working and not, I have to assume the problem is permission-related (tho I was expecting WorldSid to allow anyone and anything to access the pipe).

Is there something I overlooked? Is there really no way to connect to a pipe without admin permissions?


Update

For a bit of context, the game executable is first ran from a Game Launcher program (that I have also developed in Unity) that simply calls:

Process process = new();
process.StartInfo.FileName = "/path/to/game.exe";
process.StartInfo.UseShellExecute = true;
process.StartInfo.CreateNoWindow = false;

process.Start();

I have found that:

  • Opening Game.exe directly and then running Deeplink.exe works as expected.
  • Opening Launcher.exe, which then starts the Game.exe process, and then running Deeplink.exe does not work. UAC should not be necessary at any step in the process.

Sill dumbfounded about this. I started assuming my NamedPipeClientStream was at fault, but maybe starting Game.exe with these particular StartInfo properties is what's preventing the NamedPipeServerStream from being created correctly?


Update 2

The mystery thickens: I tried starting game.exe with UseShellExecute = false, and now the pipes communicate correctly... but only sometimes! It seems to work about 75% of the time. I truly have no idea what's sporadically denying my Client pipe access, the Server one always starts according to my logs.


Solution

  • Answering my own question as I think I have found why this was happening and how to fix it.

    It would seem the issue was that, if my Launcher was opened with elevated privileges, then any process ran by it would also inherit them. This meant that my NamedPipeServerStream would sometimes be running with elevated permissions, but my NamedPipeClientStream would always run without them. Regardless of the access rules I set during their creation, my pipe Client would always refuse to connect to it.

    Since I didn't need the game.exe to have elevated access, I looked into how to un-elevate a sub-process started by an elevated one, and ended up passing through explorer.exe as suggested in this blog post.

    // If the Launcher has UAC, use the explorer to launch the game
    if (FileUtility.HasElevatedPermissions())
    {
        ProcessStartInfo processInfo = new()
        {
            FileName = "explorer.exe",
            UseShellExecute = true,
            Arguments = $"\"{exePath}\""
        };
        Process.Start(processInfo);
    }
    // If the Launcher doesn't have UAC, open the game normally
    else
    {
        ProcessStartInfo processInfo = new()
        {
            FileName = exePath,
            UseShellExecute = true,
            Verb = "open"
        };
        Process.Start(processInfo);
    }
    

    This seems to do the trick, game.exe always opens without privileges and the deeplinker.exe pipe can communicate without issue. Thanks to both @Sinatr and @BenVoigt for their suggestions.


    If anyone's interested in HasElevatedPermissions(), I try to create a directory under C:/Program Files and return whether I succeed or not.

    public static bool HasElevatedPermissions(string _path = null)
    {
        if (string.IsNullOrEmpty(_path))
            _path = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
            
        float random = UnityEngine.Random.Range(0f, 1f);
        var path = @$"{_path}\temp{random}"; // Random to prevent user interference
        try
        {
            Directory.CreateDirectory(path);
        }
        catch (UnauthorizedAccessException)
        {
            return false;
        }
        Directory.Delete(path);
        return true;
    }
    

    Not the best approach I admit, but I couldn't find anything that could easily work from a Unity program.