• 【持续更新】C/C++ 踩坑记录(一)


    未定义行为之 NULL dereference

    下面这段代码中 is_valid() 解引用了空指针 str,我们的直觉是编译运行后将迎来 SIGSEGV,然而事情并非所期望的那样。

    /*
    * ub_null.c - 未定义行为演示 之 NULL dereference
    */
    #include
    #include
    int is_valid(const char *str)
    {
    if(*str == 0x80) return 1;
    if(str == NULL) return 0;
    return strcmp(str, "expected string") == 0;
    }
    int main(void)
    {
    const char *str = NULL;
    printf("%d\n", is_valid(str));
    return 0;
    }
    lyazj@HelloWorld:~$ gcc --version
    gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0
    Copyright (C) 2021 Free Software Foundation, Inc.
    This is free software; see the source for copying conditions.  There is NO
    warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    
    lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O0
    ub_null.c: In function ‘is_valid’:
    ub_null.c:6:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
        6 |   if(*str == 0x80) return 0;
          |           ^~
    lyazj@HelloWorld:~$ ./ub_null
    0

    结合 GCC 发出的警告,不难推断出条件表达式 *str == 0x80 在编译期被求值且相应的 if 语句被优化掉了,而且这是在 O0 的优化等级下。以下的反汇编结果验证了这一点。

    lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null
    
    ub_null:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    0000000000001169 :
        1169:	f3 0f 1e fa          	endbr64 
        116d:	55                   	push   %rbp
        116e:	48 89 e5             	mov    %rsp,%rbp
        1171:	48 83 ec 10          	sub    $0x10,%rsp
        1175:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
        1179:	48 83 7d f8 00       	cmpq   $0x0,-0x8(%rbp)
        117e:	75 07                	jne    1187 
        1180:	b8 00 00 00 00       	mov    $0x0,%eax
        1185:	eb 1e                	jmp    11a5 
        1187:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
        118b:	48 8d 15 72 0e 00 00 	lea    0xe72(%rip),%rdx        # 2004 <_IO_stdin_used+0x4>
        1192:	48 89 d6             	mov    %rdx,%rsi
        1195:	48 89 c7             	mov    %rax,%rdi
        1198:	e8 d3 fe ff ff       	call   1070 
        119d:	85 c0                	test   %eax,%eax
        119f:	0f 94 c0             	sete   %al
        11a2:	0f b6 c0             	movzbl %al,%eax
        11a5:	c9                   	leave  
        11a6:	c3                   	ret    
    

    我们在同一环境对 O3 优化等级做相同的实验,得到了相同的结果:

    lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O3
    ub_null.c: In function ‘is_valid’:
    ub_null.c:6:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
        6 |   if(*str == 0x80) return 0;
          |           ^~
    lyazj@HelloWorld:~$ ./ub_null
    0
    lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null
    
    ub_null:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    00000000000011a0 :
        11a0:	f3 0f 1e fa          	endbr64 
        11a4:	48 85 ff             	test   %rdi,%rdi
        11a7:	74 27                	je     11d0 
        11a9:	48 83 ec 08          	sub    $0x8,%rsp
        11ad:	48 8d 35 50 0e 00 00 	lea    0xe50(%rip),%rsi        # 2004 <_IO_stdin_used+0x4>
        11b4:	e8 a7 fe ff ff       	call   1060 
        11b9:	85 c0                	test   %eax,%eax
        11bb:	0f 94 c0             	sete   %al
        11be:	48 83 c4 08          	add    $0x8,%rsp
        11c2:	0f b6 c0             	movzbl %al,%eax
        11c5:	c3                   	ret    
        11c6:	66 2e 0f 1f 84 00 00 	cs nopw 0x0(%rax,%rax,1)
        11cd:	00 00 00 
        11d0:	31 c0                	xor    %eax,%eax
        11d2:	c3                   	ret    
    

    接下来我们用下面的两行代码替换被优化掉的 if 语句,看看会发生什么:

    char head = *str;
    if(head == 0x80) return 0;
    lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O0
    ub_null.c: In function ‘is_valid’:
    ub_null.c:10:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
       10 |   if(head == 0x80) return 0;
          |           ^~
    lyazj@HelloWorld:~$ ./ub_null 
    Segmentation fault
    lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null
    
    ub_null:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    0000000000001169 :
        1169:	f3 0f 1e fa          	endbr64 
        116d:	55                   	push   %rbp
        116e:	48 89 e5             	mov    %rsp,%rbp
        1171:	48 83 ec 20          	sub    $0x20,%rsp
        1175:	48 89 7d e8          	mov    %rdi,-0x18(%rbp)
        1179:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
        117d:	0f b6 00             	movzbl (%rax),%eax
        1180:	88 45 ff             	mov    %al,-0x1(%rbp)
        1183:	48 83 7d e8 00       	cmpq   $0x0,-0x18(%rbp)
        1188:	75 07                	jne    1191 
        118a:	b8 00 00 00 00       	mov    $0x0,%eax
        118f:	eb 1e                	jmp    11af 
        1191:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
        1195:	48 8d 15 68 0e 00 00 	lea    0xe68(%rip),%rdx        # 2004 <_IO_stdin_used+0x4>
        119c:	48 89 d6             	mov    %rdx,%rsi
        119f:	48 89 c7             	mov    %rax,%rdi
        11a2:	e8 c9 fe ff ff       	call   1070 
        11a7:	85 c0                	test   %eax,%eax
        11a9:	0f 94 c0             	sete   %al
        11ac:	0f b6 c0             	movzbl %al,%eax
        11af:	c9                   	leave  
        11b0:	c3                   	ret    
    

    段错误如愿以偿地发生了,且是来自读取 str 处 1 字节并进行零扩展的 movzbl 指令,之前看到的编译期求值没有再次发生。

    现在升高优化等级至 Og,编译期求值并优化掉第一个 if 语句的特效回归了:

    lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -Og
    ub_null.c: In function ‘is_valid’:
    ub_null.c:7:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
        7 |   if(head == 0x80) return 0;
          |           ^~
    lyazj@HelloWorld:~$ ./ub_null
    0
    lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null
    
    ub_null:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    0000000000001169 :
        1169:	f3 0f 1e fa          	endbr64 
        116d:	48 85 ff             	test   %rdi,%rdi
        1170:	74 1d                	je     118f 
        1172:	48 83 ec 08          	sub    $0x8,%rsp
        1176:	48 8d 35 87 0e 00 00 	lea    0xe87(%rip),%rsi        # 2004 <_IO_stdin_used+0x4>
        117d:	e8 de fe ff ff       	call   1060 
        1182:	85 c0                	test   %eax,%eax
        1184:	0f 94 c0             	sete   %al
        1187:	0f b6 c0             	movzbl %al,%eax
        118a:	48 83 c4 08          	add    $0x8,%rsp
        118e:	c3                   	ret    
        118f:	b8 00 00 00 00       	mov    $0x0,%eax
        1194:	c3                   	ret    
    

    GCC 如何优化,除取决于编译选项外,同样取决于程序员编写什么样的源代码,这一点不足为奇。然而,当优化等级升至 O2 时,更为不好的事情发生了:

    lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O2
    ub_null.c: In function ‘is_valid’:
    ub_null.c:7:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
        7 |   if(head == 0x80) return 0;
          |           ^~
    In function ‘is_valid’,
        inlined from ‘main’ at ub_null.c:15:3:
    ub_null.c:9:10: warning: argument 1 null where non-null expected [-Wnonnull]
        9 |   return strcmp(str, "expected string") == 0;
          |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    In file included from ub_null.c:2:
    ub_null.c: In function ‘main’:
    /usr/include/string.h:156:12: note: in a call to function ‘strcmp’ declared ‘nonnull’
      156 | extern int strcmp (const char *__s1, const char *__s2)
          |            ^~~~~~
    lyazj@HelloWorld:~$ ./ub_null
    Segmentation fault
    lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null
    
    ub_null:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    00000000000011b0 :
        11b0:	f3 0f 1e fa          	endbr64 
        11b4:	48 83 ec 08          	sub    $0x8,%rsp
        11b8:	48 8d 35 45 0e 00 00 	lea    0xe45(%rip),%rsi        # 2004 <_IO_stdin_used+0x4>
        11bf:	e8 9c fe ff ff       	call   1060 
        11c4:	85 c0                	test   %eax,%eax
        11c6:	0f 94 c0             	sete   %al
        11c9:	48 83 c4 08          	add    $0x8,%rsp
        11cd:	0f b6 c0             	movzbl %al,%eax
        11d0:	c3                   	ret    
    

    值得注意的是,现在段错误来自 strcmp() 中的 NULL dereference,且 is_valid() 的反汇编出奇地简单,GCC 同时干掉了两个 if 语句!因为我们首先访问了 str 处的 1 字节,由于 NULL dereference 是典型的 UB,编译器便假定了 str != NULL,这样第二个 if 语句也可以被优化掉!现在,我们产生了具有严重漏洞的 is_valid() 函数,当 str == NULL 时,程序将发生严重错误。

    解决 bug 的方法是显然的,即将 head 转换为 unsigned char 再比较,并调换两个 if 语句的顺序。NULL dereference,这是一个曾经让 Linux 内核爆出严重漏洞的 UB,我们刚刚成功复现了这一过程。诚然,此处的程序是异常简单的,看出两个写反的 if 语句非常容易;但对于代码业务特别复杂的场景,特别是对一行代码需要数行注释的底层代码或其它核心代码而言,这个 bug 可能一致遗留下来,并成为一个长期休眠的不定时炸弹。它的出现让许多可能是至关重要的代码段,如第二个 if 语句失效,可能给程序使用者造成难以预计的后果。还好当前版本的 GCC 友好地给出了应有的警告,这也再次向我们证明,随意地忽略编译器给出的 Warning 是不可取的。

    Google 等团队开发的 sanitizer 已经集成到了当前版本的 GCC 中,让我们用 sanitizer 更为有效地发现上述未定义行为:

    lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O2 -fsanitize=undefined -fno-sanitize-recover=all
    ub_null.c: In function ‘is_valid’:
    ub_null.c:7:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
        7 |   if(head == 0x80) return 0;
          |           ^~
    lyazj@HelloWorld:~$ ./ub_null
    ub_null.c:6:8: runtime error: load of null pointer of type 'const char'
    

    看到这里,是不是很轻松就发现了两个 if 语句写反的问题?

  • 相关阅读:
    工作小计 zookpeer3.8 C api环境搭建
    扭矩传感器信号模拟地、数据地与电源地
    大模型应用开发:手把手教你部署并使用清华智谱GLM大模型
    十年架构五年生活-06 离职的冲动
    Zabbix监控系统 第一部分:zabbix服务部署+自定义监控项+自动发现与自动注册(附详细部署实例)
    【JavaScript复习十八】预解析
    js 日期时间sort排序
    .NET数据交互之生成和读取YAML文件
    python数据类型
    vue 城市选择器的使用 element-china-area-data
  • 原文地址:https://www.cnblogs.com/lyazj/p/17576220.html