Repository: pocoproject/poco (2026-03-26T10:42:16Z)

Issue:
Pipe close deadlocks on
**Describe the bug**
On Linux, the Pipe's `close()` call will deadlock waiting for a blocked `writeBytes()` in a different thread to complete.

While explicitly using Pipe's `close(Poco::Pipe::CLOSE_READ)` will resolve the issue, that call is vulnerable to deadlocking if there is a blocked `readBytes()` in a different thread.

This behavior change seems to be due to the fix applied for bug #5161 and pull #5160, which assumed that only readers would be blocked on a Pipe, and never writers.  (This may be true for the socket reactors, but programs using Pipes can block on either side.)

**To Reproduce**
```c++
#include "Poco/Thread.h"
#include "Poco/Pipe.h"
#include <iostream>
#include <string>

int main() {
	Poco::Pipe pipe;
	Poco::Thread blockedThread;

	blockedThread.startFunc([&pipe]() {
		const std::string buffer(100'000, 'A');

		std::cerr << "Writing data to pipe" << std::endl;
		const int bytesWritten = pipe.writeBytes(buffer.data(), buffer.size());
		std::cerr << "pipe.writeBytes() returned " << bytesWritten << std::endl;
	});

	Poco::Thread::sleep(250); // Wait for pipe to block

	std::cerr << "Closing pipe" << std::endl;
	pipe.close();  // Dead-locks on 1.15.0 or later
	std::cerr << "Pipe closed (works before 1.15.0)" << std::endl;

	blockedThread.join();

    return 0;
}
```

**Expected behavior**
Before POCO v1.15.0, the `pipe.close()` would complete and exit.
With POCO v1.15.0 and later, the `pipe.close()` call deadlocks due to the blocked write call.

**Logs**
With POCO v1.15.0 and later:
```
Writing data to pipe
Closing pipe
```

With POCO v1.14.2 and before:
```
Writing data to pipe
Closing pipe
Pipe closed (works before 1.15.0)
pipe.writeBytes() returned 65536
```

**Please add relevant environment information:**
 - Tests performed on Debian 12 Linux (kernel 6.6.87.2)
 - POCO Version 1.15.0
 - GCC 12.2.0



Discussion:

Patch:
diff --git a/Foundation/include/Poco/PipeImpl_POSIX.h b/Foundation/include/Poco/PipeImpl_POSIX.h
index 2548c16b82..3ce8eb1787 100644
--- a/Foundation/include/Poco/PipeImpl_POSIX.h
+++ b/Foundation/include/Poco/PipeImpl_POSIX.h
@@ -40,6 +40,7 @@ class Foundation_API PipeImpl: public RefCountedObject
 	int readBytes(void* buffer, int length);
 	Handle readHandle() const;
 	Handle writeHandle() const;
+	void close();
 	void closeRead();
 	void closeWrite();
 
diff --git a/Foundation/include/Poco/PipeImpl_WIN32.h b/Foundation/include/Poco/PipeImpl_WIN32.h
index fd5095a61b..f1ddb94bba 100644
--- a/Foundation/include/Poco/PipeImpl_WIN32.h
+++ b/Foundation/include/Poco/PipeImpl_WIN32.h
@@ -41,6 +41,7 @@ class Foundation_API PipeImpl: public RefCountedObject
 	int readBytes(void* buffer, int length);
 	Handle readHandle() const;
 	Handle writeHandle() const;
+	void close();
 	void closeRead();
 	void closeWrite();
 
diff --git a/Foundation/src/Pipe.cpp b/Foundation/src/Pipe.cpp
index 0d902ffec7..848edbc837 100644
--- a/Foundation/src/Pipe.cpp
+++ b/Foundation/src/Pipe.cpp
@@ -60,8 +60,7 @@ void Pipe::close(CloseMode mode)
 		_pImpl->closeWrite();
 		break;
 	default:
-		_pImpl->closeWrite();  // close write first to send EOF to blocked readers
-		_pImpl->closeRead();
+		_pImpl->close();
 		break;
 	}
 }
