Hệ điều hành · 03/10/2026

Linux memfd_create thực chiến: Chia sẻ dữ liệu an toàn bằng file sealing

Khi hai tiến trình Linux cần trao đổi một khối dữ liệu lớn, gửi toàn bộ qua socket có thể tạo thêm lần sao chép, còn shared memory thông thường lại mở ra rủi ro bên gửi sửa hoặc thu nhỏ vùng nhớ sau khi bên nhận đã kiểm tra. memfd_create() giải quyết khoảng trống này: tạo một file ẩn danh được backing bởi RAM, có thể truyền bằng file descriptor và “niêm phong” bằng file seals.

Linux memfd_create thực chiến: Chia sẻ dữ liệu an toàn bằng file sealing

Khi hai tiến trình Linux cần trao đổi một khối dữ liệu lớn, gửi toàn bộ qua socket có thể tạo thêm lần sao chép, còn shared memory thông thường lại mở ra rủi ro bên gửi sửa hoặc thu nhỏ vùng nhớ sau khi bên nhận đã kiểm tra. memfd_create() giải quyết khoảng trống này: tạo một file ẩn danh được backing bởi RAM, có thể truyền bằng file descriptor và “niêm phong” bằng file seals.

Bài viết xây dựng quy trình an toàn từ tạo memfd, đặt kích thước, ghi dữ liệu, thêm seal, truyền FD qua UNIX domain socket đến xác minh ở phía nhận. Hành vi được đối chiếu với Linux man-pages về memfd_create(2), F_ADD_SEALS/F_GET_SEALS và mmap(2).

1. memfd là file, nhưng không có pathname thông thường

memfd_create() trả về một file descriptor mở ở chế độ đọc/ghi. Đối tượng hoạt động gần như file thường: có thể ftruncate(), read(), write(), mmap(), fstat() và truyền sang tiến trình khác. Khác biệt quan trọng là nó không cần tạo tên trong filesystem và tự được giải phóng khi mọi tham chiếu cuối cùng bị đóng.

Trong /proc/PID/fd, symlink thường hiện dạng /memfd:name (deleted). Chuỗi name phục vụ debug, không phải khóa bảo mật hay một pathname có thể mở theo cách thông thường.

“Không có pathname” không có nghĩa là “bí mật”. Tiến trình có quyền phù hợp vẫn có thể quan sát hoặc truy cập FD qua /proc; nội dung cũng có thể chịu cơ chế swap như bộ nhớ bình thường. Dữ liệu bí mật cần threat model riêng, quyền tiến trình, mã hóa hoặc API phù hợp như memfd_secret().

2. Vì sao file sealing quan trọng?

Giả sử producer tạo shared memory, ghi một message rồi giao cho consumer. Nếu producer vẫn có thể sửa nội dung sau bước validate, consumer có thể gặp lỗi time-of-check/time-of-use. Nếu producer thu nhỏ file trong lúc consumer đang đọc mapping, truy cập phần không còn backing có thể phát SIGBUS.

File seals biến các cam kết ứng dụng thành ràng buộc được kernel thực thi. Seal thuộc về inode, nên mọi file descriptor cùng trỏ đến đối tượng đều thấy cùng tập seal. Seal chỉ có thể thêm, không thể gỡ.

3. Tạo memfd đúng cờ

#define _GNU_SOURCE
#include <sys/mman.h>

