关于 getsockname 的联机帮助页和内核行为不匹配

Mismatch between manpage and kernel behavior about getsockname

我最近在尝试 运行 iperf3 时遇到了堆栈崩溃(= 缓冲区溢出)问题。我查明了 getsockname() 调用 (https://github.com/esnet/iperf/blob/master/src/net.c#L463) 的原因,它使内核在设计地址 (&sa) 复制比变量大小更多的数据 (sizeof(sin_addr))在该地址的堆栈上。 getsockname() 将呼叫重定向到 getname()AF_INET 家族): https://github.com/torvalds/linux/blob/master/net/ipv4/af_inet.c#L698

如果我相信联机帮助页 (ubuntu) 它说:

int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);  

The addrlen argument should be initialized to indicate the amount of space (in bytes) pointed to by addr. On return it contains the actual size of the socket address.

The returned address is truncated if the buffer provided is too small; in this case, addrlen will return a value greater than was supplied to the call.

但在前面的代码摘录中,getname() 不关心 addrlen 输入值,仅将参数用作输出值。

我发现 link(再也找不到了)说 BSD 尊重以前的联机帮助页摘录,这与 linux.

相反

我错过了什么吗?我发现文档会差那么多很尴尬,我检查了其他 linux XXX_getname 调用,但我所看到的并不关心输入长度。

简答

我相信内核中没有检查 addrlen 值只是为了不浪费一些 CPU 周期,因为它应该总是已知类型(例如 struct sockaddr),因此它应该始终具有已知的固定大小(即 16 字节)。所以内核只是将 addrlen 重写为 16,无论如何。

关于您遇到的问题:我不确定为什么会发生这种情况,但实际上这似乎与尺寸不匹配有关。我很确定内核和用户 space 都具有相同大小的结构,应该传递给 getsockname() 系统调用(证明如下)。所以基本上你在这里描述的情况:

...that makes the kernel copy more data (sizeof(sin_addr)) at the designed address (&sa) than the size of the variable on the stack at that address

不是这样的。我只能想象如果这是真的会有多少应用程序失败。

详细解释

用户space方

iperf 来源中,您有 sockaddr 结构 (/usr/include/bits/socket.h) 的下一个定义:

/* Structure describing a generic socket address.  */
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);    /* Common data: address family and length.  */
    char sa_data[14];           /* Address data.  */
  };

__SOCKADDR_COMMON宏定义如下(/usr/include/bits/sockaddr.h):

/* This macro is used to declare the initial common members
   of the data types used for socket addresses, `struct sockaddr',
   `struct sockaddr_in', `struct sockaddr_un', etc.  */

#define __SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

sa_family_t定义为:

/* POSIX.1g specifies this type name for the `sa_family' member.  */
typedef unsigned short int sa_family_t;

所以基本上 sizeof(struct sockaddr) 总是 16 个字节 (= sizeof(char[14]) + sizeof(short)).

内核端

inet_getname() 函数中,您会看到 addrlen 参数被下一个值重写:

*uaddr_len = sizeof(*sin);

其中 sin 是:

DECLARE_SOCKADDR(struct sockaddr_in *, sin, uaddr);

所以你看到 sin 的类型是 struct sockaddr_in *。这个结构定义如下(include/uapi/linux/in.h):

/* Structure describing an Internet (IP) socket address. */
#define __SOCK_SIZE__     16             /* sizeof(struct sockaddr)    */
struct sockaddr_in {
    __kernel_sa_family_t  sin_family;    /* Address family             */
    __be16                sin_port;      /* Port number                */
    struct in_addr        sin_addr;      /* Internet address           */

    /* Pad to size of `struct sockaddr'. */
    unsigned char         __pad[__SOCK_SIZE__ - sizeof(short int) -
              sizeof(unsigned short int) - sizeof(struct in_addr)];
};

所以sin变量也是16字节长。

更新

我会尽量回复你的评论:

if getsockname wants to allocate an ipv6 instead that may be why it overflows the buffer

当为AF_INET6套接字调用getsockname()时,内核会计算(在getsockname() syscall, by sockfd_lookup_light() function) that inet6_getname()中应该调用来处理你的请求。在这种情况下,uaddr_len将被分配下一个值:

struct sockaddr_in6 *sin = (struct sockaddr_in6 *)uaddr;
...    
*uaddr_len = sizeof(*sin);

因此,如果您也在 user-space 程序中使用 sockaddr_in6 结构,则大小将相同。当然,如果您的 userspace 应用程序将 sockaddr 结构传递给 getsockname 用于 AF_INET6 套接字,将会出现某种溢出(因为 sizeof(struct sockaddr_in6) > sizeof(struct sockaddr)).但我相信您使用的 iperf3 工具并非如此。如果是——首先应该修复的是 iperf,而不是内核。