Commit 7b41a8ee for guacamole.apache.org

commit 7b41a8ee3b6b9cea8cff27d777f67803e6246296
Author: Michael Jumper <mjumper@apache.org>
Date:   Fri Sep 25 18:08:50 2026 -0700

    GUACAMOLE-2118: Ensure socket I/O cannot block indefinitely.

diff --git a/src/libguac/guacamole/socket-fntypes.h b/src/libguac/guacamole/socket-fntypes.h
index daf96d0a..de5d95b4 100644
--- a/src/libguac/guacamole/socket-fntypes.h
+++ b/src/libguac/guacamole/socket-fntypes.h
@@ -85,7 +85,9 @@ typedef ssize_t guac_socket_flush_handler(guac_socket* socket);
 /**
  * When set within a guac_socket, a handler of this type will be called
  * whenever exclusive access to the guac_socket is required, such as when
- * guac_socket_instruction_begin() is called.
+ * guac_socket_instruction_begin() is called. Locking the socket with a handler
+ * of this type does not prevent the socket's shutdown handler from being
+ * called.
  *
  * @param socket
  *     The guac_socket to which exclusive access is required.
@@ -112,5 +114,16 @@ typedef void guac_socket_unlock_handler(guac_socket* socket);
  */
 typedef int guac_socket_free_handler(guac_socket* socket);

+/**
+ * Generic handler for the cancellation of all pending and future operations
+ * on a socket, modeled after the standard POSIX shutdown() function. When set
+ * within a guac_socket, a handler of this type will be called when the socket
+ * is shut down using guac_socket_shutdown(), and may be called more than once.
+ *
+ * @param socket
+ *     The guac_socket whose operations should be canceled.
+ */
+typedef void guac_socket_shutdown_handler(guac_socket* socket);
+
 #endif

diff --git a/src/libguac/guacamole/socket.h b/src/libguac/guacamole/socket.h
index 6075f39b..774a8a98 100644
--- a/src/libguac/guacamole/socket.h
+++ b/src/libguac/guacamole/socket.h
@@ -118,6 +118,12 @@ struct guac_socket {
      */
     pthread_t __keep_alive_thread;

+    /**
+     * Handler which will be called whenever guac_socket_shutdown() is invoked
+     * on this socket.
+     */
+    guac_socket_shutdown_handler* shutdown_handler;
+
 };

 /**
@@ -129,6 +135,19 @@ struct guac_socket {
  */
 guac_socket* guac_socket_alloc();

+/**
+ * Cancels all pending, active, and future operations on the given guac_socket.
+ * All such operations will cease blocking and fail immediately. The socket
+ * itself remains allocated and must still be freed with guac_socket_free().
+ *
+ * This function may be safely called more than once.
+ *
+ * @param socket
+ *     The guac_socket whose pending, active, and future operations should be
+ *     canceled.
+ */
+void guac_socket_shutdown(guac_socket* socket);
+
 /**
  * Frees the given guac_socket and all associated resources.
  *
diff --git a/src/libguac/recording.c b/src/libguac/recording.c
index 2cfcdf39..69294b2a 100644
--- a/src/libguac/recording.c
+++ b/src/libguac/recording.c
@@ -69,9 +69,26 @@ guac_recording* guac_recording_create(guac_client* client,
         return NULL;
     }

+    /* Wrap recording file in socket */
+    guac_socket* socket = guac_socket_open(fd);
+    if (socket == NULL) {
+        guac_client_log(client, GUAC_LOG_ERROR, "Creation of recording "
+                "failed: %s: %s", guac_error_message,
+                guac_status_string(guac_error));
+        close(fd);
+        return NULL;
+    }
+
     /* Create recording structure with reference to underlying socket */
     guac_recording* recording = guac_mem_alloc(sizeof(guac_recording));