diff --git a/Foundation/src/PipeImpl_POSIX.cpp b/Foundation/src/PipeImpl_POSIX.cpp
index 6437cbe38b..16c6739d50 100644
--- a/Foundation/src/PipeImpl_POSIX.cpp
+++ b/Foundation/src/PipeImpl_POSIX.cpp
@@ -37,8 +37,47 @@ PipeImpl::PipeImpl()
 
 PipeImpl::~PipeImpl()
 {
-	closeWrite();  // close write first to send EOF to blocked readers
-	closeRead();
+	close();
+}
+
+
+void PipeImpl::close()
+{
+	// Coordinated close of both pipe ends to unblock any blocked IO.
+	//
+	// On POSIX, a blocked write unblocks with EPIPE when the read end
+	// is closed, and a blocked read returns 0 (EOF) when the write end
+	// is closed. We must close both ends before waiting, because we
+	// don't know which end (if any) has blocked IO.
+	//
+	// This creates a benign race between close(fd) and a concurrent
+	// read(fd)/write(fd) on the same fd: the blocked syscall holds a
+	// kernel reference to the underlying file description, so it
+	// completes safely even after close() removes the fd from the
+	// table (see close(2) on Linux). The fd is cached in a local
+	// variable by readBytes/writeBytes, so fd recycling after close
+	// does not affect the in-progress syscall.
+	//
+	// TSan reports this as a data race (suppressed in tsan.suppress).
+
+	_writeLock.markClosed();
+	_readLock.markClosed();
+
+	// Close write fd — sends EOF to blocked readers.
+	int wfd = _writefd.exchange(-1, std::memory_order_acq_rel);
+	if (wfd != -1)
+		::close(wfd);
+
+	// Wait for reader to finish (it received EOF from closing write end).
+	_readLock.wait();
+
+	// Close read fd — sends EPIPE to blocked writers.
+	int rfd = _readfd.exchange(-1, std::memory_order_acq_rel);
+	if (rfd != -1)
+		::close(rfd);
+
+	// Wait for writer to finish (it received EPIPE from closing read end).
+	_writeLock.wait();
 }
 
 
@@ -48,12 +87,14 @@ int PipeImpl::writeBytes(const void* buffer, int length)
 	if (!lock)
 		return 0;
 
-	poco_assert (_writefd != -1);
+	int fd = _writefd.load(std::memory_order_relaxed);
+	if (fd == -1)
+		return 0;  // fd closed concurrently
 
 	int n;
 	do
 	{
-		n = write(_writefd.load(std::memory_order_relaxed), buffer, length);
+		n = write(fd, buffer, length);
 	}
 	while (n < 0 && errno == EINTR && !lock.isClosed());
 
@@ -73,12 +114,14 @@ int PipeImpl::readBytes(void* buffer, int length)
 	if (!lock)
 		return 0;
 
-	poco_assert (_readfd != -1);
+	int fd = _readfd.load(std::memory_order_relaxed);
+	if (fd == -1)
+		return 0;  // fd closed concurrently
 
 	int n;
 	do
 	{
-		n = read(_readfd.load(std::memory_order_relaxed), buffer, length);
+		n = read(fd, buffer, length);
 	}
 	while (n < 0 && errno == EINTR && !lock.isClosed());
 
@@ -107,20 +150,20 @@ PipeImpl::Handle PipeImpl::writeHandle() const
 void PipeImpl::closeRead()
 {
 	_readLock.markClosed();
-	_readLock.wait();  // wait for any in-progress read to finish
+	_readLock.wait();
 	int fd = _readfd.exchange(-1, std::memory_order_acq_rel);
 	if (fd != -1)
-		close(fd);
+		::close(fd);
 }
 
 
 void PipeImpl::closeWrite()
 {
 	_writeLock.markClosed();
-	_writeLock.wait();  // wait for any in-progress write to finish
+	_writeLock.wait();
 	int fd = _writefd.exchange(-1, std::memory_order_acq_rel);
 	if (fd != -1)
-		close(fd);
+		::close(fd);
 }
 
 
diff --git a/Foundation/src/PipeImpl_WIN32.cpp b/Foundation/src/PipeImpl_WIN32.cpp
index 654cb424a7..4705acfe81 100644
--- a/Foundation/src/PipeImpl_WIN32.cpp
+++ b/Foundation/src/PipeImpl_WIN32.cpp
@@ -14,6 +14,7 @@
 
 #include "Poco/PipeImpl_WIN32.h"
 #include "Poco/Exception.h"
