6

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!

Gerardo Grignoli
  • 14,058
  • 7
  • 57
  • 68

3 Answers3

14

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!

  • 2
    This is super helpfull! What I learned is that when you execute an AppExecLink, the CreateProcess function [behaves different](https://www.tiraniddo.dev/2019/09/overview-of-windows-execution-aliases.html), for example adding the `WIN:SYSAPPID` attribute to the security token. Hence, launching the link target directly will never get the same result. I've used `cmd /c wt.exe` as a workaround. I still have to re-read your info and find the best way to tell if it's an AppExecLink. – Gerardo Grignoli Jan 06 '21 at 03:33
  • 2
    You also gave me the idea to check the Get-ChildItem implementation in PowerShell, found interesting WinInternalGetTarget and WinInternalGetLinkType functions in [FileSystemProvider.cs](https://github.com/PowerShell/PowerShell/blob/v7.1.0/src/System.Management.Automation/namespaces/FileSystemProvider.cs#L8377) – Gerardo Grignoli Jan 06 '21 at 03:52
  • 1
    Looking the PowerShell Core sources is indeed a good idea! Unrelated: I've corrected an error in my reply above: ReadReparsePointW() is an internal routine in my MsvcLibX library. The WIN32 API to use is DeviceIoControl(). – Jean-François Larvoire Jan 06 '21 at 09:41
  • 3
    The best way to tell if it's an AppExecLink is first to run `GetFileAttributesW()` to check if it's a reparse point; Then if it is, do a `DeviceIoControl(FSCTL_GET_REPARSE_POINT)` to check if the reparse point tag is IO_REPARSE_TAG_APPEXECLINK. – Jean-François Larvoire Jan 06 '21 at 10:01
  • Incidentally, Microsoft has [removed `AppExecLink` reparse point support](https://github.com/PowerShell/PowerShell/pull/16044) from PowerShell as of a preview of 7.2.0.... They literally just ripped it out, with the output still showing that the files are links, with `name ->` but then just blank. I guess they didn't want people having this level of access to UWP/AppX magic, but it still sucks that PowerShell isn't immune to these politics. – Alex Fanat Nov 23 '21 at 13:01
3

You can also try adding Microsoft.PowerShell.5.1.ReferenceAssemblies nuget package to your project.

Then the following code works:

using System.Linq;
using System.Management.Automation;

string wtPath =
    Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
        "Microsoft",
        "WindowsApps",
        "wt.exe"
    );

using (var ps = PowerShell.Create())
{
    var psObject = ps.AddCommand("Get-Item").AddParameter("Path", wtPath).Invoke().Single();
    var psPropInfo = psObject.Properties["Target"];

    string[] targets = (psPropInfo.Value as List<string>).ToArray();

    Console.WriteLine($"[{psPropInfo.Name}]=[{String.Join("; ", targets)}]");
}
Gabor
  • 3,021
  • 1
  • 11
  • 20
  • Thanks Gabor. I think this will solve the problem for future visitors and deserves an upvote. But given that I need something faster than starting a PowerShell instance, I will not mark it as the accepted answer. Also, in a similar fashion, I could start a process `pwsh -NoProfile -c '(Get-Item wt.exe).Target'` and get the result. – Gerardo Grignoli Jun 19 '20 at 17:38
1

Thought I would also add this related thread on how this ReparsePoint link type is used by UWP exes and the WindowsApp store.

tl;dr;
The ~\AppData\Local\Microsoft\WindowsApps\ directory has EXEs that are using a special form of IO_REPARSE_TAG_APPEXECLINK symlink to reference the actual UWP pkg located in C:/Program Files/WindowsApps/.

smallscript
  • 643
  • 7
  • 13