找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2924

积分

0

好友

390

主题
发表于 前天 04:33 | 查看: 8| 回复: 0

前几天浏览 Linux 内核的提交记录时,一个典型的补丁引起了我的注意——改动仅有两行,但其背后的教训值得每一位驱动开发者深思。

提交者是 ALSA 子系统的核心维护者 Takashi Iwai,对于从事 Linux 音频驱动开发的同行来说,这个名字再熟悉不过。这次他修复的是 Focusrite Scarlett2 系列 USB 声卡 mixer 驱动中的一个漏洞,而发现这个漏洞的,正是 Google 的内核 fuzzer:syzbot。

先看补丁,就两行

// sound/usb/mixer_scarlett2.c
if (desc->bInterfaceClass != 255)
    continue;
+if (desc->bNumEndpoints < 1)
+    continue;

epd = get_endpoint(intf->altsetting, 0);

改动很简单:增加了一个条件判断。如果目标接口没有端点(endpoint),就跳过,不再继续处理。

就这么简单。但如果没有这两行代码,内核会因 NULL dereference(空指针解引用)而直接崩溃。

翻车现场还原

要理解这个 Bug,得先看看 scarlett2_find_fc_interface() 这个函数在做什么。

Focusrite Scarlett 系列声卡有一个私有的控制接口(Focusrite Control Interface)。驱动初始化时,需要在 USB 配置描述符中遍历并找到它。寻找的逻辑很直接:遍历所有接口,检查哪个接口的 bInterfaceClass 等于 255(Vendor Specific,即厂商自定义类):

static int scarlett2_find_fc_interface(struct usb_device *dev,
                                       struct scarlett2_data *private)
{
    struct usb_host_config *config = dev->actconfig;
    int i;

    for (i = 0; i < config->desc.bNumInterfaces; i++) {
        struct usb_interface *intf = config->interface[i];
        struct usb_interface_descriptor *desc =
            &intf->altsetting[0].desc;
        struct usb_endpoint_descriptor *epd;

        if (desc->bInterfaceClass != 255)
            continue;

        // ★ 问题就出在这一行 ★
        epd = get_endpoint(intf->altsetting, 0);
        // ... 后续使用 epd 读取端点地址、包大小、轮询间隔等信息
    }
    return -EINVAL;
}

当找到 class=255 的接口后,代码直接调用 get_endpoint() 宏去获取第 0 个端点的描述符。

看起来非常合理,不是吗?对于一个正常的 Scarlett 声卡,这个接口必然包含端点。

但问题在于——谁规定 USB 描述符必须是“正常”的?

Fuzzer:让我给你造个没有端点的接口

syzbot 的工作就是构造各种畸形的 USB 描述符并“喂”给内核。这次,它构造了这样一个接口:

  • bInterfaceClass = 255 ✅ 看起来像是 Focusrite 的控制接口。
  • bNumEndpoints = 0 ❌ 但是内部一个端点都没有

驱动看到 class=255,心中一喜:“找到了!”随即就去获取第 0 个端点。

那么,get_endpoint 的定义是什么呢?

#define get_endpoint(alt, ep)   (&(alt)->endpoint[ep].desc)

这只是一个简单的数组下标访问宏。endpoint 数组的长度由 bNumEndpoints 决定。如果 bNumEndpoints 为 0,那么访问 endpoint[0] 就是在访问一块不存在的内存。

NULL dereference,内核崩溃。

这好比你去停车场找车,保安告诉你“A区有车位”,你直接冲进去——结果A区地面都没铺好,你一脚踩进了坑里。

修复思路:在入口加一道检查

修复方法简单得令人难以置信:

if (desc->bInterfaceClass != 255)
    continue;
if (desc->bNumEndpoints < 1)      // 新增:没有端点?跳过!
    continue;

epd = get_endpoint(intf->altsetting, 0);  // 现在安全了

在尝试获取端点之前,先检查 bNumEndpoints 是否至少为 1。如果不是,直接 continue,碰都不碰。

两行代码,一个 if,一个 continue,问题解决。这正是 C/C++ 编程中防御性思维的体现。

这类 Bug 为何如此常见?

如果你去翻看内核的 git 提交历史,会发现类似的补丁多到让人麻木:

git log --oneline --all --grep="bNumEndpoints" | wc -l

我敢打赌至少有几十个。原因很简单:

驱动开发者在编写代码时,脑子里想的是“我的设备长什么样”,而不是“一个恶意或畸形的设备长什么样”。

在开发者自己的、正常的 Scarlett 声卡上,那个接口 100% 有端点,测试一百遍都不会出错。但 Linux内核 要运行在无数用户的机器上,USB 端口可能接入任何设备——可能是 fuzzer 构造的虚拟设备,也可能是故障硬件,甚至是恶意的 USB 攻击设备(如 BadUSB)。

因此,内核社区有一条不成文的铁律:

永远不要信任来自硬件或用户空间的数据。在使用它们之前,务必先进行验证。

从这个补丁能学到什么

1. 宏不是函数,没有安全网

get_endpoint 只是一个数组访问宏,它不进行任何边界检查:

#define get_endpoint(alt, ep)   (&(alt)->endpoint[ep].desc)

调用者必须自行确保 ep 索引在合法范围内。如果你在编写驱动时使用类似的宏,务必在调用前加入边界检查。这并非可选项,而是必选项。

2. 防御性编程的“三板斧”

在驱动中解析任何外部传入的描述符时,应遵循以下模式:

// 1. 检查数组长度再取下标
if (desc->bNumEndpoints < 1)
    return -EINVAL;

// 2. 检查指针再解引用(虽然本例宏直接返回地址,但思路类似)
struct usb_endpoint_descriptor *epd = get_endpoint(alt, 0);
// 对于可能返回NULL的函数,需要检查
// if (!epd)
//     return -EINVAL;

// 3. 检查值域再使用
if (!usb_endpoint_xfer_bulk(epd) && !usb_endpoint_xfer_int(epd))
    return -EINVAL;

多写三行检查,可能为你省去排查一整周系统崩溃的时间。这涉及到扎实的 计算机基础 与编程原则。

3. Fuzzer 是你最好的朋友

syzbot 已经为 Linux 内核发现了成千上万个 Bug。如果你正在开发内核模块或驱动,强烈建议运行一下 syzkaller 之类的 Fuzzing 工具。你自认为坚固的代码,fuzzer 分分钟就能教你做人。

最后

这个补丁只修改了2行,diff 加起来不到50个字符。但它堵住的,是一个任何人都可能通过插入恶意或畸形 USB 设备来触发的内核崩溃漏洞。

编写驱动时,脑中必须时刻绷紧一根弦:你测试所用的硬件是正常的,但你的代码必须为所有不正常的情况做好兜底。

两行代码的修复看似轻巧,背后的教训却十分沉重——在内核的世界里,少一个 if 检查,就可能多一个安全漏洞(CVE)。这种对代码严谨性的追求,也是我们云栈社区 所倡导和分享的极客精神之一。




上一篇:Claude Code 2.1.81 缓存失效解析:Token消耗激增的诊断与回滚方案
下一篇:拆解空调外机板揭秘:磁环没效果?可能是安装位置和绕法不对
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 17:07 , Processed in 0.702399 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表