What happens if you forget to pass an OVERLAPPED structure on an asynchronous handle?


A customer noticed that they were accidentally issuing some I/O's against an overlapped handle without using an OVERLAPPED structure. The calls seemed to work, and the program did not appear to suffer any ill effects, but they were worried that they just being lucky and that eventually the error will come back to bite them. So what really happens if you forget to pass an OVERLAPPED structure on an asynchronous handle?

Well, the layer of the kernel that deals with OVERLAPPED structures doesn't know whether then handle is synchronous or asynchronous. It just assumes that if you don't pass an OVERLAPPED structure, then the handle is synchronous. And the way it deals with synchronous I/O without an OVERLAPPED is that it creates a temporary OVERLAPPED structure on the stack with a null hEvent, issues an asynchronous I/O with that temporary OVERLAPPED structure, and then waits for completion with Get­Overlapped­Result(bWait = TRUE). It then returns the result.

What does this mean for you?

Well, what happens if the hEvent is null?

If the hEvent member of the OVERLAPPED structure is NULL, the system uses the state of the hFile handle to signal when the operation has been completed.

Okay, let's step back and look at what's going on here.

First of all, a file handle is a waitable object: It becomes unsignaled when an I/O operation begins, and it becomes signaled when an I/O operation ends.

Second of all, this behavior is not useful in general. If you are operating on a synchronous handle, you already know when the I/O operation ends: It ends when the synchronous I/O call returns. And if you are operating on an asynchronous handle, all the hFile tells you is that some I/O completed, but you don't know which one it was. That's why the documentation also says

Use of file, named pipe, or communications-device handles for this purpose is discouraged. It is safer to use an event object because of the confusion that can occur when multiple simultaneous overlapped operations are performed on the same file, named pipe, or communications device. In this situation, there is no way to know which operation caused the object's state to be signaled.

What's more, if somebody initiates a new I/O after your asynchronous I/O completed, the file object becomes unsignaled, and there's a possibility that this happened before you got a change to call Wait­For­Single­Object.

So why have this weird behavior if it's not useful in general? Because it's what the system itself uses internally to implement synchronous I/O! It issues the I/O asynchronously, then waits on the handle. Since the handle is synchronous, the system already knows that there can be only one I/O in progress at a time, so it can just wait on the hFile to know when that I/O is complete.

Okay, so let's look again at the case of the overlapped I/O issued with no OVERLAPPED structure. The layer that deals with OVERLAPPED structure assumes it has a synchronous handle and issues an asynchronous I/O, then waits until the handle is signaled, under the mistaken belief that the handle will be signaled when that I/O completes (since it "knows" that that's the only outstanding I/O request). But if your handle is actually asynchronous, what happens is that the OVERLAPPED layer waits on the hFile, and the call returns when any I/O on that handle completes. In other words, you're in the "... is discouraged" part of the documentation.

Theoretically speaking, then, it is legal to pass NULL as the lpOverlapped to Read­File when the handle is asynchronous, but the results may not be what you want, since the call may return prematurely if there is other I/O going on at the same time. And then when the I/O actually completes, it updates the OVERLAPPED structure that was created temporarily on the stack, and we saw that that leads to memory corruption that goes away when you try to debug it.

There are those who argue that the documentation for Read­File is overly cautious when it outright bans the use of a null lpOverlapped on asynchronous handles, because if you are really careful, you can get it to work, if you can guarantee that no I/O is outstanding on the handle at the time you issue your I/O call, and no other I/O will be issued against the handle while you're waiting for your call to complete.

I'm of the camp that it's like telling people that it's okay to change gears on your manual transmission by just slamming the gear stick into position without using the clutch. Yes, you can do it if you are really careful and get everything to align just right, but if you mess up, your transmission explodes and spews parts all over the road.

In the customer's case they were issuing the I/O without an OVERLAPPED structure after the handle was created and before asynchronous operations began, so it was indeed the case that nobody else was using the handle.¹ The usage was therefore technically safe, but the customer nevertheless chose to switch to using an explicit OVERLAPPED structure with an explicit hEvent, just in case future code changes resulted in asynchronous operations being performed on the handle at an earlier point. (Wise choice on the customer's part. Safety first!)

¹ We're assuming that there aren't any bugs that result in somebody using a handle after closing it or using an uninitialized handle variable. Even if that assumption isn't true, it would also cause problems even in the case where we we passed an explicit OVERLAPPED structure, so it's no buggier than it was before.

