疑难问题定位案例复盘(三)

news2024/10/2 20:38:50

        今天我们分享一个数据库被异常改写的案例,通过该案例我们可以学习总结出常规的文件被改写问题定位思路。

问题现象

        1、测试环境在进行特定压力测试时发现页面登陆异常,且调试日志多个进程持续打印“数据库打开失败”日志。

        2、测试环境在进行多个压力测试时产生了coredump,每次产生coredump数量不止一个,且产生coredump的进程并不完全相同。

问题分析

        首先对所有产生的coredump进行分析,发现它们的较低层栈回溯完全相同,都是在调用lmdb数据库接口lmdb_get获取数据时,lmdb在查询数据判断一些管理数据结构,发现异常数据然后产生了abort信号杀死了进程并产生了coredump。所有的进程最后都挂在了这样的流程中,只是它们的上层调用业务不同、数据库数据的key不同,因此我们怀疑问题是由于数据库被改写破坏了数据库管理结构,导致所有使用lmdb数据库的进程在使用到该异常数据块时被lmdb流程判定异常并发出abort信号杀死了进程并产生coredump。观察同一设备产生的几个coredump产生时间,发现它们基本都是同时产生的,前后只相差几秒。另外,出现问题时对数据库的应用层调用接口lmdb_get的传参也正确。因此,我们更坚信是由于数据库本身被改写导致进程异常。

        为了确认在问题复现时数据库确实被改写了,我们在coredump的收集脚本中打包core文件时也收集lmdb数据库文件,这样我们就获得了问题数据库文件。在同一台设备相同环境下运行时也获取正常使用时的lmdb数据库文件。由于业务特点,该lmdb数据库保存的都是静态数据,我们所使用的数据库文件在初始化写入后将不再改写,后面全部是读数据库场景,这样同一台设备无论重启多少次,每次数据库记录的数据都应该相同。因此,我们通过BeyondCompare软件以二进制格式比较获取的同一设备的问题数据库文件和异常数据库文件,查看两者差异。

        经过比较分析,发现确实两个数据库文件存在差异,且差异项全部集中在文件的开头,差异项数据都被改写成8字节的“00000000 00000001”,如此反复,这带有较明显的特征。因此,我们可以确认数据库被改写了,且全部被改写成了多个8字节数据“00000000 00000001”。另外,我们阅读lmdb数据库源码,发现数据库的开头应该为一个管理数据结构,其中一些标志位明显异常,此时若需要打开数据库,则肯定会打开失败,这也符合问题现象1。

        针对数据库本身可能被改写,具体有2种场景:

  1. 由于lmdb数据库本身的特性,为了提高读数据库的效率,在具体使用时会将数据库文件通过mmap映射到进程的虚拟地址空间中。若在数据库映射空间的前面部分内存越界改写,可能破坏数据库前面的数据,从而出现问题。
  2. 由于数据库文件也是一个文件,它也有自己的句柄。若进程的某个模块误操作该句柄,写入一些数据则可能导致数据库被改写,出现数据库异常现象。

针对上述2种可能,我们分别进行分析并排查。

数据库映射时被越界访问

        由于数据库通过mmap映射到进程的虚拟地址空间中,在低位地址空间可能越界改写导致数据库异常。为了验证这种可能,我们采用本地模拟的方式进行复现。具体操作步骤如下:

  1. 创建一个独立的进程,调用lmdb_open通用接口打开数据库,并通过返回的数据库句柄信息获取数据库文件mdb.data映射到进程虚拟地址空间的起始地址pMmapAddr。
  2. 在独立进程中接着从pMmapAddr往前推0x20个字节开始通过memcpy写入0x50个字节“00000000 00000001”数据,实现对这种情况的模拟,结果模拟的进程产生了coredump。
  3. 经对该coredump解析,发现在对pMmapAddr进程memcpy操作时产生了段错误。因此,我们怀疑对数据库文件映射进的虚拟地址空间前面部分没有写权限。通过在原环境产生的coredump中利用info proc mappings命令可以看到数据库映射的位置,我们再在本地正常设备查看业务进程的/proc/pid/maps虚拟地址空间,确认数据库文件mdb.data映射到虚拟地址空间的前面部分确实是只读的,因此我们无权修改该部分内存。

经上面的验证,由于数据库文件mdb.data最前面被改写的部分是通过只读方式进行映射的,其前面的内存无法通过虚拟地址空间直接改写,因此排除了这种可能。

