套接字选项 SO_BSP_STATE 因 WSAEFAULT

问题描述

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

"optvaloptlen 参数之一不是用户地址空间的有效部分,或者 optlen 参数是太小了。"

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

/* ... */

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函数的socket选项getsockopt(...)文档under the remarks section,返回值是CSADDR_INFO类型。此外,SO_BSP_STATE socket option 的 Microsoft 文档页面规定了以下要求:
optval

"[...] 此参数应指向等于或大于 CSADDR_INFO 结构大小的缓冲区。"

optlen

“[...] 此大小必须等于或大于 CSADDR_INFO 结构的大小。”

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

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

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

因此,这让我想知道调用 SO_BSP_STATE套接字选项 getsockopt(...) 实际需要多少字节。我创建了以下完整示例(通过 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)) == INVALID_SOCKET)
    {
        Result = HRESULT_FROM_WIN32 (WSAGetLastError ());
    }
    else
    {
        // Get the local address information from the raw `SOCKET`.
        if (getsockopt (RawSocket,&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 结构之外所需的额外空间可能会有所不同:

  • X86 所需的空间:sizeof(CSADDR_INFO) + 29
  • X64 所需的空间:sizeof(CSADDR_INFO) + 25

如图所示,这是完全任意的,如果您不添加此任意填充,则 getsockopt(...) 将失败。这让我怀疑我返回的数据是否正确。这看起来在已发布的文档中可能缺少脚注,但是,我很可能误解了某些内容很可能)。

我的问题:

  • SO_BSP_STATE 实际需要的缓冲区大小(即结构等)有什么关系?因为,它显然 sizeof(CSADDR_INFO) 如文档所述。
  • Microsoft 文档是否不正确 (reference)?如果不是,如果EXTRA_SPACE 设置为0,为了使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. lpSockaddr 结构的 SOCKET_ADDRESS 是一个指向 SOCK_ADDR 结构的指针那个的长度因地址族(例如 ipv4 与 ipv6)。

因此 getsockopt 需要某个地方来存储这些 SOCK_ADDR 结构,这就是您的额外数据“blob”所在的地方 - 它们就在那里,由两个 {{1} } 结构。进一步推断,这些额外数据大小的最坏情况可能会超出您的允许范围,因为如果它们是 ipv6 地址,它们将比它们是 ipv4 地址时更长。

当然,文档应该说明所有这些,但是,有时情况下,作者可能不了解事情是如何运作的。您可能喜欢raise a bug report