mcpField
O
oh-arkruntime-thread-safety-audit

oh-arkruntime-thread-safety-audit

github.com/openharmonyinsight测试与质量3541 次安装11 周前更新
代码仓库

关于 oh-arkruntime-thread-safety-audit

用于审查和修复 ArkCompiler Runtime Core 线程安全问题的技能,重点覆盖 ETS 代码,并依据证据评估风险。它指导并发路径分析、同步方案选择和相应测试。

主要功能

  1. 01扫描 ETS 共享可变状态
  2. 02追踪并发入口
  3. 03按影响和证据评估风险
  4. 04为状态不变量选择同步方式
  5. 05审查修复与并发测试

适用场景

  1. 01审计 ETS 标准库变更的数据竞争
  2. 02调查涉及 ETS API 的 TSAN 报告
  3. 03审查单例、缓存或计数器的同步

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/openharmonyinsight/openharmony-skills --skill oh-arkruntime-thread-safety-audit

安装后在 Claude Code 中输入/oh-arkruntime-thread-safety-audit确认已加载。

详细介绍

OH Ark Runtime 线程安全审计

使用此技能检查、审查或修复 ArkCompiler Runtime Core 中的线程安全问题,尤其是以下内容:

  • static_core/plugins/ets/stdlib/**/*.ets 下的 ETS 标准库文件
  • static_core/plugins/ets/** 下的 ETS 运行时/插件支持代码
  • static_core/plugins/ets/tests/ 下的测试
  • 仅当 ETS API 跨越原生边界或 TSAN 报告指向相关代码时,才检查相关原生 C++

常见触发原因是 ArkTS-Sta 的并发执行模型:公共标准库 API 可从 taskpool、EAWorker 或多个协程/工作线程调用,因此除非有证据证明静态可变状态和全局单例不在线程间共享,否则必须将其视为共享状态。

有源码依据的 API

仅使用本代码库或 OpenHarmony 官方文档中可以找到的 API/类型。以下示例均有本仓库源码依据;对于未导出的辅助函数,应将其视为内部实现参考,而非公共 API 建议。

API/类型源码
AtomicInt、AtomicLong、AtomicBooleanstatic_core/plugins/ets/stdlib/std/concurrency/Atomics.ets:83、static_core/plugins/ets/stdlib/std/concurrency/Atomics.ets:194、static_core/plugins/ets/stdlib/std/concurrency/Atomics.ets:782
Atomics.loadstatic_core/plugins/ets/stdlib/std/concurrency/LegacyAtomics.ets:253
ConcurrentHashMapstatic_core/plugins/ets/stdlib/std/containers/ConcurrentHashMap.ets:131
Mutex、QueueSpinlockstatic_core/plugins/ets/stdlib/std/core/SyncPrimitives.ets:43、static_core/plugins/ets/stdlib/std/core/SyncPrimitives.ets:156
ConcurrencyHelpers.mutexCreate、ConcurrencyHelpers.lockGuardstatic_core/plugins/ets/stdlib/std/core/ConcurrencyHelpers.ets:27、static_core/plugins/ets/stdlib/std/core/ConcurrencyHelpers.ets:31;std/concurrency 和 std/containers 下也有包内变体
taskpool.executestatic_core/plugins/ets/stdlib/std/concurrency/taskpool.ets:1841、static_core/plugins/ets/stdlib/std/concurrency/taskpool.ets:1852、static_core/plugins/ets/stdlib/std/concurrency/taskpool.ets:1863
EAWorkerstatic_core/plugins/ets/stdlib/std/core/EAWorker.ets:65
CoroutineExtras.stopTaskpoolstatic_core/plugins/ets/stdlib/std/debug/concurrency/CoroutineExtras.ets:42

如果建议示例使用了无法通过 rg 在目标代码库中解析的 API/类型,请删除该示例或将其标注为内部伪代码。

扫描工作流

从用户提供的文件、差异或 API 范围开始。对这些文件或其最近的所属目录使用下方的 <target-paths>。仅当请求没有更窄的范围时,才扩展到整个 static_core/plugins/ets。

阶段 1:检测 ETS 静态可变状态

查找 ETS/TS 文件中的静态字段、类似全局变量的可变对象、缓存、锁和原子变量:

rg -n "static .*[:=]|private static|public static|const .*[:=]|let .*[:=]|new Map|new Array|StringBuilder|ConcurrentHashMap|Mutex|QueueSpinlock|Atomic" \
    --type-add 'ets:*.ets' --type-add 'ets:*.ts' -t ets \
    <target-paths>
阶段 2:分析并发入口

判断候选状态是否可从并发路径访问:

  • 公共 API 和导出函数
  • taskpool.execute/EAWorker
  • 运行时或标准库回调
  • 声明为 native 的 ETS 方法

在候选 API 附近搜索本地调用点:

rg -n "taskpool\\.execute|EAWorker|callback|setTimeout|setInterval|native .*\\(|public .*\\(" \
    --type-add 'ets:*.ets' --type-add 'ets:*.ts' -t ets \
    <target-paths>

注册或初始化函数本身并非并发入口。只有在有证据表明它们可能在工作线程或用户代码启动后并发调用时,才将其视为风险。

可选的原生代码后续分析

只有当 ETS 候选项是原生方法、堆栈跟踪指向 C++,或用户明确要求分析原生代码/TSAN 时,才检查原生代码。

rg -n "static .*|std::once_flag|std::mutex|os::memory::Mutex|std::atomic|thread_local|std::vector|std::map|std::set|ani_native_function" \
    --type cpp \
    <related-native-paths>

完成这些阶段后,在提出修复方案前,先对每个候选项进行分类:

类别通常安全吗?检查事项
static readonly 不可变常量是内部没有隐藏的可变对象
静态基本类型计数器/标志否++、直接读写、可见性
静态 Map/Array/StringBuilder否并发修改、检查后执行
单例实例否延迟初始化竞态
注册表/缓存否获取或创建、配对过时、失效
每对象对象中的实例字段不一定对象可能被全局共享
受 ConcurrentHashMap/Atomic/Mutex 保护的状态不一定临界区是否涵盖整个不变量

不要仅仅因为某个文件中存在锁,就判定它是安全的。请确认锁保护了完整的不变量。

风险判定标准

使用以下四项条件识别实质性风险:

  1. 共享可变状态:静态字段、单例字段、全局缓存,或可从单例访问的共享对象。
  2. 并发入口:API 可由用户代码、标准库回调、taskpool、EAWorker、定时器或工作线程调用。
  3. 缺少或不完整的同步:没有原子操作/锁/并发容器,或锁只保护了检查后执行序列的一部分。
  4. 具体影响:说明可能出现的问题,例如更新丢失、状态过时、可见结果错误、任务泄漏、回调重复或容器损坏。

不要仅仅因为扫描匹配到 static、ConcurrentHashMap、Map 或延迟初始化模式,就判定为高风险。首先证明并发路径,以及对用户可见或运行时可见的影响。如果无法描述具体故障模式,优先保持代码不变。

风险级别:

  • 高风险:在公共并发 API 中,已证实会导致崩溃、死锁、内存/对象/容器损坏、无界资源泄漏或确定性错误结果。
  • 中风险:在可触达的并发路径中,已证实会导致更新丢失或生命周期异常,例如任务元数据泄漏、重复执行、输出损坏或清理不正确。
  • 低风险:有限或可恢复的竞态,例如缓存过时、ID/名称重复、诊断信息不准确、统计遗漏、仅调试/内省时出现的不一致,或罕见的时序窗口。
  • 候选项:可疑模式,但可触达性证据较弱或影响不明确。除非有进一步证据,否则不要建议修改代码。
  • 无风险:不可变常量、局部变量、每次调用的数据、单线程初始化/注册,或所有权明确且已完全同步的状态。

如有疑问,请将问题报告为候选项,不要提高其严重级别。对于候选项和无风险发现,优先不修改代码。

常见错误模式

本节中的代码片段是有源码依据的模式,并非独立测试。将它们复制到代码中之前,请确认目标文件中的导入、包可见性,以及辅助方法是公开的还是标准库内部的。

延迟初始化单例

错误示例:

if (Console.instance == undefined) {
    Console.instance = new Console()
}
return Console.instance!

修复方案:

Console.instanceLock.guard(() => {
    if (Console.instance == undefined) {
        Console.instance = new Console()
    }
})
return Console.instance!

检查和赋值必须位于同一个临界区中。

共享输出缓冲区

当 API 构建字符串后再输出时,应保护整个操作:

this.outputLock.guard(() => {
    let buf = this.lvl2Buf.get(level)!
    buf.append(s)
    this.printString(buf.toString(), level.valueOf())
    this.lvl2Buf.set(level, new StringBuilder)
})

只锁定 append 或只锁定 printString,仍可能导致缓冲区内容混杂。

共享 Map 的获取或创建

错误示例:

let slot = map.get(key)
if (slot == undefined) {
    slot = new Slot()
    map.set(key, slot)
}
return slot

修复模式:

const existing = states.get(key)
if (existing !== undefined) {
    return existing
}

slotLock.lockGuard(() => {
    if (states.get(key) === undefined) {
        states.set(key, new Slot())
    }
})

const slot = states.get(key)
if (slot === undefined) {
    throw new Error(`Failed to create slot for '${key}'`)
}
return slot

使用 ConcurrentHashMap 进行并发存储;如果没有原子 computeIfAbsent,则使用单独的锁进行延迟创建。

未同步的计数器

错误示例:

private static uniqueNameCounter: long = -1
return (++Proxy.uniqueNameCounter).toString()

修复:

private static uniqueNameCounter: AtomicLong = new AtomicLong(0)
return Proxy.uniqueNameCounter.fetchAdd(1).toString()

对于从 0 开始的后置递增:

oldCounter++

对应的原子操作同样是:

counter.fetchAdd(1)

初始值为 0。

未同步的布尔标志

错误示例:

private static LOG_ENABLED: boolean = false
Logger.LOG_ENABLED = true
if (Logger.LOG_ENABLED) { ... }

修复:

private static LOG_ENABLED: AtomicBoolean = new AtomicBoolean(false)
Logger.LOG_ENABLED.store(true)
if (Logger.LOG_ENABLED.load()) { ... }
成对的缓存字段

错误示例:

private static lastParsedTag: string | undefined = undefined
private static lastParsedResult: ParsedLocaleData | undefined = undefined

如果 tag 和 result 必须匹配,它们就构成一个不变量。使用同一把锁保护读写,或者将它们替换为一个不可变的缓存条目对象。

最小修复:

private static localeCacheMutex: Object = ConcurrencyHelpers.mutexCreate()

private static getCachedLocale(tag: string): ParsedLocaleData | undefined {
    let result: ParsedLocaleData | undefined = undefined
    ConcurrencyHelpers.lockGuard(Locale.localeCacheMutex, () => {
        if (Locale.lastParsedTag == tag && Locale.lastParsedResult != undefined) {
            result = Locale.lastParsedResult
        }
    })
    return result
}

private static setCachedLocale(tag: string, data: ParsedLocaleData): void {
    ConcurrencyHelpers.lockGuard(Locale.localeCacheMutex, () => {
        Locale.lastParsedTag = tag
        Locale.lastParsedResult = data
    })
}

不要只锁定一个字段。不要在锁外读取 tag,却在锁内读取 result。

槽位中的复合状态

如果一个值包含多个必须同时变化的字段,请使用内部锁:

final class TimerSlot {
    public tryStart(now: long): boolean {
        let started = false
        this.lock.guard(() => {
            if (!this.active) {
                this.startTime = now
                this.active = true
                started = true
            }
        })
        return started
    }

    private startTime: long = 0
    private active: boolean = false
    private lock: QueueSpinlock = new QueueSpinlock()
}

除非状态机经过有意设计和审查,否则不要对多字段不变量使用单独的原子变量。

修复选择指南

选择能够保护实际不变量的最小范围同步方式:

问题首选修复
单个数值计数器AtomicInt / AtomicLong
单个布尔标志AtomicBoolean
必须匹配的成对/三元字段使用一把互斥锁保护所有读写
单例延迟初始化使用锁保护检查后执行
共享键值注册表ConcurrentHashMap + 使用锁保护获取或创建
每键可变复合状态每个槽位使用一把锁
全局输出行构造使用一把输出锁保护构造和输出

避免:

  • 只锁定写操作,却让读操作不加锁
  • 未设计一致性方案,就为相关字段添加原子变量
  • 创建了锁,却仍在锁外执行检查后操作
  • 更改初始值,却没有保留原有的可见序列

审查清单

审查补丁时,请确认:

  1. 旧竞态确实已被消除,而不只是更难复现。
  2. 保留了公共行为。
    • 前置递增 ++x 与 fetchAdd(1) 的对应关系要求调整初始值。
    • 后置递增 x++ 可自然映射为 fetchAdd(1)。
  3. 临界区覆盖了整个不变量。
  4. 除非有意为之,否则持有锁时不要调用任意用户回调。
  5. 如果要获取多把锁,锁的获取顺序应保持一致。
  6. ConcurrentHashMap 的值不包含未同步的可变字段。
  7. 删除未使用的字段或为其添加说明;不要把未使用的字段标记为已修复的真实竞态。

测试策略

根据风险选择适当的测试。

功能并发测试

使用现有仓库测试作为模板,不要凭空设计新的 API 形式:

  • 使用函数参数的 taskpool.execute:static_core/plugins/ets/tests/ets_func_tests/std/containers/BlockingQueue/AddAndPollStress.ets:47
  • 测试中的 taskpool 清理:static_core/plugins/ets/tests/ets-common-tests/taskpool/common_tasks_new.ets:423
  • CoroutineExtras.stopTaskpool 声明:static_core/plugins/ets/stdlib/std/debug/concurrency/CoroutineExtras.ets:42
  • 用于类型化数组屏障的 Atomics.load 声明:static_core/plugins/ets/stdlib/std/concurrency/LegacyAtomics.ets:253

优先采用现有测试中使用的简单工作线程任务签名。如果需要启动屏障,请引用屏障所用原子操作的来源,并将代码片段放在测试文件中。

TSAN 测试

如果验证流程包含 TSAN,请简要说明:发生竞态的变量/函数,以及竞态是在 ETS 代码中还是原生代码中。

针对性测试

为受影响的共享状态添加有针对性的测试:

  • 输出 API:断言行内容与缓冲区内容不会混杂;除非 API 明确保证,否则不要要求跨线程顺序具有确定性
  • 有状态计数器/计时器:运行大量并发调用,并验证总数/状态转换
  • 缓存:覆盖相同键命中、不同键替换、选项/覆盖项以及无效输入路径

请注意:并发缓存未命中的测试可能会进入较慢的解析路径,而非缓存路径。仅当这会影响所选测试设计时,才提及这一点。

验证命令

如有可用的仓库本地工具,请使用:

git diff --check -- <changed-files>
ninja -C static_core/build plugins/ets/etsstdlib.abc
static_core/build/bin/es2panda --extension=ets --stdlib=static_core/build/plugins/ets/etsstdlib.abc --output=/tmp/test.abc <test.ets>
static_core/build/bin/ark --boot-panda-files=static_core/build/plugins/ets/etsstdlib.abc --load-runtimes=ets --verification-mode=ahead-of-time /tmp/test.abc <entrypoint>

可通过以下命令查找入口点:

static_core/build/bin/ark_disasm /tmp/test.abc /tmp/test.pa
rg -n "\\.function .* main\\(" /tmp/test.pa

评论

支持 Markdown