Search code examples
c#aliassymlink

How do I find the target of a Windows App Execution Alias in C#/Win32 api?


Microsft Windows Terminal (installed via the Microsoft Store) creates a 0 bytes wt.exe file which is a Windows execution alias. AFAIK it is somthing similar to a symbolic link, except it seems to be resolved at the CreateProcess Api level as opposed to a symlink that is translated at the file system.

In powershell:

❯ dir ~\AppData\Local\Microsoft\WindowsApps\wt.exe

Mode    Name
----    ----
la---   wt.exe -> C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.0.1401.0_x64__8wekyb3d8bbwe\WindowsTerminal.exe

❯ Get-Item .\wt.exe | fl

Name           : wt.exe
Length         : 0
LinkType       : AppExeCLink
Target         : C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.0.1401.0_x64__8wekyb3d8bbwe\WindowsTerminal.exe

I wasn't able to find proper documentation for these "new" kind of aliases. Even googling PS AppExeCLink value is not very useful.

I need a fast way to resolve an execution alias (get the target file) in a C# App. Given my requirements, I prefer an unmanaged (Win32 Api) way over adding a reference to the slow WMI or an external 300kb nuget package.

Thanks!


Solution

  • I faced the same problem in my System Tools Library, which choked on these new type of links. This library is written in C, and uses the WIN32 API.

    Here's what I found so far:

    • Three years after their introduction in Windows, cmd.exe and PowerShell 5.1 are still unaware of App Exec Links, and report them as 0-byte files.

      But PowerShell Core 7 knows about them:

      PS C:\Temp> dir $env:LOCALAPPDATA\Microsoft\WindowsApps | ?{$_.LinkType} | select Name,LinkType,Target
      
      Name                        LinkType    Target
      ----                        --------    ------
      GameBarElevatedFT_Alias.exe AppExeCLink C:\Program Files\WindowsApps\Microsoft.XboxGamingOverlay_5.420.11102.0_x64__8w...
      MicrosoftEdge.exe           AppExeCLink C:\WINDOWS\system32\SystemUWPLauncher.exe
      python.exe                  AppExeCLink C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.11.3162.0_x64__8w...
      python3.exe                 AppExeCLink C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.11.3162.0_x64__8w...
      ubuntu.exe                  AppExeCLink C:\Program Files\WindowsApps\CanonicalGroupLimited.UbuntuonWindows_2004.2020.8...
      ubuntu1804.exe              AppExeCLink C:\Program Files\WindowsApps\CanonicalGroupLimited.Ubuntu18.04onWindows_2020.1...
      WinFR.exe                   AppExeCLink C:\Program Files\WindowsApps\Microsoft.WindowsFileRecovery_0.1.13492.0_x64__8w...
      winget.exe                  AppExeCLink C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.11.3162.0_x64__8w...
      wt.exe                      AppExeCLink C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.4.3243.0_x64__8wekyb3...
      
      PS C:\Temp>
      
    • These App Exec Links are NTFS Reparse Points, tagged as
      IO_REPARSE_TAG_APPEXECLINK = 0x8000001b.

      You can read them in a C/C++/C# program using the WIN32 API DeviceIoControl() with the FSCTL_GET_REPARSE_POINT control code.

      My readlink.c module contains a ReadReparsePointW() routine demonstrating this.

    • You can dump the content of such a link, by running:

      fsutil reparsepoint query <REPARSE_POINT_PATHNAME>
      

      Ex:

      C:\Temp>fsutil reparsepoint query "%LOCALAPPDATA%\Microsoft\WindowsApps\wt.exe"
      Reparse Tag Value : 0x8000001b
      Tag value: Microsoft
      
      Reparse Data Length: 0x168
      Reparse Data:
      0000:  03 00 00 00 4d 00 69 00  63 00 72 00 6f 00 73 00  ....M.i.c.r.o.s.
      0010:  6f 00 66 00 74 00 2e 00  57 00 69 00 6e 00 64 00  o.f.t...W.i.n.d.
      0020:  6f 00 77 00 73 00 54 00  65 00 72 00 6d 00 69 00  o.w.s.T.e.r.m.i.
      0030:  6e 00 61 00 6c 00 5f 00  38 00 77 00 65 00 6b 00  n.a.l._.8.w.e.k.
      0040:  79 00 62 00 33 00 64 00  38 00 62 00 62 00 77 00  y.b.3.d.8.b.b.w.
      0050:  65 00 00 00 4d 00 69 00  63 00 72 00 6f 00 73 00  e...M.i.c.r.o.s.
      0060:  6f 00 66 00 74 00 2e 00  57 00 69 00 6e 00 64 00  o.f.t...W.i.n.d.
      0070:  6f 00 77 00 73 00 54 00  65 00 72 00 6d 00 69 00  o.w.s.T.e.r.m.i.
      0080:  6e 00 61 00 6c 00 5f 00  38 00 77 00 65 00 6b 00  n.a.l._.8.w.e.k.
      0090:  79 00 62 00 33 00 64 00  38 00 62 00 62 00 77 00  y.b.3.d.8.b.b.w.
      00a0:  65 00 21 00 41 00 70 00  70 00 00 00 43 00 3a 00  e.!.A.p.p...C.:.
      00b0:  5c 00 50 00 72 00 6f 00  67 00 72 00 61 00 6d 00  \.P.r.o.g.r.a.m.
      00c0:  20 00 46 00 69 00 6c 00  65 00 73 00 5c 00 57 00   .F.i.l.e.s.\.W.
      00d0:  69 00 6e 00 64 00 6f 00  77 00 73 00 41 00 70 00  i.n.d.o.w.s.A.p.
      00e0:  70 00 73 00 5c 00 4d 00  69 00 63 00 72 00 6f 00  p.s.\.M.i.c.r.o.
      00f0:  73 00 6f 00 66 00 74 00  2e 00 57 00 69 00 6e 00  s.o.f.t...W.i.n.
      0100:  64 00 6f 00 77 00 73 00  54 00 65 00 72 00 6d 00  d.o.w.s.T.e.r.m.
      0110:  69 00 6e 00 61 00 6c 00  5f 00 31 00 2e 00 34 00  i.n.a.l._.1...4.
      0120:  2e 00 33 00 32 00 34 00  33 00 2e 00 30 00 5f 00  ..3.2.4.3...0._.
      0130:  78 00 36 00 34 00 5f 00  5f 00 38 00 77 00 65 00  x.6.4._._.8.w.e.
      0140:  6b 00 79 00 62 00 33 00  64 00 38 00 62 00 62 00  k.y.b.3.d.8.b.b.
      0150:  77 00 65 00 5c 00 77 00  74 00 2e 00 65 00 78 00  w.e.\.w.t...e.x.
      0160:  65 00 00 00 30 00 00 00                           e...0...
      
      C:\Temp>
      
    • The reparse data there is a structure with four wide strings, looking like:

      typedef struct _REPARSE_APPEXECLINK_READ_BUFFER { // For tag IO_REPARSE_TAG_APPEXECLINK
        DWORD  ReparseTag;
        WORD   ReparseDataLength;
        WORD   Reserved;
        ULONG  Version;    // Currently version 3
        WCHAR  StringList[1];  // Multistring (Consecutive UTF-16 strings each ending with a NUL)
        /* There are normally 4 strings here. Ex:
          Package ID:  L"Microsoft.WindowsTerminal_8wekyb3d8bbwe"
          Entry Point: L"Microsoft.WindowsTerminal_8wekyb3d8bbwe!App"
          Executable:  L"C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.4.3243.0_x64__8wekyb3d8bbwe\wt.exe"
          Applic. Type: L"0"   // Integer as ASCII. "0" = Desktop bridge application; Else sandboxed UWP application
        */     
      } APPEXECLINK_READ_BUFFER, *PAPPEXECLINK_READ_BUFFER;
      
    • Running the App Exec Link target of wt.exe...

      "C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.4.3243.0_x64__8wekyb3d8bbwe\wt.exe"
      

      ... works, and starts the Windows Terminal.

      But running the App Exec Link target of MicrosoftEdge.exe...

      "C:\WINDOWS\system32\SystemUWPLauncher.exe"
      

      ... does NOTHING.

      ⇒ The other parameters in the reparse data are somehow important. (But I don't know how to use them.)

    • Running

      "%LOCALAPPDATA%\Microsoft\WindowsApps\MicrosoftEdge.exe"
      

      Then looking in Task Manager, I see that the real executable running for MS Edge is

      "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
      

      ⇒ This proves that the App Exec Link target program is not always the real program.

    • It is possible to start the target application using the entry point string in the reparse data, instead of the target pathname, using:

      explorer.exe shell:appsFolder\<REPARSE_POINT_ENTRY_POINT_NAME>
      

      Ex, this starts Microsoft Edge:

      explorer.exe shell:appsFolder\Microsoft.MicrosoftEdge_8wekyb3d8bbwe!MicrosoftEdge
      

      But this is not what I want. I'd really want to find how to start the application using the target application pathname AND the other two parameters.

    Anyway, I've updated the readlink() routine in my MsvcLibX library to return the target of App Exec Links. All the tools in my System Tools Library that can handle symbolic links now show that target.
    But as this target is obviously not the complete answer, I consider this current version as a makeshift implementation at best.

    If anybody finds more information on this subject, I'm very interested!