鸿蒙NEXT开发【CppCrash故障定位】开发运维

简介

CppCrash是C/C++运行时崩溃,包括空指针异常、数组越界异常、栈溢出异常等。HarmonyOS系统针对这一类故障,基于系统级DFX能力,能够进行检测并生成故障日志,生成在/data/log/faultlog/faultlogger系统目录下,在DevEcoStudio中的Faultlog工具栏也能进行汇总显示。

CppCrash故障日志

日志格式和日志获取

CppCrash故障根据报错场景可以分为运行态CppCrash故障和开发态CppCrash故障。

在开发态下,DevEco Studio会收集CppCrash、App Freeze、JS Crash、System Freeze、ASan的崩溃日志到FaultLog下,开发者可以通过FaultLog的CppCrash日志、ASAN日志定位问题的具体原因。此外,开发者可以自行获取/data/log/faultlog/faultlogger下的日志再进行分析。

在运行态下,开发者需要提前开通崩溃服务,收集运行状态下的CppCrash,具体步骤如下所示:

  1. 开发者需要在[AGC]提前创建项目和应用
  2. 在AGC上开通崩溃服务。
  3. 添加配置文件,将配置文件添加到工程目录并集成AGC插件,AGC插件可以自动将您在AGC上的应用信息加载到开发环境。
  4. 添加配置文件后,需要在DevEco Studio项目中配置SDK依赖。
  5. 配置完成后,需要测试崩溃服务是否正常运行。
  6. 具体的崩溃日志,可以在“AGC->我的项目->崩溃”中找到详细日志,进而分析异常报错问题。

crash信号分类

进程崩溃基于Linux信号机制,目前主要支持对以下崩溃异常信号的处理:

信号值信号解释触发原因
4SIGILL非法指令执行了非法指令、格式错误、未知或特权指令,通常是因为可执行文件本身出现错误,或者试图执行数据段,堆栈溢出时也有可能产生这个信号。
5SIGTRAP断点或陷阱异常由断点指令或其它trap指令产生。
6SIGABRTabort发出的信号调用abort函数生成的信号。
7SIGBUS非法内存访问非法地址,包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数,但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
8SIGFPE浮点异常在发生致命的算术运算错误时发出,不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。
11SIGSEGV无效内存访问试图访问未分配给自己的内存,或试图往没有写权限的内存地址写数据。
16SIGSTKFLT栈溢出堆栈溢出。
31SIGSYS系统调用异常非法的系统调用(Seccomp 限制)

CppCrash故障定位

辅助工具介绍

(1)反编译addr2line

Linux下addr2line命令用于将程序指令地址转换为所对应的函数名、以及函数所在的源文件名和行号。在问题定位时,可以使用llvm-addr2line.exe来替换addr2line,其对应的路径在"SDK安装路径\版本路径\base\native\llvm\bin"下,使用时可以参考以下命令。其中,libentry.so是报错的动态链接库,0000000000001c14是报错的内存地址。

llvm-addr2line.exe -a -C -i -f -e xxx...\libentry.so 0000000000001c14
参数功能描述
-C去除名称修饰
-f在反汇编结果中显示函数名
-a显示内存地址
-i解开内联函数
-e指定待还原的so的路径

(2)反汇编objdump

objdump可以用来显示二进制文件的信息,它可以进行反汇编用于观察内存异常。在问题定位时,可以使用llvm-objdump.exe来替换objdump,其对应的路径在"SDK安装路径\版本路径\base\native\llvm\bin"下,使用时可以参考以下命令。其中,libentry.so是需要反汇编的动态链接库,text.txt用于存储反汇编的代码。

llvm-objdump.exe -S -d xxx...\libentry.so > text.txt
选项功能说明
-C将低级符号名称解码(解映射)为用户级名称。除了删除系统预置的任何初始下划线外,这还使C++函数名可读。不同的编译器有不同的篡改样式。可选的 demangling 样式参数可用于为编译器选择适当的 demangling style 。
-d显示 objfile 中机器指令的汇编助记符。此选项仅反汇编那些预期包含指令的部分。
-D像 -d 一样,但反汇编所有部分的内容,而不仅仅是那些预期包含指令的部分。
-F在反汇编 sections 时,无论何时显示符号,都要显示要转储的数据区域的文件偏移量。如果跳过了零,那么当反汇编恢复时,告诉用户跳过了多少个零,以及反汇编恢复位置的文件偏移量。转储节时,显示转储开始位置的文件偏移量。0000000000000665 (File Offset: 0x665): so 共享库的某个反汇编函数入口000000000040052d (File Offset: 0x52d): 可执行文件的某个反汇编函数入口
-l用与显示的目标代码或重定位相对应的文件名和源行号标记显示(使用调试信息)
-S如果可能的话(有调试信息),显示混合了反汇编的源代码。

(3)ASan

ASan(Address-Sanitizer)是内存检测的工具,用于发现内存飞踩第一现场,DevEco Studio已集成ASan为开发者提供面向C/C++的地址越界检测能力。ASan可以解决一些踩内存导致的异常crash的补充手段,对于一些明显不可能crash的场景可以尝试开启ASan。

CppCrash故障定位方法

CppCrash常用故障定位分为本地环境定位和系统环境定位,其主要的定位思路类似,详细的定位方法如下所示:

(1)找出问题必现条件,复现问题场景;

(2)先开多线程检测配置,排查多线程安全问题;

先打开多线程检测配置,确认问题是否属于多线程安全问题,若打开多线程检测配置之后还是报当前错误栈,则进行下一步排查。打开开关的hdc指令为:

hdc shell param set persist.ark.properties 0x107c 
hdc shell reboot

说明

persist.ark.properties的默认值为0x105c(即关闭状态)。

(3)根据信号和错误码初步推断crash原因。如下图所示,错误原因为SIGSEGV(SEGV_ACCERR),说明是xmlparser崩溃相关原因。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

(4)根据堆栈信息找出报错的栈顶函数。

如下图所示,在开发态发生CppCrash时,在本地环境的DevEco Studio中,Native栈帧和JS栈帧可以通过蓝色链接直接跳转到对应的代码行。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在运行态下,开发者可以找到报错的日志,根据对应的报错日志使用llvm_addr2line解析出栈帧的对应的代码行号。案例如下所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

说明

使用addr2line后,如果得出的行号看起来不是很正确,可以考虑对地址进行微调(如减1),或者考虑关闭一些编译优化,已知使用LTO(Link Time Optimization)的二进制可能无法正确获得行号。

(5)若无法定位出具体原因,可以使用llvm-objdump反汇编代码,再进一步进行分析。

分析反汇编可参考以下步骤:

  1. 执行反汇编命令,找到报错的汇编代码;
  2. 追踪汇编代码中,寄存器数据的来源,确定导致程序出错的那部分代码。
  3. 结合报错信息和寄存器数据,推断程序报错的原因。
  4. 根据对报错原因的分析,进行代码修正或优化,以解决程序出错的问题。

(6)如果有理论上不应该发生堆栈报错,可以考虑开启ASan,再根据ASan定位分析具体原因。

如下所示,发生crash后,根据ASan故障日志定位错误代码。

TIMESTAMP:20231228152114
Pid:24923
Uid:20020033
Process name:
Reason:AddressSanitizer:heap-use-after-free
Fault thread Info:
==appspawn==24923==ERROR: AddressSanitizer: heap-use-after-free on address 0x007f9653e338 bp 0x007fd4635ae0 sp 0x007fd46352d0
READ of size 4 at 0x006116401a10 thread T0 (id.commom.babel)
    #0 0x7f9653e334  (/system/lib64/libclang_rt.asan.so+0x7e334)
    #1 0x557fe4e8c8  (/system/lib64/ndk/libnative_rdb.ndk.z.so+0xe8c8)
    #2 0x55800d3f50  (/data/storage/el1/bundle/libs/arm64/libbabel.so+0x253f50)
    #3 0x557ff6ef1c  (/data/storage/el1/bundle/libs/arm64/libbabel.so+0xeef1c)
    #4 0x557ff5f394  (/data/storage/el1/bundle/libs/arm64/libbabel.so+0xdf394)
    #5 0x55800d0a08  (/data/storage/el1/bundle/libs/arm64/libbabel.so+0x250a08)
    #6 0x7f9dd68d5c  (/system/lib64/platformsdk/libace_napi.z.so+0x28d5c)

0x006116401a10 is located 0 bytes inside of 46-byte region [0x006116404a10,0x006116404a40)
...

常见问题

应用开发时,发生CppCrash的常见场景有非法参数、多线程安全、napi_value生命周期、空指针等。常见的CppCrash的场景、原因汇总如下表所示。

常见场景常见信号及错误码定位思路崩溃原因
非法参数SIGSEGV(SEGV_MAPERR)@0X00402109F85E8090(内存地址为极大的非正常地址)如果有cppcrash栈直接崩溃在libace_napi.z.so/libark_jsruntime.so/libace_napi_ark.z.so,并且libace_napi.z.so的栈帧位置较浅。这种问题往往需要napi模块的上层使用者优先去排查。可以参考案例[传入的napi_env的虚函数表指针为大地址] 、[传入的NativeValue*为空指针] 。传入的napi_env的虚函数表指针为大地址
SIGSEGV(SEGV_MAPERR)@0X000000000000001c(内存地址为较小的地址)可能原因为传入的napi_value异常
SIGSEGV(SEGV_MAPERR)@0X0000000000000008传入的NativeValue*为空指针
SIGSEGV(SEGV_MAPERR)@0X0000000000000012传入的napi_defer为undefined
SIGSEGV(SEGV_MAPERR)@0X0000000000000022返回的ArrayBufferRef为undefined
多线程安全SIGSEGV(SEGV_MAPERR)@0X0000000000000028崩溃栈从上往下看,略过libark_jsruntime.so/libace_napi.z.so/libace_napi_ark.z.so,看是从哪个上层模块的so调用过来的,该上层模块就存在多线程安全问题。napi_env释放后仍被使用
SIGSEGV(SEGV_MAPERR)@0X0000000000001f98static变量
napi_value生存期SIGSEGV(SEGV_MAPERR)@0X000000000000002a(报错为:Can not get Prototype on non ECMA Object)根据崩溃栈反编译找到出现问题的napi接口的上层接口,在上层接口内找到出问题的napi_value,检查napi_value的使用范围是否超出了napi_handle_scope的作用域范围。napi_value超出NAPI框架的scope
xmlparser崩溃SIGSEGV(SEGV_ACCERR)@0x0000007f8e71d000如果崩溃在libxml.z.so,需要检查xml解析的代码xmlparser崩溃
native层调用iconv函数崩溃SIGSEGV(SEGV_MAPERR)@0x000000808566e685查看日志中是否有iconv关键字确定报错类型,并根据报错日志定位到错误代码native层调用iconv函数crash

