ESC
输入关键词搜索文章标题和内容

驱动性能优化与安全加固:从锁竞争到漏洞防护的实战指南

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

驱动性能优化与安全加固:从锁竞争到漏洞防护的实战指南

驱动跑起来了不等于跑得好——自旋锁争用把CPU耗在空转上,copy_to_user每次4字节搬数据,中断处理占着CPU不让出,DMA缓冲区没做边界检查直接被恶意设备改掉内核数据,性能和安全是驱动开发的两条腿,缺一条都站不稳,基于Linux 6.8内核,从性能优化的测量方法论讲起,过锁竞争、数据传输、中断延迟、内存管理四大优化方向,再从安全威胁模型出发,覆盖输入验证、权限控制、DMA安全、漏洞防护,最后给工具对比表和速查表。

一、性能优化核心原则

先测量再优化

"感觉这里慢是最差的优化理由。内核性能问题往往跟直觉相反——你觉得瓶颈在中断处理,实际瓶颈在锁竞争;你觉得该换零拷贝,实际瓶颈在内存分配。不测量就优化,大概率白费功夫还引入新bug。

优化流程

flowchart TB A(["开始"]) --> B["建立基准测试<br/>记录当前吞吐/延迟"] B --> C["定位瓶颈<br/>perf/ftrace/eBPF"] C --> D{"瓶颈类型?"} D -->|"锁竞争"| E["缩小临界区/换锁/RCU"] D -->|"数据拷贝"| F["零拷贝/批量传输"] D -->|"中断延迟"| G["线程化中断/NAPI"] D -->|"内存分配"| H["内存池/slab缓存"] E --> I["验证优化效果<br/>对比基准数据"] F --> I G --> I H --> I I --> J{"达标?"} J -->|"是"| K(["结束"]) J -->|"否"| C style A fill:#E3F2FD style B fill:#E3F2FD style C fill:#FFF8E1 style D fill:#FFF8E1 style E fill:#E8F5E9 style F fill:#E8F5E9 style G fill:#E8F5E9 style H fill:#E8F5E9 style I fill:#FFF8E1 style J fill:#FFF8E1 style K fill:#E3F2FD

三大分析工具

