Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs for making CreateProcess work on a packaged win32 exe could be improved #4651

Open
mikehearn opened this issue Aug 17, 2024 · 11 comments
Open
Labels
area-Deployment Issues related to packaging, installation, runtime (e.g., SelfContained, Unpackaged)

Comments

@mikehearn
Copy link

Describe the bug

I'm not sure this is the right repository, but I've seen @DrusTheAxe giving expert answers to related topics here, so I think bugs filed may reach the right people.

There are some aspects of making CreateProcess work correctly in an MSIX packaged app that aren't intuitive and there's a bug in Windows 10 that isn't documented. I had to work it out by trial and error.

On all versions of Windows if your main EXE attempts to execute an EXE that's included in your package as a helper, then it will mysteriously get ERROR_ACCESS_DENIED from CreateProcess ("Access is denied"). Nothing is logged when this happens 😢

The reason is that all EXEs, not just those started from the start menu or a %PATH% execution alias, must be listed in the AppX Manifest using <Application> elements even if the only thing that starts them is your own code. This isn't mentioned in the docs for <Application> nor the docs for CreateProcess (in fact the API docs for CreateProcess don't contain the word manifest anywhere).

On Win11 it is sufficient to create an entry for your helper EXEs in AppxManifest.xml. Unfortunately we must still work on Win10 where it requires more effort:

  1. The EXE must have an embedded assembly manifest that contains the <msix> tag.
  2. And it must have an <Application> element in the AppxManifest
  3. And (this part is even less documented) it must also have an app execution alias!

In other words every EXE helper tool shipped in an MSIX packaged app must be added to the %PATH% if your runtime uses CreateProcess to invoke it, even if you don't actually invoke using that execution alias.

Steps to reproduce the bug

  1. Create an MSIX package with a main EXE (e.g. an Electron app) and a helper EXE.
  2. Use exec from the child_process module to invoke the EXE.
  3. Get "Access is denied" on stderr.

Expected behavior

Everything should just work, obviously. Process creation is the sort of thing you don't expect to break just because of choice of packaging format.

Screenshots

No response

NuGet package version

None

Packaging type

Packaged (MSIX)

Windows version

Windows 11 version 22H2 (22621, 2022 Update), Windows 10 version 22H2 (19045, 2022 Update)

IDE

No response

Additional context

No response

@DarranRowe
Copy link

DarranRowe commented Aug 17, 2024

One of the big issues I have with this is that the Windows App Runtime provides a way for an application to request a restart for itself. This starts RestartAgent.exe, which is part of the Windows App Runtime package, and isn't mentioned in the AppxManifest file, and exits the process. This RestartAgent.exe then executes the process again. This means that not CreateProcess is used twice, but one of them is for a process that is not mentioned in the package's manifest. What's more, there is no alias registered for this, and the restart agent manifest doesn't contain a msix element.
One big thing that I have to ask though, is your package set to run in an appcontainer? The answer to this is yes if the entry point is set to windows.partialTrustApplication, or uap10:TrustLevel is set to appContainer.
Another thing is that an application can be blocked from creating child processes. This can be done by setting PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY in the extended startup information. The documentation implies that you should be able to query for this in the process token. TOKEN_INFORMATION_CLASS is poorly documented here, since I would assume that it is TokenChildProcessFlags, but this has no documentation at all.

@mikehearn
Copy link
Author

Entry point is "Windows.FullTrustApplication" and uap10:TrustLevel doesn't exist in the manifest. Adding the entry points and <msix> element as I describe above has resolved the issue for my test Electron app, so I doubt there are any thread attributes that block child processes.

@DrusTheAxe has posted elsewhere on GitHub about how this mechanism works internally, and what he's said does strongly imply that your EXEs have to be listed in the manifest and have the <msix> linkage from the assembly manifest on Win10. Reading these scattered posts is how I eventually figured out a combination that works.

If RestartAgent.exe isn't registered and doesn't use <msix> in the manifest then maybe it's related to the code you linked to, which does some very complex setup work that nothing else is likely to do, e.g.

