Suppose you have some code that wants to open a file and share it with another process.
HANDLE file = CreateFile(...); HANDLE dup; if (DuplicateHandle( GetCurrentProcess(), /* source process */ file, /* source handle */ targetProcess, &dup, 0, FALSE, /* no inherit */ DUPLICATE_SAME_ACCESS)) { ... tell the other process to use "dup" ... } CloseHandle(file);
This code is missing some critical error handling: What if the CreateFile
fails?
If CreateFile
fails, it returns INVALID_
. This code then passes INVALID_
as the source handle to duplicate into the other process. But instead of failing, the DuplicateÂHandle
succeeds and produces a handle. What handle just got duplicated?
Some time ago, we studied why HANDLE
return values are so inconsistent, and I mentioned that
By coincidence, the value
INVALID_
happens to be numerically equal to the pseudohandle returned byHANDLE_ VALUE Get
.Current Process()
Therefore, what happened is that we gave a copy of our own process handle to the other process.
Oops.
But wait, there’s more. The handle returned by Get
has PROCESS_
permission. Not only did you give the other process the wrong thing, you gave it possibly the worst possible thing: You gave it full control over your own process.
Double oops.
Why does Get
return a pseudo-handle value that matches a common error case that people could overlook? I don’t know, but I have an idea.
Developer 1: “Hey, what fake handle value should Get
return?”
Developer 2: “I dunno. We need to pick something that is guaranteed never to accidentally match a real handle value.”
Developer 1: “Look, there’s this special value INVALID_
that is returned to indicate that an error occurred. This is provably a handle value that can never match a real handle, since it is used to indicate a problem.”
Developer 2: “Great, let’s use that!”
It seemed like a good idea at the time. For a special value, use something that couldn’t possibly conflict with a normal value.
Unfortunately, people are fallible, and bugs can occur like
HANDLE file = INVALID_HANDLE_VALUE; if (want_file) { file = CreateFile(...); if (file == INVALID_HANDLE_VALUE) { error(); return; } /* other intervening code */ // Okay, give the file to the other process. HANDLE dup; if (DuplicateHandle( GetCurrentProcess(), /* source process */ file, /* source handle */ targetProcess, &dup, 0, FALSE, /* no inherit */ DUPLICATE_SAME_ACCESS)) { ... tell the other process to use "dup" ... } CloseHandle(file);
The code that calls DuplicateÂHandle
forgot to check whether want_file
is set and inadvertently passed INVALID_
. Oops, duplicated a full-access process handle.
This pattern of “using an invalid value to carry a special alternate meaning” is actually quite common. For example, SetÂWindowsÂHookÂEx
uses a thread ID of zero to mean “global hook”, since zero is not a valid thread ID. And many functions imbue the special value nullptr
with all sorts of special meaning.
In retrospect, the choice to use INVALID_
as a pseudohandle was unfortunate. But what happened happened. You can’t change the past, and in computer software, you have to live with your mistakes.
Bonus chatter: RPC has the system_handle
attribute which lets you pass kernel handles via RPC, and you also have to say what kind of kernel handle you intend to pass. For files, you say system_handle(
. If you pass the wrong kind of handle, the operation fails. There’s a sample in the Windows Classic Samples repo.
A system_handle can be to all kinds of files, processes, synchronization objects, all proper serious kernel stuff … or a “DirectComposition surface”. I’m sure there’s a story behind that one.
FindFirstFile also returns a HANDLE that is not really a kernel handle. FindClose existing should be your first clue for that one.
iirc it’s just -1, right .. was this done as a performance optimization — because it’s so common to need a handle to the current process?
or were there some issues with process-shutdown, if a process holds open a (real) handle to itself?
Processes can easily have a real handle to themselves. Such as doing a DuplicateHandle to its own memory.
Another pseudo-handle value that you shouldn’t accidentally copy to another process is the one returned by GetCurrentThread. That’s -2 for those who are curious. And yes, it’s pretty much as powerful as the process handle, since it has THREAD_ALL_ACCESS on your current thread.
“The 16-bit functions OpenFile, _lopen and _lcreat return -1 on failure”
Why not returning zero?
Did you notice that “_lopen” and “_lcreat” looks like POSIX’s open and creat?
So the answer is: because POSIX returns -1.
And if you want to dig deeper, POSIX could not return zero because zero is the standard input file descriptor. (Standard output and error are 1 and 2 respectively.)