很久很久没有写过博客了, 正好最近园子又挣得一线生机, 必须得凑个热闹水一篇. 事情是这样的, 在今年的早些时候, 把公司的一部分api服务器的.net版本从6升级到了8, 毕竟6马上就是EOL了(.NET6 TLS 到2024年11月12日). 没成想在升级完的3个月后竟然触发了一个.NET8 runtime JIT 的BUG, 而且是在代码没有任何改动的情况下. 也是离奇他妈给离奇开门, 离奇到家了, 下面就给大家说说这个BUG发现和发生的过程.

我干了什么?

正如上面所说, 在今年的早些时候把一部分api服务器从NET6升级到了NET8.

出现了什么问题?

在所有升级到NET8的十几个是API项目(基于服务scope/流量等原因切分了大约有10几个项目, 但是公共的功能都是通过共享的lib发布的)中, 只有其中一个项目在某些时候的其中的某一个/或少数几个部署实例(AWS ECS Task)会一直报一个业务错误(出现的时间/个数等都不定). 表现的现象就是AES解密(这一部分是所有项目公共使用的)后的plaintext总是会丢失一部分字符. 而且这个实例一旦出现这个BUG, 后续所有的业务执行到这个AES解密的时候都会出现丢失字符.

初步的检查和怀疑?

review 代码过后没有发现问题, 单元测试等也都一直pass的, 所以对目前的逻辑代码实现的怀疑初步排除. 简化后的代码如下

        public int Decrypt(byte[] buffer, string key, string iv, out byte[] decryptedData)
{
int decryptedByteCount = 0;
decryptedData = new byte[buffer.Length]; using var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
aes.KeySize = 16;
aes.Padding = PaddingMode.Zeros; var instPwdArray = Encoding.ASCII.GetBytes(key);
var instSaltArray = Encoding.ASCII.GetBytes(iv); using (var decryptor = aes.CreateDecryptor(instPwdArray, instSaltArray))
{
using (var memoryStream = new MemoryStream(buffer))
{
using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
{
int read;
do
{
read = cryptoStream.Read(decryptedData, decryptedByteCount, decryptedData.Length - decryptedByteCount);
decryptedByteCount += read;
} while (read != 0);
}
}
}
while (decryptedData[decryptedByteCount - 1] == 0 && decryptedByteCount > 0)
{
decryptedByteCount--;
} return decryptedByteCount;
}

BUG具体的现象就是这个方法解密后返回明文的byte count会和预期的不一致, 即代码中的decryptedByteCount. 比如明文明明是10个字符, 结果返回的decryptedByteCount确是8. 调用方会使用这个decryptedByteCount来读取明文.

同时在线上出现这个BUG的时候(2次)采集到的host信息上有一个相似的特征, 那就是CPU都是AMD EPYC 系列CPU, 而恰好在这不久前我们在另外 .NET8 服务上发生了一个.NET8 GC 在AMD EPYC CPU的异常问题, 心想着该不会又是类似的问题把, 都已经准备给dotnet/rumtime team 提ticket了, 结果ticket写到一半, 线上又出现了这个问题, 而且采集到的host信息里面多了intel的CPU. 哦豁, 此路不通.

话说 AMD EPYC 的 BUG 前前后后也折腾了好几天, 可以考虑下次在水一篇, 就不再这个JIT BUG的博文中占用更多文字了.

行动: 增加debug代码进行线上调试?

在这个核心方法的更外层调用的地方增加检测代码, 如果检测到触发了这个BUG, 就执行这个方法的复制出来的另外一个方法(增加了更多的调试信息, 例如记录每个参数/变量的值等), 然后, 我们就信心满满的将增加了调试代码的版本发布到线上, 就等着这个BUG出现, 然后抓住它! 更是和客户保证一切尽在掌握, 在下班前就能修复这个BUG.

结果: 调试代码上线后, 这个BUG就再也没有被触发过了. 而我就再也没有下班了. 最后鉴于这个BUG影响范围不大, 而且又没有再次发生, 因此就此打住(毕竟研发资源还是很昂贵的)

2个月后的某一天, 与BUG再次不期而遇!

在即将要下班的档口, 同事找过来和说: "我的朋友(新疆口音), 我在我这边一个项目上调用了基础类库的AES解密, 遇到了一个丢失明文的问题, 你能帮我看看吗?", 这个BUG在他的项目上表现比在我这边还更离谱, 我这边只是解密后的明文丢失最后几个字符, 而他这边则是丢得值剩下一个字符, 就是不管啥密文(同一个规则,长度相同)解密后都只剩下一个字符(比如 AAA 加密后是BBB, 解密 BBB后就都返回"A"了, 即期待decryptedByteCount返回3, 结果都返回了1). 而且他这边的项目触发这个BUG更频繁. 上线10几分钟就能被触发. 和我的同事初步了解后得到了一个重要的线索, 那就是频繁执行的时候更容易触发BUG, 于是我和我的同事说 :"我的朋友(新疆口音), 这会儿我下班了, 你先回滚到上一个版本了. 我们明天继续"

