通用通知機制

通用通知機制建立在標準管道驅動程式之上,它有效地將核心中的通知訊息拼接(splice)到使用者空間開啟的管道中。這可以與以下功能結合使用:

* Key/keyring notifications

通知緩衝區可以透過以下方式啟用:

“通用設定”/“通用通知佇列” (CONFIG_WATCH_QUEUE)

本文件包含以下部分:

概述

此功能表現為以特殊模式開啟的管道。管道的內部環形緩衝區用於儲存核心生成的 संदेश。這些 संदेश 隨後透過 read() 讀取。在此類管道上停用 splice 和類似操作,因為它們在某些情況下希望撤銷其對環形緩衝區的新增——這可能最終與通知訊息交錯。

管道的所有者必須告知核心它希望透過該管道監視哪些源。只有連線到管道的源才會將訊息插入其中。請注意,一個源可以繫結到多個管道並同時將訊息插入到所有這些管道中。

過濾器也可以放置在管道上,以便在不感興趣的情況下忽略某些源型別和子事件。

如果環形緩衝區中沒有可用槽位或沒有預分配的訊息緩衝區,則訊息將被丟棄。在這兩種情況下,read() 將在緩衝區中當前最後一個訊息被讀取後,將 WATCH_META_LOSS_NOTIFICATION 訊息插入到輸出緩衝區中。

請注意,當生成通知時,核心不會等待消費者收集它,而是繼續執行。這意味著通知可以在持有自旋鎖時生成,並且還保護核心免受使用者空間故障的無限期阻塞。

訊息結構

通知訊息以一個短報頭開始:

struct watch_notification {
        __u32   type:24;
        __u32   subtype:8;
        __u32   info;
};

“type”表示通知記錄的來源,“subtype”表示該來源的記錄型別(參見下面的“監視源”部分)。型別也可以是“WATCH_TYPE_META”。這是一種由監視佇列自身內部生成的特殊記錄型別。有兩種子型別:

  • WATCH_META_REMOVAL_NOTIFICATION

  • WATCH_META_LOSS_NOTIFICATION

前者表示安裝了監視的物件已被移除或銷燬,後者表示一些訊息已丟失。

“info”表示許多資訊,包括:

  • 訊息的長度(以位元組為單位),包括報頭(使用 WATCH_INFO_LENGTH 進行掩碼,並按 WATCH_INFO_LENGTH__SHIFT 進行移位)。這表示記錄的大小,可能在 8 到 127 位元組之間。

  • 監視 ID(使用 WATCH_INFO_ID 進行掩碼,並按 WATCH_INFO_ID__SHIFT 進行移位)。這表示呼叫者對監視的 ID,可能在 0 到 255 之間。多個監視可以共享一個佇列,這提供了一種區分它們的方法。

  • 一個型別特定欄位(WATCH_INFO_TYPE_INFO)。這由通知生產者設定,以指示特定於型別和子型別的含義。

除了長度之外,info 中的所有內容都可以用於過濾。

報頭之後可以跟補充資訊。其格式由型別和子型別自行決定和定義。

監視列表(通知源)API

“監視列表”是訂閱通知源的監視器列表。列表可以附加到物件(例如金鑰或超級塊),也可以是全域性的(例如裝置事件)。從使用者空間的視角來看,非全域性監視列表通常透過引用它所屬的物件來引用(例如使用 KEYCTL_NOTIFY 併為其提供金鑰序列號來監視該特定金鑰)。

為了管理監視列表,提供了以下函式:

  • void init_watch_list(struct watch_list *wlist,
                         void (*release_watch)(struct watch *wlist));
    

    初始化一個監視列表。如果 release_watch 不為 NULL,則表示一個函式,當 watch_list 物件被銷燬時應呼叫該函式,以丟棄監視列表對被監視物件持有的任何引用。

  • void remove_watch_list(struct watch_list *wlist);

    此函式移除訂閱到 watch_list 的所有監視並釋放它們,然後銷燬 watch_list 物件本身。

監視佇列(通知輸出)API

“監視佇列”是由應用程式分配的緩衝區,通知記錄將被寫入其中。其工作原理完全隱藏在管道裝置驅動程式內部,但有必要獲取其引用以設定監視。這些可以透過以下方式管理:

  • struct watch_queue *get_watch_queue(int fd);

    由於監視佇列透過實現緩衝區的管道的檔案描述符(fd)指示給核心,使用者空間必須透過系統呼叫傳遞該fd。這可以用於從系統呼叫中查詢監視佇列的不透明指標。

  • void put_watch_queue(struct watch_queue *wqueue);

    這會丟棄從 get_watch_queue() 獲取的引用。

監視訂閱API

“監視”是對監視列表的訂閱,指示將通知記錄寫入的監視佇列以及因此的緩衝區。監視佇列物件還可以攜帶由使用者空間設定的針對該物件的過濾規則。監視結構體的一些部分可以由驅動程式設定:

struct watch {
        union {
                u32             info_id;        /* ID to be OR'd in to info field */
                ...
        };
        void                    *private;       /* Private data for the watched object */
        u64                     id;             /* Internal identifier */
        ...
};

info_id”值應為從使用者空間獲取的 8 位數字,並按 WATCH_INFO_ID__SHIFT 移位。當通知寫入相關聯的監視佇列緩衝區時,此值將 OR 到 struct watch_notification::info 的 WATCH_INFO_ID 欄位中。

