通用面试:Unix 域套接字如何传递文件描述符?
题干与适用场景
两个本地进程通过 Unix 域套接字协作:特权进程打开文件或创建监听 socket,再交给低权限 worker 使用。请解释如何传递文件描述符、接收端得到什么、如何处理截断和权限风险。
这道题考察 Linux/Unix IPC、文件描述符表与内核 open file description 的区别,以及 sendmsg/recvmsg 的辅助数据协议。Linux unix(7) 将 SCM_RIGHTS 定义为在进程间发送或接收一组已打开文件描述符。
面试官考察点
- 是否区分进程私有的 fd 整数与内核中的 open file description。
- 是否知道
SCM_RIGHTS通过 ancillary data 传输,而不是把整数写入普通 payload。 - 是否能正确配置
cmsghdr、CMSGSPACE、CMSGLEN并检查MSG_CTRUNC。 - 是否覆盖 socket 路径权限、发送方身份、资源上限、关闭时机和失败清理。
回答前需要澄清的问题
- 进程是否同一用户、是否存在特权降级和明确的信任边界?
- 需要传递的是普通文件、已连接 socket、监听 socket 还是 epoll/驱动 fd?
- 使用
SOCKSTREAM还是SOCKDGRAM?是否有消息边界和确认协议? - 接收端能否验证发送方凭据、资源类型、只读属性和预期的 fd 数量?
30 秒回答框架
我会创建一对 Unix 域套接字,用 sendmsg 的 SOLSOCKET/SCMRIGHTS 控制消息携带 fd,并在普通数据中放一个协议版本和请求 ID。接收端用足够大的 CMSGSPACE 缓冲区调用 recvmsg,检查 cmsglevel、cmsgtype、长度和 MSGCTRUNC,然后把收到的 fd 当作本进程的新整数使用。传递的是对 open file description 的引用,接收端的 fd 数字通常不同。协议还要验证对端凭据、限制数量、设置 close-on-exec,并在异常路径关闭未接收或未使用的 fd。
分步骤深入解答
区分 fd 数字与 open file description
fd 是进程 fd table 中的整数索引;open file description 由内核保存打开状态,例如文件偏移和状态标志。SCM_RIGHTS 复制的是对后者的引用,接收进程通常得到不同的 fd 整数,语义类似把 fd dup 到另一个进程的 fd table。
用 ancillary data 传输而非普通字节
sendmsg 和 recvmsg 通过 msghdr.msgcontrol 携带 cmsghdr 链。cmsglevel 使用 SOLSOCKET,cmsgtype 使用 SCM_RIGHTS,数据区是整数 fd 数组。普通 payload 可以携带版本、用途和确认 ID,但不能代替控制消息。
正确计算控制缓冲区
发送端为实际 fd 数量使用 CMSGLEN(n sizeof(int)) 设置 cmsglen;接收端至少分配 CMSGSPACE(n sizeof(int)) 的对齐空间。解析时遍历 CMSGFIRSTHDR 和 CMSG_NXTHDR,并拒绝长度不足或类型不匹配的消息。
处理截断和流式边界
接收控制缓冲区不足会导致辅助数据被截断或丢弃,并设置 MSGCTRUNC;这时不能继续使用部分 fd。Linux 对 SOCKSTREAM 要求携带至少一个真实字节,且辅助数据形成接收屏障,因此协议需要把控制消息与请求 ID 绑定,而不能只依赖字节流位置。
建立身份与权限边界
文件系统 socket 路径的目录和 socket 权限是第一层边界。服务端还应使用 SOPEERCRED 或 SCMCREDENTIALS 验证对端,并在应用层确认租户、用途和资源类型。收到 fd 不代表获得了更高权限,发送方必须只传递经过授权的引用。
管理生命周期与资源上限
发送后发送进程可以关闭自己的 fd,但在接收方成功取出前,内核仍维护 in-flight 引用。Linux 会受 RLIMITNOFILE 和 SCMMAXFD 约束;当前 man page 记录 SCMMAX_FD 通常为 253,旧版本曾为 255。服务应限制单条消息和每个 worker 的 fd 数,并在超限时可观测地拒绝。
struct msghdr msg = {0};
struct iovec iov = {.iov_base = "F", .iov_len = 1};
union { char buf[CMSG_SPACE(sizeof(int))]; struct cmsghdr align; } control;
msg.msg_iov = &iov; msg.msg_iovlen = 1;
msg.msg_control = control.buf; msg.msg_controllen = sizeof(control.buf);
struct cmsghdr *c = CMSG_FIRSTHDR(&msg);
c->cmsg_level = SOL_SOCKET; c->cmsg_type = SCM_RIGHTS;
c->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(c), &fd, sizeof(fd));
sendmsg(sock, &msg, MSG_NOSIGNAL);高质量示范回答
我会把它设计成带确认的本地 IPC 协议。发送端用 Unix 域 socket 的 sendmsg 携带 SOLSOCKET/SCMRIGHTS,普通数据放协议版本、用途和请求 ID;接收端用 CMSGSPACE 分配控制缓冲,检查类型、长度和 MSGCTRUNC,验证对端凭据、fd 数量和资源类型后再交给 worker。关键语义是传递 open file description 的引用,所以接收端的整数通常不同,文件偏移等打开状态可能共享。系统还要设置 close-on-exec、限制 in-flight fd、处理 RLIMITNOFILE 和 SCMMAX_FD,并对路径权限、失败清理和审计事件做闭环。
常见错误
- 把 fd 整数写进普通 JSON 或字节 payload,误以为接收进程能直接使用它。
- 忽略
CMSG_SPACE对齐,只分配sizeof(int)的控制缓冲区。 - 不检查
MSG_CTRUNC,继续使用被截断的 fd 列表。 - 只检查 Unix socket 路径,不验证对端凭据和发送用途。
- 忘记接收方得到的是新 fd 数字,也没有约定共享偏移和状态标志的语义。
- 忽略 close-on-exec、fd 上限、发送失败和未使用 fd 的关闭路径。
追问及应对
接收端拿到的是同一个文件描述符吗?
整数通常不同。内核把同一个 open file description 的引用复制到接收进程的 fd table,因此文件偏移和部分打开状态可能共享;题目若要求独立偏移,应重新打开或复制数据,而不是假设 fd 数字相同。
为什么需要发送一个真实字节?
Linux 的 Unix stream socket 在传递辅助数据时要求同一 sendmsg 包含至少一个真实字节;它也帮助协议把控制消息与请求关联。datagram 在 Linux 上可以不带,但为了可移植性仍应发送一个真实字节。
如果控制缓冲区太小会怎样?
辅助数据可能被截断或丢弃,并设置 MSG_CTRUNC;接收端必须关闭已经取出的无效或多余 fd,返回协议错误并记录事件,不能把部分列表当作完整授权。
如何防止特权进程误传资源?
先用 socket 文件权限和对端凭据限制连接,再在应用层绑定请求 ID、租户、用途和资源类型。发送端只从允许清单选择 fd,接收端验证只读属性、路径或 socket 状态,并对每次授权和关闭记录审计。