Search code examples
c++windowswinapiwinsock

Socket option SO_BSP_STATE fails with WSAEFAULT


When using the function getsockopt(...) with the level SOL_SOCKET and option SO_BSP_STATE, I am receiving the WSA error code WSAEFAULT, which states the following:

"One of the optval or the optlen parameters is not a valid part of the user address space, or the optlen parameter is too small."

However, I was passing in a correctly sized, user-mode buffer:

/* ... */

HRESULT Result      = E_UNEXPECTED;
CSADDR_INFO Info    = { 0 };                // Placed on the stack.
int InfoSize        = sizeof (CSADDR_INFO); // The size of the input buffer to `getsockopt()`.

// Get the local address information from the raw `SOCKET`.
if (getsockopt (this->WsaSocket,
                SOL_SOCKET,
                SO_BSP_STATE,
                reinterpret_cast <char *> (&Info),
                &InfoSize) == SOCKET_ERROR)
{
    Result = HRESULT_FROM_WIN32 (WSAGetLastError ());
}
else
{
    Result = S_OK;
}

/* ... */

According to the socket option SO_BSP_STATE documentation under the remarks section of the getsockopt(...) function, the return value is of type CSADDR_INFO. Furthermore, the Microsoft documentation page for the SO_BSP_STATE socket option, states the following requirements:
optval:

"[...] This parameter should point to buffer equal to or larger than the size of a CSADDR_INFO structure."

optlen:

"[...] This size must be equal to or larger than the size of a CSADDR_INFO structure."

After doing some research, I stumbled upon some test code from WineHQ that was passing in more memory than sizeof(CSADDR_INFO) when calling getsockopt(...) (see lines 1305 and 1641):

union _csspace
{
    CSADDR_INFO cs;
    char space[128];
} csinfoA, csinfoB;

It also looks like the project ReacOS also references this same exact code (see reference). Even though this is a union, because sizeof(CSADDR_INFO) is always less than 128, csinfoA will always be 128 bytes in size.

Therefore, this got me wondering how many bytes the socket option SO_BSP_STATE actually requires when calling getsockopt(...). I created the following complete example (via Visual Studio 2019 / C++17) that illustrates that in fact SO_BSP_STATE requires a buffer more than sizeof(CSADDR_INFO), which is in direct contrast to the Microsoft published documentation:

/**
 *  @note  This example was created and compiled in Visual Studio 2019.
 */
#define WIN32_LEAN_AND_MEAN

#include <Windows.h>
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>

#pragma comment(lib, "ws2_32.lib")

/**
 *  @brief  The number of bytes to increase the @ref CSADDR_INFO_PLUS_EXTRA_SPACE structure by.
 *  @note   Alignment and pointer size changes when compiling for Intel x86 versus Intel x64.
 *          The extra bytes required therefore vary.
 */
#if defined(_M_X64) || defined(__amd64__)
    #define EXTRA_SPACE (25u) // Required extra space when compiling for X64
#else
    #define EXTRA_SPACE (29u) // Required extra space when compiling for X86
#endif

/**
 *  @brief  A structure to add extra space passed the `CSADDR_INFO` structure.
 */
typedef struct _CSADDR_INFO_PLUS_EXTRA_SPACE
{
    /**
     *  @brief  The standard structure to store Windows Sockets address information.
     */
    CSADDR_INFO Info;

    /**
     *  @brief  A blob of extra space.
     */
    char Extra [EXTRA_SPACE];
} CSADDR_INFO_PLUS_EXTRA_SPACE;

/**
 *  @brief  The main entry function for this console application for demonstrating an issue with `SO_BSP_STATE`.
 */