非法参数是CppCrash的常见场景之一。在napi接口中,所需参数一般有两种,一种是napi_env,一种是napi_value,实际上,这两种入参都是裸指针(非智能指针),napi接口中会对他们进行空指针拦截,然而,无法拦截其他的野指针(仅声明但未做初始化,且不为空的指针)。因此,如果有cppcrash栈直接崩溃在libace_napi.z.so/libark_jsruntime.so/libace_napi_ark.z.so,并且libace_napi.z.so的栈帧位置较浅。此类问题一般都是napi模块的上层模块在调用napi接口时传参有问题导致,这种问题往往需要napi模块的上层使用者优先去排查。

在多线程检测机制中,会判断当前线程的thread id与被使用的vm/env中的thread id是否一致,若不一致,则表明vm/env被跨线程使用,可能引发多线程安全问题,被拦截日志拦截(Fatal:ecma_vm cannot run in multi-thread!)。在定位多线程的崩溃时,需要从上往下看崩溃栈,略过libark_jsruntime.so/libace_napi.z.so/libace_napi_ark.z.so,看是从哪个上层模块的so调用过来的,该上层模块就存在多线程安全问题。

开发者在书写native代码创建napi_value时,需要配合napi_handle_scope一起使用。napi_handle_scope的作用是管理napi_value的生命周期,napi_value只能在napi_handle_scope的作用域范围内进行使用,离开napi_handle_scope作用域范围后,napi_value及它所持有的js对象的生命周期不再得到保护,一旦引用计数为0,就会被GC回收掉,此时再去使用napi_value就会访问已释放的内存,产生问题。 在产生napi_value生存期问题时,需要根据崩溃栈反编译找到出现问题的napi接口的上层接口,在上层接口内找到出问题的napi_value,检查napi_value的使用范围是否超出了napi_handle_scope的作用域范围。

libuv非法释放

问题描述

在测试hap包时,ASan出现以下报错:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

定位分析

从堆栈中可知,应用在调用libuv.so之后直接报踩内存错误,报错地址为0x21304。

根据动态链接库libuv.so和报错内存地址0x21304,执行命令 addr2line.exe -fie libuv.so 0x21304 进行反编译,得到以下信息。

/.../../libuv/src/timer.c:59

根据反编译信息可知,报错发生在uv_timer_init函数中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当前,报错定位到源码 uv__handle_init(loop, (uv_handle_t)handle, UV_TIMER),需要确认这个方法在调用中哪个地址被踩了,所以需要对0x21304进行反汇编;*

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在汇编代码中,发现0x21304是由X23寄存器赋值给X0,继续追踪X23寄存器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

X23寄存器是 X19寄存器加上立即数 0x28。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

X19寄存器是X1寄存器赋值,而X1寄存器则对应int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle)的第2个入参 handle(一般而言X0寄存器对应方法的第一个参数,依次下去)。

再返回到代码中进行下一步分析,首先分析uv_timer_t* handle的结构体,其代码如下:

typedef struct uv_timer_s uv_timer_t;
struct uv_timer_s {
  UV_HANDLE_FIELDS
  UV_TIMER_PRIVATE_FIELDS
};
#define UV_HANDLE_FIELDS
  void* data;
  uv_loop_t* loop;
  uv_handle_type type;
  uv_close_cb close_cb;
  void* handle_queue[2];
  union {
    int fd;
    void* reserved[4];
  } u;
  UV_HANDLE_PRIVATE_FIELDS

通过计算uv_timer_s的偏移,第40字节偏移应该是指向 handle_queue[1]处,接着分析uv_handle_init函数如下,可以看到该函数除了初始化之外,还有一个将handle放到loop->handle_queue的操作。

int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle) {
  uv__handle_init(loop, (uv_handle_t*)handle, UV_TIMER);
  handle->timer_cb = NULL;
  handle->timeout = 0;
  handle->repeat = 0;
  return 0;
}
#define uv__handle_init(loop_, h, type_)
     do {
       (h)->loop = (loop_);
       (h)->type = (type_); 
       (h)->flags = UV_HANDLE_REF;  /* Ref the loop when active. */ 
       QUEUE_INSERT_TAIL(&(loop_)->handle_queue, &(h)->handle_queue);
       uv__handle_platform_init(h);
     }
     while (0)
#define QUEUE_INSERT_TAIL(h, q)
     do {
       QUEUE_NEXT(q) = (h);
       QUEUE_PREV(q) = QUEUE_PREV(h);
       QUEUE_PREV_NEXT(q) = (q);
       QUEUE_PREV(h) = (q);
     }
     while (0)
#define QUEUE_NEXT(q) (*(QUEUE **) &((*(q))[0]))
#define QUEUE_PREV(q) (*(QUEUE **) &((*(q))[1]))
#define QUEUE_PREV_NEXT(q) (QUEUE_NEXT(QUEUE_PREV(q)))
#define QUEUE_PREV(h) (QUEUE_PREV(QUEUE_NEXT(q)))

通过代码流程分析,在uv_handle_init中确实有handle->handle_queue操作的动作,就是这个QUEUE_INSERT_TAIL; 现在故障现场已经找到了,但是不知道这里是如何被踩的。

继续检查QUEUE_INSERT_TAIL,通过加日志测试可知参数 h 为无效指针(已经被释放),但是在这里又被使用到了,所以导致 uaf 问题。 一路回溯上去就确认是 uv__handle_init(loop, (uv_handle_t*)handle, UV_TIMER) 中的第二个参数 handle 的问题。

最后检查代码确认,在 unschedule 函数中的 else 分支内,ptrRef 被提前 delete 掉了;此处应跟 if 分支里面的实现一样,采用libuv异步方式释放内存,用 uv_close 的形式交给 libuv 自己去管控 handle 的生命周期。