紧跟着, 第二天深入调查, 终是找到了"它"

书接昨天, 昨天我们捕获了一个重要的信息, 那就是频繁的执行就频繁的触发这个BUG, 于是我们在本地写个死循环来调用这个AES解密方法, 果然不出所料, 短时间内, 执行超过大约1万次的时候, 这个BUG总是能触发. 而且随着测试的进行, 越来越多的信息被掌握. 例如: 这个BUG只在release模式下才能被触发, 核心方法的代码要一行不改才能触发. 显然, "它" 也越来越清楚的浮出了水面, 那就是和 runtime JIT 的编译优化有关系.

我们要知道dotnet的一个方法在运行多次之后, runtime会根据调用的时间/次数等信息认定一个方法是不是热点方法, 而进行一次或多次的编译优化, 以提供执行性能. 而我们在本地触发BUG前后做了dump, 从分析dump来看也证实了这个BUG确实是因为JIT二次优化造成的.

在BUG触发前, dump中这个方法的版本是 00007ffe03122e70 (QuickJitted), 而BUG出现后 这个方法的版本是 Current CodeAddr: 00007ffe03134620(OptimizedTier1)

> ip2md 00007FFE0312323A
MethodDesc: 00007ffe031de8d0
Method Name: [......].Decrypt(Byte[], System.String, System.String, Byte[] ByRef)
Class: 00007ffe031ef318
MethodTable: 00007ffe031de918
mdToken: 000000000600036B
Module: 00007ffe031dd740
IsJitted: yes
Current CodeAddr: 00007ffe03122e70
Version History:
ILCodeVersion: 0000000000000000
ReJIT ID: 0
IL Addr: 0000018f23a1c864
CodeAddr: 00007ffe03122e70 (QuickJitted)
NativeCodeVersion: 0000000000000000
> ip2md 00007FFE03134AE3
MethodDesc: 00007ffe031de8d0
Method Name: [......].Decrypt(Byte[], System.String, System.String, Byte[] ByRef)
Class: 00007ffe031ef318
MethodTable: 00007ffe031de918
mdToken: 000000000600036B
Module: 00007ffe031dd740
IsJitted: yes
Current CodeAddr: 00007ffe03134620
Version History:
ILCodeVersion: 0000000000000000
ReJIT ID: 0
IL Addr: 0000018f23a1c864
CodeAddr: 00007ffe03129cb0 (QuickJitted + Instrumented)
NativeCodeVersion: 0000018F0054A360
CodeAddr: 00007ffe03134620 (OptimizedTier1)
NativeCodeVersion: 0000018F02253050
CodeAddr: 00007ffe03122e70 (QuickJitted)
NativeCodeVersion: 0000000000000000

调查到这里, 我们毫不犹豫的把调查的信息发给了dotnet/runtime team, 等待他们的深入调查. 而我们也就此打住, 为什么打住呢? 实在是! 汇编代码一点都看不了! 看不了一点! 优化前 1千多行, 优化后 3千多. 在加之JIT的资料属实不多, 无从下手.

runtime team 也非常给力, 不出一个小时他们就重现了这个BUG, 并找到了根源

在等待runtime team将这个fix向后移植到NET8的时间内, 我们也要进行修复以避免触发这个BUG, 而修复方案也非常简单, 只要打破这个陷入BUG的语法即可, 最终我们将代码改为了如下结构

 using (var aes = Aes.Create())
{
aes.Mode = CipherMode.CBC;
......
}
while (decryptedData[decryptedByteCount - 1] == 0 && decryptedByteCount > 0)

后话

如果我们能采集/监控足够多的信息,并分析在BUG发生前后的变化, 也许能更快的找到问题的根源, 例如在这个例子中, 如果我们采集/监控了了JIT的分层编译事件,就很有可能能在更早的时间线上解决这个问题. 更可以引用AI来分析事件前后的差异,给出建议. 好期待这样的产品.

