What version of gRPC and what language are you using?
gRPC v1.65.0 on c++
What operating system (Linux, Windows,...) and version?
iOS 17.2/ MacOS 14.3.1
What runtime / compiler are you using (e.g. python version or version of gcc)
Apple clang version 15.0.0 (clang-1500.1.0.2.5)
What did you do?
While integrating Apache Arrow Flight with gRPC, I observed an issue where sending a Flight Array would cause the operation to stall. The root cause was traced back to cfstream_endpoint.cc in gRPC, specifically the assumption that the stream is writable when DoWrite is invoked from the on_writeable callback::
size_t written_size =
CFWriteStreamWrite(cf_write_stream_, slice.begin(), slice.size());
total_written_size += written_size;
The issue arises because CFWriteStreamWrite returns a signed value, potentially -1, indicating failure. However, this return value is cast to an unsigned type, leading to an overflow in case of failure. Consequently, DoWrite completes without recognizing the failure, leaving the client waiting indefinitely for a result that will never arrive.
In scenarios where a 0-length buffer is sent, CFWriteStreamWrite returns 0, which ambiguously suggests the stream is full. Following this, any subsequent CFWriteStreamWrite calls with valid buffers fail, returning -1.
To validate this behavior, I developed a sample application that attempts to upload a file to a simple HTTP server using CFStream. The application fails to upload the file after attempting to write a 0-length buffer to CFWriteStream, confirming that CFWriteStreamWrite fails after receiving a 0-length buffer, and all subsequent calls with the same stream also fail.
#include <CoreFoundation/CoreFoundation.h>
#include <iostream>
void uploadFileToHTTP(const char *hostname, int port, const char *filePath) {
CFStringRef hostString = CFStringCreateWithCString(kCFAllocatorDefault, hostname, kCFStringEncodingUTF8);
CFReadStreamRef readStream = NULL;
CFWriteStreamRef writeStream = NULL;
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, hostString, port, &readStream, &writeStream);
if (writeStream) {
if (CFWriteStreamOpen(writeStream)) {
FILE *file = fopen(filePath, "rb");
if (file) {
fseek(file, 0, SEEK_END);
long fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
char headers[1024];
snprintf(headers, sizeof(headers),
"POST /upload HTTP/1.1\r\n"
"Host: %s\r\n"
"Content-Type: application/octet-stream\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n\r\n",
hostname, fileSize);
CFIndex numBytesWritten = CFWriteStreamWrite(writeStream, (const UInt8 *)headers, strlen(headers));
if (numBytesWritten < 0) {
// handle failure...
fclose(file);
CFWriteStreamClose(writeStream);
CFRelease(writeStream);
return;
}
const size_t bufferSize = 1024;
UInt8 buffer[bufferSize];
size_t bytesRead = 0;
size_t chunks_nr = fileSize / bufferSize;
int iter = 0;
while ((bytesRead = fread(buffer, 1, bufferSize, file)) > 0) {
if (iter == chunks_nr - 1) {
//we simulate a 0-sized buffer write
std::cout << "writing 0-length buffer to WriteStream. chunk: "<< iter << std::endl;
numBytesWritten = CFWriteStreamWrite(writeStream, buffer, 0);
assert(numBytesWritten == 0);
}
// this call fails right after writing a 0-length buffer
numBytesWritten = CFWriteStreamWrite(writeStream, buffer, bytesRead);
if (numBytesWritten < 0) {
std::cout << "failed to write to WriteStream at chunk " << iter << " out of " << chunks_nr << std::endl;
break;
}
iter++;
}
fclose(file);
} else {
std::cout << "Failed to open file." << std::endl;
}
CFWriteStreamClose(writeStream);
} else {
std::cout << "Failed to open write stream." << std::endl;
}
CFRelease(writeStream);
} else {
std::cout << "Failed to create write stream." << std::endl;
}
if (hostString) CFRelease(hostString);
}
int main(int argc, char *argv[]) {
if (argc != 4) {
std::cout << "Usage: " << argv[0] << "<Hostname> <Port> <File Path> " << std::endl;
return 1;
}
const char *hostname = argv[1];
int port = atoi(argv[2]);
const char *filePath = argv[3];
uploadFileToHTTP(hostname, port, filePath);
return 0;
}
This finding suggests that 0-length buffers should not be passed to CFWriteStreamWrite, and a check for buffer length should be implemented to prevent this issue.
Notably, this behavior differs from the POSIX implementation, which uses sendmsg for sending iovecs, where sendmsg explicitly allows 0-length buffers as part of its scatter/gather API.
What did you expect to see?
What did you see instead?
Make sure you include information that can help us debug (full error message, exception listing, stack trace, logs).
See TROUBLESHOOTING.md for how to diagnose problems better.
Anything else we should know about your project / environment?
This issue highlights a significant discrepancy between CFStream and POSIX implementations, impacting the use of gRPC with Apache Arrow Flight and potentially other applications requiring zero-length buffer transmissions.
What version of gRPC and what language are you using?
gRPC v1.65.0 on c++
What operating system (Linux, Windows,...) and version?
iOS 17.2/ MacOS 14.3.1
What runtime / compiler are you using (e.g. python version or version of gcc)
Apple clang version 15.0.0 (clang-1500.1.0.2.5)
What did you do?
While integrating Apache Arrow Flight with gRPC, I observed an issue where sending a Flight Array would cause the operation to stall. The root cause was traced back to cfstream_endpoint.cc in gRPC, specifically the assumption that the stream is writable when DoWrite is invoked from the on_writeable callback::
The issue arises because CFWriteStreamWrite returns a signed value, potentially -1, indicating failure. However, this return value is cast to an unsigned type, leading to an overflow in case of failure. Consequently, DoWrite completes without recognizing the failure, leaving the client waiting indefinitely for a result that will never arrive.
In scenarios where a 0-length buffer is sent, CFWriteStreamWrite returns 0, which ambiguously suggests the stream is full. Following this, any subsequent CFWriteStreamWrite calls with valid buffers fail, returning -1.
To validate this behavior, I developed a sample application that attempts to upload a file to a simple HTTP server using CFStream. The application fails to upload the file after attempting to write a 0-length buffer to CFWriteStream, confirming that CFWriteStreamWrite fails after receiving a 0-length buffer, and all subsequent calls with the same stream also fail.
This finding suggests that 0-length buffers should not be passed to CFWriteStreamWrite, and a check for buffer length should be implemented to prevent this issue.
Notably, this behavior differs from the POSIX implementation, which uses sendmsg for sending iovecs, where sendmsg explicitly allows 0-length buffers as part of its scatter/gather API.
What did you expect to see?
What did you see instead?
Make sure you include information that can help us debug (full error message, exception listing, stack trace, logs).
See TROUBLESHOOTING.md for how to diagnose problems better.
Anything else we should know about your project / environment?
This issue highlights a significant discrepancy between CFStream and POSIX implementations, impacting the use of gRPC with Apache Arrow Flight and potentially other applications requiring zero-length buffer transmissions.