int main (void)
{
    HRESULT Result                      = S_OK;     // The execution result of this console application.
    SOCKET RawSocket                    = { 0 };    // The raw WSA socket index variable the references the socket's memory.
    WSADATA WindowsSocketsApiDetails    = { 0 };    // The WSA implementation details about the current WSA DLL.
    CSADDR_INFO_PLUS_EXTRA_SPACE Info   = { 0 };    // The structure `CSADDR_INFO` plus an extra blob of memory.
    int InfoSize                        = sizeof (CSADDR_INFO_PLUS_EXTRA_SPACE);

    std::cout << "Main Entry!" << std::endl;

    // Request for the latest Windows Sockets API (WSA) (a.k.a. Winsock) DLL available on this system.
    if (WSAStartup (MAKEWORD(2,2),
                    &WindowsSocketsApiDetails) != 0)
    {
        Result = HRESULT_FROM_WIN32 (WSAGetLastError ());
    }

    // Create a blank TCP socket using IPv4.
    if ((RawSocket = WSASocketW (AF_INET,
                                 SOCK_STREAM,
                                 IPPROTO_TCP,
                                 nullptr,
                                 0,
                                 0)) == INVALID_SOCKET)
    {
        Result = HRESULT_FROM_WIN32 (WSAGetLastError ());
    }
    else
    {
        // Get the local address information from the raw `SOCKET`.
        if (getsockopt (RawSocket,
                        SOL_SOCKET,
                        SO_BSP_STATE,
                        reinterpret_cast <char *> (&Info),
                        &InfoSize) == SOCKET_ERROR)
        {
            std::cout << "Failed obtained the socket's state information!" << std::endl;
            Result = HRESULT_FROM_WIN32 (WSAGetLastError ());
        }
        else
        {
            std::cout << "Successfully obtained the socket's state information!" << std::endl;
            Result = S_OK;
        }
    }

    // Clean up the entire Windows Sockets API (WSA) environment and release the DLL resource.
    if (WSACleanup () != 0)
    {
        Result = HRESULT_FROM_WIN32 (WSAGetLastError ());
    }

    std::cout << "Exit Code: 0x" << std::hex << Result << std::endl;
    return Result;
}

(If you change the EXTRA_SPACE define to equal 0 or 1, then you will see the issue I am outlining.)

Due to the default structure alignment and pointer size change when compiling for either X86 or X64 in Visual Studio 2019, the required extra space beyond the CSADDR_INFO structure can vary:

  • Space required for X86: sizeof(CSADDR_INFO) + 29
  • Space required for X64: sizeof(CSADDR_INFO) + 25

This is completely arbitrary as shown and if you don't add this arbitrary padding, then getsockopt(...) will fail. This makes me call into question if the data I am getting back is even correct. This looks like there might be a missing footnote in the published documentation, however, I very well could be misunderstanding something (very likely this).

My Question(s):

  • What is tied to the buffer size that SO_BSP_STATE actually requires (i.e. a structure, etc.)? Because, it is clearly not sizeof(CSADDR_INFO) as documented.
  • Is the Microsoft documentation incorrect (reference)? If not, what issues are spotted in my above code example, if EXTRA_SPACE is set to 0, in order for getsockopt(...) to be successful?

Solution

  • I think what's happening here is as follows:

    1. CSADDR_INFO is defined like so:
    typedef struct _CSADDR_INFO {
      SOCKET_ADDRESS LocalAddr;
      SOCKET_ADDRESS RemoteAddr;
      INT            iSocketType;
      INT            iProtocol;
    } CSADDR_INFO;
    

    Specifically, it contains two SOCKET_ADDRESS structures.

    1. SOCKET_ADDRESS is defined like so:
    typedef struct _SOCKET_ADDRESS {
      LPSOCKADDR lpSockaddr;
      INT        iSockaddrLength;
    } SOCKET_ADDRESS;
    
    1. The lpSockaddr of the SOCKET_ADDRESS structure is a pointer to a SOCK_ADDR structure, and the length of that varies by address family (ipv4 vs ipv6, for example).

    It follows that getsockopt needs somewhere to store these SOCK_ADDR structures and that's where your 'blob' of extra data comes in - they're in there, pointed to by the two SOCKET_ADDRESS structures. It further follows that the worst-case scenario for the size of this extra data may be more than you are allowing, since, if they are ipv6 addresses, they will be longer than if they are ipv4 addressses.

    Of course, the documentation should spell all of this out, but, as is sometimes the case, the authors probably didn't understand how things work. You might like to raise a bug report.