Don’t forget, the fourth parameter to ReadFile and WriteFile is sometimes mandatory

The Read­File and Write­File functions have a parameter called lp­Number­Of­Byte­Read, which is documented as

  __out_opt LPDWORD lpNumberOfBytesRead,
// or
  __out_opt LPDWORD lpNumberOfBytesWritten,

"Cool," you think. "That parameter is optional, and I can safely pass NULL."

My program runs fine if standard output is a console, but if I redirect standard output, then it crashes on the Write­File call. I verified that the handle is valid.

int __cdecl main(int, char **)
  // error checking removed for expository purposes
  WriteFile(hStdOut, "hello", 5, NULL, NULL);
  return 0;

The crash occurs inside the Write­File function trying to write to a null pointer.

But you need to read further in the documentation for Write­File:

lp­Number­Of­Bytes­Written [out, optional]

A pointer to the variable that receives the number of bytes written when using a synchronous hFile parameter. Write­File sets this value to zero before doing any work or error checking. Use NULL for this parameter if this is an asynchronous operation to avoid potentially erroneous results.

This parameter can be NULL only when the lp­Over­lapped parameter is not NULL.

That second paragraph is the catch: The parameter is sometimes optional and sometimes mandatory. The annotation language used in the function head is not expressive enough to say, "Sometimes optional, sometimes mandatory," so it chooses the weakest annotation ("optional") so as not to generate false positives when run through static code analysis tools.

With the benefit of hindsight, the functions probably should have been split into pairs, one for use with an OVERLAPPED structure and one without. That way, one version of the function would have a mandatory lp­Number­Of­Bytes­Written parameter and no lp­Over­lapped parameter at all; the other would have a mandatory lp­Over­lapped parameter and no lp­Number­Of­Bytes­Written parameter at all.

The crash trying to write to a null pointer is consistent with the remark in the documentation that the lp­Number­Of­Bytes­Written is set to zero before any work is performed. As for why the code runs okay if output is not redirected: Appearing to succeed is a valid form of undefined behavior. It appears that when the output handle is a console, the rule about lp­Number­Of­Bytes­Written is not consistently enforced.

At least for now.

Comments (11)
  1. Damien says:

    But why read the documentation when you can randomly ask strangers for help?

  2. skSdnW says:

    If the Read/Write functions were split you could not use the "overlapped with synchronous handle" trick so I guess it's a good thing the design stayed this way. People just need to RTFM.

    Read/WriteFile have a special check for console handles and they take a different code-path ending with IPC to csrss/conhost, I'm guessing there is a null pointer check in there somewhere…

  3. A slightly less elegant alternative to having two functions would have been to have WriteFile call SetLastError(ERROR_INVALID_PARAMETER) and return FALSE, if lp­Number­Of­Bytes­Written and lpOverlapped are both NULL, or both non-NULL. But it's probably too late for that as well.

  4. skSdnW says:

    @Maurits: Both non-NULL is legal…

  5. jpa says:

    It would be easy to make the parameter truly optional. A simple

    DWORD dummy;

    if (!lpNumberOfBytesRead) lpNumberOfBytesRead = &dummy;

    in the function would suffice.

  6. Myria says:

    The difference in behavior with consoles is because ReadFile and WriteFile have a total hack for console I/O.  Consoles are assigned fake HANDLE values that WriteFile specifically recognizes, and redirects such requests into LPCs to csrss.exe or conhost.exe.  This ends up being why a lot of Windows features don't work properly with console handles – they're not true kernel handles at all.

  7. Zan Lynx' says:

    @jpa: I think it is better that it crash. Otherwise it will lead to problems I find in Posix programs all the time. If the programmer ignores the bytes value from read() or write(), and assumes that because they specified 4096 bytes to write, 4096 bytes were written, well, bad things happen. Especially since it works most of the time, except for when it doesn't. In Posix world a signal can interrupt a read or write. Or the file, pipe or socket may not contain the expected 4096 bytes because of an error or timing issue. In Windows Read and WriteFile have similar issues.

  8. Joshua says:


    "…until the second man questions him."

  9. @Zan Lynx':

    Or the simple case of being at the end of file and the file not being an exact multiple of the size you specified to read.


    That isn't quite right, the handles are actually recognised by the Windows Object Manager. However, these are special handles that are redirected to the process specific handles there. This isn't a hack in ReadFile or WriteFile, but a kernel mode redirect for some special handles.

  10. Joshua says:

    In practice all synchronous buffered ReadFile operations that target a block device (i.e. a disk) will return the amount of data you asked for.

    I've seen it not do that. Now WriteFile on the other hand seems to always write it all unless it hits an error.

  11. Mike Dimmick says:

    @WndSks: 'just need to RTFM' – the trouble is that busy developers don't read the manual, or don't read it thoroughly enough, particularly for functions they 'know' how to use. The trouble with ReadFile is that it looks deceptively simple.

    In theory, any ReadFile operation could return fewer bytes than were requested: therefore Windows has to be able to tell you how much of your buffer is valid. In the case of overlapped operations, this is stored in the OVERLAPPED structure; for non-overlapped it's the parameter you passed. In practice all synchronous buffered ReadFile operations that target a block device (i.e. a disk) will return the amount of data you asked for.

    I wouldn't say that ReadFile and WriteFile have special checks for console handles. They have checks for the type of device they're talking to – remember that ReadFile can read from serial ports, consoles, raw devices, etc. *nix goes further: *everything* is modelled as a file (but again the file handle can reference a file that's part of a file system, a block device, a character device, etc).

Comments are closed.