已开启
[Bug]: procmgr 查找进程节点后缺少生命周期保护,退出并发时可能访问已释放对象 #38
CupCupSir创建于  18 天前
CupCupSir
18 天前 创建

问题概述

procmgr 使用哈希表保存 struct proc_nodeget_proc_nodeproc_nodes_lock 保护下找到节点,但会先释放锁,再把裸指针返回给调用者。调用者使用完毕后也没有对应的引用释放操作。

进程回收路径使用同一把锁把节点从哈希表移除并释放内部资源,但释放锁后会立即执行 free(proc)。因此,查找线程可能已经拿到指针,回收线程随后移除并释放同一节点,查找线程再访问节点字段或节点内的互斥锁。

这不是每次进程退出都会发生的问题。只有相应节点实际进入释放分支,并且线程调度落在“查找返回之后、首次或后续访问之前”的窗口内才会触发。当前静态证据支持罕见并发时序下的 use-after-free 和 procmgr 进程异常,不支持把影响直接扩大为权限提升、可控代码执行或信息泄露。

该问题仍存在于已验证的 master

789b0fec1597c80ca6cb2a11aab3222b02e333b6

问题详情

查找锁只覆盖哈希表遍历,不覆盖返回后的引用

文件:user/system-services/system-servers/procmgr/proc_node.c

struct proc_node *get_proc_node(badge_t client_badge)
{
    struct proc_node *proc;
    struct hlist_head *buckets;

    pthread_mutex_lock(&proc_nodes_lock);
    buckets = htable_get_bucket(&badge2proc, client_badge);

    for_each_in_hlist (proc, hash_node, buckets) {
        if (client_badge == proc->badge) {
            goto out;
        }
    }
    pthread_mutex_unlock(&proc_nodes_lock);
    return NULL;
out:
    pthread_mutex_unlock(&proc_nodes_lock);
    return proc;
}

锁能保证遍历哈希链时节点不会同时被摘除,但返回值不是快照,也没有引用计数。struct proc_node 中没有引用计数字段或其他延迟释放状态,API 也没有 put_proc_node 一类的配对操作。

TEE 配置下的 get_proc_node_by_pid 使用相同模式:在 pid2proc 中找到节点后释放 proc_nodes_lock,再返回裸指针。它是同一生命周期缺陷的相邻入口。

回收路径会在移除哈希项后释放节点

同一文件中的 free_proc_node_resourceproc_nodes_lock 下移除 badge 和 pid 哈希项、释放 ID 和名称,然后释放锁:

void free_proc_node_resource(struct proc_node *proc)
{
    pthread_mutex_lock(&proc_nodes_lock);

    htable_del(&proc->hash_node);
#ifdef CHCORE_OH_TEE
    htable_del(&proc->pid_hash_node);
    clean_sharemem(proc->badge);
#endif
    /* release pid, pcid and name */

    pthread_mutex_unlock(&proc_nodes_lock);
}

del_proc_node 随后在两类情况下释放结构体本身:

for_each_in_list_safe (child, tmp, node, &proc->children) {
    child->parent = NULL;
    if (child->state == PROC_STATE_EXIT) {
        free_proc_node_resource(child);
        free(child);
    }
}

if (!proc->parent) {
    free_proc_node_resource(proc);
    free(proc);
}

因此,退出节点仍有父进程时通常不会由自己的回收事件立即释放。实际释放发生在以下情况之一:

  1. 退出节点已经是孤儿节点,del_proc_node 直接释放它。
  2. 父进程退出时,将已经退出的子节点设为孤儿并释放。
  3. 父进程执行等待操作,确认子进程已经退出后移除并释放子节点。

这一区分决定了真实触发条件。仅有并发退出但没有进入上述释放分支,不足以形成悬空指针。

独立回收线程与 IPC handler 并发运行

文件:user/system-services/system-servers/procmgr/procmgr.c

int main(int argc, char *argv[], char *envp[])
{
    pthread_create(&recycle_thread, NULL, recycle_routine, NULL);

    init_procmgr();
    cap = chcore_pthread_create(
        &procmgr_handler_tid, NULL, handler_thread_routine, NULL);
    /* ... */
}

handler_thread_routine 通过 ipc_register_server(procmgr_dispatch, DEFAULT_CLIENT_REGISTER_HANDLER) 注册服务。默认注册回调会为客户端创建被动 IPC handler 线程。回收线程则独立接收内核回收通知。

文件:user/system-services/system-servers/procmgr/recycle.c

proc_to_recycle = get_proc_node(msg.badge);
assert(proc_to_recycle != 0);

pthread_mutex_lock(&recycle_lock);
/* set exit status and recycle capability group */
del_proc_node(proc_to_recycle);
pthread_mutex_unlock(&recycle_lock);

recycle_lock 在查找之后才获取。它会串行化已知的回收和子节点释放操作,但普通查找调用者没有持有这把锁。因此,它不能阻止其他调用者在节点释放后继续使用早先取得的指针。