我被 .NET8 JIT 的一个BUG反复折磨了半年之久的更多相关文章

  1. 有人向我反馈了一个bug

    我是一个前端开发者,但我想这个故事对任何开发者都会引起共鸣的有人向你反馈了一个 bug. “26 楼会议室的灯亮着.它需要被熄灭.”bug 的备注里写道“你应该能在 5 分钟内搞定,只要按一下开关就好 ...

  2. 花了5天时间,终于解决了一个bug,心情非常愉快,憋了这么久,不吐不快

    http://www.cnweblog.com/fly2700/archive/2011/12/06/318916.html (转载) 花了5天时间,终于解决了一个bug,心情非常愉快,憋了这么久,不 ...

  3. 有人向你扔了一个bug,哈哈哈哈

    有人向你扔了一个bug. "26楼会议室的灯亮着.它应该是熄灭着的." bug的备注里写道"你应该能在5分钟内搞定,只要按一下开关就好了."你去了26楼的会议室 ...

  4. Tomcat一个BUG造成CLOSE_WAIT

    之前应该提过,我们线上架构整体重新架设了,应用层面使用的是Spring Boot,前段日子因为一些第三方的原因,略有些匆忙的提前开始线上的内测了.然后运维发现了个问题,服务器的HTTPS端口有大量的C ...

  5. MySQL关于exists的一个bug

    今天碰到一个很奇怪的问题,关于exists的, 第一个语句如下: SELECT ) FROM APPLY t WHERE EXISTS ( SELECT r.APPLY_ID FROM RECORD ...

  6. 由一个bug引发的SQLite缓存一致性探索

    问题 我们在生产环境中使用SQLite时中发现建表报“table xxx already exists”错误,但DB文件中并没有该表.后面才发现这个是SQLite在实现过程中的一个bug,而这个bug ...

  7. Win10系统菜单打不开问题的解决,难道是Win10的一个Bug ?

    Win10左下角菜单打不开,好痛苦,点击右下角的时间也没反应,各种不爽,折磨了我好几天,重装又不忍心,实在费劲,一堆开发环境要安装,上网找了很多方法都不适用.今天偶然解决了,仔细想了下,难道是Win1 ...

  8. 你可能不知道的 NaN 以及 underscore 1.8.3 _.isNaN 的一个 BUG

    这篇文章并不在我的 underscore 源码解读计划中,直到 @pod4g 同学回复了我的 issue(详见 https://github.com/hanzichi/underscore-analy ...

  9. 标准模板库(STL)的一个 bug

    今天敲代码的时候遇到 STL 的一个 bug,与 C++ 的类中的 const 成员变量有关.什么,明明提供了默认的构造函数和复制构造函数,竟然还要类提供赋值运算符重载.怎么会这样? 测试代码 Tes ...

  10. 是uibutton跟tableviewcell同步使用一个bug

    这个问题是uibutton跟tableviewcell同步使用一个bug,不关delay一点毛事,证据就是点击事件没问题,so,搜到一个方法解决了这个问题.uibutton分类symbian2+ios ...

随机推荐

  1. 多网卡系统下如何使用tcp协议实现MPI的分布式多机运行(mpi的实现使用openmpi)

    如题: 最近在看MPI方面的东西,主要是Python下的MPI4PY,学校有超算机房可以使用MPI,但是需要申请什么的比较麻烦,目的也本就是为了学习一下,所以就想着在自己的电脑上先配置一下. 现有硬件 ...

  2. 读论文《IMPALA: Scalable Distributed Deep-RL with Importance Weighted Actor-Learner Architectures》

    论文地址: https://arxiv.org/pdf/1802.01561v2.pdf 论文<IMPALA: Scalable Distributed Deep-RL with Importa ...

  3. 多线程之深入理解park与unpark

    1.背景 面试官问,如何暂停一个线程勒..... 说说你对park的理解....... 2.代码 package com.ldp.demo01; import com.common.MyThreadU ...

  4. .NET 屏幕录制

    窗口/屏幕截图适用于截图.批注等工具场景,时时获取窗口/屏幕图像数据流呢,下面讲下视频会议共享桌面.远程桌面这些场景是如何实现画面录制的. 常见的屏幕画面时时采集方案,主要有GDI.WGC.DXGI. ...

  5. 使用Django-Channels实现websocket通信+大模型对话

    前言 最近一直在做这个大模型项目,我选了 Django 作为框架(现在很多大模型应用都用的 FastAPI,不过我已经用习惯 Django 了) 之前使用 AspNetCore 作为后端的时候,我先后 ...

  6. VIM正则替换行尾

    https://nfcwar.lofter.com/post/1d1ee5d7_670890a 替换/删除某个字符后面的所有内容::%s/b.*/c/g   删除b后面所有的字符,以c替换.用g表示全 ...

  7. 网络/命令行抓包工具tcpdump详解

    概述 用简单的话来定义tcpdump,就是:dump the traffic on a network,根据使用者的定义对网络上的数据包进行截获的包分析工具. tcpdump可以将网络中传送的数据包的 ...

  8. cloud compare PCA插件开发详细步骤(二)附代码

    在上一节 https://blog.csdn.net/csy1021/article/details/141200135 我们已经完成了 具体开发前的准备工作,包括 各级 CMakelists.txt ...

  9. Excel 导入的开发经验

    2020 年开始接触 Java Excel 导入的开发工作. 一家建筑机器人的公司离职后,来到广州找的是一家工厂,开始接触导入的开发工作.之前也没有什么使用开发经验, 是 教学视频   中看到过有些老 ...

  10. 生产者消费者模式,以及基于BlockingQueue的快速实现

    生产者消费者模式,以及基于BlockingQueue的快速实现什么是生产者消费者模式,简单来说就是有两个角色,一个角色主要负责生产数据,一个角色主要负责消费(使用)数据.那么生产者直接依赖消费者,然后 ...