文件句柄误操作改写

        为了验证某个进程可能直接通过数据库句柄利用write系统调用误写入数据,我们采用了和上面类似的模拟方法。具体步骤如下:

  1. 创建一个独立的进程,调用lmdb_open通用接口打开数据库,并通过返回的数据库句柄信息获取数据库文件mdb.data的文件句柄fd。
  2. 在独立进程中通过write系统调用直接对数据库文件mdb.data从开头写入和问题现场相同数量的“00000000 00000001”数据。
  3. 在2的基础上再利用lmdb_open打开数据库文件失败,即复现了问题现象1。
  4. 在2的基础上再启动一个写数据库的进程保持运行不结束(不需要真的写数据库),对于2中的进程在通过write将异常数据写入数据库后再去读数据库,在和问题环境相同位置产生了abort信号,读进程立刻挂掉,即复现了问题现象2。而且获取本地复现的数据库文件,并和同设备正常数据库文件进行对比,确实改写了数据库文件mdb.data文件的开头部分,成功地将一系列“00000000 00000001”数据库写入到了数据库文件中,这也和测试环境复现的问题数据库文件保持一致。

通过上面的实验验证,我们可以确认是数据库被异常改写了,且较大概率是某个进程通过write系统调用直接写入异常数据导致。下一步就是需要确认是哪个进程什么场景下通过write改写了数据库文件。

定位根因

        在上面我们已知数据库文件被改写后的异常数据为一系列重复的“00000000 00000001”,虽然该数据带有一定的特征,但我们无法凭此找到“肇事者”。因此,为了定位根因我们还需要想起他办法。

        考虑到问题数据库只有在product_init进程启动时才会写入数据初始化,后面其他业务进程只会进行读操作,因此只要是非product_init进程对数据库文件的写入都可以认为是非法的。根据这个特征,我们去寻找那些写入数据库的非product_init进程。由于通过write系统调用必将走内核的一套写文件流程:write--->sys_write--->vfs_write...。因此,我们可以考虑修改内核中write相关代码,在写文件的必经途径vfs_write函数中添加打印,具体步骤如下:

  1. 首先根据句柄信息获取待写入文件名,然后判断待写入文件名是否为数据库文件“mdb.data”。若不是,则结束;否则继续下面的判断流程。
  2. 判断当前进程名是否为product_init。若是,则结束;否则说明当前进程在误写入数据库文件,其为“罪魁祸首”,继续下面的操作。
  3. 为了帮助我们获取更多的现场信息,通过调用do_send_signal函数给当前进程发送SIGSEGV信号杀死当前进程,并产生coredump。这样我们可以当问题复现时,通过分析coredump找到具体误写入数据库的进程以及调用流程,帮助我们更清晰地还原现场、定位根因。

        基于上述思路,编写临时版本给测试进行复现。在经过较长时间压力测试后问题终于复现,产生了coredump。通过解析该coredump,可以发现shdmain进程在调用write写入数据产生的coredump。通过x /wx命令打印待写入的内存地址,发现地址正常,因此怀疑产生coredump的原因是由于我们上面在write添加的调试流程导致,而不是真的产生了段错误。

        接下来,我们看到write需要写入的数据长度是8,而刚刚通过x /wx命令打印出的待写入内存的前8个字节数据刚好为“00000000 00000001”,这和问题复现被改写的数据库异常部分完全一致,进一步验证了猜想,即shdmain进程通过write系统调用改写了lmdb数据库破坏了数据库的管理结构,导致在使用时产生了多个coredump。

        虽然我们通过上面的调试打印找到了改写数据库的进程以及流程,但由于该流程是一个开源进程,肯定不会直接对数据库进行写入,大概率是其他流程误操作了数据库导致。那么问题来了,又是哪个模块导致开源线程没有写自己的文件,而误写入数据库文件呢?本来这一步定位挺棘手的,刚好另外一个模块的同学在定位问题时发现当前代码中存在重复关闭文件的问题,这给我带来了启发。我们可以假设一个场景,同一个进程下有A、B、C3个线程都需要操作文件,它们的操作分别如下:

  1. A线程打开了文件x获得了句柄9。
  2. A线程在使用完后关闭了文件x,释放了句柄9,此后发生了进程调度切换,开始执行B线程。
  3. B线程根据业务需要,打开了文件y。由于刚刚A线程已经释放了句柄9,同一个进程下不同线程共享文件资源,此时内核将句柄9分配给文件y。执行一段时间后发生了进程调度切换,又开始执行A线程。
  4. A线程此时由于编码错误,再次关闭了句柄9。此后又发生了进程调度切换,开始执行C线程。
  5. C线程由于业务需要,打开了lmdb数据库文件mdb.data。由于在步骤4中A线程第二次关闭了句柄9,此时内核将句柄9分配给了数据库文件mdb.data。此后又发生了进程调度切换,又开始执行B线程。
  6. B线程并不感知到句柄9已经被重新分配给数据库文件mdb.data,不再指向自身原有的文件y。B线程继续通过句柄9写入数据,其实际即写入了数据库文件mdb.data,从而改写了数据库文件,造成数据库异常。

