memfd_create() 给出的 fd 上的 mmap() 有时会因文件描述符错误而失败
mmap() on fd given by memfd_create() sometimes fails with Bad file descriptor
我有两个进程,一个客户端和一个服务器。
服务器使用 Linux memfd_create()
系统调用创建匿名文件。然后它 mmap()
s fd,工作正常。它还将 fd 打印到标准输出。
现在,当我将此 fd 传递给客户端程序时,它也会尝试 mmap()
但这次不知何故失败了。
server.c:
#include <stdio.h>
#include <stddef.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <linux/memfd.h>
const size_t SIZE = 1024;
int main() {
int fd = memfd_create("testmemfd", MFD_ALLOW_SEALING);
// replacing the MFD_ALLOW_SEALING flag with 0 doesn't seem to change anything
if (fd == -1) {
perror("memfd_create");
}
if (ftruncate(fd, SIZE) == -1) {
perror("ftruncate");
}
void * data = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (data == MAP_FAILED) {
perror("mmap");
}
close(fd);
// removing close(fd) or the mmap() code doesn't seem to change anything
printf("%d\n", fd);
while (1) {
}
return 0;
}
client.c:
#include <stdio.h>
#include <stddef.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <linux/memfd.h>
const size_t SIZE = 1024;
int main() {
int fd = -1;
scanf("%d", &fd);
printf("%d\n", fd);
void * data = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (data == MAP_FAILED) {
perror("mmap");
}
return 0;
}
(注意使用memfd_create()
系统调用需要在编译时定义_GNU_SOURCE
)
现在我运行他们:
$ ./server
3
# in another terminal, since server process won't exit:
$ ./client
3
3
mmap: Bad file descriptor
$
既然服务器进程还在开启,为什么fd无效?为什么 fd 在服务器上与 mmap
一起工作正常,但在另一个进程上却不行?
我也尝试了这里的代码:a-darwish/memfd-examples,它使用套接字将数据从服务器传递到客户端。
它工作正常,但是当我将服务器更改为将 fd 输出到 stdout 并且客户端从 stdin 而不是整个套接字业务读取它时,mmap 再次抱怨文件描述符错误。
为什么 mmap 与从套接字接收的 fd 一起工作而不与 stdin 一起工作?
然后我更改了 memfd-examples 代码以再次使用套接字,这使其再次工作。所以我向服务器和客户端添加了一个 printf 来打印它们是 sending/receiving 的 fd。代码运行良好,尽管有这种奇怪之处:
$ ./memfd-examples/server
[Mon Jun 8 18:43:27 2020] New client connection!
sending fd = 5
# and in another terminal
$ ./memfd-examples/client
got fd = 4
Message: Secure zero-copy message from server: Mon Jun 8 18:43:27 2020
所以代码运行良好,fd 似乎完全错误?
然后我尝试在我的客户端程序中递减接收到的 fd -- 不起作用("No such device",正如人们所期望的那样)。
所以,我在 mmap()
上做错了什么?
(请注意,此答案不仅针对 OP,还针对遇到类似问题的任何人。)
OP 看到的问题发生在不同的进程中,根本问题是如何在进程之间传递文件描述符。
文件描述符是进程在处理文件时用来引用文件描述的数字、套接字、FIFO 或任何类似 Unix 或 POSIX 系统中的文件。
文件描述是指类文件对象的内部内核数据结构,包括位置(如果可查找)、记录锁等在.
文件描述符特定于进程。也就是说,一个进程中的描述符 3 与另一个进程中的描述符 3 无关,除非它们恰好引用同一个对象。
进程可以共享相同的文件描述。 Unix 域套接字可用于在进程之间将文件描述符从一端传递到另一端。这不仅仅是传递一个数字;这是一种特殊的技术,使用 OS 内核支持的 辅助数据 。本质上,OS 内核确保(通常不同的)文件描述符引用相同的文件描述,即使它们在不同的进程中。这也意味着描述符编号在运行中被内核修改。
有两种不同类型的 Unix 域套接字:stream 和 datagram。 Stream 与双向管道或 TCP 流非常相似:没有消息或消息边界,只有顺序数据流。数据报是 "packets",每个都有特定的长度。 (请避免零长度数据报。)
几乎总是可以使用 Unix 域流套接字代替父进程和子进程之间的管道:它们的行为非常相似。
如果接收进程使用 recv()
或 read()
而不是 recvmsg()
(即不准备接收辅助数据,只有 recvmsg() 和Linux 扩展 recvmmsg() 处理辅助数据),当前 Linux C 库和内核 不 在接收端创建文件描述符。也就是说,恶意端无法向毫无戒心的端发送任意数量的描述符;接收端只有准备好接收描述符(通过使用 recvmsg() 或 recvmmsg())。
在Linux中,当procfs可用时,每个文件描述符都有一个系统可访问的名称,/proc/PID/fd/FD
,其中PID
是进程ID,FD
是该进程中的文件描述符编号。 (procfs 和 sysfs,通常分别挂载在 /proc 和 /sys,实际上并没有存储在任何媒体上,而是在访问它们时由 OS 内核动态生成。因此,它们通常被称为 伪文件或伪文件系统:它们在大多数方面表现得像文件(尽管它们的长度通常报告为零,因为内容在您读取之前不存在),但实际上不存在。)
但是,/proc/PID/
目录通常只能由特定进程 运行 的用户帐户访问;如果使用像 SELinux 这样的 Linux 安全模块,甚至可以进一步限制。这是一项重要的安全功能,任何绕过它的尝试都应被视为严重的潜在风险——在我看来可能是邪恶的。
因此,Linux中有两种可能的方法:将 procfs 路径传递给文件描述符 (/proc/PID/fd/FD
),并希望另一端可以访问它,或者使用 Unix 域套接字两者之间的(流或数据报),并使用它来传递描述符。
有关辅助数据管理的详细信息,请参阅man 2 sendmsg and man 3 cmsg。
我个人推荐描述符传递方法。它不仅更健壮,而且可以在 Linux 和许多 Unixy 系统之间移植,例如 BSD 变体(包括 Mac OS)。
这也是为什么许多使用非特权或受限子进程的特权服务(例如 Apache 和 Nginx HTTP 守护程序(例如用于 fastcgi 实现))使用 Unix 域套接字进行进程间通信的原因。
(另一个原因是SCM_CREDENTIALS辅助数据,它由内核验证的发送进程的进程ID、用户ID和组ID组成;它允许接收方验证身份特定消息的发送者,在发送的那一刻。这个措辞听起来可能很复杂,但是由于发送者进程可能在收到消息但尚未处理后立即用新的东西替换了自己,我们必须小心并理解情况正确,不会在我们的软件中留下巨大的安全漏洞。)
不幸的是,OP 已经使用 POSIX 消息队列(参见 man 7 mq_overview)实现了进程间通信,但它们不支持辅助数据或传递描述符。重构是有序的。
我有两个进程,一个客户端和一个服务器。
服务器使用 Linux memfd_create()
系统调用创建匿名文件。然后它 mmap()
s fd,工作正常。它还将 fd 打印到标准输出。
现在,当我将此 fd 传递给客户端程序时,它也会尝试 mmap()
但这次不知何故失败了。
server.c:
#include <stdio.h>
#include <stddef.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <linux/memfd.h>
const size_t SIZE = 1024;
int main() {
int fd = memfd_create("testmemfd", MFD_ALLOW_SEALING);
// replacing the MFD_ALLOW_SEALING flag with 0 doesn't seem to change anything
if (fd == -1) {
perror("memfd_create");
}
if (ftruncate(fd, SIZE) == -1) {
perror("ftruncate");
}
void * data = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (data == MAP_FAILED) {
perror("mmap");
}
close(fd);
// removing close(fd) or the mmap() code doesn't seem to change anything
printf("%d\n", fd);
while (1) {
}
return 0;
}
client.c:
#include <stdio.h>
#include <stddef.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <linux/memfd.h>
const size_t SIZE = 1024;
int main() {
int fd = -1;
scanf("%d", &fd);
printf("%d\n", fd);
void * data = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (data == MAP_FAILED) {
perror("mmap");
}
return 0;
}
(注意使用memfd_create()
系统调用需要在编译时定义_GNU_SOURCE
)
现在我运行他们:
$ ./server
3
# in another terminal, since server process won't exit:
$ ./client
3
3
mmap: Bad file descriptor
$
既然服务器进程还在开启,为什么fd无效?为什么 fd 在服务器上与 mmap
一起工作正常,但在另一个进程上却不行?
我也尝试了这里的代码:a-darwish/memfd-examples,它使用套接字将数据从服务器传递到客户端。
它工作正常,但是当我将服务器更改为将 fd 输出到 stdout 并且客户端从 stdin 而不是整个套接字业务读取它时,mmap 再次抱怨文件描述符错误。
为什么 mmap 与从套接字接收的 fd 一起工作而不与 stdin 一起工作?
然后我更改了 memfd-examples 代码以再次使用套接字,这使其再次工作。所以我向服务器和客户端添加了一个 printf 来打印它们是 sending/receiving 的 fd。代码运行良好,尽管有这种奇怪之处:
$ ./memfd-examples/server
[Mon Jun 8 18:43:27 2020] New client connection!
sending fd = 5
# and in another terminal
$ ./memfd-examples/client
got fd = 4
Message: Secure zero-copy message from server: Mon Jun 8 18:43:27 2020
所以代码运行良好,fd 似乎完全错误?
然后我尝试在我的客户端程序中递减接收到的 fd -- 不起作用("No such device",正如人们所期望的那样)。
所以,我在 mmap()
上做错了什么?
(请注意,此答案不仅针对 OP,还针对遇到类似问题的任何人。)
OP 看到的问题发生在不同的进程中,根本问题是如何在进程之间传递文件描述符。
文件描述符是进程在处理文件时用来引用文件描述的数字、套接字、FIFO 或任何类似 Unix 或 POSIX 系统中的文件。
文件描述是指类文件对象的内部内核数据结构,包括位置(如果可查找)、记录锁等在.
文件描述符特定于进程。也就是说,一个进程中的描述符 3 与另一个进程中的描述符 3 无关,除非它们恰好引用同一个对象。
进程可以共享相同的文件描述。 Unix 域套接字可用于在进程之间将文件描述符从一端传递到另一端。这不仅仅是传递一个数字;这是一种特殊的技术,使用 OS 内核支持的 辅助数据 。本质上,OS 内核确保(通常不同的)文件描述符引用相同的文件描述,即使它们在不同的进程中。这也意味着描述符编号在运行中被内核修改。
有两种不同类型的 Unix 域套接字:stream 和 datagram。 Stream 与双向管道或 TCP 流非常相似:没有消息或消息边界,只有顺序数据流。数据报是 "packets",每个都有特定的长度。 (请避免零长度数据报。)
几乎总是可以使用 Unix 域流套接字代替父进程和子进程之间的管道:它们的行为非常相似。
如果接收进程使用
recv()
或read()
而不是recvmsg()
(即不准备接收辅助数据,只有 recvmsg() 和Linux 扩展 recvmmsg() 处理辅助数据),当前 Linux C 库和内核 不 在接收端创建文件描述符。也就是说,恶意端无法向毫无戒心的端发送任意数量的描述符;接收端只有准备好接收描述符(通过使用 recvmsg() 或 recvmmsg())。
在Linux中,当procfs可用时,每个文件描述符都有一个系统可访问的名称,/proc/PID/fd/FD
,其中PID
是进程ID,FD
是该进程中的文件描述符编号。 (procfs 和 sysfs,通常分别挂载在 /proc 和 /sys,实际上并没有存储在任何媒体上,而是在访问它们时由 OS 内核动态生成。因此,它们通常被称为 伪文件或伪文件系统:它们在大多数方面表现得像文件(尽管它们的长度通常报告为零,因为内容在您读取之前不存在),但实际上不存在。)
但是,/proc/PID/
目录通常只能由特定进程 运行 的用户帐户访问;如果使用像 SELinux 这样的 Linux 安全模块,甚至可以进一步限制。这是一项重要的安全功能,任何绕过它的尝试都应被视为严重的潜在风险——在我看来可能是邪恶的。
因此,Linux中有两种可能的方法:将 procfs 路径传递给文件描述符 (/proc/PID/fd/FD
),并希望另一端可以访问它,或者使用 Unix 域套接字两者之间的(流或数据报),并使用它来传递描述符。
有关辅助数据管理的详细信息,请参阅man 2 sendmsg and man 3 cmsg。
我个人推荐描述符传递方法。它不仅更健壮,而且可以在 Linux 和许多 Unixy 系统之间移植,例如 BSD 变体(包括 Mac OS)。
这也是为什么许多使用非特权或受限子进程的特权服务(例如 Apache 和 Nginx HTTP 守护程序(例如用于 fastcgi 实现))使用 Unix 域套接字进行进程间通信的原因。
(另一个原因是SCM_CREDENTIALS辅助数据,它由内核验证的发送进程的进程ID、用户ID和组ID组成;它允许接收方验证身份特定消息的发送者,在发送的那一刻。这个措辞听起来可能很复杂,但是由于发送者进程可能在收到消息但尚未处理后立即用新的东西替换了自己,我们必须小心并理解情况正确,不会在我们的软件中留下巨大的安全漏洞。)
不幸的是,OP 已经使用 POSIX 消息队列(参见 man 7 mq_overview)实现了进程间通信,但它们不支持辅助数据或传递描述符。重构是有序的。