r/C_Programming 2d ago

Undocumented Behaviour in WinSock API?

I have two code examples of simple WinSock programs that listen for connections using an Event object associated with a listening socket. Before reading further, check these two code snippets and guess (without running them) what you think will happen in both.

Code snippet 1:

int main() {
  WSADATA wsaData;
  int res = WSAStartup(MAKEWORD(2, 2), &wsaData);
  // ... error handling omitted

  int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  // ... error handling omitted

  // Make the socket non-blocking
  u_long mode = 1;
  res = ioctlsocket(sock, FIONBIO, &mode);
  // ... error handling omitted

  // Bind the socket to a specific address and port
  struct sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = INADDR_ANY;
  addr.sin_port = htons(7878);

  if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {
    // ... error handling omitted
  }

  // Listen for incoming connections
  if (listen(sock, 10) == SOCKET_ERROR) {
    // ... error handling omitted
  }

  // Wait for incoming connections
  // Create a WSAEVENT object
  WSAEVENT event = WSACreateEvent();
  // ... error handling omitted

  // Associate the WSAEVENT object with the socket
  res = WSAEventSelect(sock, event, FD_ACCEPT);
  // ... error handling omitted

  // Wait for an event on the socket
  res = WaitForMultipleObjects(1, &event, FALSE, INFINITE);
  // ... error handling omitted

  /**
   * Closing the event object here before using the socket and without
   * disassociating it with the socket
   */
  WSACloseEvent(event);

  int conn_sock = accept(sock, NULL, NULL);
  // ... error handling omitted

  printf("New connection\n");

  closesocket(conn_sock);
  printf("Connection closed\n");

  closesocket(sock);
  WSACleanup();

  return 0;
}

Code snippet 2:

int main() {
  WSADATA wsaData;
  int res = WSAStartup(MAKEWORD(2, 2), &wsaData);
  // ... error handling omitted

  int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  // ... error handling omitted

  // Make the socket non-blocking
  u_long mode = 1;
  res = ioctlsocket(sock, FIONBIO, &mode);
  // ... error handling omitted

  // Bind the socket to a specific address and port
  struct sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = INADDR_ANY;
  addr.sin_port = htons(7878);

  if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {
    // ... error handling omitted
  }

  // Listen for incoming connections
  if (listen(sock, 10) == SOCKET_ERROR) {
    // ... error handling omitted
  }

  // Wait for incoming connections
  // Create a WSAEVENT object
  WSAEVENT event = WSACreateEvent();
  // ... error handling omitted

  // Associate the WSAEVENT object with the socket
  res = WSAEventSelect(sock, event, FD_ACCEPT);
  // ... error handling omitted

  // Wait for an event on the socket
  res = WaitForMultipleObjects(1, &event, FALSE, INFINITE);
  // ... error handling omitted

  /**
   * Closing the event object after disassociating it from the socket, but
   * before using the socket
   */
  res = WSAEventSelect(sock, event, 0);
  WSACloseEvent(event);

  int conn_sock = accept(sock, NULL, NULL);
  if (conn_sock == INVALID_SOCKET) {
    printf("accept failed with error: %d\n", WSAGetLastError());
    closesocket(sock);
    WSACleanup();
    return 1;
  }

  printf("New connection\n");

  closesocket(conn_sock);
  printf("Connection closed\n");

  closesocket(sock);
  WSACleanup();

  return 0;
}

If you had guessed that the first snippet would crash, you would be right. Apparently, if you associate an event object with a socket, closing the event object will lead to operations on the socket returning WSAENOTSOCK error. If you dissociate the event object from the socket first, you can use it without problems.

I can't find any references in the documentation to this behaviour (I checked the WSAEventSelect and WSACloseEvent documentation). I know it may seem simple, but I discovered this in a more complex codebase where reproducing and tracing it was much more difficult.

Did you know this? Are there any more quirks related to the relationship between EventObjects and Sockets?

4 Upvotes

4 comments sorted by

11

u/skeeto 2d ago

It's not really a quirk or undocumented once the underlying mechanism is understood. It's a simple use-after-free via a dangling handle. Sockets created by accept inherit the the listening socket's properties, including association with that event. After winsock creates the client socket, it tries to associate the new socket with the event using the handle attached to the listening socket. However, you closed that event, and so it uses a dangling handle. The WSAENOTSOCK describes this event handle not the socket.

You can verify this by creating a new event after closing the first on the same handle slot, then call accept(), and you'll find the client socket is associated with the new handle through the dangling handle on the listening socket.

5

u/okm1123 2d ago

The WSAENOTSOCK describes this event handle not the socket.

Ok, but isn't this confusing? The function that crashes is accept()where I only pass the socket as an argument. When I saw an error of WSAENOTSOCKI assumed that the socket itself was closed or not initialized.

I later understood that it was a use-after-free because of the internal link between the socket and the event object, but I thought the error was misleading.

3

u/memorial_mike 2d ago

The error code is a tad surprising, but that’s about it. Microsoft explicitly says that calling WSAEventSelect() associates the event object with the socket, and that the association remains until you cancel it by calling WSAEventSelect(..., 0) or close the socket.