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?
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:
Game.exe
directly and then running Deeplink.exe
works as expected.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?
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.
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.