Một tiến trình trong sandbox gọi openat(), mount() hoặc mknod(). Filter seccomp không muốn cho syscall chạy trực tiếp, nhưng trả lỗi ngay cũng làm hỏng workload. Seccomp user notification cho phép kernel tạm dừng thread đó, gửi yêu cầu tới một supervisor ở user space và nhận lại kết quả được kiểm soát.
Cơ chế này rất hữu ích cho container runtime, compatibility layer và privilege separation. Tuy nhiên, nó thường bị mô tả sai như một “firewall syscall động”. Bài viết xây dựng mô hình đúng, trình bày vòng đời listener, NOTIF_RECV, ID_VALID, NOTIF_SEND, NOTIF_ADDFD, event loop và các race quan trọng. Nội dung được đối chiếu với tài liệu Seccomp BPF của Linux kernel, seccomp_unotify(2) và seccomp(2).
1. Bài toán user notification giải quyết
Classic seccomp BPF chỉ nhìn thấy số syscall, kiến trúc, instruction pointer và giá trị các thanh ghi đối số. Filter không thể dereference con trỏ, mở file hay gọi dịch vụ bên ngoài. Nó có thể allow, trả errno, trap, log hoặc kill, nhưng không phù hợp với quyết định cần thực hiện một thao tác thay mặt target.
SECCOMP_RET_USER_NOTIF tách công việc thành hai phía:
- Target cài filter rồi chạy với quyền hạn chế. Khi chạm syscall được chọn, thread bị chặn trong kernel.
- Supervisor giữ listener FD, nhận metadata, thực hiện thao tác thay target hoặc giả lập kết quả, rồi gửi phản hồi.
Ví dụ điển hình là runtime mở một tài nguyên đã được kiểm tra trong namespace phù hợp rồi đưa file descriptor vào tiến trình container, thay vì để container tự mở tài nguyên đó.
2. Đây không phải bộ máy policy bảo mật
User notification không được dùng như ranh giới duy nhất để quyết định syscall có an toàn hay không.
Supervisor có thể đọc một chuỗi từ bộ nhớ target, thấy nó hợp lệ, rồi gửi SECCOMP_USER_NOTIF_FLAG_CONTINUE. Trong thời gian target bị chặn, thread khác của cùng process có thể sửa vùng nhớ chứa chuỗi. Khi kernel thực thi syscall thật, đối số đã khác: đây là TOCTOU.
Mô hình an toàn là supervisor đặc quyền thực hiện một hành động cụ thể thay cho target ít quyền hơn, còn kernel, namespace, LSM, capability và quyền filesystem vẫn là lớp cưỡng chế. Nếu dùng CONTINUE, phải giả định các đối số dạng con trỏ có thể thay đổi và một cơ chế khác vẫn chặn trạng thái nguy hiểm.
3. Tạo listener FD
Filter trả SECCOMP_RET_USER_NOTIF cho một tập syscall hẹp. Khi cài filter, truyền SECCOMP_FILTER_FLAG_NEW_LISTENER:
int listener = syscall(
SYS_seccomp,
SECCOMP_SET_MODE_FILTER,
SECCOMP_FILTER_FLAG_NEW_LISTENER,
&prog
);
if (listener == -1) {
/* fail closed */
}
Target cần PR_SET_NO_NEW_PRIVS hoặc capability thích hợp theo contract của seccomp. Chỉ một filter listener được phép trên một thread. Listener gắn với filter chứ không gắn với một target đơn lẻ; nếu target fork và filter được kế thừa, sự kiện từ nhiều task có thể tới cùng FD.
Trong kiến trúc thực tế, process bootstrap cài filter, chuyển listener FD cho supervisor qua Unix socket bằng SCM_RIGHTS, rồi bỏ các đặc quyền còn lại. Không gửi số nguyên FD qua pipe: số FD chỉ có nghĩa trong bảng descriptor của từng process.
4. Đừng hard-code kích thước cấu trúc
Kernel có thể mở rộng các cấu trúc notification. Hãy hỏi kích thước khi chạy:
struct seccomp_notif_sizes sizes = {0};
if (syscall(SYS_seccomp, SECCOMP_GET_NOTIF_SIZES,
0, &sizes) == -1) {
/* unsupported or fatal */
}
struct seccomp_notif *req = calloc(1, sizes.seccomp_notif);
struct seccomp_notif_resp *resp = calloc(1, sizes.seccomp_notif_resp);
Zero toàn bộ buffer trước mỗi SECCOMP_IOCTL_NOTIF_RECV. Trên kernel hiện đại, field khác 0 ngoài dữ liệu đầu vào hợp lệ có thể dẫn tới EINVAL. Đặt giới hạn bộ nhớ và dừng an toàn nếu kích thước kernel báo về bất thường.
5. Vòng đời một notification
- Target gọi syscall và filter trả
USER_NOTIF. - Kernel tạo cookie
id, chặn thread và đánh thức listener. - Supervisor gọi
SECCOMP_IOCTL_NOTIF_RECV. - Supervisor kiểm tra architecture, syscall number và các đối số scalar.
- Nếu cần dữ liệu pointer, supervisor đọc snapshot có giới hạn từ memory target.
- Supervisor xác nhận cookie còn sống bằng
SECCOMP_IOCTL_NOTIF_ID_VALID. - Supervisor giả lập thành công/lỗi, inject FD hoặc rất thận trọng cho syscall tiếp tục.
NOTIF_SENDhoặc ADDFD atomic giải phóng target.
Cookie chỉ duy nhất trong phạm vi filter. Không dùng TID làm danh tính notification và không cache quyết định theo TID vì ID của thread có thể được tái sử dụng.
6. Skeleton của supervisor loop
for (;;) {
memset(req, 0, sizes.seccomp_notif);
memset(resp, 0, sizes.seccomp_notif_resp);
if (ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, req) == -1) {
if (errno == EINTR || errno == ENOENT)
continue;
fatal("NOTIF_RECV");
}
if (req->data.arch != AUDIT_ARCH_X86_64 ||
req->data.nr != SYS_openat) {
deny(resp, req->id, EPERM);
} else {
handle_open_request(listener, req, resp);
}
if (resp->id != 0 &&
ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, resp) == -1 &&
errno != ENOENT) {
log_error("NOTIF_SEND");
}
}
Luôn kiểm tra arch trước syscall number vì ABI khác có thể dùng số syscall trùng nhau. Filter BPF cũng phải kiểm tra architecture ngay từ đầu. Code production cần xử lý shutdown, listener hangup, memory allocation, backpressure và logging có giới hạn.
7. Phản hồi thành công và thất bại
Để giả lập lỗi, field error nhận giá trị errno âm:
resp->id = req->id;
resp->val = 0;
resp->error = -EACCES;
resp->flags = 0;
Để giả lập giá trị trả về thành công:
resp->id = req->id;
resp->val = emulated_result;
resp->error = 0;
resp->flags = 0;
Nếu target chết, nhận signal hoặc bỏ syscall trước khi phản hồi, NOTIF_SEND có thể trả ENOENT. Đây là race bình thường, không nhất thiết là lỗi hệ thống. Tuyệt đối không gửi lại response cũ cho request mới.
8. ID_VALID và bộ nhớ target
seccomp_data.args[] chứa giá trị thanh ghi. Với syscall nhận con trỏ, nó chỉ chứa địa chỉ trong address space của target. Supervisor có thể đọc qua /proc/<tid>/mem hoặc API phù hợp nếu có quyền, nhưng phải:
- giới hạn độ dài và yêu cầu terminator đối với chuỗi;
- copy một lần vào buffer riêng rồi parse buffer bất biến;
- không dereference trực tiếp địa chỉ target;
- gọi
SECCOMP_IOCTL_NOTIF_ID_VALIDsau khi đọc và trước khi dùng dữ liệu; - hủy công việc nếu cookie không còn hợp lệ.
__u64 id = req->id;
if (ioctl(listener, SECCOMP_IOCTL_NOTIF_ID_VALID, &id) == -1) {
/* target abandoned the syscall; discard copied data */
return;
}
ID_VALID giảm nguy cơ thao tác theo TID đã tái sử dụng, nhưng không biến CONTINUE thành race-free. Sau khi check, thread khác vẫn có thể sửa memory trước khi kernel dùng con trỏ.
9. ADDFD: cách tốt hơn để giả lập open
Từ Linux 5.9, SECCOMP_IOCTL_NOTIF_ADDFD cho phép supervisor sao chép một FD vào bảng descriptor của target. Supervisor tự mở tài nguyên theo root, namespace và policy đã kiểm soát, rồi inject descriptor:
struct seccomp_notif_addfd add = {
.id = req->id,
.srcfd = safe_fd,
.newfd = 0,
.newfd_flags = O_CLOEXEC,
.flags = SECCOMP_ADDFD_FLAG_SEND,
};
int target_fd = ioctl(listener,
SECCOMP_IOCTL_NOTIF_ADDFD,
&add);
SECCOMP_ADDFD_FLAG_SEND, có từ Linux 5.14, vừa inject FD vừa trả kết quả trong một thao tác atomic, tránh khoảng hở giữa ADDFD và NOTIF_SEND. Nếu kernel cũ không hỗ trợ cờ này, cần state machine kỹ để đóng FD và xử lý target biến mất.
Không sao chép mù dirfd từ target sang supervisor: số descriptor thuộc hai bảng khác nhau. Hãy thiết kế namespace/root rõ ràng hoặc dùng primitive lấy handle phù hợp với threat model.
10. CONTINUE là nhánh nguy hiểm
resp->id = req->id;
resp->val = 0;
resp->error = 0;
resp->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
Cờ này yêu cầu kernel tiếp tục syscall ban đầu. Nó phù hợp khi supervisor chỉ cần quan sát hoặc khi một lớp kernel độc lập chắc chắn cưỡng chế điều kiện an toàn. Nó không phù hợp với mô hình “đọc pathname, allow nếu pathname nằm trong prefix”.
Giới hạn CONTINUE bằng allowlist syscall rất nhỏ, audit mọi call site và kiểm tra rằng target không thể cài filter mới có action precedence cao hơn để bypass notifier. Nếu không chứng minh được lớp cưỡng chế độc lập, hãy giả lập thao tác hoặc từ chối.
11. Event loop, timeout và backpressure
Listener FD hỗ trợ poll/epoll. Một notification đang chờ làm FD readable; khi các target dùng filter đã kết thúc và được reap, listener báo hangup. Thiết kế production nên có:
- giới hạn số request đang xử lý cho từng sandbox;
- worker pool riêng cho thao tác có thể block;
- timeout fail-closed và hủy công việc khi ID hết hiệu lực;
- metric cho độ trễ RECV-to-SEND, queue depth, deny và ENOENT;
- không log nguyên pathname, token hoặc dữ liệu target nhạy cảm;
- supervisor crash policy rõ ràng, vì target có thể bị treo chờ response.
Nhiều reader có thể dùng chung listener vì kernel đồng bộ access, nhưng routing response, lifecycle và fairness vẫn là trách nhiệm của ứng dụng.
12. Mô hình quyền của supervisor
Supervisor thường mạnh hơn target, vì vậy compromise supervisor có blast radius lớn. Hãy giảm bề mặt:
- chỉ intercept syscall thật sự cần emulation;
- tách parser dữ liệu không tin cậy khỏi worker đặc quyền nếu có thể;
- drop capability không dùng, dùng user/mount namespace và LSM;
- dùng root directory FD thay vì đường dẫn tuyệt đối tùy ý;
- không cho target điều khiển raw flags ngoài allowlist;
- kiểm tra loại file bằng
fstat()sau open; - giới hạn CPU, memory, FD và tần suất notification.
Seccomp filter của supervisor cũng có thể giảm syscall surface, nhưng phải bảo toàn các ioctl, Unix socket và filesystem operation mà vòng lặp cần.
13. Kiểm thử đối kháng
- Target chết hoặc nhận signal ngay sau RECV, trong lúc đọc memory và trước SEND.
- Một thread liên tục sửa pathname trong khi thread khác phát syscall.
- Request mang arch hoặc syscall number không mong đợi.
- Chuỗi không có NUL, vượt giới hạn, nằm qua trang không mapped hoặc thay đổi giữa các lần đọc.
- FD table đầy,
RLIMIT_NOFILEthấp và ADDFD bị LSM từ chối. - Supervisor chậm, restart hoặc crash khi queue đầy.
- Target fork hàng loạt và nhiều task dùng chung filter.
- Kernel không hỗ trợ NEW_LISTENER, ADDFD hoặc ADDFD_FLAG_SEND.
- Target cố cài thêm seccomp filter hoặc dùng ABI syscall khác.
Chạy test trong VM/namespace cô lập, kèm sanitizer, fault injection và race workload. Assertion quan trọng nhất là supervisor không bao giờ thực hiện hành động đặc quyền cho notification đã hết hiệu lực.
Checklist production
- Filter kiểm tra architecture và chỉ trả USER_NOTIF cho allowlist hẹp.
- Lấy kích thước cấu trúc bằng SECCOMP_GET_NOTIF_SIZES.
- Zero request/response trước mỗi vòng.
- Giới hạn và snapshot dữ liệu đọc từ target.
- Gọi ID_VALID giữa bước đọc và sử dụng dữ liệu.
- Ưu tiên supervisor thực hiện thao tác và ADDFD thay vì CONTINUE.
- Dùng ADDFD_FLAG_SEND khi kernel hỗ trợ.
- Xử lý ENOENT như race lifecycle bình thường.
- Có timeout, backpressure, metric và supervisor crash policy.
- Giữ namespace, LSM, capability và DAC làm lớp cưỡng chế thật.
Kết luận
Seccomp user notification tạo một cầu nối mạnh giữa sandbox và supervisor: target bị chặn, supervisor nhận request, thực hiện hành động có kiểm soát rồi trả kết quả. Giá trị lớn nhất nằm ở privilege separation và emulation, đặc biệt khi kết hợp NOTIF_ADDFD. Nhưng cơ chế này không xóa race của bộ nhớ chia sẻ và không thay thế policy kernel. Thiết kế tốt luôn thu hẹp syscall được intercept, xác thực lifecycle bằng cookie, ưu tiên thao tác thay mặt target và để namespace, LSM, capability cùng quyền filesystem giữ vai trò ranh giới bảo mật cuối cùng.




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