Why Rust executable fails to start with error code 0xC0000005 (Access Violation) on older Windows versions?

When I try to run an executable created by Rust 1.93.1 on, e.g., Windows 7, then I get this:

Now, I understand that Windows 7 is no longer officially supported as a target. But what exactly is the reason why we get an Access Violation (0xC0000005) error?

I could perfectly understand, if we would get a standard "The version of this file is not compatible with the version of Windows you're running" error message, because Windows detects that the .exe isn't compatible with my Windows version. However, this is not what is happening!


Actually, I have checked the PE header (NT header) of the .exe that Rust produces, and it turns out that the OS version and Sub-System version both are set to NT 6.0 in the .exe header – which is Windows Vista! So, at least from what the .exe header claims, the executable file is supposed to run on Windows Vista or later. That seems rather strange :thinking:


Anyhow, even with a "wrong" minimum required Windows version indicated in the .exe header, it should not crash with an Access Violation error, right? Actually, what you would be expecting is an error message like "The application cannot start because entry point SomeFunctionThatDidNotExistInWindows7 could not be found in KERNEL32.DLL".

And, indeed, Dependency Walker confirms that an missing entry point is the real reason why the .exe file produces by Rust cannot run on the Windows 7 machine:

Why the Access Violation error, then? Seems a bit fishy :fearful:

Again: I understand that .exe produced by Rust 1.93.1 won't run on Windows 7, since this target is not supported anymore, but I want to understand the technical reason why we get the Access Violation – rather than a proper loader error complaining about the missing entry point.

Regards.

1 Like

There are some of these odd gaps in Windows. Another fun one is if you run a binary with a missing DLL dependency from a terminal window, the process will just silently fail to load. I complained about this to a friend of mine and insisted that it used to spell out which DLL it was missing and he remarked "Are you sure you aren't thinking about OS/2?" (which did spell out which dependency it was missing). My friend also said that the system event log should contain a log entry detailing the missing DLL, though I never checked if this was true.

With regards to your specific question: I don't know the answer, but one might speculate that their reasoning is something along the line of "You must only run on supported platforms. Running on supported platforms should always succeed in loading supported DLL functions. If loading a DLL function that should always succeed for some reason doesn't succeed, the system is in such a bad state that there's no way to recover, so there's no point in trying to report any more details.".

It's sort of like how do you handle the situation if a heap API returns an error when you try to free a memory block that you just allocated and that hasn't been freed before? At some point you just give up and crash.

At least, that's my guess.

1 Like

I think microsoft's CRT initialization is most likely to be blamed.

ever since microsoft introduced the side-by-side assembly technology (a.k.a. WinSxS), in attempts to alleviate the "DLL hell" problem, it just created even more problems. the relative "new" ucrt was invented to cleanup some of the mess created by WinSxS, but it was never solved 100%.

actually, it depends, and it has been like this forever.

on "modern" windows, the loading process of an executable file gets rather complex, there's more than the kernel's PE loader, some export table entries are redirected to different modules, some modules are resolved by the msvc runtime. you may see completely different error popping up depending on which stage of the loader triggered the error.

things you may try:

  • update/install the latest ucrt or visual C++ "redistributable" package for the target operating system;
  • using an older version of msvc build tools;
  • if the rust program doesn't have ffi dependencies or all the ffi libraries can be built portably, try rebuild your project using the -windows-gnu toolchain, which uses the libc from the mingw project, based on the "legacy" os C runtime;
  • you can also try the -windows-gnullvm toolchain, which is based on the newer ucrt, but it is a tier 2 toolchain.

Well, I think at least that one makes sense, because you don't want a terminal program to deadlock your script with a GUI error dialog. Rather, the executable should simply fail to start and set the proper exit status, such as 0xC0000135 (STATUS_DLL_NOT_FOUND), 0xC0000139 (STATUS_ENTRYPOINT_NOT_FOUND) or 0xC000007B (STATUS_INVALID_IMAGE_FORMAT).

There are two ways to import a function from a DLL in Windows: (1) Via the .exe file's "import" table, which causes the required DLL to be loaded and the required function to be resolved by the OS' loader before the execution even starts, or (2) at runtime via the LoadLibrary() and GetProcAddress() functions. Only method (2) allows the application to catch and handle possible errors. Meanwhile, if any required DLL file or any required entry point is missing with method (1), then the application never gets a chance to execute; it will simply fail to load.

We are talking about method (1) here, because Dependency Walker (by default) only shows DLLs and functions that are imported via the "import" table! And, as we can see in Dependency Walker, there is a missing entry point in the Windows 7 version of KERNEL32.DLL. Therefore, our application should never get a chance to start execution, and therefore it should never get a chance to do anything that might cause an Access Violation – instead, what we should see is an error message from the OS loader, stating that the required entry point is missing.