https://github.com/microsoft/WindowsAppSDK/blob/main/dev/AppLifecycle/AppInstance.cpp#L419-L426

        if (IsPackagedProcess())
        {
            // Desktop Bridge applications by default have their child processes break away from the parent process.  In order to recreate the calling process'
            // environment correctly, this code must prevent child breakaway semantics when calling the agent.  Additionally the agent must do the same when
            // restarting the caller.
            DWORD policy = PROCESS_CREATION_DESKTOP_APP_BREAKAWAY_OVERRIDE;
            THROW_IF_WIN32_BOOL_FALSE(UpdateProcThreadAttribute(attributeList.get(), 0, PROC_THREAD_ATTRIBUTE_DESKTOP_APP_POLICY, &policy, sizeof(policy), nullptr, nullptr));
        }

This sounds highly related.

@mikehearn
Copy link
Author

BTW, I suspect the reason this may be required for Node is this code:

https://github.com/libuv/libuv/blob/5cc7175514571e41cc31219df1ae9dbd8e6b69f9/src/win/process.c#L1072

But really what we need here is someone with source code access to explain or better update CreateProcess docs with an explanation of what combinations yield what outcomes.

@DarranRowe
Copy link

DarranRowe commented Aug 18, 2024

Just in case you are wondering where I got my information from, it is in UpdateProcThreadAttribute, which is the primary function for setting information in the extended startup information.
Anyway, by the looks of it, for Windows 11, any use of CreateProcess targetting an executable inside the package's directory will automatically launch with the package identity, even if it isn't explicitly marked as being part of the package (either using the msix element in the application manifest or be part of the extended startup information).
I'm wondering if this is subtly what is happening here. CreateProcess sees that the executable is in the package directory and is failing. As another question, if you try to use CreateProcess to launch an executable that isn't in your application's package directory, does it work correctly without any extra information? For the little test sample that I wrote, I placed an executable in my user profile under AppData and had the test executable run it, and it ran without issue.

With regards to Windows 10, I don't think it needs the msix element and an Application entry, but I think it does require some form of telling it to inherit the package identity. So the extended startup information should work just as well.

--Edit--
Just to test it out, I tried targetting the executable in the AppData directory, and used CreateProcess. As documented, CreateProcess without the extended startup info or with PROCESS_CREATION_DESKTOP_APP_BREAKAWAY_ENABLE_PROCESS_TREE will start the executable successfully, and the process doesn't have a package identity. Interestingly, using PROCESS_CREATION_DESKTOP_APP_BREAKAWAY_DISABLE_PROCESS_TREE or PROCESS_CREATION_DESKTOP_APP_BREAKAWAY_OVERRIDE will start it with a package identity.

Anyway, I agree with your point that there is information missing.

@mikehearn
Copy link
Author

As another question, if you try to use CreateProcess to launch an executable that isn't in your application's package directory, does it work correctly without any extra information?

Yes this isn't a problem. Only running EXEs inside the app package.

With regards to Windows 10, I don't think it needs the msix element and an Application entry, but I think it does require some form of telling it to inherit the package identity.

Sure, but changing the code that runs CreateProcess is hard. It sits inside a bunch of language runtimes that people upgrade slowly, and where code changes are reviewed carefully. Given the docs on this are scattered and hard to interpret, it's hard to convince people to change their code when it works fine outside of the MSIX container. So changing the manifests has to be the way to go here. Clearer direction from Microsoft would help here: CreateProcess could/should discuss all these topics, as package identity is clearly an aspect of the Windows API that becomes increasingly important.

@mikehearn
Copy link
Author

mikehearn commented Aug 27, 2024

Continuing my mission to document the hidden internals of CreateProcess on Win10 things get much hairier still. It's possible that my report of a bug above is wrong because more investigation reveals that ERROR_ACCESS_DENIED can be returned based on what other packages are installed on the system. For instance:

  1. Package the same app twice with two different package names.
  2. Install the first. CreateProcess in the first package works (apparently, even without the <msix> element so that might be a red herring)
  3. Install the second. CreateProcess in the second package fails, it continues to work in the first package. This is identical code.
  4. Uninstall the first. CreateProcess in the second package now works.

