现象
调用realpath函数进程崩溃,可使用如下代码复现:
//test.cpp
#include "stdio.h"
#include "stdlib.h"
#include <limits.h>
int main()
{
char *p=new char[300];
realpath("1.txt",p);
printf("%s\n",p);
delete[] p;
return 0;
}
编译命令:g++ test.cpp -O1 -g
如果将O1改成O0则不复现,O2、O3都复现。说明这个BUG与gcc优化有关。
开发环境
ubuntu 22.04 x86_64虚拟机
gcc 11.4
笔者另有一个ubuntu 20.04 x86_64 + gcc 9.4的虚拟机也能复现。但在另外2个gcc 7.3的服务器上不能复现。猜测是gcc版本差异,9.4以上会出现。
问题原因
realpath的第二个参数需要至少PATH_MAX字节的空间才能保证内存安全,即使路径没有那么长。
您可以阅读参考资料获取realpath和PATH_MAX的详细用法。
解决方案
申请PATH_MAX字节的内存即可,该值在多数linux系统中定义为4096。
//test.cpp
#include "stdio.h"
#include "stdlib.h"
#include <limits.h>
int main()
{
char *p=new char[PATH_MAX];
realpath("1.txt",p);
printf("%s\n",p);
delete[] p;
return 0;
}
为什么O0不复现
realpath内部是否优化与上述优化参数无关,因为它由glibc实现。通常都是有优化的,除非你手动编译glibc并关闭优化。
crash堆栈在realpath内部,为了搞清同一个环境下优化参数为什么会影响realpath内部,我们对main函数进行反汇编。
下面是O0和O1的反汇编结果:
Dump of assembler code for function main():
0x00005555555551a9 <+0>: endbr64
0x00005555555551ad <+4>: push %rbp
0x00005555555551ae <+5>: mov %rsp,%rbp
0x00005555555551b1 <+8>: sub $0x10,%rsp
0x00005555555551b5 <+12>: mov $0x12c,%edi
0x00005555555551ba <+17>: callq 0x555555555080 <_Znam@plt>
0x00005555555551bf <+22>: mov %rax,-0x8(%rbp)
0x00005555555551c3 <+26>: mov -0x8(%rbp),%rax
0x00005555555551c7 <+30>: mov %rax,%rsi
0x00005555555551ca <+33>: lea 0xe33(%rip),%rax # 0x555555556004
0x00005555555551d1 <+40>: mov %rax,%rdi
=> 0x00005555555551d4 <+43>: callq 0x555555555090 <realpath@plt>
0x00005555555551d9 <+48>: mov -0x8(%rbp),%rax
0x00005555555551dd <+52>: mov %rax,%rdi
0x00005555555551e0 <+55>: callq 0x5555555550b0 <puts@plt>
0x00005555555551e5 <+60>: cmpq $0x0,-0x8(%rbp)
0x00005555555551ea <+65>: je 0x5555555551f8 <main()+79>
0x00005555555551ec <+67>: mov -0x8(%rbp),%rax
0x00005555555551f0 <+71>: mov %rax,%rdi
0x00005555555551f3 <+74>: callq 0x5555555550a0 <_ZdaPv@plt>
0x00005555555551f8 <+79>: mov $0x0,%eax
0x00005555555551fd <+84>: leaveq
0x00005555555551fe <+85>: retq
End of assembler dump.
Dump of assembler code for function main():
0x00005555555551a9 <+0>: endbr64
0x00005555555551ad <+4>: push %rbx
0x00005555555551ae <+5>: mov $0x12c,%edi
0x00005555555551b3 <+10>: callq 0x555555555080 <_Znam@plt>
0x00005555555551b8 <+15>: mov %rax,%rbx
0x00005555555551bb <+18>: mov $0x12c,%edx
0x00005555555551c0 <+23>: mov %rax,%rsi
0x00005555555551c3 <+26>: lea 0xe3a(%rip),%rdi # 0x555555556004
=> 0x00005555555551ca <+33>: callq 0x5555555550b0 <__realpath_chk@plt>
0x00005555555551cf <+38>: mov %rbx,%rdi
0x00005555555551d2 <+41>: callq 0x5555555550a0 <puts@plt>
0x00005555555551d7 <+46>: mov %rbx,%rdi
0x00005555555551da <+49>: callq 0x555555555090 <_ZdaPv@plt>
0x00005555555551df <+54>: mov $0x0,%eax
0x00005555555551e4 <+59>: pop %rbx
0x00005555555551e5 <+60>: retq
End of assembler dump.
可以看到优化后的代码调用了一个__realpath_chk函数,这个函数与realpath是不同的。
结论:开启优化后,编译器会用__realpath_chk代替realpath,这个函数的性能更好但容错更低,无论输出长度是多少都会使用PATH_MAX字节,长度不足会越界。
参考资料
realpath(3) - Linux man page (die.net)https://linux.die.net/man/3/realpathC语言MAX_PATH和PATH_MAX的区别-CSDN博客文章浏览阅读40次。MAX_PATH和PATH_MAX
https://blog.csdn.net/louObaichu/article/details/140496066
-------------完--------------