fwctl 子系统

作者:

Jason Gunthorpe

概述

现代设备包含大量的固件,并且在许多情况下,很大程度上是软件定义的硬件。这种方法的演变在很大程度上是对摩尔定律的反应,现在芯片流片非常昂贵,并且芯片设计非常庞大。用灵活且紧密集成的固件/硬件组合替换固定的硬件逻辑,是降低芯片重置风险的有效方法。设备固件可以抵消硬件设计中的问题。对于向操作系统驱动程序呈现稳定且向后兼容的接口(例如 NVMe)的设备尤其如此。

设备中的固件层已经增长到令人难以置信的大小,设备经常集成快速处理器的集群来运行它。例如,mlx5 设备具有超过 30MB 的固件代码,并且大型配置以超过 1GB 的固件管理的运行时状态运行。

这种灵活层的可用性在行业中创造了相当多的多样性,现在单个硅片是可配置的软件定义设备,并且可以根据需要以截然不同的方式运行。此外,我们经常看到特定站点希望以高度专业化的方式操作设备,并且需要针对其独特配置量身定制的应用程序的情况。

此外,设备已经变得多功能和集成,以至于它们不再完全适合内核的子系统划分。现代多功能设备具有跨越多个子系统的驱动程序,例如 bnxt/ice/mlx5/pds,同时使用辅助设备系统共享底层硬件。

总而言之,这给操作系统带来了挑战,设备的固件环境非常庞大,需要强大的设备特定调试支持,以及不适合“通用”接口的固件驱动的功能。fwctl 旨在允许从用户空间访问设备的全部功能,包括可调试性、管理和首次启动/第 N 次启动配置。

fwctl 面向常见的设备设计模式,其中操作系统和固件通过使用队列或邮箱方案构建的 RPC 消息层进行通信。在这种情况下,驱动程序通常会有一个层来传递 RPC 消息并从设备固件收集 RPC 响应。为主要目的操作设备的内核内子系统驱动程序将使用这些 RPC 来构建其驱动程序,但设备通常也有一组不真正适合任何特定子系统的辅助 RPC。例如,硬件 RAID 控制器主要由块层操作,但也带有一组 RPC 来管理硬件 RAID 中的驱动器构建。

过去,当设备功能更多时,各个子系统会发展出不同的方法来解决这些常见问题。例如,监视设备运行状况、操作其 FLASH、调试固件、配置等,在内核中都有各种独特的接口。

fwctl 的目的是定义一组有限的通用规则(如下所述),允许用户空间安全地在设备固件内部构建和执行 RPC。这些规则充当操作系统和固件之间关于如何正确设计 RPC 接口的协议。作为 uAPI,该子系统提供了一个薄的发现层和一个通用的 uAPI 来传递 RPC 并收集响应。它支持一个用户空间库和工具系统,该系统将使用此接口来使用设备本机协议控制设备。

操作范围

fwctl 驱动程序严格限制为操作设备固件的方式。它不是访问随机内核内部结构或其他操作系统软件状态的途径。

fwctl 实例必须在定义明确的设备功能上运行,并且设备应该具有定义明确的安全模型,以确定允许该功能访问物理设备内的范围。例如,当今最复杂的 PCIe 设备可能大致具有多个功能级范围

  1. 具有完全访问设备上全局状态和配置的特权功能

  2. 多个虚拟机监控程序功能,可以控制自身以及与虚拟机一起使用的子功能

  3. 多个虚拟机功能,严格限定在虚拟机内

设备可以在这些范围之间创建逻辑父/子关系。例如,子虚拟机的固件可能在虚拟机监控程序固件的范围内。在 VFIO 世界中,虚拟机监控程序环境通常负责为 VFIO 分配给虚拟机的函数进行复杂的配置/分析/配置。