+#include <ioapiset.h>  // CancelIoEx
 
 
 namespace Poco {
@@ -36,8 +37,47 @@ PipeImpl::PipeImpl()
 
 PipeImpl::~PipeImpl()
 {
-	closeWrite();  // close write first to send EOF to blocked readers
-	closeRead();
+	close();
+}
+
+
+void PipeImpl::close()
+{
+	// On Windows, CloseHandle while another thread is in ReadFile/WriteFile
+	// is undefined behavior — unlike POSIX where close(2) is safe due to
+	// kernel file-description refcounting. Use CancelIoEx to cancel pending
+	// IO, wait for completion, then safely close handles.
+	//
+	// CancelIoEx causes the blocked call to return FALSE with
+	// ERROR_OPERATION_ABORTED; the IO thread checks isClosed() and returns 0.
+	// Returns FALSE/ERROR_NOT_FOUND if no pending IO — safe to ignore.
+	//
+	// See: https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-cancelioex
+
+	_writeLock.markClosed();
+	_readLock.markClosed();
+
+	// Cancel any pending IO to unblock blocked ReadFile/WriteFile.
+	HANDLE rh = _readHandle.load(std::memory_order_acquire);
+	if (rh != INVALID_HANDLE_VALUE)
+		CancelIoEx(rh, nullptr);
+
+	HANDLE wh = _writeHandle.load(std::memory_order_acquire);
+	if (wh != INVALID_HANDLE_VALUE)
+		CancelIoEx(wh, nullptr);
+
+	// Wait for IO threads to finish (they received ERROR_OPERATION_ABORTED).
+	_readLock.wait();
+	_writeLock.wait();
+
+	// Safe to close handles — no IO is in progress.
+	rh = _readHandle.exchange(INVALID_HANDLE_VALUE, std::memory_order_acq_rel);
+	if (rh != INVALID_HANDLE_VALUE)
+		CloseHandle(rh);
+
+	wh = _writeHandle.exchange(INVALID_HANDLE_VALUE, std::memory_order_acq_rel);
+	if (wh != INVALID_HANDLE_VALUE)
+		CloseHandle(wh);
 }
 
 
@@ -47,10 +87,12 @@ int PipeImpl::writeBytes(const void* buffer, int length)
 	if (!lock)
 		return 0;
 
-	poco_assert (_writeHandle != INVALID_HANDLE_VALUE);
+	HANDLE handle = _writeHandle.load(std::memory_order_relaxed);
+	if (handle == INVALID_HANDLE_VALUE)
+		return 0;  // handle closed concurrently
 
 	DWORD bytesWritten = 0;
-	BOOL ok = WriteFile(_writeHandle.load(std::memory_order_relaxed), buffer, length, &bytesWritten, nullptr);
+	BOOL ok = WriteFile(handle, buffer, length, &bytesWritten, nullptr);
 
 	if (lock.isClosed())
 		return 0;
@@ -67,10 +109,12 @@ int PipeImpl::readBytes(void* buffer, int length)
 	if (!lock)
 		return 0;
 
-	poco_assert (_readHandle != INVALID_HANDLE_VALUE);
+	HANDLE handle = _readHandle.load(std::memory_order_relaxed);
+	if (handle == INVALID_HANDLE_VALUE)
+		return 0;  // handle closed concurrently
 
 	DWORD bytesRead = 0;
-	BOOL ok = ReadFile(_readHandle.load(std::memory_order_relaxed), buffer, length, &bytesRead, nullptr);
+	BOOL ok = ReadFile(handle, buffer, length, &bytesRead, nullptr);
 	DWORD lastError = GetLastError();
 
 	if (lock.isClosed())
@@ -98,7 +142,7 @@ PipeImpl::Handle PipeImpl::writeHandle() const
 void PipeImpl::closeRead()
 {
 	_readLock.markClosed();
-	_readLock.wait();  // wait for any in-progress read to finish
+	_readLock.wait();
 	HANDLE handle = _readHandle.exchange(INVALID_HANDLE_VALUE, std::memory_order_acq_rel);
 	if (handle != INVALID_HANDLE_VALUE)
 		CloseHandle(handle);
@@ -108,7 +152,7 @@ void PipeImpl::closeRead()
 void PipeImpl::closeWrite()
 {
 	_writeLock.markClosed();
-	_writeLock.wait();  // wait for any in-progress write to finish
+	_writeLock.wait();
 	HANDLE handle = _writeHandle.exchange(INVALID_HANDLE_VALUE, std::memory_order_acq_rel);
 	if (handle != INVALID_HANDLE_VALUE)
 		CloseHandle(handle);
diff --git a/tsan.suppress b/tsan.suppress
index 8701cfc4aa..a16a408b36 100644
--- a/tsan.suppress
+++ b/tsan.suppress
@@ -23,3 +23,11 @@ race:ActiveDispatcherTest
 race:ArchiveStrategy::moveFile
 
 race:ThreadTest::testNotJoin
+
+# Pipe close races with blocked read()/write() on the same fd.
+# This is the standard POSIX mechanism for unblocking pipe IO:
+# close the other end to trigger EPIPE/EOF. The blocked syscall
+# holds a kernel reference to the file description, so it
+# completes safely after close() removes the fd from the table.
+# See close(2) and PipeImpl::close() for details.
+race:PipeImpl::close


Test patch:
diff --git a/Foundation/testsuite/src/PipeTest.cpp b/Foundation/testsuite/src/PipeTest.cpp
index 3958b5104d..ab3516ee76 100644
--- a/Foundation/testsuite/src/PipeTest.cpp
+++ b/Foundation/testsuite/src/PipeTest.cpp
@@ -190,6 +190,53 @@ void PipeTest::testCloseRace()
 }
 
 
+void PipeTest::testCloseBlockedWrite()
+{
+	// Test that close() unblocks a writer that is blocked because the pipe buffer is full.
+	Pipe pipe;
+	std::atomic<bool> writeFinished{false};
+
+	Thread writerThread;
+	writerThread.startFunc([&pipe, &writeFinished]()
+	{
+		// Write enough data to fill the pipe buffer and block.
+		const std::string buffer(100'000, 'A');
+		pipe.writeBytes(buffer.data(), static_cast<int>(buffer.size()));
+		writeFinished.store(true, std::memory_order_release);
+	});
+
+	Thread::sleep(250);  // let the writer block
+
+	pipe.close();  // must not deadlock
+
+	writerThread.join();
+	assertTrue(writeFinished.load(std::memory_order_acquire));
+}
+
+
+void PipeTest::testCloseBlockedRead()
+{
+	// Test that close() unblocks a reader that is blocked waiting for data.
+	Pipe pipe;
+	std::atomic<bool> readFinished{false};
+
+	Thread readerThread;
+	readerThread.startFunc([&pipe, &readFinished]()
+	{
+		char buffer[64];
+		pipe.readBytes(buffer, sizeof(buffer));
+		readFinished.store(true, std::memory_order_release);
+	});
+
+	Thread::sleep(250);  // let the reader block
+
+	pipe.close();  // must not deadlock
+
+	readerThread.join();
+	assertTrue(readFinished.load(std::memory_order_acquire));
+}
+
+
 void PipeTest::setUp()
 {
 }
@@ -210,6 +257,8 @@ CppUnit::Test* PipeTest::suite()
 	CppUnit_addTest(pSuite, PipeTest, testCloseWrite);
 	CppUnit_addTest(pSuite, PipeTest, testCopy);
 	CppUnit_addTest(pSuite, PipeTest, testCloseRace);
+	CppUnit_addTest(pSuite, PipeTest, testCloseBlockedWrite);
+	CppUnit_addTest(pSuite, PipeTest, testCloseBlockedRead);
 
 	return pSuite;
 }
diff --git a/Foundation/testsuite/src/PipeTest.h b/Foundation/testsuite/src/PipeTest.h
index 72da07aa04..fc246bf7e6 100644
--- a/Foundation/testsuite/src/PipeTest.h
+++ b/Foundation/testsuite/src/PipeTest.h
@@ -30,6 +30,8 @@ class PipeTest: public CppUnit::TestCase
 	void testCloseWrite();
 	void testCopy();
 	void testCloseRace();
+	void testCloseBlockedWrite();
+	void testCloseBlockedRead();
 
 	void setUp();
 	void tearDown();