Comments (13)
  1. Damien says:

    I know that there is a cost associated with change, but I still find it difficult to comprehend the attitude of people who discover that they're doing the "wrong thing", and then ask "can I carry on doing the wrong thing?"

  2. SI says:

    I would assume the question was more along the lines of "It seems to work, but if we get weird random crash reports from our customers, could this be a potential source", since the code in question might already be in released products.

  3. Brian says:

    @Damien: This seems smart behavior to me.  The customer may be resolving many important questions:

    -How urgent is it that I fix this bug, compared to fixing other bugs?

    -Is this bug the cause of any outstanding issues?

    -Does this bug have any effects which I may be inadvertently relying on?

  4. parkrrrr says:

    In a modern transmission, all you do is cause undue wear on the synchronizers, which will eventually wear out and make you unable to shift with or without the clutch. I used to shift without a clutch pretty frequently in my '93 Saturn SL1, because the hydraulic clutch had a small leak somewhere, so it tended to lose fluid over time and just randomly stop working. (A better workaround for this condition was to keep a new bottle of brake fluid in the car, but I wasn't very good about remembering to replace it after I used it.)

    What happens from the user point of view is that you just can't shift into the desired gear until you hit precisely the right engine RPM for your speed, and then it just slips into gear. (What this means in practice is that if you're downshifting, you'll need to shift into neutral, hit the gas briefly, then hold the gearshift against the new gear while the engine winds down. When upshifting, you can skip the engine-revving part. This holding-the-gearshift-against-the-gear part is what wears out the synchros.)

  5. alegr1 says:

    @Stephen O:

    Exactly. If you serialize IO operations, as a synchronous handle promises, you MUST have the IO inside a mutex-protected critical section. Obviously, this can't be done in user mode. But the IO manager must only use the mutex for synchronous handles. Thus, at the decision point whether to create a temporary OVERLAPPED and wait on the handle, the IO manager already knows the type of the handle.

    Also, what if you opened a file for async IO, but used no standard GENERIC mask (which includes SYNCHRONIZE), but passed an explicit mask without SYNCHRONIZE.

  6. Joshua says:

    It actually sounds useful, but only on pipes (for which two outstanding I/O requests are bad anyway).

    Basically, a crude method of simulating the Unix select() call on pipes.

  7. This design seems like it would make synchronous handles not-thread-safe.  Consider a synchronous serial port handle:

    (1) Thread A calls WriteFile to transmit; transmit buffer is full, so operation blocks.

    (2) Thread B calls ReadFile to receive

    (3) After a several milliseconds some transmit buffer space becomes available and the IRP_MJ_WRITE operation completes.

       The handle is signaled, and both A and B wake up.

    What protects against this?  Some sort of system-maintained critical section or mutex?  Or nothing at all? (in which case, FILE_FLAG_OVERLAPPED would be *critical* for any handle that might be accessed by multiple threads simultaneously)

  8. jon says:

    How come the system doesn't know if a handle is sync or async anyway?

    What's the point of the FILE_FLAG_OVERLAPPED flag when creating the handle in that case?

    [There are multiple parts to the system. The part of the system that deals with OVERLAPPED structures doesn't know. (The lower-level part that handles the I/O does know. That lower-level part is the one who cares about FILE_FLAG_OVERLAPPED.) -Raymond]
  9. Ian Boyd says:

    My bosses were called to drive 3 hours, to get a 2 hour scolding from the customer over a bug i introduced when fixing a bug that wasn't causing any problems.

    Some customers don't care about technical correctness or purity, only that they don't get fined thousands of dollars when financial information isn't being recorded properly.

  10. Neil says:

    I like the analogy here.

    Handle = Gearbox

    OVERLAPPED = Clutch

    Synchronous fallback = Synchromesh fallback

  11. Henning Makholm says:

    This is all well and fine for disk files, but it bears notice that the situation is a lot murkier for socket I/O.

    In particular, it seems (based on anecdotal experience) that the kernel feature Raymond describes that reduces everything to asynchronous overlapped operations is not used for socket I/O. Instead the socket provider gets to see all the details of the call issued by the program (synchronous? asynchronous? OVERLAPPED? completion routine?) and is left to interpreting them correctly itself. II assume Microsoft's default socket provider knows how to do this the right way, but third-party socket providers for personal firewalls, VPNs or the like … generally don't. Often it seems they have only ever been tested with the Berkeley sockets subset of the Winsock interface.

    When you're writing for servers, you can generally get away with telling customers not to use a buggy network stack, but for the desktop/laptop market, the prudent developer will be a lot more conservative than MSDN tells you you need to be.

  12. Someone says:

    [There are multiple parts to the system. The part of the system that deals with OVERLAPPED structures doesn't know. (The lower-level part that handles the I/O does know. That lower-level part is the one who cares about FILE_FLAG_OVERLAPPED.) -Raymond]

    MSDN (msdn.microsoft.com/…/aa363858%28v=vs.85%29.aspx) says:

    "FILE_FLAG_OVERLAPPED: … If this flag is not specified, then I/O operations are serialized, even if the calls to the read and write functions specify an OVERLAPPED structure. …"

    This contradicts your statement that the layer dealing with the OVERLAPPED structures don't care about that flag. So, I'm still confused and still think, the MSDN documentation needs to be improved to document the real behavior.

    [The level that deals with OVERLAPPED structures doesn't care whether the handle is synchronous or asynchronous. If you pass an OVERLAPPED, it converts it to a kernel-thingie and calls the next lower level. If you don't pass an OVERLAPPED, it creates a temporary kernel-thingie and calls the next lower level. The lower-level part is the one that knows whether the handle is synchronous or asynchronous. But it no longer knows whether you passed an OVERLAPPED or not. (All it gets is a kernel-thingie.) -Raymond]
  13. Someone says:

    [The lower-level part is the one that knows whether the handle is synchronous or asynchronous.]

    But as you described it works like this: WriteFile() and ReadFile() with lpOverlapped = NULL are using an temporary OVERLAPPED structure and Get­Overlapped­Result, which gives such calls "blocking" behavior. When called with an actual OVERLAPPED structure, the temporary OVERLAPPED structure and Get­Overlapped­Result are not employed, making such calls "nonblocking".

    The quoted desribtion in MSDN gives FILE_FLAG_OVERLAPPED the additional semantics of making WriteFile() and ReadFile() "blocking" even if you are using an OVERLAPPED structure, which by your describtion seems not to be true.

    So the question remains: What else of the semantics of FILE_FLAG_OVERLAPPED is left for the lower level part to take care about? What does it control at all?

    [I simplified the story so I could get to the point. The Get­Overlapped­Result actually happens at the lower level, which knows whether the handle is synchronous or asynchronous. (It is also the lower level which serializes I/O operations on synchronous handles.) -Raymond]

Comments are closed.

Skip to main content