哈喽,各位好!今天咱们来聊聊 C++ 里一个有点酷,但又经常被忽略的家伙:memfd_create
加上 sealing
。它们俩联手,能让你在内存里创建一个“只读”文件,而且超级安全,进程之间共享数据的时候特别有用。
一、memfd_create
:在内存里“变”出文件
首先,想象一下,你需要在两个进程之间共享一些数据。最简单的办法就是写到文件里,对吧?但是,读写磁盘毕竟慢,而且还有安全风险(比如,其他进程偷偷篡改你的文件)。
memfd_create
就解决了这个问题。它就像一个魔法师,能在内存里给你“变”出一个文件,根本不用真的写到磁盘上。
#include <iostream>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <sys/syscall.h> // For syscall()
// 辅助函数,检测系统调用是否可用
bool isMemfdCreateSupported() {
return syscall(SYS_memfd_create, "test", MFD_CLOEXEC) >= 0;
}
int main() {
if (!isMemfdCreateSupported()) {
std::cerr << "memfd_create is not supported on this system." << std::endl;
return 1;
}
const char* name = "my_shared_memory";
int fd = syscall(SYS_memfd_create, name, MFD_CLOEXEC); // 使用 syscall 避免 glibc 的问题
if (fd == -1) {
perror("memfd_create");
return 1;
}
std::cout << "File descriptor created: " << fd << std::endl;
// 现在,你可以像操作普通文件一样操作 fd 了
// 1. 设置文件大小
size_t size = 4096; // 4KB
if (ftruncate(fd, size) == -1) {
perror("ftruncate");
close(fd);
return 1;
}
// 2. 映射到内存
void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 3. 写入数据
const char* message = "Hello, shared memory!";
memcpy(ptr, message, strlen(message));
std::cout << "Data written to shared memory." << std::endl;
// 4. 其他进程可以通过 fd 访问这块内存
// 5. 清理
munmap(ptr, size);
close(fd);
std::cout << "Shared memory released." << std::endl;
return 0;
}
这段代码做了什么?
-
memfd_create(name, MFD_CLOEXEC)
: 创建了一个匿名文件描述符。name
只是一个名字,方便你识别,实际上这个文件并没有真正的文件名。MFD_CLOEXEC
标志确保这个文件描述符在exec
之后会自动关闭,防止泄露给子进程。 注意这里使用syscall
直接调用系统调用,规避了一些 glibc 版本的问题。 -
ftruncate(fd, size)
: 设置文件的大小。因为memfd_create
创建的文件默认大小是 0,所以你需要手动设置。 -
mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
: 将文件映射到内存。MAP_SHARED
标志表示多个进程可以共享这块内存。 -
memcpy(ptr, message, strlen(message))
: 往内存里写入数据。 -
munmap(ptr, size)
和close(fd)
: 释放内存和关闭文件描述符。
重点: memfd_create
创建的文件是匿名的,它存在于内存中,速度快,而且避免了磁盘 I/O 的开销。
二、sealing
:给你的内存文件加把锁
memfd_create
虽然好用,但有个问题:别的进程拿到文件描述符后,也可以随意修改里面的内容。这在某些场景下是不安全的。
这时候,sealing
就派上用场了。它可以给你的内存文件“加锁”,限制对文件的操作,比如禁止写入、禁止扩展大小等等。
#include <iostream>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <linux/memfd.h> // 需要这个头文件
#include <sys/syscall.h> // For syscall()
// 辅助函数,检测系统调用是否可用
bool isMemfdCreateSupported() {
return syscall(SYS_memfd_create, "test", MFD_CLOEXEC) >= 0;
}
int main() {
if (!isMemfdCreateSupported()) {
std::cerr << "memfd_create is not supported on this system." << std::endl;
return 1;
}
const char* name = "my_sealed_memory";
int fd = syscall(SYS_memfd_create, name, MFD_CLOEXEC);
if (fd == -1) {
perror("memfd_create");
return 1;
}
// 1. 设置文件大小
size_t size = 4096;
if (ftruncate(fd, size) == -1) {
perror("ftruncate");
close(fd);
return 1;
}
// 2. 写入数据
void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
const char* message = "This data is now sealed!";
memcpy(ptr, message, strlen(message));
munmap(ptr, size); // Unmap before sealing
// 3. 添加 sealing
int seals = F_SEAL_SHRINK | F_SEAL_SEAL | F_SEAL_WRITE | F_SEAL_GROW; // 禁止收缩、完全密封、写入和增长
if (fcntl(fd, F_ADD_SEALS, seals) == -1) {
perror("fcntl - F_ADD_SEALS");
close(fd);
return 1;
}
std::cout << "Seals added to the memory file." << std::endl;
// 尝试修改文件大小(会失败)
if (ftruncate(fd, size * 2) == -1) {
perror("ftruncate (after sealing)"); // 预期会失败
} else {
std::cout << "ftruncate succeeded unexpectedly!" << std::endl; // 不应该执行到这里
}
// 尝试写入数据(会失败)
ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr != MAP_FAILED) {
memcpy(ptr, "new data", 8);
perror("memcpy after sealing"); //预期会失败
munmap(ptr, size);
} else {
perror("mmap after sealing");
}
// 4. 其他进程现在只能读取数据
close(fd); // Close the file descriptor.
return 0;
}
这段代码的关键部分:
-
fcntl(fd, F_ADD_SEALS, seals)
: 添加 sealing。seals
是一个位掩码,用于指定要添加的 sealing 类型。 -
F_SEAL_SHRINK
: 禁止收缩文件大小。 -
F_SEAL_SEAL
: 一旦添加了 sealing,就不能再删除或修改。这是最关键的 sealing,相当于“最终锁定”。 -
F_SEAL_WRITE
: 禁止写入文件。 -
F_SEAL_GROW
: 禁止扩展文件大小。
重点: 添加了 sealing
之后,任何尝试违反这些限制的操作都会失败,并返回 EPERM
错误。
三、sealing
的类型
Sealing 类型 | 描述 |
---|---|
F_SEAL_SEAL |
一旦添加了 sealing,就不能再删除或修改任何 sealing。 这是最强的 sealing,相当于给文件加上了“最终锁定”。 |
F_SEAL_SHRINK |
禁止收缩文件大小。 也就是说,不能使用 ftruncate 减小文件的大小。 |
F_SEAL_GROW |
禁止扩展文件大小。 也就是说,不能使用 ftruncate 增加文件的大小。 |
F_SEAL_WRITE |
禁止写入文件。 任何尝试写入文件的操作都会失败。 |
F_SEAL_FUTURE_WRITE |
(从 Linux 5.13 开始)禁止在当前文件大小之外写入数据。 已经写入的数据仍然可以修改,但是不能扩展文件并写入新的数据。 |
四、应用场景
memfd_create
和 sealing
在很多场景下都非常有用:
-
安全地共享配置数据: 你可以把配置数据放到内存文件里,然后用
sealing
保护起来,防止被恶意修改。 -
进程间通信(IPC): 可以创建一个只读的共享内存区域,用于进程间传递数据。
-
安全容器: 容器可以用
sealing
来限制容器内的进程对文件系统的访问。 -
沙箱环境: 创建一个受限的内存环境,防止恶意代码破坏系统。
五、C++ 封装
虽然我们可以直接使用系统调用,但为了方便,我们可以把 memfd_create
和 sealing
封装成 C++ 类。
#include <iostream>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <linux/memfd.h>
#include <sys/syscall.h>
class SealedMemory {
public:
SealedMemory(const std::string& name, size_t size, int seals) : fd_(-1), ptr_(nullptr), size_(size) {
if (!isMemfdCreateSupported()) {
std::cerr << "memfd_create is not supported on this system." << std::endl;
return;
}
fd_ = syscall(SYS_memfd_create, name.c_str(), MFD_CLOEXEC);
if (fd_ == -1) {
perror("memfd_create");
return;
}
if (ftruncate(fd_, size_) == -1) {
perror("ftruncate");
close(fd_);
fd_ = -1;
return;
}
// Map the memory before sealing
ptr_ = mmap(nullptr, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0);
if (ptr_ == MAP_FAILED) {
perror("mmap");
close(fd_);
fd_ = -1;
return;
}
// Add seals *before* unmapping
if (fcntl(fd_, F_ADD_SEALS, seals) == -1) {
perror("fcntl - F_ADD_SEALS");
munmap(ptr_, size_);
close(fd_);
fd_ = -1;
return;
}
// Unmap after sealing to prevent modifications
munmap(ptr_, size_);
ptr_ = nullptr; // Set to nullptr after unmapping
std::cout << "Sealed memory created successfully." << std::endl;
}
~SealedMemory() {
if (fd_ != -1) {
close(fd_);
fd_ = -1;
}
}
int getFd() const {
return fd_;
}
//Map for reading only
void* mapReadOnly() {
if (fd_ == -1) return MAP_FAILED;
if(read_only_ptr_ != nullptr) return read_only_ptr_; // Avoid multiple mapping
read_only_ptr_ = mmap(nullptr, size_, PROT_READ, MAP_SHARED, fd_, 0);
if (read_only_ptr_ == MAP_FAILED) {
perror("mmap read only");
}
return read_only_ptr_;
}
void unmapReadOnly() {
if (read_only_ptr_ != nullptr && read_only_ptr_ != MAP_FAILED) {
munmap(read_only_ptr_, size_);
read_only_ptr_ = nullptr;
}
}
private:
int fd_;
void* ptr_; //Not used directly after sealing
void* read_only_ptr_ = nullptr;
size_t size_;
bool isMemfdCreateSupported() {
return syscall(SYS_memfd_create, "test", MFD_CLOEXEC) >= 0;
}
};
int main() {
// Create a sealed memory region that is read-only and cannot be resized
SealedMemory sealedMemory("my_safe_data", 4096, F_SEAL_SEAL | F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW);
if (sealedMemory.getFd() != -1) {
std::cout << "File descriptor: " << sealedMemory.getFd() << std::endl;
//Write some data before sealing
int fd = sealedMemory.getFd();
void *ptr = mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr != MAP_FAILED) {
memcpy(ptr, "Initial Data", 12);
munmap(ptr, 4096);
}
// Now, let's try to read from it
void* readPtr = sealedMemory.mapReadOnly();
if (readPtr != MAP_FAILED) {
std::cout << "Data from shared memory: " << static_cast<char*>(readPtr) << std::endl;
sealedMemory.unmapReadOnly();
} else {
std::cerr << "Failed to map memory for reading." << std::endl;
}
} else {
std::cerr << "Failed to create sealed memory." << std::endl;
}
return 0;
}
这个 SealedMemory
类封装了 memfd_create
和 sealing
的操作。
- 构造函数: 创建内存文件,设置大小,添加 sealing。在添加 sealing 之后,立即 unmap 这块内存,确保无法再通过这个进程修改它。
- 析构函数: 关闭文件描述符。
getFd()
: 返回文件描述符,其他进程可以通过这个描述符访问共享内存。mapReadOnly()
和unmapReadOnly()
: 用于映射和取消映射只读内存区域,供读取数据使用。
六、安全性注意事项
- 尽早 sealing: 在创建内存文件之后,尽快添加 sealing。这样可以最大程度地减少被恶意修改的风险。
- 选择合适的 sealing 类型: 根据你的需求,选择合适的 sealing 类型。比如,如果只需要读取数据,就添加
F_SEAL_WRITE
sealing。 - 文件描述符的传递: 仔细控制文件描述符的传递。只有信任的进程才能访问这个文件。
- 资源清理: 确保在使用完内存文件后,及时释放内存和关闭文件描述符。
七、总结
memfd_create
和 sealing
是一对强大的组合,可以让你安全地创建匿名文件描述符,并限制对文件的操作。它们在进程间通信、安全容器等领域都有广泛的应用。掌握它们,可以让你写出更安全、更高效的代码。
希望今天的讲解对大家有所帮助!下次有机会再和大家分享其他有趣的 C++ 技巧。