-    recording->socket = guac_socket_open(fd);
+    if (recording == NULL) {
+        guac_client_log(client, GUAC_LOG_ERROR, "Creation of recording "
+                "failed: %s", guac_status_string(guac_error));
+        guac_socket_free(socket);
+        return NULL;
+    }
+
+    recording->socket = socket;
     recording->include_output = include_output;
     recording->include_mouse = include_mouse;
     recording->include_touch = include_touch;
@@ -79,8 +96,20 @@ guac_recording* guac_recording_create(guac_client* client,

     /* Replace client socket with wrapped recording socket only if including
      * output within the recording */
-    if (include_output)
-        client->socket = guac_socket_tee(client->socket, recording->socket);
+    if (include_output) {
+
+        guac_socket* client_socket = guac_socket_tee(client->socket, recording->socket);
+        if (client_socket == NULL) {
+            guac_client_log(client, GUAC_LOG_ERROR, "Creation of recording "
+                    "failed: %s", guac_status_string(guac_error));
+            guac_socket_free(recording->socket);
+            guac_mem_free(recording);
+            return NULL;
+        }
+
+        client->socket = client_socket;
+
+    }

     /* Recording creation succeeded */
     guac_client_log(client, GUAC_LOG_INFO, "Recording of session will be "
diff --git a/src/libguac/socket-broadcast.c b/src/libguac/socket-broadcast.c
index b87a6717..fe9d98e5 100644
--- a/src/libguac/socket-broadcast.c
+++ b/src/libguac/socket-broadcast.c
@@ -313,6 +313,37 @@ static void __guac_socket_broadcast_unlock_handler(guac_socket* socket) {

 }

+/**
+ * Callback which is invoked by the broadcast handler to cancel all pending
+ * and future operations on the given user's socket.
+ *
+ * @param user
+ *     The user whose socket should be shut down.
+ *
+ * @param data
+ *     Arbitrary data passed to the broadcast handler. This is not needed
+ *     by this callback, and should be left as NULL.
+ *
+ * @return
+ *     Always NULL.
+ */
+static void* __shutdown_callback(guac_user* user, void* data) {
+    guac_socket_shutdown(user->socket);
+    return NULL;
+}
+
+/**
+ * Shutdown handler which cancels all pending and future operations on the
+ * sockets of all broadcasted users.
+ *
+ * @param socket
+ *     The broadcast socket to shut down.
+ */
+static void __guac_socket_broadcast_shutdown_handler(guac_socket* socket) {
+    guac_socket_broadcast_data* data = (guac_socket_broadcast_data*) socket->data;
+    data->broadcast_handler(data->client, __shutdown_callback, NULL);
+}
+
 /**
  * Callback which handles select operations on the broadcast socket, waiting
  * for data to become available such that the next read operation will not
@@ -402,13 +433,14 @@ static guac_socket* __guac_socket_init(
     pthread_mutex_init(&(data->socket_lock), &lock_attributes);

     /* Set read/write handlers */
-    socket->read_handler   = __guac_socket_broadcast_read_handler;
-    socket->write_handler  = __guac_socket_broadcast_write_handler;
-    socket->select_handler = __guac_socket_broadcast_select_handler;
-    socket->flush_handler  = __guac_socket_broadcast_flush_handler;
-    socket->lock_handler   = __guac_socket_broadcast_lock_handler;
-    socket->unlock_handler = __guac_socket_broadcast_unlock_handler;
-    socket->free_handler   = __guac_socket_broadcast_free_handler;
+    socket->read_handler     = __guac_socket_broadcast_read_handler;
+    socket->write_handler    = __guac_socket_broadcast_write_handler;
+    socket->select_handler   = __guac_socket_broadcast_select_handler;
+    socket->flush_handler    = __guac_socket_broadcast_flush_handler;
+    socket->lock_handler     = __guac_socket_broadcast_lock_handler;
+    socket->unlock_handler   = __guac_socket_broadcast_unlock_handler;
+    socket->free_handler     = __guac_socket_broadcast_free_handler;
+    socket->shutdown_handler = __guac_socket_broadcast_shutdown_handler;

     return socket;

diff --git a/src/libguac/socket-fd.c b/src/libguac/socket-fd.c
index e8d8707c..c2517ee1 100644
--- a/src/libguac/socket-fd.c
+++ b/src/libguac/socket-fd.c
@@ -34,6 +34,8 @@

 #ifdef ENABLE_WINSOCK
 #include <winsock2.h>
+#else
+#include <sys/socket.h>
 #endif

 /**
@@ -51,6 +53,13 @@ typedef struct guac_socket_fd_data {
      */
     int written;

+    /**
+     * Whether a write has failed, thus invalidating further use of the socket.
+     * If a failed write has occurred, this value is set to the value of errno
+     * associated with that failure.
+     */
+    int failed_errno;
+
     /**
      * The main write buffer. Bytes written go here before being flushed
      * to the open file descriptor.
@@ -74,7 +83,8 @@ typedef struct guac_socket_fd_data {
 /**
  * Writes the entire contents of the given buffer to the file descriptor
  * associated with the given socket, retrying as necessary until the whole
- * buffer is written, and aborting if an error occurs.
+ * buffer is written, and aborting if an error occurs. If an error occurs, errno
+ * and the failed_errno member of guac_socket_fd_data are set appropriately.
  *
  * @param socket
  *     The guac_socket associated with the file descriptor to which the given
@@ -111,6 +121,11 @@ ssize_t guac_socket_fd_write(guac_socket* socket,

         /* Record errors in guac_error */
         if (retval < 0) {
+#ifdef ENABLE_WINSOCK
+            data->failed_errno = errno = WSAGetLastError();
+#else
+            data->failed_errno = errno;
+#endif
             guac_error = GUAC_STATUS_SEE_ERRNO;
             guac_error_message = "Error writing data to socket";
             return retval;
@@ -182,14 +197,28 @@ static ssize_t guac_socket_fd_flush(guac_socket* socket) {

     guac_socket_fd_data* data = (guac_socket_fd_data*) socket->data;

+    /* Drop any further writes to failed socket (instruction stream is no longer
+     * valid) */
+    if (data->failed_errno) {
+        errno = data->failed_errno;
+        guac_error = GUAC_STATUS_SEE_ERRNO;
+        guac_error_message = "Error writing data to socket";
+        return 1;
+    }
+
     /* Flush remaining bytes in buffer */
     if (data->written > 0) {

         /* Write ALL bytes in buffer immediately */
-        if (guac_socket_fd_write(socket, data->out_buf, data->written))
-            return 1;
+        int flush_failed = guac_socket_fd_write(socket, data->out_buf, data->written);

+        /* All outstanding content in the buffer is either fully written or no
+         * longer valid (failed/partial write) */
         data->written = 0;
+
+        if (flush_failed)
+            return 1;
+
     }

     return 0;
@@ -250,6 +279,14 @@ static ssize_t guac_socket_fd_write_buffered(guac_socket* socket,
     const char* current = buf;
     guac_socket_fd_data* data = (guac_socket_fd_data*) socket->data;

+    /* Refuse further writes/buffering if the stream is already broken */
+    if (data->failed_errno) {
+        errno = data->failed_errno;
+        guac_error = GUAC_STATUS_SEE_ERRNO;
+        guac_error_message = "Error writing data to socket";
+        return -1;
+    }
+
     /* Append to buffer, flush if necessary */
     while (count > 0) {

@@ -361,6 +398,25 @@ static int guac_socket_fd_select_handler(guac_socket* socket,

 }

+/**
+ * Cancels all pending and future operations on the given socket by shutting
+ * down the underlying file descriptor.
+ *
+ * @param socket
+ *     The guac_socket whose operations should be canceled.
+ */
+static void guac_socket_fd_shutdown_handler(guac_socket* socket) {
+
+    guac_socket_fd_data* data = (guac_socket_fd_data*) socket->data;
+
+#ifdef ENABLE_WINSOCK
+    shutdown(data->fd, SD_BOTH);
+#else
+    shutdown(data->fd, SHUT_RDWR);
+#endif
+
+}
+
 /**
  * Frees all implementation-specific data associated with the given socket, but
  * not the socket object itself.
@@ -424,11 +480,21 @@ guac_socket* guac_socket_open(int fd) {

     /* Allocate socket and associated data */
     guac_socket* socket = guac_socket_alloc();
+    if (socket == NULL)
+        return NULL;
+
     guac_socket_fd_data* data = guac_mem_alloc(sizeof(guac_socket_fd_data));
+    if (data == NULL) {
+        guac_mem_free(socket);
+        guac_error = GUAC_STATUS_NO_MEMORY;
+        guac_error_message = "Could not allocate memory for socket data";
+        return NULL;
+    }

     /* Store file descriptor as socket data */
     data->fd = fd;
     data->written = 0;
+    data->failed_errno = 0;
     socket->data = data;

     pthread_mutexattr_init(&lock_attributes);
@@ -439,13 +505,14 @@ guac_socket* guac_socket_open(int fd) {
     pthread_mutex_init(&(data->buffer_lock), &lock_attributes);

     /* Set read/write handlers */
-    socket->read_handler   = guac_socket_fd_read_handler;
-    socket->write_handler  = guac_socket_fd_write_handler;
-    socket->select_handler = guac_socket_fd_select_handler;
-    socket->lock_handler   = guac_socket_fd_lock_handler;
-    socket->unlock_handler = guac_socket_fd_unlock_handler;
-    socket->flush_handler  = guac_socket_fd_flush_handler;
-    socket->free_handler   = guac_socket_fd_free_handler;
+    socket->read_handler     = guac_socket_fd_read_handler;
+    socket->write_handler    = guac_socket_fd_write_handler;
+    socket->select_handler   = guac_socket_fd_select_handler;
+    socket->lock_handler     = guac_socket_fd_lock_handler;
+    socket->unlock_handler   = guac_socket_fd_unlock_handler;
+    socket->flush_handler    = guac_socket_fd_flush_handler;
+    socket->free_handler     = guac_socket_fd_free_handler;
+    socket->shutdown_handler = guac_socket_fd_shutdown_handler;

     return socket;

diff --git a/src/libguac/socket-ssl.c b/src/libguac/socket-ssl.c
index 1e295f1e..c0ece9e5 100644
--- a/src/libguac/socket-ssl.c
+++ b/src/libguac/socket-ssl.c
@@ -25,6 +25,11 @@

 #include <pthread.h>
 #include <stdlib.h>
+#ifdef ENABLE_WINSOCK
+#include <winsock2.h>
+#else
+#include <sys/socket.h>
+#endif

 #include <openssl/ssl.h>

@@ -56,16 +61,59 @@ typedef struct guac_socket_ssl_data {
      */
     pthread_mutex_t socket_lock;

+    /**
+     * Lock which serializes all use of the SSL connection. OpenSSL does not
+     * allow a connection to be read and written at the same time.
+     */
+    pthread_mutex_t ssl_lock;
+
 } guac_socket_ssl_data;

+/**
+ * Returns whether data already decrypted by OpenSSL awaits a read from the
+ * given SSL socket.
+ *
+ * @param socket
+ *     The guac_socket to test.
+ *
+ * @return
+ *     Non-zero if decrypted data awaits a read, zero otherwise.
+ */
+static int __guac_socket_ssl_pending(guac_socket* socket) {
+
+    guac_socket_ssl_data* data = (guac_socket_ssl_data*) socket->data;
+
+    pthread_mutex_lock(&(data->ssl_lock));
+    int pending = SSL_pending(data->ssl);
+    pthread_mutex_unlock(&(data->ssl_lock));
+
+    return pending > 0;
+
+}
+
 static ssize_t __guac_socket_ssl_read_handler(guac_socket* socket,
         void* buf, size_t count) {

-    /* Read from socket */
     guac_socket_ssl_data* data = (guac_socket_ssl_data*) socket->data;
     int retval;

+    /* Wait for input before locking the SSL connection (SSL_read() must not
+     * block while we hold ssl_lock or writes will be blocked, as well) */
+    if (!__guac_socket_ssl_pending(socket)) {
+        char peeked;
+        int received;
+        GUAC_RETRY_EINTR(received, recv(data->fd, &peeked, 1, MSG_PEEK));
+        if (received < 0) {
+            guac_error = GUAC_STATUS_SEE_ERRNO;
+            guac_error_message = "Error reading data from secure socket";
+            return -1;
+        }
+    }
+
+    /* Read from socket */
+    pthread_mutex_lock(&(data->ssl_lock));
     retval = SSL_read(data->ssl, buf, count);
+    pthread_mutex_unlock(&(data->ssl_lock));

     /* Record errors in guac_error */
     if (retval <= 0) {
@@ -84,7 +132,9 @@ static ssize_t __guac_socket_ssl_write_handler(guac_socket* socket,
     guac_socket_ssl_data* data = (guac_socket_ssl_data*) socket->data;
     int retval;

+    pthread_mutex_lock(&(data->ssl_lock));
     retval = SSL_write(data->ssl, buf, count);
+    pthread_mutex_unlock(&(data->ssl_lock));

     /* Record errors in guac_error */
     if (retval <= 0) {
@@ -99,6 +149,12 @@ static ssize_t __guac_socket_ssl_write_handler(guac_socket* socket,
 static int __guac_socket_ssl_select_handler(guac_socket* socket, int usec_timeout) {

     guac_socket_ssl_data* data = (guac_socket_ssl_data*) socket->data;
+
+    /* Data already decrypted by OpenSSL is not visible to a wait on the
+     * descriptor */
+    if (__guac_socket_ssl_pending(socket))
+        return 1;
+
     int retval = guac_wait_for_fd(data->fd, usec_timeout);

     /* Properly set guac_error */
@@ -116,6 +172,18 @@ static int __guac_socket_ssl_select_handler(guac_socket* socket, int usec_timeou

 }

+static void __guac_socket_ssl_shutdown_handler(guac_socket* socket) {
+
+    guac_socket_ssl_data* data = (guac_socket_ssl_data*) socket->data;
+
+#ifdef ENABLE_WINSOCK
+    shutdown(data->fd, SD_BOTH);
+#else
+    shutdown(data->fd, SHUT_RDWR);
+#endif
+
+}
+
 static int __guac_socket_ssl_free_handler(guac_socket* socket) {

     /* Shutdown SSL */
@@ -127,6 +195,7 @@ static int __guac_socket_ssl_free_handler(guac_socket* socket) {
     close(data->fd);

     pthread_mutex_destroy(&(data->socket_lock));
+    pthread_mutex_destroy(&(data->ssl_lock));

     guac_mem_free(data);
     return 0;
@@ -194,18 +263,20 @@ guac_socket* guac_socket_open_secure(SSL_CTX* context, int fd) {
     pthread_mutexattr_init(&lock_attributes);
     pthread_mutexattr_setpshared(&lock_attributes, PTHREAD_PROCESS_SHARED);
     pthread_mutex_init(&(data->socket_lock), &lock_attributes);
+    pthread_mutex_init(&(data->ssl_lock), &lock_attributes);

     /* Store file descriptor as socket data */
     data->fd = fd;
     socket->data = data;

     /* Set read/write handlers */
-    socket->read_handler   = __guac_socket_ssl_read_handler;
-    socket->write_handler  = __guac_socket_ssl_write_handler;
-    socket->select_handler = __guac_socket_ssl_select_handler;
-    socket->free_handler   = __guac_socket_ssl_free_handler;
-    socket->lock_handler   = __guac_socket_ssl_lock_handler;
-    socket->unlock_handler = __guac_socket_ssl_unlock_handler;
+    socket->read_handler     = __guac_socket_ssl_read_handler;
+    socket->write_handler    = __guac_socket_ssl_write_handler;
+    socket->select_handler   = __guac_socket_ssl_select_handler;
+    socket->free_handler     = __guac_socket_ssl_free_handler;
+    socket->shutdown_handler = __guac_socket_ssl_shutdown_handler;
+    socket->lock_handler     = __guac_socket_ssl_lock_handler;
+    socket->unlock_handler   = __guac_socket_ssl_unlock_handler;

     return socket;

diff --git a/src/libguac/socket-tee.c b/src/libguac/socket-tee.c
index c6450b70..473b79ed 100644
--- a/src/libguac/socket-tee.c
+++ b/src/libguac/socket-tee.c
@@ -124,8 +124,8 @@ static ssize_t __guac_socket_tee_flush_handler(guac_socket* socket) {
 }

 /**
- * Callback function which delegates the lock operation to the primary
- * socket alone.
+ * Callback function which delegates the lock operation to both underlying
+ * sockets.
  *
  * @param socket
  *     The tee socket on which guac_socket_instruction_begin() was invoked.
@@ -141,8 +141,8 @@ static void __guac_socket_tee_lock_handler(guac_socket* socket) {
 }

 /**
- * Callback function which delegates the unlock operation to the primary
- * socket alone.
+ * Callback function which delegates the unlock operation to both underlying
+ * sockets.
  *
  * @param socket
  *     The tee socket on which guac_socket_instruction_end() was invoked.
@@ -182,6 +182,22 @@ static int __guac_socket_tee_select_handler(guac_socket* socket,

 }

+/**
+ * Callback function which delegates the shutdown operation to both underlying
+ * sockets.
+ *
+ * @param socket
+ *     The tee socket whose underlying sockets should be shut down.
+ */
+static void __guac_socket_tee_shutdown_handler(guac_socket* socket) {
+
+    guac_socket_tee_data* data = (guac_socket_tee_data*) socket->data;
+
+    guac_socket_shutdown(data->primary);
+    guac_socket_shutdown(data->secondary);
+
+}
+
 /**
  * Callback function which frees all underlying data associated with the
  * given tee socket, including both primary and secondary sockets.
@@ -218,13 +234,14 @@ guac_socket* guac_socket_tee(guac_socket* primary, guac_socket* secondary) {
     socket->data = data;

     /* Assign handlers */
-    socket->read_handler   = __guac_socket_tee_read_handler;
-    socket->write_handler  = __guac_socket_tee_write_handler;
-    socket->select_handler = __guac_socket_tee_select_handler;
-    socket->flush_handler  = __guac_socket_tee_flush_handler;
-    socket->lock_handler   = __guac_socket_tee_lock_handler;
-    socket->unlock_handler = __guac_socket_tee_unlock_handler;
-    socket->free_handler   = __guac_socket_tee_free_handler;
+    socket->read_handler     = __guac_socket_tee_read_handler;
+    socket->write_handler    = __guac_socket_tee_write_handler;
+    socket->select_handler   = __guac_socket_tee_select_handler;
+    socket->flush_handler    = __guac_socket_tee_flush_handler;
+    socket->lock_handler     = __guac_socket_tee_lock_handler;
+    socket->unlock_handler   = __guac_socket_tee_unlock_handler;
+    socket->free_handler     = __guac_socket_tee_free_handler;
+    socket->shutdown_handler = __guac_socket_tee_shutdown_handler;

     return socket;

diff --git a/src/libguac/socket-wsa.c b/src/libguac/socket-wsa.c
index ec17d827..3bccda31 100644
--- a/src/libguac/socket-wsa.c
+++ b/src/libguac/socket-wsa.c
@@ -21,6 +21,7 @@
 #include "guacamole/error.h"
 #include "guacamole/socket.h"

+#include <errno.h>
 #include <pthread.h>
 #include <stddef.h>
 #include <stdio.h>
@@ -45,6 +46,13 @@ typedef struct guac_socket_wsa_data {
      */
     int written;

+    /**
+     * Whether a write has failed, thus invalidating further use of the
+     * socket. If a failed write has occurred, this value is set to the error
+     * code reported by WSAGetLastError() for that failure.
+     */
+    int failed_wsa_error;
+
     /**
      * The main write buffer. Bytes written go here before being flushed
      * to the open socket.
@@ -68,7 +76,9 @@ typedef struct guac_socket_wsa_data {
 /**
  * Writes the entire contents of the given buffer to the SOCKET handle
  * associated with the given socket, retrying as necessary until the whole
- * buffer is written, and aborting if an error occurs.
+ * buffer is written, and aborting if an error occurs. If an error occurs, errno
+ * and the failed_wsa_error member of guac_socket_wsa_data are set to the error
+ * code reported by WSAGetLastError().
  *
  * @param socket
  *     The guac_socket associated with the SOCKET handle to which the given
@@ -99,6 +109,7 @@ ssize_t guac_socket_wsa_write(guac_socket* socket,

         /* Record errors in guac_error */
         if (retval < 0) {
+            data->failed_wsa_error = errno = WSAGetLastError();
             guac_error = GUAC_STATUS_SEE_ERRNO;
             guac_error_message = "Error writing data to socket";
             return retval;
@@ -165,14 +176,29 @@ static ssize_t guac_socket_wsa_flush(guac_socket* socket) {

     guac_socket_wsa_data* data = (guac_socket_wsa_data*) socket->data;

+    /* Drop any further writes to failed socket (instruction stream is no longer
+     * valid) */
+    if (data->failed_wsa_error) {
+        WSASetLastError(data->failed_wsa_error);
+        errno = data->failed_wsa_error;
+        guac_error = GUAC_STATUS_SEE_ERRNO;
+        guac_error_message = "Error writing data to socket";
+        return 1;
+    }
+
     /* Flush remaining bytes in buffer */
     if (data->written > 0) {

         /* Write ALL bytes in buffer immediately */
-        if (guac_socket_wsa_write(socket, data->out_buf, data->written))
-            return 1;
+        int flush_failed = guac_socket_wsa_write(socket, data->out_buf, data->written);

+        /* All outstanding content in the buffer is either fully written or no
+         * longer valid (failed/partial write) */
         data->written = 0;
+
+        if (flush_failed)
+            return 1;
+
     }

     return 0;
@@ -233,6 +259,15 @@ static ssize_t guac_socket_wsa_write_buffered(guac_socket* socket,
     const char* current = buf;
     guac_socket_wsa_data* data = (guac_socket_wsa_data*) socket->data;

+    /* Refuse further writes/buffering if the stream is already broken */
+    if (data->failed_wsa_error) {
+        WSASetLastError(data->failed_wsa_error);
+        errno = data->failed_wsa_error;
+        guac_error = GUAC_STATUS_SEE_ERRNO;
+        guac_error_message = "Error writing data to socket";
+        return -1;
+    }
+
     /* Append to buffer, flush if necessary */
     while (count > 0) {

@@ -368,6 +403,18 @@ static int guac_socket_wsa_select_handler(guac_socket* socket,

 }

+/**
+ * Cancels all pending and future operations on the given socket by shutting
+ * down the underlying Windows socket in both directions.
+ *
+ * @param socket
+ *     The guac_socket whose operations should be canceled.
+ */
+static void guac_socket_wsa_shutdown_handler(guac_socket* socket) {
+    guac_socket_wsa_data* data = (guac_socket_wsa_data*) socket->data;
+    shutdown(data->sock, SD_BOTH);
+}
+
 /**
  * Frees all implementation-specific data associated with the given socket, but
  * not the socket object itself.
@@ -436,6 +483,7 @@ guac_socket* guac_socket_open_wsa(SOCKET sock) {
     /* Store socket as socket data */
     data->sock = sock;
     data->written = 0;
+    data->failed_wsa_error = 0;
     socket->data = data;

     pthread_mutexattr_init(&lock_attributes);
@@ -446,13 +494,14 @@ guac_socket* guac_socket_open_wsa(SOCKET sock) {
     pthread_mutex_init(&(data->buffer_lock), &lock_attributes);

     /* Set read/write handlers */
-    socket->read_handler   = guac_socket_wsa_read_handler;
-    socket->write_handler  = guac_socket_wsa_write_handler;
-    socket->select_handler = guac_socket_wsa_select_handler;
-    socket->lock_handler   = guac_socket_wsa_lock_handler;
-    socket->unlock_handler = guac_socket_wsa_unlock_handler;
-    socket->flush_handler  = guac_socket_wsa_flush_handler;
-    socket->free_handler   = guac_socket_wsa_free_handler;
+    socket->read_handler     = guac_socket_wsa_read_handler;
+    socket->write_handler    = guac_socket_wsa_write_handler;
+    socket->select_handler   = guac_socket_wsa_select_handler;
+    socket->lock_handler     = guac_socket_wsa_lock_handler;
+    socket->unlock_handler   = guac_socket_wsa_unlock_handler;
+    socket->flush_handler    = guac_socket_wsa_flush_handler;
+    socket->free_handler     = guac_socket_wsa_free_handler;
+    socket->shutdown_handler = guac_socket_wsa_shutdown_handler;

     return socket;

diff --git a/src/libguac/socket.c b/src/libguac/socket.c
index 1d51168a..d908721e 100644
--- a/src/libguac/socket.c
+++ b/src/libguac/socket.c
@@ -166,13 +166,14 @@ guac_socket* guac_socket_alloc() {
     socket->__keep_alive_enabled = 0;

     /* No handlers yet */
-    socket->read_handler   = NULL;
-    socket->write_handler  = NULL;
-    socket->select_handler = NULL;
-    socket->free_handler   = NULL;
-    socket->flush_handler  = NULL;
-    socket->lock_handler   = NULL;
-    socket->unlock_handler = NULL;
+    socket->read_handler     = NULL;
+    socket->write_handler    = NULL;
+    socket->select_handler   = NULL;
+    socket->free_handler     = NULL;
+    socket->shutdown_handler = NULL;
+    socket->flush_handler    = NULL;
+    socket->lock_handler     = NULL;
+    socket->unlock_handler   = NULL;

     return socket;

@@ -203,13 +204,15 @@ void guac_socket_instruction_end(guac_socket* socket) {

 }

-void guac_socket_free(guac_socket* socket) {
+void guac_socket_shutdown(guac_socket* socket) {

-    guac_socket_flush(socket);
+    /* Call shutdown handler if defined */
+    if (socket->shutdown_handler)
+        socket->shutdown_handler(socket);

-    /* Call free handler if defined */
-    if (socket->free_handler)
-        socket->free_handler(socket);
+}
+
+void guac_socket_free(guac_socket* socket) {

     /* Mark as closed */
     socket->state = GUAC_SOCKET_CLOSED;
@@ -220,6 +223,12 @@ void guac_socket_free(guac_socket* socket) {
         pthread_join(socket->__keep_alive_thread, NULL);
     }

+    guac_socket_flush(socket);
+
+    /* Call free handler if defined */
+    if (socket->free_handler)
+        socket->free_handler(socket);
+
     guac_mem_free(socket);
 }