节点自身的 lock 只用于协调子进程列表。生产调用者也是在 get_proc_node 返回后才执行 pthread_mutex_lock(&client_proc->lock)。如果对象在两者之间被释放,尝试获取这个互斥锁本身已经访问了失效内存。wait_lock 保护退出状态和条件变量,同样不是对象生命周期引用。

生产调用者会在解锁后继续解引用节点

procmgr_dispatch 的多个请求处理函数使用该查找接口。例如,handle_waithandle_get_thread_cap 都先取得 client_proc,再访问节点锁和子进程列表:

client_proc = get_proc_node(client_badge);
assert(client_proc);

pthread_mutex_lock(&client_proc->lock);
/* traverse client_proc->children */

srvmgr.c 中的 do_launch_process 根据父进程 badge 查找父节点,稍后把该指针传给 new_proc_node。后者会访问 parent->lock 并修改父节点的子进程列表。

TEE 内存请求路径 oh_mem_ops.c 还会在查找后读取 proc_node->pidproc_node->proc_capproc_node->puuid。按 pid 查找的 handle_killhandle_info_proc_by_pid、任务映射和任务取消映射路径也存在同一返回后使用窗口。

user/system-services/system-servers/procmgr/Makefile 通过 SRCS := $(wildcard *.c) 编译目录中的 C 文件,并链接生成 procmgr.srv。因此,proc_node.crecycle.cprocmgr.csrvmgr.coh_mem_ops.c 都属于实际 procmgr 构建,不是未引用的辅助代码。

根本原因

proc_nodes_lock 同时被当作哈希表结构锁和节点存在性检查锁,但 API 在释放这把锁后仍把节点地址暴露给调用者。节点没有引用计数、读侧临界区或其他延迟回收机制,释放路径也不会等待现有调用者结束。

recycle_lockproc->lockwait_lock 分别保护回收操作、子进程列表及退出状态。它们都没有覆盖从哈希查找到最后一次节点访问的完整区间,所以不能承担对象生命周期保护。

触发条件

必须同时满足以下条件:

  1. procmgr 的 IPC handler 或其他生产调用路径通过 badge 或 pid 查找到一个节点,并释放 proc_nodes_lock
  2. 在调用者完成节点访问前,该节点进入真实释放路径:孤儿节点回收、父进程回收已退出子节点,或父进程等待并释放已退出子节点。
  3. 另一个线程在窗口内完成 free_proc_node_resourcefree
  4. 原调用者随后访问节点字段、节点互斥锁或子进程链表。

进程正常退出、崩溃或被终止都可能产生内核回收通知,但调度窗口很短,且非孤儿退出节点会暂时保留到父进程处理。因此,源码确认的是可发生的生命周期竞态,不是稳定或可重复的外部触发方式。

普通客户端进程可能参与与自身或子进程生命周期相关的 IPC 操作,但当前静态分析没有证明普通应用可以精确控制释放时刻、重分配内容或悬空指针的后续用途。该问题不应按已验证的恶意利用链描述。

影响分析

一旦命中窗口,procmgr 会对已经释放的 struct proc_node 执行字段读取、互斥锁操作或链表访问。最直接的结果包括:

  • procmgr 崩溃、断言失败或挂起。
  • 请求读取到已经变化的字段并返回错误结果。
  • 在已释放节点的锁或链表成员上操作,破坏 procmgr 自身状态。

procmgr 是系统服务,其异常可能阻止后续进程创建、等待、回收或 TEE 内存管理请求,具体恢复方式取决于产品如何监控和重启该服务。

当前没有运行时证据表明释放后的内存可以被外部请求按需要重新占用,也没有确认可控字段、稳定堆布局或安全边界跨越。因此,不应据此声称权限提升、任意代码执行、敏感信息泄露或 TEE 内核崩溃。受影响的是用户态 procmgr 服务的内存生命周期;更强影响需要单独的设备测试和可控性证据。

验证状态

已对 master 提交 789b0fec1597c80ca6cb2a11aab3222b02e333b6 进行静态源码复核:

  • 确认 get_proc_nodeget_proc_node_by_pid 在哈希表锁外返回无引用保护的指针。
  • 确认 free_proc_node_resource 在同一哈希表锁下移除节点,但 del_proc_node 会在解锁后释放结构体。
  • 确认孤儿退出节点、父进程回收已退出子节点和父进程等待子节点三种实际释放情况。
  • 确认 recycle_routine 是独立线程,IPC 服务使用按客户端创建的 handler 线程。
  • 确认 recycle_lock 不由普通查找调用者持有,节点锁也在裸指针返回后才获取。
  • 确认等待、获取主线程 capability、启动子进程及 TEE 内存处理等生产调用在查找后继续访问节点。
  • 确认相关源码通过 procmgr/Makefile 编入 procmgr.srv

