现象
容器中有一个文件 agreement.js
发现其内容有时候存在对于 UTF8 来说非法的字符,导致App启动闪退。
打开这个文件可以看到以下位置存在乱码:
分析乱码来源
可以看到乱码位置前一个字符虽然能够显示,但是也不正常,选乱码位置及其前一个字符,中使用Notepad++的ASCII转换HEX功能:
把这些乱码位置全部都转换后得到这样一个结果:
可以看到共同点都是多了一个hex 93
,把所有93
去掉,
然后使用Notepad++ HEX转换ASCII功能:
发现结果正常:
可以得到结论,就是文件在某些位置被插入了Hex 93
(对应ASCII 147
)字节,导致文件渲染异常。
分析乱码位置是否有规律
因为这个文件实际上是通过环境变量 + envsubst
渲染模板文件形成的,而这段文本就是来源于一个环境变量。
把 agreement.js
中的这段文本截取出来,然后用16进制编辑器看看出现 93
的位置上是否存在共同点。
Hex | offset | delta | delta - 1 | 512 x |
---|---|---|---|---|
E8 AE 93 AE |
1535 | 0 | ||
E7 B3 93 BB |
3072 | 1537 | 1536 | 3 |
E7 94 93 A8 |
3585 | 513 | 512 | 1 |
E5 9D 93 8F |
4098 | 513 | 512 | 1 |
E5 BE 93 97 |
4611 | 513 | 512 | 1 |
E6 9C 93 AF |
5636 | 1025 | 1024 | 2 |
E6 8A 93 A5 |
6149 | 513 | 512 | 1 |
可以看见首次出现 Hex 93
的位置,以及两个 Hex 93
offset 相差的是 512 字节的倍数,
更新的脚本
开发人员后来调整了脚本,得到的文件不一样了,但是依然存在相同问题:
通过同样的方法,发现被插入的字节是 Hex B5
(对应ASCII 181
),而且插入位置同样是 512 的倍数:
Hex | offset | delta | delta - 1 | 512 x |
---|---|---|---|---|
EF BC B5 9A |
2560 | 0 | ||
E6 9D B5 83 |
3585 | 1025 | 1024 | 2 |
E5 9E B5 8B |
7170 | 3585 | 3584 | 7 |
E4 BE B5 9B |
7683 | 513 | 512 | 1 |
E6 AD B5 A4 |
8196 | 513 | 512 | 1 |
E4 BD B5 93 |
11269 | 3073 | 3072 | 6 |
E6 BF B5 80 |
15878 | 4609 | 4608 | 9 |
E5 B0 B5 86 |
16391 | 513 | 512 | 1 |
而更新后的脚本(简化后)是:
|
|
环境变量 FOO
是容器运行时外部给的,脚本先把值写到 foo-1.txt
,然后执行一些脚本,更新了这个环境变量,然后把结果写到 foo-2.txt
,而问题就出在 foo-2.txt
上,说明问题出在第二行。
插入字节的位置
回顾本问题,本问题是有一定概率会出现,而不是每次都会出现,因此使用Hex Friend(Mac上的软件)打开“好的文件”和几个“坏的文件”,对比插入的字节的位置有何不同。
和第一个坏的文件对比,可以看到字节插入位置,以及插入的值是0x82
:
和第二个坏的文件对比,可以看到字节插入位置,以及插入的值是0xB9
:
可以发现:
- 只要出现问题,那么插入的字节位置是固定的
- 每次出现问题所插入的字节是随机的,但是对同一个文件来说这个值是相同的。
推理
先总结一下:
- 环境变量值存在汉字,而且长度超出了 512 字节,
- 有一定概率会出现脚本执行结果文件里,插入了随机字节
- 随机字节插入的位置是固定的
- 出现问题的文件里,随机字节的值是相同的
- 不同的问题文件,随机字节的值不相同
那么推理:
- 汉字 UTF-8 编码占用 3 个字节,在脚本执行过程是 512 字节 一块一块读取的,那么就会有概率正好截断在汉字 UTF-8 3 字节的中间,导致了这个问题。
到这里可以先排除gojq
的问题,理由有二:
- 和开发人员沟通,脚本之前使用的是
jq
,而现在使用的是gojq
,不同工具,遇到相同问题,两个工具都有BUG的概率不大。 - 另外,经测试
nginx:1.21.5-alpine
会发生问题,而nginx:1.21.5
不会发生问题,而且gojq
这个工具都是同一个二进制,因此gojq
有BUG可能性排除。
测试脚本
测试脚本在这里。
因为这个问题出现存在一定概率,因此写一个测试脚本循环跑以下几个测试:
1)测试变量 output 到文件,是否会损坏(md5sum检查):
|
|
2)测试 |
管道符读取变量时,是否会损坏(md5sum 字符模式):
|
|
3)测试 |
管道符读取变量时,是否会损坏(md5sum 二进制模式):
|
|
4)测试 gojq
结果赋予新变量,新变量输出到文件,是否损坏(md5检查):
|
|
5)测试 gojq
结果赋予新变量,直接检查新变量值,是否损坏(md5检查):
|
|
6)测试 gojq
结果赋予新变量,新变量输出到文件,是否损坏(md5检查),使用double quote 变量的方式:
|
|
7)测试 gojq
结果赋予新变量,直接检查新变量值,是否损坏(md5检查),使用double quote 变量的方式:
|
|
8)测试 gojq
,跳过变量赋值,直接检查结果是否损坏(md5检查):
|
|
脚本执行结果(例子):
|
|
运行一段时间后,发现只要出错,case 4, 5, 6, 7 是一起出错的。
- 4、5 和 6、7 的差别在于是否使用了 double quote 变量的方式 ,都有错说明 double quote 变量不能解决这个问题
- 4、6 和 5、7 的差别在于是输出到文件检查,还是直接检查变量,都有错说明 输出文件不是关键,而是变量的值本身就损坏了
同时注意到,case 8 虽然在逻辑上和 case 4、5、6、7 一样,但是却不出错。两者的区别在于是否使用新变量来接收gojq
的返回值。
这就说明,问题出在新变量赋值上。
所以问题出在,在bash脚本中给一个变量赋值一个很长的包含中文的值,就会有概率出错。
而且测试脚本的执行方式有两种:
- 循环创建容器执行测试脚本
- 创建单个容器,脚本内循环执行
发现,前一种方式,概率出现问题。而后一种方式如果一开始没有问题,就一直没有问题,如果一开始有问题,就一直有问题。
测试脚本2
现在有了一个强有力的推测:在bash脚本中给一个变量赋值一个很长的包含中文的值,就会有概率出错。
那么就新写一个测试脚本,这里。
也不需要gojq
了,直接把一个文件的内容赋值给一个变量:
|
|
测试结果:
|
|
到这里就可以证实这个猜测了。
后续跟踪
到GNU Bash的邮件列表中搜索到这个BUG:
- Corrupted multibyte characters in command substitutions,符合我们遇到的问题,提到在 5.1.16 版本里修复了这个问题
- Long variable value get corrupted sometimes,我这里也提了一个issue(提之前没有好好搜索)
- 关于这个 BUG 的 patch 见 这里、这里 和 这里
- 我在 Alpine Linux 提议更新 Bash 版本,见 issue
- 我在 Debian 邮件列表 提议更新 Bash 版本,见这里,Debian Bullseye 关于这个问题的 Bug Report
评论