C++ `memfd_create` 与 `sealing`:安全创建匿名文件描述符

哈喽,各位好!今天咱们来聊聊 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;
}

这段代码做了什么?

  1. memfd_create(name, MFD_CLOEXEC): 创建了一个匿名文件描述符。name 只是一个名字,方便你识别,实际上这个文件并没有真正的文件名。MFD_CLOEXEC 标志确保这个文件描述符在 exec 之后会自动关闭,防止泄露给子进程。 注意这里使用 syscall 直接调用系统调用,规避了一些 glibc 版本的问题。

  2. ftruncate(fd, size): 设置文件的大小。因为 memfd_create 创建的文件默认大小是 0,所以你需要手动设置。

  3. mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0): 将文件映射到内存。MAP_SHARED 标志表示多个进程可以共享这块内存。

  4. memcpy(ptr, message, strlen(message)): 往内存里写入数据。

  5. 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;
}

这段代码的关键部分:

  1. fcntl(fd, F_ADD_SEALS, seals): 添加 sealing。seals 是一个位掩码,用于指定要添加的 sealing 类型。

  2. F_SEAL_SHRINK: 禁止收缩文件大小。

  3. F_SEAL_SEAL: 一旦添加了 sealing,就不能再删除或修改。这是最关键的 sealing,相当于“最终锁定”。

  4. F_SEAL_WRITE: 禁止写入文件。

  5. 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_createsealing 在很多场景下都非常有用:

  1. 安全地共享配置数据: 你可以把配置数据放到内存文件里,然后用 sealing 保护起来,防止被恶意修改。

  2. 进程间通信(IPC): 可以创建一个只读的共享内存区域,用于进程间传递数据。

  3. 安全容器: 容器可以用 sealing 来限制容器内的进程对文件系统的访问。

  4. 沙箱环境: 创建一个受限的内存环境,防止恶意代码破坏系统。

五、C++ 封装

虽然我们可以直接使用系统调用,但为了方便,我们可以把 memfd_createsealing 封装成 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_createsealing 的操作。

  1. 构造函数: 创建内存文件,设置大小,添加 sealing。在添加 sealing 之后,立即 unmap 这块内存,确保无法再通过这个进程修改它。
  2. 析构函数: 关闭文件描述符。
  3. getFd() 返回文件描述符,其他进程可以通过这个描述符访问共享内存。
  4. mapReadOnly()unmapReadOnly() 用于映射和取消映射只读内存区域,供读取数据使用。

六、安全性注意事项

  • 尽早 sealing: 在创建内存文件之后,尽快添加 sealing。这样可以最大程度地减少被恶意修改的风险。
  • 选择合适的 sealing 类型: 根据你的需求,选择合适的 sealing 类型。比如,如果只需要读取数据,就添加 F_SEAL_WRITE sealing。
  • 文件描述符的传递: 仔细控制文件描述符的传递。只有信任的进程才能访问这个文件。
  • 资源清理: 确保在使用完内存文件后,及时释放内存和关闭文件描述符。

七、总结

memfd_createsealing 是一对强大的组合,可以让你安全地创建匿名文件描述符,并限制对文件的操作。它们在进程间通信、安全容器等领域都有广泛的应用。掌握它们,可以让你写出更安全、更高效的代码。

希望今天的讲解对大家有所帮助!下次有机会再和大家分享其他有趣的 C++ 技巧。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注