此外,在功能范围内,设备通常具有属于某些常规操作范围内的 RPC 命令(请参阅enum fwctl_rpc_scope

  1. 访问在功能重置时生效的功能和子配置、FLASH 等。 访问对任何驱动程序或虚拟机透明或非破坏性的功能和子运行时配置。

  2. 对功能调试信息的只读访问,该调试信息可以报告功能和子功能中的固件对象,包括其他内核子系统拥有的固件对象。

  3. 对与内核锁定和内核完整性保护原则严格兼容的功能和子调试信息进行写入访问。 触发内核污染。

  4. 完全调试设备访问。 触发内核污染,需要 CAP_SYS_RAWIO。

用户空间将在每个 RPC 上提供一个范围标签,并且内核必须根据该范围强制执行上述 CAP 和污染。内核和固件的组合可以强制用户空间将 RPC 放置在正确的范围内。

拒绝的行为

此接口不能允许用户空间做很多事情(没有污染或 CAP),这些事情大致源自内核锁定原则。 一些例子

  1. DMA 到/从任意内存、挂起系统、使用不受信任的代码破坏固件完整性,或以其他方式破坏设备或系统安全性和完整性。

  2. 为内核驱动程序提供异常的“后门”。 不要操纵内核驱动程序拥有的内核对象。

  3. 直接配置或以其他方式控制内核驱动程序。 子系统内核驱动程序可以在功能重置/驱动程序加载时对设备配置做出反应,但在其他情况下不得与 fwctl 耦合。

  4. 以与另一个主要内核子系统的核心目的重叠的方式操作硬件,例如读/写 LBA、发送/接收网络数据包或操作加速器的数据平面。

fwctl 不能替代设备直接访问子系统,例如 uacce 或 VFIO。

通过 fwctl 的非污染接口公开的操作应与其他设备用户完全共享。 例如,通过 fwctl 公开 RPC 永远不应阻止内核子系统将来同时使用相同的 RPC 或硬件单元。 在这种情况下,fwctl 将不如最终出现的适当内核子系统重要。 在此区域中导致冲突的错误将优先考虑内核实现来解决。

fwctl 用户 API

通用 ioctl 格式

ioctl 接口遵循通用格式以允许扩展。 每个 ioctl 都传递一个结构指针作为参数,在第一个 u32 中提供结构的大小。 内核检查超出其理解范围的任何结构空间是否为 0。 这允许用户空间使用向后兼容的部分,同时一致地使用更新的、更大的结构。

ioctls 对常见的错误代码使用标准含义

  • ENOTTY:完全不支持 IOCTL 号码本身

  • E2BIG:支持 IOCTL 号码,但提供的结构在内核不理解的部分中具有非零值。

  • EOPNOTSUPP:支持 IOCTL 号码,并且理解结构,但是已知字段具有内核不理解或不支持的值。

  • EINVAL:理解了关于 IOCTL 的所有内容,但是字段不正确。

  • ENOMEM:内存不足。

  • ENODEV:底层设备已热插拔,并且 FD 为

    孤立。

以及特定 ioctl 中的其他错误代码。

struct fwctl_info

ioctl(FWCTL_INFO)

定义:

struct fwctl_info {
    __u32 size;
    __u32 flags;
    __u32 out_device_type;
    __u32 device_data_len;
    __aligned_u64 out_device_data;
};

成员

size

sizeof(struct fwctl_info)

flags

必须为 0

out_device_type

从 enum fwctl_device_type 返回设备类型

device_data_len

在输入时,out_device_data 内存的长度。 在输出时,内核 device_data 的大小,可能大于或小于输入。 在输入时可能为 0。

out_device_data

指向 device_data_len 字节内存的指针。 内核将填充整个内存,根据需要清零。

描述

返回关于此 fwctl 实例的基本信息,特别是用于定义 device_data 格式的驱动程序。

enum fwctl_rpc_scope

RPC 的访问范围

常量

FWCTL_RPC_CONFIGURATION

设备配置访问范围

对设备配置的读/写访问。 当配置写入设备时,它保持完全受支持的状态。

FWCTL_RPC_DEBUG_READ_ONLY

对调试信息的只读访问

可读的调试信息。 调试信息与内核锁定兼容,并且不泄露任何敏感信息。 例如,禁止从此信息中公开任何加密密钥。

FWCTL_RPC_DEBUG_WRITE

对与锁定兼容的调试信息的可写访问

允许写入设备中的数据,这可能会导致设备脱离完全受支持的状态。 这旨在允许密集且可能具有侵入性的调试。 此范围将污染内核。

FWCTL_RPC_DEBUG_WRITE_FULL

对所有调试信息的写入访问

允许读/写访问所有内容。 需要 CAP_SYS_RAW_IO,因此不需要遵循锁定原则。 如果有疑问,调试应放置在此范围内。 此范围将污染内核。

描述

有关这些范围的更详细讨论,请参阅fwctl 子系统

struct fwctl_rpc

ioctl(FWCTL_RPC)

定义:

struct fwctl_rpc {
    __u32 size;
    __u32 scope;
    __u32 in_len;
    __u32 out_len;
    __aligned_u64 in;
    __aligned_u64 out;
};

成员

size

sizeof(struct fwctl_rpc)

scope

enum fwctl_rpc_scope 之一,RPC 的必需范围

in_len

内存中的长度

out_len

内存中的长度

in

设备特定格式的请求消息

out

设备特定格式的响应消息

描述

将远程过程调用传递到设备固件并返回响应。 调用的参数和返回值被编组到线性内存缓冲区中。 任何错误代码都指示将 RPC 传递到设备失败。 在成功传递期间设备中产生的返回状态必须编码到 out 中。

缓冲区的格式与 FWCTL_INFO 中的 out_device_type 匹配。

struct fwctl_info_mlx5

ioctl(FWCTL_INFO) out_device_data

定义:

struct fwctl_info_mlx5 {
    __u32 uid;
    __u32 uctx_caps;
};

成员

uid

此 FD 绑定到的 FW UID。 每个命令标头都将强制执行此值。

uctx_caps

为 uid 启用的 FW 功能。

描述

返回关于可用 FW 接口的基本信息。

struct fwctl_info_pds

定义:

struct fwctl_info_pds {
    __u32 uctx_caps;
};

成员

uctx_caps

固件功能位图

描述

返回关于可用 FW 接口的基本信息。

enum pds_fwctl_capabilities

常量

PDS_FWCTL_QUERY_CAP

可以查询固件以获取信息

PDS_FWCTL_SEND_CAP

可以向固件发送命令

struct fwctl_rpc_pds

定义:

struct fwctl_rpc_pds {
    struct {
        __u32 op;
        __u32 ep;
        __u32 rsvd;
        __u32 len;
        __aligned_u64 payload;
    } in;
    struct {
        __u32 retval;
        __u32 rsvd[2];
        __u32 len;
        __aligned_u64 payload;
    } out;
};

成员

in

rpc in 参数

in.op

请求的操作码

in.ep

要操作的固件端点

in.rsvd

保留

in.len

有效负载数据的长度

in.payload

有效负载缓冲区的地址

out

rpc out 参数

out.retval

操作结果值

out.rsvd

保留

out.len

结果数据缓冲区的长度

out.payload

有效负载数据缓冲区的地址

sysfs 类

fwctl 具有一个 sysfs 类 (/sys/class/fwctl/fwctlNN/) 和字符设备 (/dev/fwctl/fwctlNN),具有一个简单的编号方案。 字符设备操作上述 iotcl uAPI。

fwctl 设备可以通过 sysfs 与其他子系统中的驱动程序组件相关联

$ ls /sys/class/fwctl/fwctl0/device/infiniband/
ibp0s10f0

$ ls /sys/class/infiniband/ibp0s10f0/device/fwctl/
fwctl0/

$ ls /sys/devices/pci0000:00/0000:00:0a.0/fwctl/fwctl0
dev  device  power  subsystem  uevent

用户空间社区

从 nvme-cli 中汲取灵感,参与内核端必须带有一个通用的 TBD git 树中的用户空间,至少要有效地操作内核驱动程序。 提供此类实现是合并内核驱动程序的先决条件。

目标是围绕我们所有人都遇到的一些共同问题建立用户空间社区,理想情况下开发一些具有以下一些起始主题的常见用户空间程序

  • 设备现场调试

  • 硬件配置

  • 虚拟机启动前的 VFIO 子设备分析

  • 机密计算主题(证明、安全配置)

跨越内核中的所有子系统。 fwupd 是一个很好的例子,说明了如何从内核端的多样性中涌现出优秀的用户空间体验。

fwctl 内核 API

int fwctl_register(struct fwctl_device *fwctl)

将新设备注册到子系统

参数

struct fwctl_device *fwctl

先前分配的 fwctl_device

描述

返回时,该设备通过 sysfs 和 /dev 可见,可能会调用驱动程序操作。

void fwctl_unregister(struct fwctl_device *fwctl)

从子系统注销设备

参数

struct fwctl_device *fwctl

先前分配和注册的 fwctl_device

描述

撤消 fwctl_register()。 返回时,不会调用任何驱动程序操作。 调用者仍然必须调用 fwctl_put() 来释放 fwctl。

即使用户空间仍然有打开的文件描述符,注销也会返回。 这将对任何打开的 FD 调用 ops->close_uctx(),并且在返回后不会调用驱动程序操作。 FD 保持打开状态,但所有 fops 都将返回 -ENODEV。

fwctl 的设计允许驱动程序与子系统进行这种分离,这主要是通过使内存分配归核心子系统所有来实现的。 可以释放 fwctl_device 和 fwctl_uctx,而无需驱动程序回调。 这允许在 FD 打开时模块保持未锁定状态。

struct fwctl_ops

驱动程序提供的操作

定义:

struct fwctl_ops {
    enum fwctl_device_type device_type;
    size_t uctx_size;
    int (*open_uctx)(struct fwctl_uctx *uctx);
    void (*close_uctx)(struct fwctl_uctx *uctx);
    void *(*info)(struct fwctl_uctx *uctx, size_t *length);
    void *(*fw_rpc)(struct fwctl_uctx *uctx, enum fwctl_rpc_scope scope, void *rpc_in, size_t in_len, size_t *out_len);
};

成员

device_type

驱动程序分配的 device_type 号码。 这是 uABI。

uctx_size

要分配的 fwctl_uctx 结构的大小。 此内存的第一个字节将是一个 fwctl_uctx。 驱动程序可以使用剩余的字节作为其私有内存。

open_uctx

在首次使用 uctx 之前打开文件描述符时调用。

close_uctx

销毁 uctx 时调用,通常在 FD 关闭时调用。

info

实现 FWCTL_INFO。 返回复制到 out_device_data 的 kmalloc() 内存。 在输入时,length 指示用户缓冲区的大小,在输出时,它指示内存的大小。 驱动程序可以在输入时忽略 length,核心代码将处理所有内容。

fw_rpc

实现 FWCTL_RPC。 将 rpc_in/in_len 传递到 FW 并返回响应并设置 out_len。 rpc_in 可以作为响应指针返回。 否则,返回的指针将使用 kvfree() 释放。

描述

fwctl_unregister() 将等到所有正在执行的操作完成后才会返回。 驱动程序应注意不要让他们的操作运行太长时间,因为它会阻止设备热插拔和模块卸载。

struct fwctl_device

每个驱动程序的注册结构

定义:

struct fwctl_device {
    struct device dev;
};

成员

dev

sysfs (class/fwctl/fwctlXX) 设备

描述

每个驱动程序实例都将具有其中一个结构,驱动程序私有数据紧随其后。 此结构是引用计数的,它通过调用 fwctl_put() 释放。

fwctl_alloc_device

fwctl_alloc_device (parent, ops, drv_struct, member)

分配一个 fwctl

参数

parent

提供 FW 接口的物理设备

ops

要注册的驱动程序操作

drv_struct

保存 struct fwctl_device 的“struct driver_fwctl”

member

drv_structstruct fwctl_device 的名称

描述

这会分配并初始化嵌入在 drv_struct 中的 fwctl_device。 成功后,必须通过 fwctl_put() 释放指针。 成功时返回“drv_struct *”,错误时返回 NULL。

struct fwctl_uctx

每个用户 FD 上下文

定义:

struct fwctl_uctx {
    struct fwctl_device *fwctl;
};

成员

fwctl

拥有上下文的 fwctl 实例

描述

用户空间打开的每个 FD 都将获得唯一的上下文分配。 任何驱动程序私有数据都将紧随其后。

fwctl 驱动程序设计

在许多情况下,fwctl 驱动程序将是更大的跨子系统设备的一部分,可能使用辅助设备机制。 在这种情况下,多个子系统将共享同一个设备和 FW 接口层,因此设备设计必须已经提供内核子系统之间的隔离和协作。 fwctl 应该适合相同的模型。

驱动程序的一部分应包括对其范围限制和安全模型如何工作的描述。 驱动程序和固件必须共同确保用户空间提供的 RPC 映射到适当的范围。 如果验证是在驱动程序中完成的,则验证可以从设备读取“命令效果”报告,或者硬连接执行。 如果验证是在固件中完成的,则驱动程序应将 fwctl_rpc_scope 与命令一起传递到固件。

驱动程序和固件必须协同工作以确保 fwctl 不能分配任何 FW 资源,或者它分配的任何资源都在 FD 关闭时释放。 主要围绕 FW RPC 构建的驱动程序可能会发现其核心 PCI 功能和 RPC 层属于 fwctl,辅助设备连接到其他子系统。

每种设备类型都必须注意 Linux 的稳定 ABI 理念。 FW RPC 接口不必满足严格的稳定 ABI,但确实需要满足这样一个期望:已部署并大量使用的用户空间工具不会不必要地中断。 FW 升级和内核升级应使广泛部署的工具正常工作。

在更宽松的范围下以开发和调试为重点的 RPC 如果使用它们的工具仅在特殊情况下运行而不是用于设备的日常使用,则可以具有较少的稳定性。 调试工具甚至可能需要精确的版本匹配,因为它们可能需要类似于 FW 二进制文件中的 DWARF 调试信息。

安全响应

内核仍然是此接口的守门人。 如果发现违反范围、安全或隔离原则的情况,我们可以选择通过 FW 更新来修复这些问题,推送内核补丁来解析和阻止 RPC 命令,或推送内核补丁来阻止整个固件版本/设备。

虽然内核始终可以直接解析和限制 RPC,但预计允许驱动程序将验证委托给 FW 的现有内核模式将是一个有用的设计。

现有类似示例

本文档中描述的方法并不是一个新想法。 几十年来,内核一直在不同的领域提供直接或接近直接的设备访问。 随着越来越多设备想要遵循这种设计模式,很明显它并没有被完全理解,更重要的是,安全注意事项没有得到很好的定义或一致同意。

一些例子

  • 硬件 RAID 控制器。 这包括执行诸如将驱动器组合到 RAID 卷中、配置 RAID 参数、监视硬件等操作的 RPC。

  • 基板管理器。 用于配置设备中的设置等的 RPC

  • NVMe 供应商命令胶囊。 nvme-cli 提供了对不同产品定义的一些监视功能的访问,但存在更多功能。

  • CXL 也有一个类似 NVMe 的供应商命令系统。

  • DRM 允许用户空间驱动程序通过内核中介将命令发送到设备

  • RDMA 允许用户空间驱动程序直接将命令推送到设备,而无需内核参与

  • 各种“原始”API,原始 HID (SDL2)、原始 USB、NVMe 通用接口等。

前 4 个示例是 fwctl 旨在涵盖的领域。 后三个示例是被拒绝的行为,因为它们与内核子系统的主要目的完全重叠。

从过去的这些努力中吸取的一些关键教训是,必须有一个通用的用户空间项目才能获得内核驱动程序。 在用户空间中围绕有用的软件开发良好的社区是公司资助参与以启用其产品的关键。