Skip to content

Commit b0ea38b

Browse files
committed
Clamp write to bufsize-1 bytes to afford sentinel value for no-progress
1 parent 1169502 commit b0ea38b

File tree

2 files changed

+39
-7
lines changed

2 files changed

+39
-7
lines changed

Diff for: src/io.c

+31-6
Original file line numberDiff line numberDiff line change
@@ -2518,13 +2518,38 @@ _dispatch_operation_perform(dispatch_operation_t op)
25182518
NTSTATUS status = _dispatch_NtQueryInformationFile(hFile,
25192519
&iosb, &fpli, sizeof(fpli), FilePipeLocalInformation);
25202520
if (NT_SUCCESS(status)) {
2521-
// WriteQuotaAvailable is unreliable in the presence
2522-
// of a blocking reader, when it can return zero, so only
2523-
// account for it otherwise
2524-
if (fpli.WriteQuotaAvailable > 0) {
2525-
len = MIN(len, fpli.WriteQuotaAvailable);
2521+
// WriteQuotaAvailable is the free space in the output buffer
2522+
// that has not already been reserved for reading. In other words,
2523+
// WriteQuotaAvailable =
2524+
// OutboundQuota - WriteQuotaUsed - QueuedReadSize.
2525+
// Unfortunately, this means that it is not possible to distinguish
2526+
// between a full output buffer and a reader blocked waiting for a
2527+
// full buffer's worth of data. This is a problem because if the
2528+
// output buffer is full and no reader is waiting for data, then
2529+
// attempting to write to the buffer of a PIPE_WAIT, non-
2530+
// overlapped I/O pipe will block the dispatch queue thread.
2531+
//
2532+
// In order to work around this idiosynchrasy, we bound the size of
2533+
// the write to be OutboundQuota - 1. This affords us a sentinel value
2534+
// in WriteQuotaAvailable that can be used to detect if a reader is
2535+
// making progress or not.
2536+
// WriteQuotaAvailable = 0 => a reader is blocked waiting for data.
2537+
// WriteQuotaAvailable = 1 => the pipe has been written to, but no
2538+
// reader is making progress.
2539+
// When we detect that WriteQuotaAvailable == 1, we write 0 bytes to
2540+
// avoid blocking the dispatch queue thread.
2541+
if (fpli.WriteQuotaAvailable == 1) {
2542+
len = 0;
2543+
} else if (fpli.WriteQuotaAvailable > 1) {
2544+
// Subtract 1 from WriteQuotaAvailable to ensure we do not fill
2545+
// the pipe and preserve the sentinel value of
2546+
// WriteQuotaAvailable == 1.
2547+
len = MIN(len, fpli.WriteQuotaAvailable - 1);
25262548
}
2527-
len = MIN(len, fpli.OutboundQuota);
2549+
2550+
// Always bound the write size by the OutboundQuota - 1 to preserve
2551+
// the sentinel value of WriteQuotaAvailable == 1.
2552+
len = MIN(len, fpli.OutboundQuota - 1);
25282553
}
25292554

25302555
OVERLAPPED ovlOverlapped = {};

Diff for: tests/dispatch_io_pipe.c

+8-1
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,14 @@ test_dispatch_write(int kind, int delay)
404404
dispatch_group_t g = dispatch_group_create();
405405
dispatch_group_enter(g);
406406

407-
const size_t bufsize = test_get_pipe_buffer_size(kind);
407+
// We must subtract 1 from the bufsize because the write operation
408+
// uses 1 byte of the buffer as a sentinel value to ensure that a
409+
// reader is making progress. Because these tests operate serially,
410+
// the reader will not make progress until the write finishes, and
411+
// a write of >= bufsize will not finish until the reader starts
412+
// draining the pipe. In practice, a serial full-buffer
413+
// read-after-write will not be encountered.
414+
const size_t bufsize = test_get_pipe_buffer_size(kind) - 1;
408415

409416
char *buf = calloc(bufsize, 1);
410417
assert(buf);

0 commit comments

Comments
 (0)