private 欄位是與 watch_list 關聯的驅動程式資料,並透過 watch_list::release_watch() 方法進行清理。

id 欄位是源的 ID。以不同 ID 釋出通知將被忽略。

提供了以下函式來管理監視:

  • void init_watch(struct watch *watch, struct watch_queue *wqueue);

    初始化一個監視物件,設定其指向監視佇列的指標,使用適當的屏障以避免 lockdep 投訴。

  • int add_watch_to_object(struct watch *watch, struct watch_list *wlist);

    將監視訂閱到監視列表(通知源)。在呼叫此函式之前,必須已設定監視結構中可由驅動程式設定的欄位。

  • int remove_watch_from_object(struct watch_list *wlist,
                                 struct watch_queue *wqueue,
                                 u64 id, false);
    

    從監視列表中移除一個監視,其中監視必須與指定的監視佇列(wqueue)和物件識別符號(id)匹配。一個通知(WATCH_META_REMOVAL_NOTIFICATION)被髮送到監視佇列,以指示該監視已被移除。

  • int remove_watch_from_object(struct watch_list *wlist, NULL, 0, true);

    從監視列表中移除所有監視。預計這將在銷燬之前呼叫,並且此時監視列表將對新監視不可訪問。一個通知(WATCH_META_REMOVAL_NOTIFICATION)被髮送到每個已訂閱監視的監視佇列,以指示該監視已被移除。

通知釋出API

要將通知釋出到監視列表,以便訂閱的監視能夠看到它,應使用以下函式:

void post_watch_notification(struct watch_list *wlist,
                             struct watch_notification *n,
                             const struct cred *cred,
                             u64 id);

通知應預先格式化,並傳入指向報頭(n)的指標。通知可能大於此大小,其在緩衝區槽位中的大小在 n->info & WATCH_INFO_LENGTH 中註明。

cred 結構體指示源(主體)的憑據,並傳遞給 LSMs(如 SELinux),以根據該佇列(物件)的憑據允許或抑制在每個獨立佇列中記錄通知。

id 是源物件的 ID(例如金鑰的序列號)。只有在其中設定了相同 ID 的監視才會看到此通知。

監視源

任何特定的緩衝區都可以從多個源獲取資料。源包括:

  • WATCH_TYPE_KEY_NOTIFY

    此型別通知指示金鑰和金鑰環的更改,包括金鑰環內容的更改或金鑰屬性的更改。

    更多資訊請參見核心金鑰保留服務

事件過濾

建立監視佇列後,可以使用以下方式應用一組過濾器來限制接收的事件:

struct watch_notification_filter filter = {
        ...
};
ioctl(fd, IOC_WATCH_QUEUE_SET_FILTER, &filter)

過濾器描述是一個型別為:

struct watch_notification_filter {
        __u32   nr_filters;
        __u32   __reserved;
        struct watch_notification_type_filter filters[];
};

其中“nr_filters”是 filters[] 中過濾器的數量,“__reserved”應為 0。“filters”陣列包含以下型別的元素:

struct watch_notification_type_filter {
        __u32   type;
        __u32   info_filter;
        __u32   info_mask;
        __u32   subtype_filter[8];
};

其中:

  • type 是要過濾的事件型別,應類似“WATCH_TYPE_KEY_NOTIFY”

  • info_filterinfo_mask 對通知記錄的 info 欄位進行過濾。只有在以下情況下,通知才會被寫入緩衝區:

    (watch.info & info_mask) == info_filter
    

    例如,這可以用於忽略不是精確在掛載樹中被監視點的事件。

  • subtype_filter 是一個位掩碼,指示感興趣的子型別。subtype_filter[0] 的第 0 位對應於子型別 0,第 1 位對應於子型別 1,依此類推。

如果 ioctl() 的引數為 NULL,則過濾器將被移除,並且來自被監視源的所有事件都將透過。

使用者空間程式碼示例

緩衝區透過類似以下程式碼建立:

pipe2(fds, O_TMPFILE);
ioctl(fds[1], IOC_WATCH_QUEUE_SET_SIZE, 256);

然後可以將其設定為接收金鑰環更改通知:

keyctl(KEYCTL_WATCH_KEY, KEY_SPEC_SESSION_KEYRING, fds[1], 0x01);

然後通知可以透過類似以下程式碼消費:

static void consumer(int rfd, struct watch_queue_buffer *buf)
{
        unsigned char buffer[128];
        ssize_t buf_len;

        while (buf_len = read(rfd, buffer, sizeof(buffer)),
               buf_len > 0
               ) {
                void *p = buffer;
                void *end = buffer + buf_len;
                while (p < end) {
                        union {
                                struct watch_notification n;
                                unsigned char buf1[128];
                        } n;
                        size_t largest, len;

                        largest = end - p;
                        if (largest > 128)
                                largest = 128;
                        memcpy(&n, p, largest);

                        len = (n->info & WATCH_INFO_LENGTH) >>
                                WATCH_INFO_LENGTH__SHIFT;
                        if (len == 0 || len > largest)
                                return;

                        switch (n.n.type) {
                        case WATCH_TYPE_META:
                                got_meta(&n.n);
                        case WATCH_TYPE_KEY_NOTIFY:
                                saw_key_change(&n.n);
                                break;
                        }

                        p += len;
                }
        }
}