经过上面的详细分析,我们基本可以还原数据库文件被改写的流程。根据其他模块同学的定位结论,目前代码中存在的句柄重复关闭场景如下:

在上面的实例代码中,我们首先通过open获取了文件夹句柄iFd,然后又通过iFd调用fdopendir获得了目录流。注意,此时iFd和pstDirFile指向同一个文件file结构体。处理结束后,调用closedir和close先后关闭了pstDirFile和iFd。由于两者指向的是同一个文件file结构体,因此该句柄关闭了两次,这有可能异常关闭了其他文件,导致数据异常。因此,我们只需要调用closedir和close其中一个即可完成关闭文件的操作

还有另外一组文件接口也可能存在类似的问题:open和fdopen。

当我们使用fdopen完成文件句柄转化为高级文件流FILE *时,在使用完毕后也只需要选择fclose和close其中之一完成文件的关闭即可,不能重复调用,否则会重复关闭句柄,出现异常

        当我们梳理所有使用fdopendir/fdopen接口的场景,完成修改所有重复关闭句柄的错误代码后,再进行多次的压力测试尝试复现,运行多天后仍然未复现,说明问题即为上述的因fdopendir/fdopen错误使用出现重复关闭句柄导致数据库异常。

        其实为了找到异常关闭数据库的具体位置,我们也可以继续仿照查找改写数据库位置的流程,在内核中关闭文件的接口close的必经流程中添加特殊处理。若关闭的文件是数据库文件mdb.data,且当前进程不是product_init,则说明数据库文件被异常关闭,此时发送SIGSEGV信号杀死进程并产生coredump。当问题复现后,我们分析coredump即可找到数据库文件具体被关闭的流程,从而找出问题根源

问题总结

        fdopendir/fdopen接口返回的目录流DIR *和文件流FILE *和open接口返回的句柄是指向的同一个文件file结构体,因此我们在使用fdopendir/fdopen接口完后,只需要选择close和closedir/fclose其中一个即可完成文件的关闭。若我们同时调用两类接口关闭文件将出现重复关闭句柄的现象,有可能关闭了其他正在使用的文件,造成数据异常。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/484545.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Unity教程||Unity添加中文字体||Unity知识记录--制作UI粒子特效

Unity添加中文字体 ## 1、拷贝字体文件 拷贝C:\Windows\Fonts文件夹下,华文细黑常规文件到项目中 ## 2、下载中文字库 链接: https://pan.baidu.com/s/1KW31KB5vEImZHUWvQ9PLEQ 提取码: bgug 3、添加字体字库 选择Window->TextMeshPro->Font Asset Crea…

HTML的基础语法

文章目录 前言一.HTML结构1.1 什么是html语言1.2 html的结构是什么 二.HTML常见标签2.1 文本格式标签标题标签段落标签换行标签格式化标签图片标签超链接标签 2.2 表格标签2.3 表单标签from标签input标签文本框密码框单选框复选框普通按钮提交按钮清空按钮选择文件select标签te…

延迟队列与SpringBoot实战

延迟队列与SpringBoot实战 概念 延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列 …

基于jQuery------购物车案例

目录 基于jQuery------购物车案例 案例:购物车案例模块-增减商品数量分析 案例:购物车案例模块-修改商品小计分析 案例:购物车案例模块-计算总计和总额 案例:购物车案例模块-删除商品模块 案例:购物车案例模块-选…

从‘discover.partitions‘=‘true‘分析Hive的TBLPROPERTIES

从’discover.partitions’true’分析Hive的TBLPROPERTIES 前言 Hive3.1.2先建表: show databases ;use db_lzy;show tables ;create external table if not exists test_external_20230502(id int,comment1 string,comment2 string ) stored as parquet ;creat…

C语言通过控制台命令行传入参数

