A customer observed that the formal requirements for the Read­File function specify that if the handle was opened with FILE_FLAG_OVERLAPPED, then the lpOverlapped parameter is mandatory. But the customer observed that in practice, passing NULL results in strange behavior. Sometimes the call succeeds, and sometimes it even returns (horrors!) valid data. (Actually the more horrifying case is where the call succeeds and returns bogus data!)

Now sure, you violated one of the requirements for the function, so the behavior is undefined. But why doesn't Read­File just flat-out fail if you call it incorrectly?

The answer is that the Read­File function doesn't know whether you're calling it correctly.

The Read­File function doesn't know whether the handle you passed was opened for overlapped or synchronous access. It just trusts that you're calling it correctly and builds an asynchronous call to pass into the kernel. If you passed a synchronous handle, well, it just issues the I/O request into the kernel anyway, and you get what you get.

This quirk traces its history all the way back to the Microsoft Windows NT OS/2 Design Workbook. As originally designed, Windows NT had a fully asynchronous kernel. There was no such thing as a blocking read. If you wanted a blocking read, you had to issue an asynchronous read (the only kind available), and then block on it.

As it turns out, developers vastly prefer synchronous reads. Writing asynchronous code is hard. So the kernel folks relented and said, "Okay, we'll have a way for you to specify at creation time whether you want a handle to be synchronous or asynchronous. And since lazy people prefer synchronous I/O, we'll make synchronous I/O the default, so that lazy people can keep being lazy."

The Read­File function is a wrapper around the underlying Nt­Read­File function. If you pass an lpOverlapped, then it takes the OVERLAPPED structure apart so it can pass the pieces as an Io­Status­Block and a Byte­Offset. (And if you don't pass an lpOverlapped, then it needs to create temporary buffers on the stack.) All this translation takes place without the Read­File function actually knowing whether the handle you passed is asynchronous or synchronous; that information isn't available to the Read­File function. It's relying on you, the caller, to pass the parameters correctly.

As it happens, the Nt­Read­File function does detect that you are trying to perform synchronous I/O on an asynchronous handle and fails with STATUS_INVALID_PARAMETER (which the Read­File function turns into ERROR_INVALID_PARAMETER), so you know that something went wrong.

Unless you are a pipe or mailslot.

For some reason, if you attempt to issue synchronous I/O on an asynchronous handle to a pipe or mailslot, the I/O subsystem says, "Sure, whatever." I suspect this is somehow related to the confusing no-wait model for pipes.

Long before this point, the basic ground rules for programming kicked in. "Pointers are not NULL unless explicitly permitted otherwise," and the documentation clearly forbids passing NULL for asynchronous handles. The behavior that results from passing invalid parameters is undefined, so you shouldn't be surprised that the results are erratic.