工具 定位 典型场景 开销
perf 采样分析,找热点函数 哪个函数占CPU最多 低(采样率可调)
ftrace 函数追踪,看调用链和耗时 函数被谁调的、耗时多少 中(需启用tracer
eBPF 动态注入追踪点,几乎零开销 实时监控内核事件 极低(JIT编译)

二、锁竞争优化

问题:自旋锁持锁时间过长

自旋锁的设计前提是临界区极短——几条指令的事。但实际代码里经常看到临界区塞了一堆操作:打印日志、分配内存、甚至调用可能睡眠的函数。持锁时间一长,其他CPU就在那空转,性能直接崩。

优化1:缩小临界区

只锁真正需要保护的数据,把不依赖锁的操作移到外面:

// 优化前:临界区太大
spin_lock(&drv->lock);
data = drv->buffer[idx];       // 读数据
process_data(data);            // 处理数据(不需要锁)
drv->count++;                  // 更新计数
spin_unlock(&drv->lock);

// 优化后:只锁共享数据
spin_lock(&drv->lock);
data = drv->buffer[idx];
drv->count++;
spin_unlock(&drv->lock);
process_data(data);            // 移到锁外```

### 优化2:读写锁替代自旋。
读多写少的场景,用读写锁让多个读者并行:

```c
// 自旋锁:读者也互斥
spin_lock(&lock);
val = shared_data;    // 多个读者也要排队
spin_unlock(&lock);

// 读写锁:读者并发
rwlock_t rwlock;
DEFINE_RWLOCK(rwlock);

// 读端:多个读者可并行
read_lock(&rwlock);
val = shared_data;
read_unlock(&rwlock);

// 写端:独占
write_lock(&rwlock);
shared_data = new_val;
write_unlock(&rwlock);

优化3:per-cpu变量避免。

每个CPU一份副本,彻底消除锁竞争:

// 定义per-cpu变量
static DEFINE_PER_CPU(struct stats, cpu_stats);

// 更新:不需要锁,操作本CPU副本
void update_stats(int delta)
{
    // 禁止抢占,确保不会被迁移到其他CPU
    preempt_disable();
    this_cpu_inc(cpu_stats.count);
    this_cpu_add(cpu_stats.total, delta);
    preempt_enable();
}

// 汇总:遍历所有CPU
u64 get_total_count(void)
{
    u64 total = 0;
    int cpu;
    for_each_possible_cpu(cpu)
        total += per_cpu(cpu_stats.count, cpu);
    return total;
}

优化4:RCU替代读写。

读端零开销,写端延迟回收:

// RCU保护的数据结构struct config {
    int mode;
    int threshold;
    struct rcu_head rcu;
};

static struct config __rcu *cur_config;

// 读端:无锁,极快
int get_mode(void)
{
    struct config *cfg;
    int mode;
    rcu_read_lock();
    cfg = rcu_dereference(cur_config);
    mode = cfg->mode;          // 读端完全不加载    rcu_read_unlock();
    return mode;
}

// 写端:分配新结构,原子替换,延迟释放旧结构
void set_mode(int new_mode)
{
    struct config *old, *new_cfg;
    new_cfg = kmalloc(sizeof(*new_cfg), GFP_KERNEL);
    *new_cfg = *rcu_dereference(cur_config);
    new_cfg->mode = new_mode;
    old = rcu_replace_pointer(cur_config, new_cfg, true);
    synchronize_rcu();          // 等待所有读者退。    kfree(old);
}

四种优化对比

优化方式 适用场景 读开销 写开销 实现复杂度
缩小临界区 所有锁场景 不变 不变
读写锁 读多写少 低(并行读)
per-cpu 统计计数/每CPU独立 无锁 无锁
RCU 读极多写极少 无锁 高(延迟回收)

三、数据传输优先

零拷贝:DMA-BUF/splice/sendfile

传统数据路径:内核缓冲区 。CPU拷贝到用户空。。CPU拷贝回内核(或另一个设备)。零拷贝的目标是让数据不经过CPU搬运。

flowchart TB A["传统路径<br/>2次CPU拷贝"] --> B["内核缓冲"] B -->|"copy_to_user"| C["用户空间"] C -->|"copy_from_user"| D["另一个内核缓冲区"] D --> E["设备B"] F["零拷贝路径<br/>0次CPU拷贝"] --> G["内核缓冲<br/>DMA-BUF"] G -->|"DMA直接传输"| H["设备B"] style A fill:#FFEBEE style B fill:#FFEBEE style C fill:#FFEBEE style D fill:#FFEBEE style E fill:#FFEBEE style F fill:#E8F5E9 style G fill:#E8F5E9 style H fill:#E8F5E9

DMA-BUF:跨设备共享缓冲区,V4L2采集→GPU处理→DRM显示全程零拷贝:

// 导出端:V4L2驱动导出DMA-BUF fd
int v4l2_export_buf(struct file *file, void *priv,
                    struct v4l2_exportbuffer *exp)
{
    // vb2核心层完成导。    return vb2_core_expbuf(&q->vb2_buf,
                exp->dmabuf_fd, exp->type,
                exp->index, exp->plane,
                exp->flags, exp->mem_offset);
}

// 导入端:GPU/DRM通过fd导入
struct dma_buf *dmabuf = dma_buf_get(fd);
struct dma_buf_attachment *attach;
attach = dma_buf_attach(dmabuf, dev);
sgt = dma_buf_map_attachment(attach, DMA_BIDIRECTIONAL);
// 拿到scatterlist,直接用DMA访问同一块物理内存```

**splice/sendfile**:文件到socket的零拷贝,内核内部直接搬运页缓存。
```c
// sendfile:文件→socket零拷贝
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

// splice:任意两个fd之间零拷贝
ssize_t splice(int fd_in, off_t *off_in,
               int fd_out, off_t *off_out,
               size_t len, unsigned int flags);

减少copy_to_user/copy_from_user调用

每次copy_to_user/copy_from_user都有用户空间访问检查和可能的缺页中断。批量传输比多次小块传输高效得多。

// 差:每次拷贝4字节
for (i = 0; i < count; i++) {
    if (copy_to_user(&user_buf[i], &kern_buf[i], 4))
        return -EFAULT;
}

// 好:一次拷贝全部
if (copy_to_user(user_buf, kern_buf, count * 4))
    return -EFAULT;

scatter-gather DMA

连续的虚拟地址不需要连续的物理页面,scatter-gather DMA让硬件直接遍历散列表,省掉大块连续内存分配:

// 分配scatter-gather列表
int nents = dma_map_sg(dev, sg_list, nents, DMA_TO_DEVICE);
// nents是硬件可用的DMA映射段数,可能少于原始段。// 因为相邻物理页可能被合并

// 遍历映射后的段
for_each_sg(sg_list, sg, nents, i) {
    dma_addr_t addr = sg_dma_address(sg);
    size_t len = sg_dma_len(sg);
    // 把addr/len填入硬件描述。}

四、中断与延迟优化

线程化中断:request_threaded_irq

硬中断上下文不能睡眠,耗时操作必须延后处理。request_threaded_irq把中断处理拆成上半部(硬中断)和下半部(内核线程):

// 上半部:只做最紧急的硬件应答
static irqreturn_t my_hardirq(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;
    u32 status = readl(dev->regs + IRQ_STATUS);
    if (!(status & IRQ_PENDING))
        return IRQ_NONE;
    writel(IRQ_ACK, dev->regs + IRQ_CLEAR);  // 应答硬件
    dev->irq_status = status;
    return IRQ_WAKE_THREAD;  // 唤醒线程化下半部
}

// 下半部:可以睡眠,做耗时处理
static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;
    // 可以睡眠:互斥锁、内存分配、IO操作
    mutex_lock(&dev->mtx);
    process_interrupt(dev);
    mutex_unlock(&dev->mtx);
    return IRQ_HANDLED;
}

// 注册线程化中。int ret = request_threaded_irq(irq, my_hardirq, my_thread_fn,
                               IRQF_ONESHOT, "my_dev", dev);

IRQF_ONESHOT保证线程化下半部执行完之前不会再次触发该中断,避免中断风暴。

NAPI:网络驱动的中断+轮询混合

高负载下网卡每个包都触发中断,CPU光处理中断就忙不过来。NAPI(New API)的做法:第一个包触发中断,之后切换到轮询模式批量收包,包少了再切回中断:

// NAPI轮询函数
static int my_napi_poll(struct napi_struct *napi, int budget)
{
    struct my_dev *dev = container_of(napi, struct my_dev, napi);
    int rx_count = 0;

    while (rx_count < budget) {
        struct sk_buff *skb = receive_packet(dev);
        if (!skb)
            break;
        netif_receive_skb(skb);
        rx_count++;
    }

    if (rx_count < budget) {
        // 包收完了,切回中断模块        napi_complete_done(napi, rx_count);
        enable_interrupt(dev);  // 重新使能中断
    }
    return rx_count;
}

// 中断处理:调度NAPI轮询
static irqreturn_t my_net_irq(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;
    disable_interrupt(dev);     // 关中断,切轮。    napi_schedule(&dev->napi);  // 调度轮询
    return IRQ_HANDLED;
}

// 初始化NAPI
netif_napi_add(dev->netdev, &dev->napi, my_napi_poll);

中断合并(coalescing)

网卡和NVMe控制器都支持中断合并:攒够N个请求或等够T微秒再触发一次中断,减少中断频率。

// ethtool配置网卡中断合并
// ethtool -C eth0 rx-usecs 50 rx-frames 64
// 50微秒。4帧,哪个先到触发中断

// NVMe设置中断合并
struct nvme_completion_queue {
    u16 intr_vector;
    u16 intr_coal_thresh;  // 完成多少个命令后触发中断
};

IRQ亲和性绑定CPU

把特定中断绑定到指定CPU,减少跨CPU缓存失效和锁竞争。

# 查看当前中断亲和。cat /proc/irq/32/smp_affinity

# 把IRQ 32绑定到CPU 0。(位掩码b0101 = 5。echo 5 > /proc/irq/32/smp_affinity

# 驱动中设。irq_set_affinity_hint(irq, cpumask_of(cpu));

五、内存管理优先

内存。mempool)避免分配失败

GFP_KERNEL在内存紧张时可能睡眠甚至失败。mempool预分配一批对象作为紧急储备,保证关键路径不失败:

// 创建内存池:最少保护个对。mempool_t *pool = mempool_create(8, mempool_alloc_slab,
                                  mempool_free_slab, my_cachep);

// 从内存池分配:不会失。void *obj = mempool_alloc(pool, GFP_KERNEL);
if (!obj)  // 实际上不会走到这。    return -ENOMEM;

// 归还
mempool_free(obj, pool);

// 销。mempool_destroy(pool);

slab缓存预分支

频繁分配/释放固定大小对象,用slab缓存避免反复调用底层分配器:

// 创建专用slab缓存
struct kmem_cache *my_cache = kmem_cache_create("my_driver_obj",
                                    sizeof(struct my_obj), 0,
                                    SLAB_HWCACHE_ALIGN, NULL);

// 从缓存分支struct my_obj *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);

// 归还缓存
kmem_cache_free(my_cache, obj);

// 销毁缓。kmem_cache_destroy(my_cache);

SLAB_HWCACHE_ALIGN让对象对齐到缓存行,避免false sharing。

减少内存碎片

  • 用get_zeroed_page()/__get_free_pages()分配2的幂次页面,避免碎片
  • 用alloc_pages(GFP_KERNEL | __GFP_COMP)分配复合。- 用kmalloc分配小对象而不是整。- 用mempool和slab缓存保持对象复用

高阶页分支

单页分配器最。KB(默认配置),大块连续内存需要高阶分配:

// 分配4个连续页。6KB。struct page *pages = alloc_pages(GFP_KERNEL, 2);  // 2^2 = 4。if (!pages)
    return -ENOMEM;

void *vaddr = page_address(pages);
// 使用...

__free_pages(pages, 2);  // 释放,注意order要匹```

高阶分配在系统运行久了之后容易失败。如果必须用大块连续内存,在模块初始化时尽早分配,或者考虑用scatter-gather代替连续内存。
## 六、性能分析工具

### perf:采样分支
```bash
# 记录性能数据
perf record -g ./my_test          # -g记录调用。perf record -e cpu-clock -g -p 1234  # 追踪指定进程

# 分析热点
perf report                       # 交互式报。perf report --stdio               # 文本输出

# 实时热点
perf top                          # 类似top,但按函数采```

### ftrace:函数追。
```bash
# 追踪指定函数
echo my_driver_func > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 运行测试 ...
cat /sys/kernel/debug/tracing/trace

# 函数耗时统计
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo my_driver_func > /sys/kernel/debug/tracing/set_graph_function

# 函数耗时直方图(6.8内核。echo 'hist:keys=func,latency:sort=latency' > \
    /sys/kernel/debug/tracing/events/my_driver/trigger

eBPF:bcc工具。

# 安装bcc工具
# apt install bpfcc-tools

# 追踪内核函数耗时
funclatency-bpfcc -p 1234 do_sys_open

# 追踪块设备IO延迟
biosnoop-bpfcc

# 追踪内存分配
memleak-bpfcc -p 1234

# 追踪锁竞。offcputime-bpfcc -p 1234

/proc接口

# 中断统计
cat /proc/interrupts            # 硬中断计数cat /proc/softirqs              # 软中断计数
# 查看哪个CPU处理哪个中断
watch -n1 "cat /proc/interrupts | grep my_dev"

工具对比

工具 优势 局限 适用场景
perf 采样开销低,支持硬件事件 需要调试符号 找热点函数
ftrace 内核原生,无需额外安装 只能追踪内核函数 函数调用链分支
eBPF/bcc 几乎零开销,可编程 学习曲线陡 实时监控、自定义追踪
/proc 零开销,最简单 只有汇总数据 快速查看当前内存状态

七、驱动安全威胁模块

攻击。

驱动运行在内核态,一个漏洞就是整个系统的灾难。驱动的攻击面比想象的大。

来自 linuxros.cn · linuxROS
flowchart TB A["驱动攻击"] --> B["ioctl参数<br/>用户空间可构造任意"] A --> C["用户空间指针<br/>copy_from_user/copy_to_user"] A --> D["设备。ACPI<br/>恶意固件修改"] A --> E["固件加载<br/>替换恶意固件"] A --> F["DMA<br/>恶意设备直接访问内存"] A --> G["sysfs/procfs<br/>用户空间可写属"] A --> H["netlink<br/>伪造消"] style A fill:#FFEBEE style B fill:#FFF8E1 style C fill:#FFF8E1 style D fill:#FFF8E1 style E fill:#FFF8E1 style F fill:#FFEBEE style G fill:#FFF8E1 style H fill:#FFF8E1

常见漏洞类型

漏洞类型 典型成因 危害等级 内核案例
缓冲区越界 未校验数组下标 严重 CVE-2023-3106
整数溢出 size计算溢出后分配小缓冲 严重 CVE-2022-4280
竞态条 多线程访问共享数据未加锁 CVE-2023-0386
信息泄露 未初始化栈变量拷贝到用户空间 CVE-2023-0590
权限提升 缺少capable()检 严重 CVE-2023-0179

八、输入验证与边界检。

copy_from_user后必须验。

用户空间可以传入任何值,不验证就是给攻击者送钥匙:

// 危险:直接信任用户数。struct my_config cfg;
if (copy_from_user(&cfg, user_ptr, sizeof(cfg)))
    return -EFAULT;
buffer[cfg.index] = cfg.value;  // cfg.index可能越界。
// 安全:验证所有字。struct my_config cfg;
if (copy_from_user(&cfg, user_ptr, sizeof(cfg)))
    return -EFAULT;
if (cfg.index >= ARRAY_SIZE(buffer))  // 边界检。    return -EINVAL;
if (cfg.value > MAX_VALUE)            // 范围检。    return -ERANGE;
buffer[cfg.index] = cfg.value;

整数溢出检。

分配缓冲区前必须检查size计算是否溢出。

// 危险:整数溢。int count;
if (copy_from_user(&count, user_ptr, sizeof(count)))
    return -EFAULT;
// count = 0x40000001, count * 4 = 0x432位溢出)
buf = kmalloc(count * sizeof(u32), GFP_KERNEL);  // 分配4字节
// 但后续写入count个u32,越界!

// 安全:用kmalloc_array检查溢buf = kmalloc_array(count, sizeof(u32), GFP_KERNEL);
if (!buf)
    return -ENOMEM;
// kmalloc_array内部会检查count * sizeof(u32)是否溢出

安全的ioctl实现

static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct my_dev *dev = file->private_data;
    void __user *uptr = (void __user *)arg;

    // 第一步:检查cmd合法。    if (_IOC_TYPE(cmd) != MY_IOC_MAGIC)
        return -ENOTTY;
    if (_IOC_NR(cmd) > MY_IOC_MAXNR)
        return -ENOTTY;

    switch (cmd) {
    case MY_IOCTL_SET_CONFIG: {
        struct my_config cfg;

        // 第二步:检查用户空间指针可访问
        if (!access_ok(uptr, sizeof(cfg)))
            return -EFAULT;

        // 第三步:拷贝并验。        if (copy_from_user(&cfg, uptr, sizeof(cfg)))
            return -EFAULT;

        // 第四步:边界检。        if (cfg.index >= MAX_INDEX)
            return -EINVAL;
        if (cfg.size == 0 || cfg.size > MAX_SIZE)
            return -EINVAL;

        // 第五步:权限检。        if (cfg.privileged && !capable(CAP_SYS_ADMIN))
            return -EPERM;

        // 第六步:加锁操作共享数据
        mutex_lock(&dev->mtx);
        dev->config = cfg;
        mutex_unlock(&dev->mtx);
        break;
    }
    default:
        return -ENOTTY;
    }
    return 0;
}

九、权限控制

CAP_SYS_ADMIN检。

不是谁都能操作你的设备,capable()是第一道防线:

static long my_ioctl(struct file *file, unsigned int cmd,
                     unsigned long arg)
{
    // 危险操作必须检查权限    switch (cmd) {
    case MY_IOCTL_RESET:
    case MY_IOCTL_FLASH_FW:
    case MY_IOCTL_DEBUG:
        if (!capable(CAP_SYS_ADMIN))
            return -EPERM;
        break;
    default:
        break;
    }
    // ...
}

capable(CAP_SYS_ADMIN)是内核最常用的权限检查,基本等价。需要root"。更细粒度的可以用CAP_NET_ADMIN、CAP_SYS_RAWIO等。

设备节点权限

控制谁能打开设备文件。

// 初始化时设置设备节点权限
static int my_dev_init(void)
{
    struct device *dev;

    dev = device_create(my_class, NULL, devt, NULL, "mydev");
    // 默认crw-rw----,属主root,组device
    // 修改权限:只允许root访问
    dev_set_mode(dev, 0600);  // crw-------

    // 或者用devtmpfs自动设置
    // 在class_create时指。}

也可以通过udev规则动态调整:

# /etc/udev/rules.d/99-mydev.rules
KERNEL=="mydev", MODE="0660", GROUP="device"

seccomp过滤

seccomp限制用户空间程序能调用哪些系统调用,即使被攻破也无法调用ioctl。

// 用户空间设置seccomp过滤
#include <seccomp.h>

scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
// 只允许read/write/exit
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_load(ctx);

SELinux策略

SELinux在强制访问控制层面限制进程对设备节点的访问:

# SELinux策略示例:只允许特定域访问mydev
allow my_app_domain mydev_device_t:chr_file { read write ioctl };

十、DMA安全

IOMMU/SMMU:设备DMA地址隔离

没有IOMMU的系统中,DMA设备可以访问任意物理地址——恶意设备或被攻破的固件能直接读写内核内存。IOMMU(Intel。SMMU(ARM)在设备和物理内存之间加了一层地址翻译,把DMA限制在授权范围内存

flowchart TB A["设备发起DMA"] --> B{"IOMMU/SMMU"} B -->|"地址在授权范围内"| C["翻译为物理地址<br/>访问内存"] B -->|"地址越界"| D["DMA fault<br/>访问被拒"] style A fill:#E3F2FD style B fill:#FFF8E1 style C fill:#E8F5E9 style D fill:#FFEBEE
// 驱动中使用IOMMU。.8内核自动处理。// dma_alloc_coherent内部会通过IOMMU映射
void *vaddr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
// dma_handle是设备看到的IOVA(IO Virtual Address。// IOMMU把IOVA翻译成物理地址,设备只能访问这块内存
// 启用IOMMU:内核启动参数// intel_iommu=on    (x86)
// arm-smmu.enable=1 (ARM)

dma_set_mask_and_coherent

告诉内核设备支持多宽的DMA地址。

// 设置DMA地址掩码
int ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
if (ret) {
    // 64位DMA不支持,尝试32。    ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
    if (ret)
        return ret;  // 设备不支持DMA
}

DMA缓冲区边界检。

设备可能写入超出分配范围的数据,驱动必须做边界防护:

// 分配DMA缓冲区时多留一些guard空间
#define BUF_SIZE    4096
#define GUARD_SIZE  256

void *vaddr = dma_alloc_coherent(dev, BUF_SIZE + GUARD_SIZE,
                                  &dma_handle, GFP_KERNEL);

// 只告诉设备BUF_SIZE的大。// 设备写入超过BUF_SIZE的部分落入guard区域
// 驱动检查guard区域是否被改写,检测越界写。if (memcmp(vaddr + BUF_SIZE, guard_pattern, GUARD_SIZE) != 0) {
    dev_err(dev, "DMA buffer overflow detected!\n");
    // 触发安全告警
}

防止DMA攻击

攻击方式 防御措施
恶意设备读取任意内存 启用IOMMU/SMMU,限制DMA地址范围
DMA写入覆盖内核数据 IOMMU隔离 + 缓冲区边界检查
DMA重放攻击 使用IOMMU的无效化队列(Invalidation Queue
Thunderbolt/USB4 DMA攻击 内核CONFIG_IOMMU_DEFAULT_PASSTHROUGH=n

十一、漏洞防护最佳实。

栈保护:CONFIG_STACKPROTECTOR

编译器在函数栈帧中插入canary值,函数返回时检查canary是否被覆盖:

# 内核配置
CONFIG_STACKPROTECTOR=y
CONFIG_STACKPROTECTOR_STRONG=y   # 更强的保护,更多函数插入canary

# 驱动编译时自动启用,无需额外代码
# 如果栈溢出覆盖了canary,内核打印:
# stack-protector: Kernel stack is corrupted in: my_func+0x5c/0x60

堆保护:CONFIG_SLAB_FREELIST_HARDENED

硬化slab分配器的空闲链表,防止堆溢出篡改空闲对象指针。

# 内核配置
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_SLAB_FREELIST_RANDOM=y    # 随机化空闲链表顺。CONFIG_HARDENED_USERCOPY=y       # 加强copy_to/from_user检```

### 用户空间访问:access_ok + copy_from_user

两步验证,缺一不可。
```c
// 第一步:检查用户空间地址范围是否合法
if (!access_ok(uptr, size))
    return -EFAULT;

// 第二步:安全拷贝(内部处理缺页等异常。if (copy_from_user(kbuf, uptr, size))
    return -EFAULT;

access_ok做快速范围检查,copy_from_user做实际拷贝并处理缺页。.8内核中access_ok的语义是检查[uptr, uptr+size)是否在用户空间地址范围内存

竞态条件:正确的锁使用

// 典型竞态:检查与使用分离(TOCTOU)
// 危险
if (dev->running) {        // 检。    // 另一个线程可能在这里把dev->running设为0并释放资。    writel(data, dev->regs);  // 使用——可能访问已释放的内存!
}

// 安全:加锁保护mutex_lock(&dev->mtx);
if (dev->running) {
    writel(data, dev->regs);
}
mutex_unlock(&dev->mtx);

安全审计清单

检查项 审计要点 通过标准
ioctl参数验证 所有字段都做了边界检查 无未验证的用户输入
整数溢出 size计算用kmalloc_array/sizes_mul 无乘法溢出风险
权限检查 危险操作有capable() 无未授权操作
锁保护 共享数据访问在锁内 无竞态条件
用户空间访问 access_ok + copy_from_user 无直接解引用用户指针
DMA安全 启用IOMMU,缓冲区有边界检查 无越界DMA访问
信息泄露 拷贝到用户空间前清零 无未初始化数据泄露
固件验证 加载固件前校验签名 无未签名固件加载

十二、常见问题

*Q1:perf采样不到内核函数

需要安装linux-perf包和调试符号。Debian/Ubuntu:apt install linux-perf-6.8 dbgsym。确认/proc/sys/kernel/kptr_restrict设为0,否则非root看不到内核符号地址。
*Q2:ioctl被非root用户滥用

在ioctl处理函数开头加capable(CAP_SYS_ADMIN)检查。对于部分操作需要权限、部分不需要的场景,按cmd分支检查,不要一刀切。
*Q3:DMA缓冲区被恶意设备篡改

启用IOMMU/SMMU,内核启动参数加intel_iommu=on(x86)或确认arm-smmu已加载(ARM)。IOMMU把DMA地址空间限制在驱动显式映射的范围内,设备无法越界访问题
*Q4:per-cpu变量在进程上下文和中断上下文都能用吗

进程上下文用this_cpu_inc()前要preempt_disable(),防止被抢占后迁移到其他CPU。中断上下文天然不会被抢占,直接用就行。.8内核推荐用this_cpu_inc()代替__this_cpu_inc(),前者有抢占检查看
*Q5:RCU回调在什么上下文执行

call_rcu的回调在软中断上下文执行,不能睡眠。如果回调里需要睡眠,用call_rcu_sched。.8内核的rcu_barrier配合工作队列。

十三、总结

速查看

场景 优化/防护手段 关键API
锁竞争 缩小临界区 把无关操作移到锁外
锁竞 读写 DEFINE_RWLOCK / read_lock / write_lock
锁竞 per-cpu变量 DEFINE_PER_CPU / this_cpu_inc
锁竞 RCU rcu_read_lock / rcu_dereference / synchronize_rcu
数据拷贝 零拷贝DMA-BUF dma_buf_get / dma_buf_attach
数据拷贝 splice/sendfile splice() / sendfile()
数据拷贝 scatter-gather dma_map_sg / for_each_sg
中断延迟 线程化中 request_threaded_irq
中断延迟 NAPI netif_napi_add / napi_schedule
中断延迟 中断合并 ethtool -C / NVMe coalescing
中断延迟 IRQ亲和 irq_set_affinity_hint
内存分配 内存 mempool_create / mempool_alloc
内存分配 slab缓存 kmem_cache_create / kmem_cache_alloc
输入验证 边界检 access_ok + copy_from_user
输入验证 整数溢出 kmalloc_array / size_mul
权限控制 capable检 capable(CAP_SYS_ADMIN)
DMA安全 IOMMU隔离 dma_alloc_coherent(自动IOMMU映射)
漏洞防护 栈保护 CONFIG_STACKPROTECTOR_STRONG
漏洞防护 堆保护 CONFIG_SLAB_FREELIST_HARDENED
性能分析 perf perf record -g / perf report
性能分析 ftrace set_ftrace_filter / function_graph
性能分析 eBPF funclatency-bpfcc / biosnoop-bpfcc

本文首发于linuxros.cn,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页