Linux 与 windows运行c语言程序 切换到对应目录下 1. gcc hello.c -o hello 2.Linux: ./hello Windows: hello.exe int main(){}默认无参数 但在一些情况下想要直接通过在上述过程中第二步就传入参数而不是使用scanf..之类的输入语句就需要使用有参数的main方法: int main() {…

Docker--harbor私有库部署与管理

目录 一、本地私有仓库 搭建本地私有仓库 Docker容器的重启策略 二、Harbor 1、什么是Harbor 2、Harbor特性 3、Harbor的构成 三、Harbor部署 实验步骤 1、安装Docker-Compose服务 2、部署Harbor服务 1、下载或上传Harbor安装程序 2、修改Harbor安装的配置文件 3、…

基于TI板MSP430 玩转PID

文章目录 前言一、整体框架二、PID算法1. 位置式PID2. 增量式PID3. 比例外置式PID4. 积分限幅、输出限幅和PID参数整定5. 位置式PID和增量式PID的区别及抉择 三、初值获取1. 定时器输入捕获2. 外部中断3. ADC采样 前言 具体啥是PID,我这里不做介绍,网上…

SpringMVC(后)SSM整合

10、文件上传和下载 10.1、文件下载 ResponseEntity用于控制器方法的返回值类型&#xff0c;该控制器方法的返回值就是响应到浏览器的响应报文 使用ResponseEntity实现下载文件的功能 RequestMapping("/testDown") public ResponseEntity<byte[]> testResp…

【Hello Algorithm】复杂度 二分法

作者&#xff1a;小萌新 专栏&#xff1a;算法 作者简介&#xff1a;大二学生 希望能和大家一起进步 本篇博客简介&#xff1a;介绍算法的复杂度 对数器和二分法 复杂度 对数器 二分法 复杂度常数时间操作非常数时间操作时间复杂度空间复杂度 二分法有序数组中找一个值寻找有序…

树的存储和遍历

文章目录 6.5 树与森林6.5.1 树的存储结构1. 双亲表示法(顺序存储结构)2 孩子链表表示法3 孩子兄弟表示法(二叉树表示法) 6.5.2 森林与二叉树的转换1 树转换成二叉树2 二叉树转换成树3 森林转换成二叉树4 二叉树转换成森林 6.5.3 树和森林的遍历1. 树的遍历2. 森林的遍历 6.6 赫…

数据库篇:表设计、创建编辑以及导出导入数据

微信小程序云开发实战-答题积分赛小程序系列 数据库篇:表设计、添加编辑以及导出导入数据 原型: 最终实现界面截图:

Moqui REST API的两种实现方法

实现Restful API的方法 实现REST API有两种方法。第一种&#xff1a; The main tool for building a REST API based on internal services and entity operations is to define resource paths in a Service REST API XML file such as the moqui.rest.xml file in moqui-fr…

chatGPT国内可用镜像源地址

chatGPT国内可用镜像源地址 彷丶徨丶 关注 IP属地: 湖北 0.811 2023.03.15 16:02:16 字数 1,152 阅读 249,582 如果你正在尝试访问Chatgpt网站&#xff0c;但由于某些原因无法访问该网站&#xff0c;那么你可以尝试使用Chatgpt的国内镜像网站。以下是一些Chatgpt国内镜像网站的…

java基础知识——27.动态代理

这篇文章&#xff0c;我们来学一下java的动态代理 目录 1.动态代理的介绍 2.具体的代码实现 1.动态代理的介绍 动态代理&#xff1a;无侵入式的额外给代码增加功能 很不好理解&#xff0c;下面&#xff0c;我们通过两个例子来说明一下什么是动态代理&#xff1a; 例一&a…

shell编程 -- 基础

shell是一个命令行解释器&#xff0c;它接收应用程序/用户命令&#xff0c;然后调用操作系统内核。 linux笔记 链接&#xff1a;https://pan.baidu.com/s/16GZCPfUTRzUqIyGnYwPuUg?pwds5xt 提取码&#xff1a;s5xt 脚本执行 采用bash或者sh脚本的相对路径或绝对路径&#x…

TikTok跨境电商如何选品和营销?

鑫优尚电子商务&#xff1a;TikTok目前发展飞速&#xff0c;全球的MAU是5.6亿。现在作为全球炙手可热的短视频平台&#xff0c;全球流量相当庞大&#xff0c;覆盖75个语种、全球150个国家和地区。 对于从事跨境电商行业的人来说&#xff0c;又怎能错过一个流量这么好的平台呢&a…

ChatGPT注册详细步骤教程-ChatGPT申请教程

注册chatGPT账号的详细经验教程 注册ChatGPT账号是使用这一自然语言生成技术的关键步骤。下面是注册ChatGPT账号的详细经验教程&#xff1a; 访问OpenAI注册页面 在Web浏览器中打开OpenAI注册页面。 2.输入个人信息 在注册页面上&#xff0c;您需要提供以下个人信息&#…

树莓派 二维云台调零控制

目录 舵机的工作原理 案例程序 要求&#xff1a; 程序&#xff1a; 二维云台是通过IIC总线进行控制的&#xff0c;我们可以通过窗口命令输入&#xff1a;i2cdetect -y 1来检测IIC总线是否连接正常。 当有40显示的时候就说明IIC总线正常。 操控舵机我们需要一个PCA9685的模…

【移动端网页布局】流式布局案例 ④ ( Banner 栏制作 | 固定定位 | 标准流 | 百分比宽度设置 )

文章目录 一、Banner 栏样式及核心要点1、实现效果2、核心要点分析 二、完整代码示例1、HTML 标签结构2、CSS 样式3、展示效果 一、Banner 栏样式及核心要点 1、实现效果 在上一篇博客中 , 实现了 搜索栏 , 在本篇博客开始实现 搜索栏 下方的 Banner 栏 ; 2、核心要点分析 Bann…