本次没有运行或引用 PoC,也没有在设备上人为控制线程调度。因此,当前确认范围是源码中的释放后访问窗口及其最直接的服务异常风险,不声称已经观测到崩溃或其他运行时结果。

当前查找与释放实现由仓库最早可验证的代码导入提交引入。已检查的发布标签 OpenHarmony-v6.0-Beta1OpenHarmony-v6.0-ReleaseOpenHarmony-v6.0.0.1-ReleaseOpenHarmony-v6.0.0.2-ReleaseOpenHarmony-v6.1-ReleaseOpenHarmony-v6.1-LTSOpenHarmony-v7.0-Beta1OpenHarmony-v7.0-Release 均保留相同的生命周期语义。当前 master 和已获取的 2026 年 8 月 weekly 分支也仍保留该行为。

2026 年 7 月的 fix: avoid NULL deref on stale TASK_UNMAP_NS after TA exit 只在按 pid 查找已经返回 NULL 时提前结束请求。它不能保护“查找成功后才被并发释放”的指针,不是本问题的完整修复。所有可用分支历史中未发现为 proc_node 增加引用计数、读侧保护或延迟释放的变更。

已检索目标仓库全部 29 个公开 GitCode Issue、Issue 评论以及 21 个公开 Merge Request。以 get_proc_nodeproc_node、use-after-free、UAF、悬空指针、生命周期、竞态和 recycle 等关键词检查标题与正文,未发现描述同一根因的条目。Issue #15 讨论 handle_spawn 的输入边界校验,Merge Request !12 处理 procmgr 编译告警,均不是本问题的重复项。

修复建议

建议为 proc_node 建立明确的引用生命周期:

  1. 哈希表持有一个基础引用。get_proc_nodeget_proc_node_by_pid 必须在 proc_nodes_lock 内确认节点仍可获取,并为返回值增加引用。
  2. 每个调用者在最后一次访问后调用配对的 put_proc_node。需要逐一更新所有 badge 和 pid 查找调用点,避免部分路径仍返回无保护指针。
  3. 回收路径先在 proc_nodes_lock 内把节点标记为退出或正在销毁,并从两个哈希表移除,阻止新查找取得引用。
  4. 回收路径释放哈希表基础引用。只有最后一个调用者释放引用后,才清理名称、ID、互斥锁、条件变量和结构体内存。
  5. 父子关系需要各自明确持有引用。移除父子链接时释放对应引用,避免父进程回收和等待路径再次提前释放子节点。

不要简单地让所有调用者在执行完整 IPC 操作期间一直持有 proc_nodes_lock。现有路径会获取节点锁、recycle_lock、共享内存锁并调用可能阻塞的系统接口,扩大全局锁范围容易造成死锁或长时间阻塞。

建议增加可控制调度点的并发回归测试:

  • badge 查找成功后暂停调用者,同时回收孤儿节点;确认节点在调用者 put 前不会释放。
  • pid 查找成功后并发执行节点摘除;确认查找者仍可安全读取所需字段。
  • 子节点先退出、父进程随后等待并释放,确认现有引用会延迟最终销毁。
  • 已退出子节点仍被引用时回收父进程,确认父子链接清理不会提前释放子节点。
  • 覆盖 handle_waithandle_get_thread_cap、进程启动和 TEE 内存请求等实际调用点。
  • 节点从哈希表移除后发起新查找,确认新请求得到 NULL,而已有引用仍保持有效。

涉及文件

  • user/system-services/system-servers/procmgr/proc_node.c
  • user/system-services/system-servers/procmgr/include/proc_node.h
  • user/system-services/system-servers/procmgr/recycle.c
  • user/system-services/system-servers/procmgr/procmgr.c
  • user/system-services/system-servers/procmgr/srvmgr.c
  • user/system-services/system-servers/procmgr/oh_mem_ops.c
  • user/system-services/system-servers/procmgr/Makefile

版本信息

  • 当前核查分支:master
  • 当前核查提交:789b0fec1597c80ca6cb2a11aab3222b02e333b6
  • 最早可验证包含该行为的代码:仓库初始代码导入
  • 已验证包含该行为的发布标签:OpenHarmony-v6.0-Beta1OpenHarmony-v7.0-Release
  • 已发布修复:尚未发现
likedislike
openharmony_ci
openharmony_ci成员
18 天前 评论:

感谢提交Issue!关于Issue的交互操作,请访问OpenHarmony社区支持命令清单。如果有问题,请联系 [@peterli](https://gitcode.com/peterli) [@heyanhong](https://gitcode.com/heyanhong) [@sjtugjy](https://gitcode.com/sjtugjy) 。如果需要调整订阅PR、Issue的变更状态,请访问链接


Thanks for submitting the issue. For more commands, please visit OpenHarmony Command List. If you have any questions, please refer to committer gitcode for help. If you need to change the subscription of a Pull Request or Issue, please visit the link.

likedislike
openharmony_ciopenharmony_ci成员
18 天前 添加了label:waiting_for_assign