The interesting thing about this is that some syscalls are versioned, even though the syscall interface is internal and private. There's NtLoadKey, NtLoadKey2, NtLoadKey3 and even NtLoadKeyEx.
This kind of versioning on public APIs, I understand, but syscalls are only meant to be invoked by ntdll. Why do they need more than one?
The consumers of the Native API are things like the original POSIX subsystem, the Interix POSIX subsystems, the OS/2 subsystem, the fourth POSIX subsystem used in WSLv1, NTVDMs, and of course the Win32 subsystem. Some of these were frozen long ago. The still live ones do not necessarily change in lockstep.
That said, for those particular API functions there is an interesting history that is, rather, about mitigating security holes:
Yes, but the Native API is NTDLL, a userspace wrapper around the system calls. On Windows nothing except NTDLL is meant to invoke system calls directly, and my experience was that this is basically true. Some apps will bypass the Win32 subsystem and link against NTDLL directly (which they aren't meant to do), but outside of a handful of very obfuscated video game DRM systems and malware, not much is invoking system calls directly.
Changes to the system calls to close exploits are clear, but I'm really curious what software is invoking NtLoadKey directly that Microsoft themselves can't change, and then kept doing it even as the system call evolved over time. These aren't documented even in headers so it takes some reverse engineering to be able to do that.
> This kind of versioning on public APIs, I understand, but syscalls are only meant to be invoked by ntdll. Why do they need more than one?
You got three common suffixes for function names in Windows. A and W relate to the encoding of the string parameters - A refers to the strings being encoded in plain ASCII, W to UTF-16.
And Ex, 2, 3, whatever - these refer to extensions with different parameters. Like, the "original" function may have had only two parameters or a tiny struct, but for more modern usecases they might have added a few parameters, expanded the struct or, even worse, re-arrange fields in the struct.
Now, of course they could have gone the Java/C++ path - basically, overload the function name with different parameters and have the "old" functions call whatever the most detailed constructor is and set default values for the newly expected parameters (or convert, if needed). But that doesn't work with C code at all, which is why the functions/syscalls have to have different names, and additionally the Java/C++ way imposes another function call with its related expenses while having dedicated functions/entrypoints allows for a tiny bit more performance, at the cost of sometimes significant code duplication.
And on top of all of that, MS has to take into account that there are a lot of third party code that doesn't use ntdll, user32 and god knows what else is supposed to be the actual API interface, but instead goes as close to the kernel as possible. Virus scanners, DRM solutions, anti-cheat measures, audit/compliance tools - these all hook themselves in everywhere they can. It's a routine source of issues with Windows updates...
The versioned syscalls exist to maintain binary compatibility with older applications while adding new functionality - when Microsoft needs to extend a syscall with new parameters, they create a new version rather than breaking existing internal callers that might be used by third-party applications reverse-engineering ntdll.
I can't tell you if that is actually the case here but most private Win32 API is actually public API since so many things are using it anyways. They never drew the line there and a lot of people go "well, they never changed this in the past, why would they now".
This is not the Win32 API, and Raymond Chen and others at Microsoft very much did draw a line when it came to people using the Native API of Windows NT.
Some third party software like antivirus (and kernel mode drivers?) does use the native API, so they probably want to avoid gratuitously breaking things even at the NTDLL level (although as this table shows, making raw syscalls is something they'll break).
Drivers are a very different kettle of fish, and don't really involve NTDLL.DLL at all. They don't have to involve a full syscall transition, for starters, and there is a whole flavour of the Native API that doesn't. (I'm glossing over the details a bit. Go and read about Zw versus Nt in any good device driver book.)
Stability of the Native API per se isn't really a big concern for drivers, not least because Microsoft invented other APIs for drivers, such as the one for graphics drivers that made drivers so loosely coupled to other things that it let the entire graphics subsystem be moved into the kernel in Windows NT 4.
And the reliance of antivirus softwares on the Native API, and thus the need for stability, is somewhat overblown, not least because the Native API is far too high level a layer for antivirus softwares to trust. The flashy GUI bits might as well be just written in ordinary Win32 et al., and the other bits are busy being … well … essentially rootkits. (-:
This kind of versioning on public APIs, I understand, but syscalls are only meant to be invoked by ntdll. Why do they need more than one?