bool Timer::unschedule(shared_ptr<Loop> loop, shared_ptr<Source> source) {
        ...
    if (uv_is_active((uv_handle_t *)_timer)) {
        uv_close((uv_handle_t *)_timer, [](uv_handle_t *handle) {
            auto ptrRef = static_cast<shared_ptr<Source> *>(handle->data);
            handle->data = nullptr;
            delete ptrRef;
        });
    } else {
        auto ptrRef = static_cast<shared_ptr<Source> *>(handle->data);
        handle->data = nullptr;
        delete ptrRef;
    }
}

传入的napi_env的虚函数表指针为大地址

问题描述

如果有cppcrash栈直接崩溃在libace_napi.z.so/libark_jsruntime.so/libace_napi_ark.z.so,并且libace_napi.z.so的栈帧位置较浅。此类问题一般都是napi模块的上层模块在调用napi接口时传参有问题导致,这种问题往往需要napi模块的上层使用者优先去排查。 案例崩溃栈如下所示:

Timestamp:2024-03-12 15:26:53.703
Pid:20330 
Uid:0
Process name:com.example.xxx
Reason:Signal:SIGSEGV(SEGV_MAPERR)@0x00402109f85e8090
Fault thread Info:
Tid:20330, Name:m.example.xxx
#00 pc 00000000000184cc /system/lib64/platformsdk/libace_napi.z.so(napi_get_undefined+36)(555556bbed21a6ac076967a1f0c9786f)
#01 pc 000000000007a4a0 /data/storage/el1/bundle/libs/arm64/libesharehm.so(ScreenCastPlayer::onArkTsMessage(int)+196)(6bd7286a4b818945d585a2a67f9758a45339e587)

定位分析

在崩溃栈中,可以发现崩溃地址是一个极大的非正常地址。使用addr2line工具反编译解析代码行后看到,崩溃在46行,也就是尝试调用env里面的CreateUndefined方法挂掉了,而且是还没调用进去就挂了,CreateUndefined是engine上的虚方法,调用这个函数分为三步,一是从engine类上取出虚表,二是从虚表中拿出函数指针,三是跳转。第一步会对env解一次引用,拿出虚表指针,第二步会对虚表指针偏移后解引用,拿出函数指针。对于本案例来说,出问题的可能会出现在第一、二步,现在就要排查到底是env是个大地址,还是虚表是个大地址。

说明

如果是第三步出了问题,报的崩溃栈是not mapped pc错误

// Getters for defined singletons
NAPI_EXTERN napi_status napi_get_undefined(napi_env env, napi_value* result)
{
    CHECK_ENV(env);
    CHECK_ARG(env, result);
    
    auto engine = reinterpret_cast<NativeEngine*>(env);
    auto resultValue = engine->CreateUndefined();
    
    *result = reinterpret_cast<napi_value>(resultValue);
    return napi_clear_last_error(env);
}

接下来我们看带寄存器信息,x0是第一个入参,也就是env,我们可以看到,env落在libuv.so里面,这明显有问题,也就是libesharehm.so传入的env这个裸指针已经非法。从这个非法地址中取出第一个域(虚表)是一个大地址b9402109f85e8008,然后从这个虚表中偏移一定位数之后解引用,尝试取函数指针,b9402109f85e8008解引用导致崩溃,报SEGV_MAPERR错误。崩溃栈顶为@0x00402109f85e8090 (dfx将高位两0抹除)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于本案例,有两个上层的排查建议:

  • 打开多线程检测后压测,看是否是多线程安全问题引起的问题。如果打开之后还是报这个栈,则进行第二步排查。
hdc shell param set persist.ark.properties 0x107c
hdc shell reboot
  • 重点排查是否env非上层so自己保存下来了,注意,保存env这个行为很危险,因为保存者并不知道env什么时候被释放掉。可以在上层中加维测日志打印env地址,并且ArkNativeEngine析构时打印地址,可以看看是否是env被析构后还在使用。

传入的napi_value异常

问题描述

此类问题基本都是参数传的有问题,其崩溃栈如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

定位分析

首先,使用addr2line工具反编译找到出现问题的接口及行号。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从崩溃原因 SIGSEGV(SEGV_MAPERR)@0x000000000000001c ,大致可以推断出 napi_typeof 的第二个参数 value 有问题。但是是其本身的值有问题,还是其指向的内存有问题,需要用objdump工具反汇编并结合寄存器的值进一步分析。

从cppcrash文件Registers可以看出value本身即 x1 的值000000558473f264 看起来没什么问题(不确定地址是否是8字节对齐的地址,这里暂且假设地址是正常的),所以我们继续分析其指向的内存。