So it seems like something inside Windows is trying to work out the link between the EXE and the MSIX, and in some cases it gets confused and can't work it out because there are multiple similar packages installed, so it gives up and returns ERROR_ACCESS_DENIED.

I've also seen behavior not be consistent across upgrades, e.g. change from not having the <msix> element, to having it, to not having it, and the behavior is fails/works/works instead of fails/works/fails as you'd expect. I thought this was due to Windows having a cache, but now I wonder if it's something to do with how registration works on the upgrade path, or whether I was confused once I started changing the package name (looked like clearing a cache, in fact was due to package conflicts).

@mikehearn
Copy link
Author

Systematic testing shows the following:

  1. no app alias, no <msix>, alone = fail
  2. no app alias, <msix>, alone = fail
  3. app alias, no <msix>, alone = ok
  4. app alias, no <msix>, others = first ok, second fail, second ok when first removed

This is all Win10 + NodeJS + apps having an <Application> tag for every EXE. It might not be correct for other combinations.

So in the end, the <msix> element does seem a red herring on Win10. What's really going on is something like: apps need a global app alias for internal EXEs, even when the absolute path is passed to CreateProcess, which seems like a bug that got fixed in Win11. If two packages attempt to register the same app alias they go into a sort of queue, and the first package to grab it wins. The others all get ERROR_ACCESS_DENIED even when invoking the absolute path of the EXE, so the %PATH% alias shouldn't be involved at all. Once the package that was sitting on the alias is removed, the alias transitions to the second and it will start to work there.

Is it OK if the app alias name is different to the EXE name? No: they have to match.

This poses quite the conundrum. If two programs that use MSIX both ship the same helper EXE, then it looks like on Win10 they must pollute the %PATH%, and cannot be installed simultaneously. Hopefully digging reveals some way to dodge this because this otherwise appears to be a critical bug in Win10 - you don't control what other packages are installed or what EXEs they use. And we can't tell users to just upgrade to 11 due to the hardware requirements.

@codendone codendone added the documentation Improvements or additions to documentation label Aug 29, 2024
@DrusTheAxe
Copy link
Member

What's really going on is something like: apps need a global app alias for internal EXEs

What are you trying to do?

  • CreateProcess(pkgdir\some_random.exe)
  • CreateProcess(pkgdir\app_executable_per_appxmanifest.exe)
  • CreatePrcess(some_appexecutionalias.exe)
  • ShellExecute(some_appexecutionalias.exe)
  • ?

@mikehearn
Copy link
Author

It's the first. CreateProcess(pkgdir\some_random.exe), except:

  1. Adding it to the manifest fixes Win11
  2. Adding an execution alias fixes it on Win10 even if you don't then use the alias, still CreateProcess(pkgdir\some_random.exe).

@mikehearn
Copy link
Author

Update: it turns out this is more than a documentation problem. Adding the manifest entries for the helper EXE breaks Windows Store submission because they consider an app that contains any CLI exe to be a "headless" app, even if the package also contains a desktop/start menu entry point. And then for some inexplicable reason they require you to apply for special permission to create such apps. Windows happily installs them outside of the store without special permissions, but in store, you have to ask.

So the workaround for this Windows bug breaks Store submission. Great. :( We really do need some sort of fix here. This is a sharp edge that surfaces to most developers as "I packaged with MSIX and now stuff randomly doesn't work".

@mikehearn
Copy link
Author

@codendone See last comment, I think this is not (only) a documentation bug but a more general problem with Windows.

@codendone codendone added area-Deployment Issues related to packaging, installation, runtime (e.g., SelfContained, Unpackaged) and removed documentation Improvements or additions to documentation labels Oct 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-Deployment Issues related to packaging, installation, runtime (e.g., SelfContained, Unpackaged)
Projects
None yet
Development

No branches or pull requests

4 participants