int fd = memfd_create("report-payload",
                      MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (fd == -1) {
    /* xử lý errno */
}
  • MFD_CLOEXEC đặt FD_CLOEXEC, tránh FD vô tình rò sang chương trình mới sau execve().
  • MFD_ALLOW_SEALING cho phép thêm seals. Nếu bỏ cờ này, đối tượng bắt đầu với F_SEAL_SEAL, khiến không thể thêm seal khác.
  • MFD_HUGETLB tạo đối tượng trên hugetlbfs và chỉ phù hợp khi đã cấu hình huge pages, quyền cùng mô hình bộ nhớ tương ứng. Đừng bật chỉ vì cho rằng “huge page luôn nhanh hơn”.

memfd_create() có từ Linux 3.17; wrapper glibc có từ 2.27. Trên môi trường libc hoặc kernel cũ, ứng dụng cần kiểm tra khả năng hỗ trợ và có fallback rõ ràng.

4. Vòng đời chuẩn: size → populate → unmap → seal

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

static int build_sealed_payload(const void *data, size_t len)
{
    int fd = memfd_create("payload",
                          MFD_CLOEXEC | MFD_ALLOW_SEALING);
    if (fd == -1)
        return -1;

    if (ftruncate(fd, (off_t)len) == -1)
        goto fail;

    void *p = mmap(NULL, len, PROT_READ | PROT_WRITE,
                   MAP_SHARED, fd, 0);
    if (p == MAP_FAILED)
        goto fail;

    memcpy(p, data, len);

    /* F_SEAL_WRITE yêu cầu không còn shared writable mapping. */
    if (munmap(p, len) == -1)
        goto fail;

    int seals = F_SEAL_SHRINK | F_SEAL_GROW |
                F_SEAL_WRITE  | F_SEAL_SEAL;
    if (fcntl(fd, F_ADD_SEALS, seals) == -1)
        goto fail;

    return fd;

fail:
    {
        int saved = errno;
        close(fd);
        errno = saved;
        return -1;
    }
}

Với payload rỗng, cần tránh gọi mmap() với length 0; có thể tạo, giữ size 0 rồi thêm seals trực tiếp. Code production cũng phải kiểm tra chuyển đổi size_t sang off_t và đặt giới hạn kích thước trước khi cấp phát.

5. Mỗi seal bảo vệ điều gì?

SealHiệu lực chínhLưu ý
F_SEAL_SHRINKChặn thu nhỏ file.Giảm nguy cơ SIGBUS do peer truncate mapping.
F_SEAL_GROWChặn tăng kích thước và ghi vượt EOF.Khóa boundary đã được validate.
F_SEAL_WRITEChặn thay đổi nội dung và tạo shared writable mapping mới.Thêm seal thất bại với EBUSY nếu còn shared writable mapping.
F_SEAL_FUTURE_WRITEChặn write và shared writable mapping mới, nhưng mapping writable đang tồn tại vẫn có thể ghi.Phù hợp cho handoff theo giai đoạn; không phải snapshot bất biến ngay lập tức.
F_SEAL_SEALChặn mọi lần thêm seal tiếp theo.Thêm cuối cùng khi policy đã hoàn chỉnh.

F_SEAL_WRITE không tự khóa kích thước, vì vậy snapshot bất biến thường kết hợp WRITE | GROW | SHRINK | SEAL. Thứ tự tư duy quan trọng: thiết lập mọi hạn chế cần thiết rồi mới khóa tập seal bằng F_SEAL_SEAL.

6. F_SEAL_WRITE và F_SEAL_FUTURE_WRITE khác nhau

F_SEAL_WRITE là trạng thái mạnh: không thể thêm khi vẫn còn bất kỳ shared writable mapping nào. Producer phải unmap hoặc chuyển kiến trúc sao cho không tồn tại mapping như vậy trước khi seal.

F_SEAL_FUTURE_WRITE cho phép mapping shared writable đã tồn tại tiếp tục ghi, nhưng chặn write() và mapping writable mới. Điều này hữu ích khi producer cần hoàn tất công việc qua mapping cũ trong khi không cho consumer tạo quyền ghi mới. Consumer không được xem nó tương đương một blob bất biến nếu producer vẫn giữ mapping cũ.

7. Truyền FD qua UNIX domain socket

Không gửi số nguyên FD như dữ liệu thông thường: số FD chỉ có nghĩa trong bảng descriptor của từng tiến trình. Linux truyền tham chiếu bằng ancillary data SCM_RIGHTS của sendmsg()/recvmsg().

/* Phần cốt lõi ở phía gửi; cần kiểm tra đầy đủ lỗi trong production. */
char marker = 'M';
struct iovec iov = { .iov_base = &marker, .iov_len = 1 };
char control[CMSG_SPACE(sizeof(int))] = {0};
struct msghdr msg = {
    .msg_iov = &iov,
    .msg_iovlen = 1,
    .msg_control = control,
    .msg_controllen = sizeof(control)
};

struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));

if (sendmsg(sock, &msg, MSG_NOSIGNAL) == -1) {
    /* xử lý lỗi */
}

Phía nhận nên dùng recvmsg(..., MSG_CMSG_CLOEXEC) để descriptor nhận được có close-on-exec ngay tại thời điểm tạo, tránh race trong chương trình đa luồng. Sau khi gửi thành công, producer vẫn phải close(fd) khi không dùng nữa; receiver có tham chiếu riêng.