出现问题的汇编代码是 ldr x8 [x8, #24] , x8 是 x1 值所指向的内容,其值为0x4 ,很明显 x8 加上立即数 #24 就是崩溃的地址 0x1c 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

另外,x8的值从Memory near registers也可以看出来,000000558473f264地址对应的值为0x4。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

回到出现问题的行号,可以看出是在调用TypeOf函数,它是一个虚函数,因此x8就是虚函数表地址,24就是虚函数TypeOf在虚函数表中的偏移量。正常情况下,ldr x8 [x8, #24]执行完后,x8就是TypeOf函数地址,紧接着再执行blr x8就完成了TypeOf函数调用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通常这类问题有两个原因:

  • value本身地址不对,此类问题要排查传参赋值的地方有没有初始化。
  • value地址正常,其指向的内存有问题,此类问题要排查内存是不是已经被GC回收了。

传入的NativeValue*为空指针

问题描述

调用用SetProperty发生崩溃,堆栈如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

定位分析

通过addr2line工具,得知崩溃时的调用代码如下。

bool ArkNativeObject::SetProperty(const char* name, NativeValue* value)
{
    auto vm = engine_->GetEcmaVm();
    LocalScope scope(vm);

    Global<ObjectRef> obj = value_;
    Local<StringRef> key = StringRef::NewFromUtf8(vm, name);
    Global<JSValueRef> val = *value;

    return obj->Set(vm, key, val.ToLocal(vm));
}

但这里看不出具体原因,所以继续使用objdump反汇编看一下具体的汇编指令。

从堆栈上看,最后崩溃在 2bc0c 上,反汇编结果对应的指令如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上面的指令ldr x1, [x19, #8],其中x19为参数value(x19最近一次是通过x2赋值的,x2就是第二个参数value,x0为this,x1为第一个参数name),结合上面Registers可以看出,x19寄存器的值为0x0,加上了偏移8,也就是取地址0x8的内容。

到此,可以得出初步结论SetProperty的时候,传入的第二个参数value为nullptr。 另外一方面从日志也可以看出,创建JsResourceManager也发生了异常。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

NativeValue* CreateJsBaseContext(NativeEngine& engine, std::shared_ptr<Context> context, bool keepContext)
{
    NativeValue* objValue = engine.CreateObject();
    NativeObject* object = ConvertNativeValueTo<NativeObject>(objValue);
    if (object == nullptr) {
        HILOG_WARN("invalid object");
        return objValue;
    }
    auto jsContext = std::make_unique<JsBaseContext>(context);
    SetNameNativePointer(engine, *object, BASE_CONTEXT_NAME, jsContext.release(), JsBaseContext::Finalizer);
    auto appInfo = context->GetApplicationInfo();
    if (appInfo!= nullptr) {
        object->SetProperty("applicationInfo", CreateJsApplicationInfo(engine, *appInfo));
    }
    auto hapModuleInfo = context->GetApplicationInfo();
    if (hapModuleInfo!= nullptr) {
        object->SetProperty("currentHapModuleInfo", CreateJsApplicationInfo(engine, *hapModuleInfo));
    }
    auto resourceManager = context->GetApplicationInfo();
    if (appInfo!= nullptr) {
        object->SetProperty("resourceManager", CreateJsApplicationInfo(engine, resourceManager, context));
    }
    BindNativeProperty(*object, "cacheDir", JsBaseContext::GetCacheDir);
    BindNativeProperty(*object, "tempDir", JsBaseContext::GetTempDir);
    BindNativeProperty(*object, "filesDir", JsBaseContext::GetFilesDir);
    BindNativeProperty(*object, "distributeFilesDir", JsBaseContext::GetDistributeFilesDir);
    BindNativeProperty(*object, "databaseDir", JsBaseContext::GetDatabaseDir);
    BindNativeProperty(*object, "preferencesDir", JsBaseContext::GetPreferencesDir);
    BindNativeProperty(*object, "bundleCodeDir", JsBaseContext::GetBundleCodeDir);
    BindNativeProperty(*object, "area", JsBaseContext::GetArea);
    const char *moduleName = "JsBaseContext";
    BindNativeFunction(engine, *object, "createBundleContext", moduleName, JsBaseContext::CreateBundleContext);
    BindNativeFunction(engine, *object, "getApplicationContext", moduleName, JsBaseContext::GetApplicationContext);
    BindNativeFunction(engine, *object, "switchArea", moduleName, JsBaseContext::SwitchArea);
    BindNativeFunction(engine, *object, "getArea", moduleName, JsBaseContext::GetArea);
    BindNativeFunction(engine, *object, "createModuleContext", moduleName, JsBaseContext::CreateModuleContext);

    return objValue;
}

接下来,继续分析一下x19 需要加上#8的原因。下面3条指令是调用 GetHandleAddr(const EcmaVM *vm, uintptr_t localAddress) 方法,这样对应起来[x19, #8]保存的应该是address。对比代码,可以看出下面的指令对应的逻辑为val.ToLocal 。

2bc0c: f9400661 ldr x1, [x19, #8]
2bc10: aa1603e0 mov x0, x22
2bc14: 94000abf bl -0x2e710 <_ZN5panda6JSNApi13GetHandleAddrEPKNS_10ecmascript6EcmaVMEm@plt>
Local<T> ToLocal() const
{
    if (IsEmpty()) {
        return Local<T>();
    }
    return Local<T>(vm_, *this);
}
class NativeValue {
public:
    virtual ~NativeValue() {}
    template<typename T> operator T()
    {
        return value_;
    }
    ...
}

NativeValue到Global涉及了多层的转换, NativeValue* 指向的内容和Global是一样的,所以通过Global拿address等同于通过NativeValue*去取其第一个成员的值。因为NativeValue析构函数为virtual,所以取成员需要加上虚表偏移#8 。

class NativeValue {
public:
    virtual ~NativeValue() {}
    template<typename T> operator T()
    {
        return value_;
    }
    ...
}

因此,问题根因已找到:CreateJsResourceManager返回了nullptr。

返回的ArrayBufferRef为undefined

问题描述

此类问题崩溃栈如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

定位分析

造成上述现象的原因是因为this为undefined,undefined在运行时中的编码是0x02,0x22是undefined去取 ArrayBufferData 这个域导致的崩溃。

因此需要往上分析:

ArkNativeArrayBuffer::ArkNativeArrayBuffer(ArkNativeEngine* engine, uint8_t** value, size_t length)
    : ArkNativeArrayBuffer(engine, JSValueRef::Undefined(engine->GetEcmaVm()))
{
    auto vm = engine->GetEcmaVm();
    LocalScope scope(vm);
    value_ = Global<ArrayBufferRef>(vm, ArrayBufferRef::New(vm, length));
    if (value != nullptr) {
        Global<ArrayBufferRef> obj = value_;
        *value = reinterpret_cast<uint8_t*>(obj->GetBuffer());
    }
}

从构造函数可以看出,JSArrayBuffer为undefined说明value_为undefined。

此时有两种情况:

  • 应用存在异常导致 ArrayBufferRef::New(vm, length) 返回了一个undefined,需要看流水日志确认是否有"print exception info: "打印,如果有,则根据日志提示的具体异常信息排查对应位置的代码。
  • engine存在问题,需要上层根据代码去排查一下,该崩溃栈中的上层so是libnapi-adapter.so。

排查建议:

  • 打开多线程检测开关后重新复现问题,验证是否是多线程安全问题。如果打开之后还是报相同的崩溃栈,则进行第二步排查。
hdc shell param set persist.ark.properties 0x107c
hdc shell reboot
  • 重点排查env是否被非上层模块so(非libnapi-adapter.so)自己保存下来了,注意,保存env这个行为很危险,因为保存者并不知道env什么时候被释放掉。可以在上层模块中加维测日志打印env地址,并且ArkNativeEngine析构时打印地址,可以看看是否是env被析构后还在使用。

传入的argv与argc大小不一致

问题描述

napi_get_cb_info接口的入参包括argv和argc。argv表示存放一定数量的napi_value的数组,argc表示数组argv的长度。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在使用napi_get_cb_info接口时,应注意argc的值需要与argv的有效长度保持一致:

  • 如果argc大于argv的实际长度,则会在后续遍历argv时造成数组越界访问,读溢出;
  • 如果argc等于argv的实际长度,但argv中的有效元素个数小于argv的实际长度(例如声明了argv[2],但只对argv[0]和argv[1]进行了初始化),则napi接口内会将argv中的剩余元素全部设置成undefined(与node代码的规则是一致的)。此时若使用nullptr去判断argv中的某个napi_value是否有效,是不合理的,因为undefined一定不等于nullptr,那么这些绕开nullptr判空检查的napi_value(值为undefined)会被误认为是有效的napi_value,很有可能在后续使用时产生预料之外的问题。

定位分析

传入的argc,等于argv的实际长度,但大于argv的有效长度(例如声明了argv[2],但只对argv[0]和argv[1]进行了初始化,这种情况下,argv实际长度为3,有效长度为2):

size_t argc = 3;
napi_value argv[3]= {nullptr};
double v1 = 1.1;
napi_create_double(env, v1, &argv[0]);
double v2 = 2.1;
napi_create_double(env, v1, &argv[1]);
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); 

这种情况下,不能通过argv[i] != nullptr作为条件去判断元素是否有效,而应该通过napi_typeof去检查argv[i]的类型是否为napi_undefined去判断元素是否有效。在一些代码中,会先对argc参数个数进行检查,再对argv[i]参数类型进行检查,最后又检查argv[i]是否为nullptr,此时的判空逻辑是多余的,可以去除。以下列举多余判空的例子:

napi_value NapiShareManager::Cancel(napi_env env, napi_callback_info info)
{
    SHARE_MANAGER_HILOGI(JS_NAPI, "NapiShareManager::Cancel enter.");
    // argument count
    size_t argc = ARG_1;
    size_t expectArgc = ARG_1;
    // argument vector
    napi_value argv[ARG_1] = { 0 };
    napi_value thisVar = nullptr;
    void *data = nullptr;
    napi_status status = napi_get_cb_info(env, info, &argc, argv, &thisVar,&data);
    NAPI_ASSERT(env, status == napi_ok, "Bad parameters");
    NAPI_ASSERT(env, argc == expectArgc, "Cancel requires 1 parameter");
    napi_valuetype valueType = napi_null;

    // argv[0] is obj which type is ShareDeviceInfo
    napi_typeof(env, argv[ARG_0], &valueType);
    NAPI_ASSERT(env, valueType == napi_object, "type mismatch for parameter 1");
    napi_value ret;
    // 多余判空,前面已经判断过type和参数个数
    if (argv[ARG_0] == nullptr) {
        napi_get_boolean(env, false, &ret);
        return ret;
    }
    ...
    return ret;
} 

TaskPool & Worker多线程问题

问题描述

部分应用在使用TaskPool或Worker时出现了多线程问题,主要的形式是底层使用了std::map<napi_env, napi_ref>等形式,直接或间接通过env地址作为key来存取napi_ref。

static std::shared_ptr<ClearCacheListener> g_clearCacheListener;
static std::unordered_map<Query, napi_ref, QueryHash> cache;
static std::string g_ownBundleName;
struct Query {
    std::string bundleName_;
    std::string interfaceType_;
    int32_t flags_ = 0;
    int32_t userId_ = Constants::UNSPECIFIED_USERID;
    napi_env env_;
    Query(const std::string &bundleName, const std::string &interfaceType, int32_t flags, int32_t userId, napi_env env)
        : bundleName_(bundleName), interfaceType_(interfaceType), flags_(flags), userId_(userId), env_(env) {}
        
    bool operator==(const Query &query) const
    {
        return bundleName_ == query.bundleName_ && interfaceType_ == query.interfaceType_ && 
            flags_ == query.flags_ && userId_ == query.userId_ && env_ == query.env_;
    }
};

定位分析

排查步骤如下所示:

  1. 多线程场景下TaskPool使能了负载均衡机制,而Worker也提供了 new Worker 及 terminate 接口。两种机制都能够新建和释放线程,这种情况下env的生命周期具有不确定性。
  2. 基于C++内存管理机制,env释放后该地址仍有重用的可能。因此,内存中某一个已经释放的env地址会被重新分配给新的TaskPool和Worker线程env。
  3. 新线程和已经释放线程的env地址可能是一样的,因此通过env作为key访问全局数据结构时,就可能取到了已经失效的ref value(含有已经析构的vm)。此时会出现多线程、非法内存访问等问题。
  4. 子系统需要排查是否存在对应的逻辑,并通过注册 napi_add_env_cleanup_hook 合理维护env及对应数据的生命周期。
  5. 此外,TaskPool无法感知底层是否保存env,因而在满足回收策略时会释放空闲线程。这种情况下,若通过IPC等线程再次使用env,会出现crash。但同时,TaskPool也提供了引用计数机制。
  6. 若某些场景下env确实不能被释放( On 等接口),可以在接口处调用IncreaseListeningCounter() 来增加引用计数,使线程池感知到env被引用,从而不会回收对应线程。
  7. IncreaseListeningCounter() 需要搭配 DecreaseListeningCounter() 合理使用( Off 等调用),保证一一对应,否则会造成线程长期释放不了,内存超基线。

napi_value超出NAPI框架的scope

问题描述

js侧通过Add接口添加数据,native侧以napi_value保存到vector。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

js侧通过get接口获取添加的数据,native侧将保存的napi_value以数组形式返回回去,然后js侧读取数据的属性。

出现报错:Can not get Prototype on non ECMA Object。

定位分析

报错的主要原因是跨napi的native_value未使用napi_ref保存,导致native_value失效。

说明

NAPI框架的scope即napi_handle_scope,napi开发者可以通过napi_handle_scope来管理napi_value的生命周期。

框架层的scope嵌入在js call native的端到端流程中,即:进入开发者自己写的native方法前open scope,native方法结束后close scope。

通过使能asan定位heap-use-after-free问题

问题描述

开启Address Sanitizer后获取到的asan故障日志如下:

TIMESTAMP:20231228165420
Pid:9402Uid:20010132
Process name:com.xxxx.common.xxx
Reason:AddressSanitizer:heap-use-after-freeFault thread Info:
==appspawn==9402== ERROR: AddressSanitizer: heap-use-after-free on address0x0061043bd290 at pc 0x007f852fe338 bp 0x007febe69440 sp 0x007febe68c30
READ of size 4 at 0x0061043bd290 thread T0 (id.common.babel)
    #0 0x7f852fe334 (/system/lib64/libclang_rt.asan.so+0x7e334)
    #1 0x557f28e8c8 (/system/lib64/ndk/libnative_rdb_ndk.z.so+0xe8c8)
    #2 0x557f513f50 (/data/storage/el1/bundle/libs/arm64/libbabel.so+0x253f50)
    #3 0x557f3aef1c (/data/storage/el1/bundle/libs/arm64/libbabel.so+0xeef1c)
    #4 0x557f39f394 (/data/storage/el1/bundle/libs/arm64/libbabel.so+0xdf394)
    #5 0x557f510a08 (/data/storage/el1/bundle/libs/arm64/libbabel.so+0x250a08)
    #6 0x7f855e8d5c (/system/lib64/platformsdk/libace_napi.z.so+0x28d5c) 
...
SUMMARY: AddressSanitizer: heap-use-after-free(/system/lib64/libclang_rt.asan.so+0x7e334)
Shadow bytes around the buggy address:
0x001c20877a00: fa fa 00 00 00 00 00 00 fa fa fd fd fd fd fa fa
0x001c20877a10: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fa fa 
...

定位分析

根据错误日志定位分析,发现错误发生在如下代码:

int HMDBManager::openDB() {
    OH_Rdb_Config config;
    config.dataBaseDir = this->dbPath.c_str();
    config.storeName = "xxx.db";
    config.bundleName = BabelDataManager::GetInstance() -> getInitConfiguration().appName.c_str();
    config.moduleName = "xxx";
    config.isEncrypt = false;
    this->config = config;
    int errCode;
    OH_Rdb_Store *store = OH_Rdb_GetOrOpen(&config, &errCode);
    this->store = store;
    return errCode;
}

直接获取appName并将其转换为C风格的字符串会存在一个问题。因为appName.c_str()返回的是一个临时字符串的指针。这个临时字符串在表达式结束后就会被销毁。这就以为着config.bundleName可能会指向一个已经被销毁的字符串,这将导致未定义的行为。

修改错误代码如下所示,这种方式中,首先获取了appName的一个副本,然后将其转换为C风格的字符串。在这种情况下,appName的生命周期会持续到当前的代码块结束。因此,config.bundleName指向的字符串在当前代码块内是有效的。

string appName = BabelDataManager::GetInstance() -> getInitConfiguration().appName;
config.bundleName = appName.c_str();

锁范围不足导致的Crash问题

问题描述

设备开关机压测时,崩溃在libcesfwk_core.z.so,崩溃栈如下:

Timestamp:1970-11-28 13:44:49.206
Pid:2906
Uid:10006
Process name:com.ohos.xxx
Reason:Signal:SIGSEGV(SEGV_MAPERR)@0x00492e6b7766746e
Fault thread Info:
Tid:2978, Name:com.ohos.system
#00 pc 000000000003fea8 /system/lib64/libipc_core.z.so(OHOS::PeerHolder::Remote()+12)
#01 pc 000000000001c5a4 /system/lib64/libcesfwk_core.z.so(OHOS::EventFwk::CommonEventProxy::SendRequest(OHOS::EventFwk::ICommonEvent::Message, OHOS::MessageParcel&,OHOS::MessageParcel&)+168)
#02 pc 000000000001cff8 /system/lib64/libcesfwk_core.z.so(OHOS::EventFwk::CommonEventProxy::SubscribeCommonEvent(OHOS::EventFwk::CommonEventSubscribeInfo const&,OHOS::sptr<OHOS::IRemoteObject> const&)+540)
#03 pc 0000000000016518 /system/lib64/libcesfwk_core.z.so(OHOS::EventFwk::CommonEvent::SubscribeCommonEvent(std::__1::shared_ptr<OHOS::EventFwk::CommonEventSubscriber> const&)+468)
#04 pc 0000000000012c20 /system/lib64/libcesfwk_innerkits.z.so(OHOS::EventFwk::CommonEventManager::SubscribeCommonEvent(std::__1::shared_ptr<OHOS::EventFwk::CommonEventSubscriber> const&)+56)
#05 pc 000000000003253c /system/lib64/module/libcommonevent.z.so
#06 pc 0000000000019808 /system/lib64/libace_napi.z.so(NativeAsyncWork::AsyncWorkCallback(uv_work_s*)+316)
#07 pc 000000000001156c /system/lib64/libuv.so
#08 pc 00000000000d02a0 /system/lib64/libc.so(__pthread_start(void*)+40)
#09 pc 0000000000072128 /system/lib64/libc.so(__start_thread+68)
...

定位分析

根据Reason可知为野指针,根据 #01 定位到具体的代码行有:

$ addr2line -Cpie ./notification/common_event_service/libcesfwk_core.z.so 000000000001c5a4
/mnt/disk/jenkins/ci/workspace/zidane_system_pipeline_release/compile/component_code/out/baltimore/../../base/notification/common_event_service/frameworks/core/src/common_event_proxy.cpp:385

对应的代码如下所示:

bool CommonEventProxy::SendRequest(ICommonEvent::Message code, MessageParcel &data, MessageParcel &reply) {
    EVENT_LOGD("start");
    sptr <IRemoteObject> remote = Remote();
    if (remote == nullpte) {
        EVENT_LOGD("Remote is NULL, %{public}d", code);
    }
    MessageOption option(MessageOption::TF_SYNC);
    int32_t result = remote->SendRequest(static_cast<uint32_t>(code), data, reply, option);
    if (result != OHOS::NO_ERROR) {
        return false;
    }
    EVENT_LOGD("end");
    return true;
}

Remote的代码如下所示:

PeerHolder::PeerHolder(const sptr<IRemoteObject> &object) : remoteObject_(object)
{}

sptr<IRemoteObject> PeerHolder::Remote()
{
    return remoteObject_;
}

继续查找remoteObject_的来源,可以知道CommonEventProxy有如下继承关系:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

IRemoteObject 有如下构造函数:

template <typename INTERFACE>
IRemoteProxy<INTERFACE>::IRemoteProxy(const sptr<IRemoteObject> &object) :
PeerHolder(object)
{
}

CommonEventProxy 有如下构造函数:

CommonEventProxy::CommonEventProxy(const sptr<IRemoteObject> &object) :
IRemoteProxy<ICommonEvent>(object)
{
    EVENT_LOGD("CommonEventProxy instance created");
}

继续查看CommonEventProxy,CommonEventProxy 的构造函数在文件 common_event.cpp 中:

bool CommonEvent::GetCommonEventProxy()
{
    EVENT_LOGI("enter");
    if (!commonEventProxy_ || !isProxyValid_) {
        std::lock_guard<std::mutex> lock(mutex_);
        if (!commonEventProxy_ || !isProxyValid_) {
            sptr<ISystemAbilityManager> systemAbilityManager = SystemAbilityManagerClient::GetInstance().GetSystemAbilityManager();
            if (!systemAbilityManager) {
                EVENT_LOGE("Failed to get system ability mgr.");
                return false;
            }
            sptr<IRemoteObject> remoteObject = systemAbilityManager -> GetSystemAbility(COMMON_EVENT_SERVICE_ID);
            if (!remoteObject) {
                EVENT_LOGE("Failed to get COMMON Event Manager.");
                return false;
            }
            commonEventProxy_ = iface_cast<ICommonEvent>(remoteObject);
            if ((!commonEventProxy_) || (!commonEventProxy_ -> AsObject())) {
                EVENT_LOGE("Failed to get COMMON Event Manager's proxy");
                return false;
            }
            recipient_ = new CommonEventDeathRecipient();
            if (!recipient_) {
                EVENT_LOGE("Failed to create death Recipient ptrCommonEventDeathRecipient!");
                return false;
            }
            commonEventProxy_ -> AsObject() -> AddDeathRecipient(recipient_);
        }
    }
    isProxyValid_ = true;
    return true;
}

可以看到在如下代码中,构造了CommonEvent 实例,但是isProxyValid_并不在锁保护的范围内,可能锁所保护的代码段会重入,从而导致崩溃。锁实际上需要保护的是commonEventProxy_和isProxyValid_,但实际上只保护了commonEventProxy_,存在重入的可能,因此使用锁的时候,需要明确具体到底需要保护的是什么数据。这里将isProxyvalid_加入到锁保护的范围内即可解决问题。

commonEventProxy_ = iface_cast(remoteObject); 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值