[Linux Kernel] Giải phẫu Fanotify: Cách hệ điều hành "đóng băng" tiến trình để chặn rò rỉ dữ liệu
Bạn có bao giờ chạy lệnh cat hay mở một file trên Linux và thấy nó khựng lại vài trăm mili-giây trước khi nội dung hiện ra — dù cho ổ cứng của bạn là chuẩn NVMe tốc độ cao?
Rất có thể ngay lúc đó, tiến trình của bạn đang bị "đóng băng" hoàn toàn bởi một cơ chế nằm sâu trong nhân Linux. Số phận của lệnh cat ấy — được đi tiếp hay nhận về Permission Denied — hoàn toàn nằm trong tay một phần mềm Antivirus/DLP đang chạy nền.
Cơ chế đứng sau khả năng chặn đứng một tiến trình ngay tại thời điểm nó mở file, trước khi ứng dụng kịp đọc bất kỳ byte dữ liệu nào, chính là Fanotify.
Trong bài viết này, chúng ta đã cùng mổ xẻ sâu vào bên trong nhân Linux (Linux Kernel) để xem Fanotify được cấu thành từ những thành phần nào, và cách nó đóng băng một tiến trình để kiểm duyệt file ra sao.
Mục lục
Phần 1: Kiến trúc cốt lõi của Fanotify
Để hiểu được Fanotify, chúng ta cần nắm được các "bánh răng" bên trong Kernel đang vận hành cùng nó.
1. Syscall Entry Interface (Cửa ngõ Giao tiếp Hệ thống)
Phân tích & Giải thích: Khi một chương trình ở User-Space muốn tương tác với phần cứng hoặc tài nguyên hệ thống (như mở một file), nó không thể tự ý làm điều đó. Thay vào đó, nó phải "xin phép" Kernel thông qua các System Calls (Syscalls). Syscall Entry Interface đóng vai trò như một gateway duy nhất để chuyển đổi ngữ cảnh từ User-Mode (quyền hạn cực kỳ hạn chế) sang Kernel-Mode (toàn quyền kiểm soát).
2. VFS Layer (Virtual File System)
Phân tích & Giải thích:
Linux hỗ trợ hàng chục loại File System (hệ thống tập tin) khác nhau như ext4, NTFS, XFS, FAT32... VFS là một tầng trừu tượng (abstraction layer) giúp Kernel vận hành trơn tru mà không cần quan tâm ổ cứng bên dưới đang dùng định dạng gì. Mọi lệnh mở/đọc/ghi từ Syscall đều phải đi qua VFS, sau đó VFS mới biên dịch lệnh này đẩy xuống driver của File System tương ứng.
Các bước xử lý (Ví dụ khi sys_openat() được gọi):
path_openat(): Phân giải chuỗi đường dẫn (ví dụ:/etc/shadow) thành các đối tượng dữ liệu thực sự trong bộ nhớ Kernel.do_dentry_open(): Khởi tạo cấu trúc dữ liệu cho file đang được chuẩn bị mở.vfs_open(): Thực thi việc mở file thực sự tại tầng VFS. Tại bước này, VFS bắt đầu gọi các hooks (điểm neo) để kiểm tra phân quyền bảo mật.
3. fsnotify Core Subsystem
Phân tích & Giải thích:
fsnotify chính là "hệ thống báo động trung tâm" của toàn bộ Kernel đối với các sự kiện liên quan đến file. Bản thân nó không phải là inotify, dnotify, hay fanotify; đúng hơn, nó là bộ khung nền tảng (core framework) nằm bên dưới cả 3 công cụ kia. Khi VFS hoàn thành (hoặc sắp hoàn thành) một thao tác, nó sẽ bắn thông báo cho fsnotify.
4. fanotify Kernel Engine
Phân tích & Giải thích:
Đây là engine xử lý chuyên dụng của Fanotify. Nó đóng vai trò như một client đăng ký nhận thông báo từ fsnotify. Nhiệm vụ của nó là: Nhận sự kiện từ fsnotify, đóng gói lại, đẩy lên cho ứng dụng giám sát ở User-Space (ví dụ: phần mềm Antivirus / DLP), và đóng băng (freeze) tiến trình của người dùng cho đến khi Antivirus trả lời.
5. fanotify Data Structures (Cấu trúc dữ liệu)
Đây là các cấu trúc dữ liệu được khởi tạo trong RAM để Kernel quản lý trạng thái của Fanotify:
| Cấu trúc | Vai trò |
|---|---|
struct fsnotify_group |
Đại diện cho một phiên làm việc (session) của Agent. Được tạo khi Antivirus gọi fanotify_init(), dùng để quản lý các hàng đợi (queues). |
struct fsnotify_mark |
"Nhãn dán" gắn vào đối tượng VFS. Khi Antivirus gọi fanotify_mark() lên /etc, Kernel dán nhãn này vào thư mục đó để VFS biết cần kích hoạt Fanotify khi có truy cập. |
struct fanotify_perm_event |
Payload chứa thông tin chi tiết về sự kiện đang chờ cấp phép. |
notification_queue |
Hàng đợi lưu các object sự kiện, chờ Antivirus gọi read() để lấy lên phân tích. |
access_waitq (Wait queue) |
"Phòng chờ" — nơi tiến trình người dùng (ví dụ cat) bị đưa vào ngủ đông, chờ phán quyết từ Antivirus. |
6. VFS Core Structures
Đây là những cấu trúc dữ liệu nền tảng nhất của Linux để mô tả hệ thống file vật lý trên RAM. Các fsnotify_mark của Fanotify sẽ được gắn trực tiếp lên các cấu trúc này:
| Cấu trúc | Vai trò |
|---|---|
struct super_block |
Đại diện cho toàn bộ một phân vùng (ví dụ /dev/sda1). Nếu Fanotify giám sát cả một ổ đĩa, mark được gắn ở đây. |
struct vfsmount / struct mount |
Đại diện cho một điểm gắn kết (mount point) trong cây thư mục ảo. Khả năng gắn mark vào đây là tính năng mạnh nhất của Fanotify so với Inotify. |
struct inode |
Trái tim của hệ thống file — mỗi file/thư mục có một inode duy nhất lưu metadata. Khi giám sát một file cụ thể, mark được gắn thẳng vào inode này. |
Phần 2: Quy trình thực thi tuần tự của Fanotify (Step-by-Step)
Kịch bản tham chiếu: Một phần mềm Data Loss Prevention (DLP Agent) đang giám sát hệ thống. Người dùng cố gắng dùng lệnh
catđể đọc một file chứa dữ liệu nhạy cảm (/var/mydlp_test/pii.txt).
Step 1: User Process (Yêu cầu mở file)
Mọi thứ luôn bắt đầu tại User-Space. Ứng dụng muốn truy cập nội dung file sẽ gọi System call openat(). Các tham số truyền vào bao gồm: thư mục làm việc hiện tại (AT_FDCWD), đường dẫn tuyệt đối của file, và quyền truy cập mong muốn (O_RDONLY - chỉ đọc).
Step 2: Syscall Entry (Chuyển giao quyền lực)
Ứng dụng ở User-Space không có quyền đọc trực tiếp từ ổ cứng. Lệnh openat kích hoạt một ngắt mềm (software interrupt). CPU ngay lập tức chuyển ngữ cảnh từ User-Mode sang Kernel-Mode. Kernel tiếp nhận yêu cầu này tại cổng sys_openat().
Step 3: VFS Layer (Phân giải đường dẫn - Path Resolution)
Kernel lúc này chỉ nhận được một chuỗi text /var/mydlp_test/pii.txt. Tầng VFS phải dịch chuỗi này thành các đối tượng vật lý. Hàm path_openat() sẽ đi qua từng cấp thư mục: tìm root /, vào var, vào mydlp_test để truy xuất thành công dentry (tên file) và inode (dữ liệu thực) của file.
Step 4: VFS Layer (Khởi tạo file & Chuẩn bị thanh tra)
Hàm do_dentry_open() tạo ra một struct file trong RAM đại diện cho phiên làm việc này. Tuy nhiên, trước khi chính thức cho phép ứng dụng đọc nội dung, VFS thiết lập một "trạm kiểm soát" bằng cách gọi hàm fsnotify_file_perm(file, MAY_READ).
Step 5: fsnotify Core (Phát hiện mục tiêu)
Bên trong hàm fsnotify_file_perm(), Kernel kiểm tra inode, điểm gắn kết (vfsmount), và ổ đĩa (super_block). Nó phát hiện ra một nhãn (fsnotify_mark) đã được DLP Agent đính kèm từ trước, có chứa cờ yêu cầu cấp phép: FAN_OPEN_PERM.
Step 6: fanotify Engine (Đóng gói sự kiện)
Nhận thấy cờ Fanotify, luồng thực thi được chuyển giao cho fanotify_handle_event(). Kernel cần tạo một báo cáo gửi lên DLP Agent. Thay vì tốn thời gian cấp phát RAM mới, nó chộp ngay một cấu trúc fanotify_perm_event trống có sẵn trong cache để ghi chú thông tin.
Step 7: fanotify Engine (Khởi tạo File Descriptor vô danh - anon_fd)
Kernel đánh dấu trạng thái sự kiện là FAN_EVENT_INIT. Quan trọng nhất, nó tạo ra một Anonymous File Descriptor (anon_fd) trỏ thẳng tới struct file của pii.txt. Tính năng này cho phép DLP Agent có thể quét trực tiếp nội dung file một cách an toàn mà không cần biết tên file là gì, cũng như không sợ file bị đổi tên giữa chừng.
Step 8: fanotify Engine (Đánh thức Agent - Waking the agent)
Báo cáo hoàn tất và được đẩy vào notification_queue. Kernel sau đó bắn tín hiệu đánh thức notification_waitq. Hành động này làm cho lệnh read() của DLP Agent (đang chạy nền chờ chực ở User-Space) nhận được dữ liệu và bừng tỉnh.
Step 9: Kernel Scheduler (Đóng băng tiến trình người dùng)
Trong lúc chờ DLP Agent phân tích và phản hồi, tiến trình cat của người dùng không thể chạy tiếp. Kernel gọi wait_event_killable() để ép tiến trình này vào trạng thái ngủ sâu (TASK_KILLABLE). Nó sẽ bị treo cứng tại hàm VFS này, không tiêu thụ CPU, và chỉ thức dậy khi trạng thái sự kiện đổi thành FAN_EVENT_ANSWERED (đã có kết quả) hoặc bị người dùng ép kill (ví dụ: bấm Ctrl+C).
Step 10: Agent Daemon (Nhận dữ liệu từ Kernel)
Tại User-Space, DLP Agent liên tục chạy vòng lặp read(fanotify_fd, ...). Ở Bước 8, khi Kernel báo hiệu, lệnh read() này lập tức hút dữ liệu lên. Hàm fanotify_read() của Kernel sẽ chuyển giao (transfer) cấu trúc metadata kèm theo cái "chìa khóa" anon_fd lên cho Agent.
Step 11: Agent Daemon (Phân tích nội dung & Dò tìm vi phạm)
Đây là bước cốt lõi của tính năng bảo mật Content-aware. Thay vì dùng đường dẫn file thông thường (việc này sẽ tạo ra vòng lặp vô hạn do kích hoạt Fanotify lần nữa), Agent sử dụng luôn anon_fd do Kernel cấp để trích xuất thẳng nội dung file vào bộ nhớ. Sau đó, nó chạy các thuật toán Regex để quét. Kết quả: Nó phát hiện file này chứa dữ liệu PII/Credit Card nhạy cảm.
Step 12: Agent Daemon (Đưa ra phán quyết)
Xác định đây là hành vi vi phạm chính sách, Agent quyết định block tiến trình. Nó tạo một cấu trúc fanotify_response, chứa 2 tham số then chốt:
response.fd = anon_fd(để Kernel biết Agent đang phản hồi cho sự kiện nào).response.response = FAN_DENY(Từ chối truy cập).
Sau đó, Agent gọi hàm write(), bơm gói dữ liệu phán quyết này ngược trở lại fanotify_fd.
Step 13: Fanotify Engine (Kernel tiếp nhận phán quyết)
Lệnh write() từ User-Space lại kích hoạt một System Call (sys_write), chuyển quyền kiểm soát trở lại Kernel-Mode. Kernel nhận ra luồng ghi này đang nhắm vào File Descriptor của Fanotify, nên nó tự động điều hướng luồng dữ liệu tới hàm xử lý chuyên dụng fanotify_write().
Step 14: fanotify Engine (Khớp kết quả và Chuyển đổi trạng thái)
Hàm fanotify_write() không áp dụng phán quyết một cách bừa bãi. Nó tìm kiếm trong danh sách các sự kiện đang bị đình chỉ (nằm trong access_waitq) để xem tiến trình nào khớp với anon_fd vừa gửi xuống. Khi tìm đúng cấu trúc fanotify_perm_event của lệnh cat ban đầu, nó tiến hành cập nhật:
- Ghi lại kết quả:
event->response = FAN_DENY. - Cập nhật trạng thái sự kiện:
event->state = FAN_EVENT_ANSWERED.
Lúc này, tiến trình người dùng sẽ được đánh thức để nhận thông báo "Permission Denied".
Step 15: Kernel Scheduler (Đánh thức tiến trình người dùng)
Ngay khi hàm fanotify_write() (ở Step 14) hoàn tất việc cập nhật kết quả FAN_DENY cho sự kiện, nó sẽ lập tức gọi hàm wake_up(&event->waitq). Lệnh này đánh động Kernel Scheduler (Bộ lập lịch của nhân hệ điều hành), phát đi tín hiệu: "Tiến trình bị đóng băng đã nhận được phán quyết, hãy cho phép nó chạy tiếp".
Tiến trình cat ngay lập tức thoát khỏi trạng thái ngủ sâu (TASK_KILLABLE), chuyển sang trạng thái sẵn sàng chạy (TASK_RUNNING), và tiếp tục luồng thực thi (execution thread) đang bị tạm dừng ngay bên trong hàm fanotify_handle_event().
Step 16: fsnotify Core (Đọc kết quả và dọn dẹp)
Sau khi "tỉnh dậy", luồng thực thi tiến hành kiểm tra biến event->response và nhận thấy giá trị lúc này là FAN_DENY. Tại thời điểm này, sự kiện đã hoàn thành "sứ mệnh lịch sử" của nó.
Kernel bắt đầu công đoạn dọn dẹp chiến trường: nó gọi hàm close() để đóng Anonymous File Descriptor (anon_fd), giải phóng vùng nhớ của struct fanotify_perm_event trả về cho cache. Và quan trọng nhất, nó ném ngược mã lỗi -EPERM (Error: Permission Denied) về cho hàm đã gọi nó là fsnotify_file_perm().
Step 17: VFS Layer (Hủy bỏ toàn bộ quá trình mở file)
Hàm fsnotify_file_perm() nhận được mã lỗi -EPERM và báo cáo ngược lại cho tầng VFS (cụ thể là hàm do_dentry_open()). Bởi vì bước kiểm duyệt an ninh đã thất bại, tầng VFS buộc phải hủy bỏ toàn bộ quá trình mở file.
Cấu trúc struct file trung gian được khởi tạo "hờ" ở Step 4 (chưa kịp đưa vào sử dụng) giờ đây bị tiêu hủy hoàn toàn. Tệp pii.txt chưa bao giờ thực sự được mở ra để đọc bất kỳ một byte dữ liệu nào.
Step 18: User Process (Ứng dụng nhận về thất bại)
Mã lỗi -EPERM tiếp tục dội ngược lên trên, xuyên qua ranh giới Kernel-Space để trở về lại User-Space. Lời gọi hệ thống (System Call) sys_openat() trả về giá trị -1 (một chỉ báo lỗi chung chung) cho thư viện C tiêu chuẩn (glibc).
Nhận thấy có lỗi xảy ra, glibc bóc tách mã lỗi thực tế và gán nó cho biến toàn cục mang tên errno. Ứng dụng cat nhận được kết quả -1, tiến hành kiểm tra errno và in thông báo lỗi tương ứng ("Permission denied") ra màn hình terminal. Quy trình bảo vệ dữ liệu kết thúc thành công mỹ mãn!
Qua 18 bước ở trên, có thể thấy điều làm nên sức mạnh thực sự của Fanotify không phải là việc nó biết file đang bị mở — Inotify cũng làm được điều đó — mà là khả năng tạm dừng cả một tiến trình đang chạy để chờ phán quyết, rồi mới quyết định cho nó sống tiếp hay chết bằng -EPERM. Đây chính là ranh giới giữa một công cụ giám sát (monitoring) và một công cụ kiểm duyệt thực sự (enforcement) — và cũng là lý do mọi giải pháp DLP/Antivirus nghiêm túc trên Linux đều phải dựa vào nó thay vì Inotify.
Nếu thấy bài viết hữu ích và dễ hiểu, đừng quên để lại một Like và Follow nhé! Ở bài viết tiếp theo, chúng ta sẽ bước ra khỏi Kernel-Space để đi vào thực chiến phần lõi của ứng dụng bảo mật: Tự tay code tính năng Content-Aware (Nhận thức nội dung) cho Agent. Tôi sẽ hướng dẫn chi tiết cách xây dựng các bộ Detector quét sâu vào ruột file để bóc tách dữ liệu nhạy cảm (sử dụng Regex bắt số CCCD, Keyword, và các logic tính điểm rủi ro)—chính xác theo cách mà các hệ thống DLP thương mại đang vận hành. Cùng đón chờ nhé!
All Rights Reserved