8. Receiver phải xác minh, không chỉ tin producer

struct stat st;
if (fstat(fd, &st) == -1)
    fail();

if (st.st_size < 0 || (uint64_t)st.st_size > MAX_PAYLOAD)
    reject();

int seals = fcntl(fd, F_GET_SEALS);
if (seals == -1)
    reject();

int required = F_SEAL_SHRINK | F_SEAL_GROW |
               F_SEAL_WRITE  | F_SEAL_SEAL;
if ((seals & required) != required)
    reject();

void *p = mmap(NULL, (size_t)st.st_size,
               PROT_READ, MAP_PRIVATE, fd, 0);
if (p == MAP_FAILED)
    fail();

Kiểm tra kích thước trước khi map để ngăn peer buộc process cấp phát không giới hạn. Xác minh magic, version, offset, length, checksum và cấu trúc dữ liệu như với mọi input không tin cậy. Seal đảm bảo payload không đổi theo các chiều đã khóa; nó không chứng minh payload hợp lệ hay đáng tin.

9. Những lỗi thiết kế thường gặp

  • Quên MFD_ALLOW_SEALING: đối tượng có sẵn F_SEAL_SEAL, mọi lần thêm seal khác thất bại.
  • Thêm F_SEAL_WRITE khi mapping writable còn sống: nhận EBUSY.
  • Chỉ thêm F_SEAL_WRITE: peer vẫn có thể đổi kích thước nếu thiếu GROW/SHRINK.
  • Thêm F_SEAL_SEAL quá sớm: không thể bổ sung policy và cũng không thể gỡ seal.
  • Gửi số FD thô: receiver mở nhầm descriptor của chính nó; phải dùng SCM_RIGHTS.
  • Bỏ giới hạn size: biến IPC thành đường gây memory pressure hoặc integer overflow.
  • Xem memfd là secret storage: sai threat model; memfd chủ yếu cung cấp file ẩn danh và sealing.
  • Quên CLOEXEC: descriptor sống qua execve() ngoài dự kiến.

10. Khi nào nên và không nên dùng?

Phù hợp: truyền blob lớn trong cùng host; snapshot cấu hình/model bất biến; IPC cần random access; pipeline muốn tách metadata nhỏ trên socket khỏi payload lớn; sandbox nhận một artifact đã khóa.

Không phù hợp: dữ liệu phải bền qua reboot; giao tiếp qua mạng; hàng đợi cần retry/durability; streaming không giới hạn; dữ liệu bí mật chỉ dựa vào việc không có pathname. Với object cần tên chia sẻ, shm_open() có thể phù hợp hơn; với temporary file có thể link vào filesystem, xem xét O_TMPFILE.

Checklist production

  • Dùng MFD_CLOEXEC | MFD_ALLOW_SEALING có chủ đích.
  • Giới hạn payload trước ftruncate(), mmap() và ở phía nhận.
  • Populate hoàn chỉnh và unmap shared writable mapping trước F_SEAL_WRITE.
  • Khóa cả nội dung lẫn kích thước khi cần snapshot bất biến.
  • Chỉ thêm F_SEAL_SEAL sau khi policy hoàn chỉnh.
  • Truyền descriptor bằng SCM_RIGHTS; nhận với MSG_CMSG_CLOEXEC.
  • Receiver kiểm tra seals, size, format, offset và checksum.
  • Đóng mọi FD/mapping trên success, error và cancellation path.
  • Đặt quota/cgroup và quan sát memory pressure cho workload lớn.
  • Có fallback hoặc báo lỗi rõ ràng trên kernel/libc không hỗ trợ.

Kết luận

memfd_create() kết hợp đặc tính file descriptor với backing memory và file sealing để tạo một primitive IPC rất mạnh. Mẫu an toàn là: giới hạn kích thước, tạo và ghi payload, bỏ mapping ghi, thêm đầy đủ seals, truyền FD bằng SCM_RIGHTS, rồi để receiver xác minh lại mọi invariant. Khi giữ đúng ranh giới—memfd không phải persistent storage và cũng không tự bảo mật bí mật—đây là cách gọn, hiệu quả để chia sẻ blob bất biến giữa các tiến trình Linux.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.