套接字选项 SO_BSP_STATE 因 WSAEFAULT 而失败

Socket option SO_BSP_STATE fails with WSAEFAULT

当使用具有级别 SOL_SOCKET 和选项 SO_BSP_STATE 的函数 getsockopt(...) 时,我收到 WSA 错误代码 WSAEFAULT,其中说明如下:

"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."

但是,我传入了一个大小正确的用户模式缓冲区:

/* ... */

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;
}

/* ... */

根据套接字选项 SO_BSP_STATE 文档 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,规定了以下要求:
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."

在做了一些研究之后,我偶然发现了一些来自 WineHQ 的测试代码,在调用 getsockopt(...) 时传递的内存比 sizeof(CSADDR_INFO) 多(参见第 1305 and 1641 行):

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

ReacOS 项目似乎也引用了相同的代码 (see reference)。即使这是 union,因为 sizeof(CSADDR_INFO) 总是小于 128csinfoA 的大小总是 128 字节。

因此,这让我想知道调用 getsockopt(...) 时套接字选项 SO_BSP_STATE 实际需要多少字节。我创建了以下完整示例(通过 Visual Studio 2019 / C++17),说明实际上 SO_BSP_STATE 需要的缓冲区多于 sizeof(CSADDR_INFO),这与 Microsoft 发布的直接对比文档:

/**
 *  @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;
}

(如果您将 EXTRA_SPACE 定义更改为等于 01,那么您将看到我概述的问题。 )

由于在 Visual Studio 2019 中为 X86 或 X64 编译时默认结构对齐和指针大小发生变化,CSADDR_INFO 结构之外所需的额外 space 可能会有所不同:

如图所示,这完全是任意的,如果您不添加这个任意填充,那么 getsockopt(...) 将会失败。这让我怀疑我得到的数据是否正确。看起来发布的文档中可能缺少脚注,但是,我很可能误解了一些东西(很可能是这个)。

我的问题:

我认为这里发生的事情如下:

  1. CSADDR_INFO 定义如下:
typedef struct _CSADDR_INFO {
  SOCKET_ADDRESS LocalAddr;
  SOCKET_ADDRESS RemoteAddr;
  INT            iSocketType;
  INT            iProtocol;
} CSADDR_INFO;

具体来说,它包含两个SOCKET_ADDRESS结构。

  1. SOCKET_ADDRESS 定义如下:
typedef struct _SOCKET_ADDRESS {
  LPSOCKADDR lpSockaddr;
  INT        iSockaddrLength;
} SOCKET_ADDRESS;
  1. SOCKET_ADDRESS结构的lpSockaddr是指向SOCK_ADDR结构的指针的长度 因地址系列而异(例如 ipv4 与 ipv6)。

因此 getsockopt 需要在某个地方存储这些 SOCK_ADDR 结构,这就是您的 'blob' 额外数据的来源 - 它们就在那里,由两个指向SOCKET_ADDRESS 结构。进一步得出,对于这些额外数据的大小,最坏的情况可能会超过您所允许的,因为如果它们是 ipv6 地址,它们将比如果它们是 ipv4 地址更长。

当然,文档应该把所有这些都拼写出来,但是,有时是这样,作者可能不明白事情是如何运作的。你可能喜欢 raise a bug report.