通用通知机制

通用通知机制建立在标准管道驱动程序之上,它有效地将内核中的通知消息拼接(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;
                }
        }
}