深入分析HWASAN检测内存错误原理

2023年 7月 31日 44.0k 0

导语:ASAN(AddressSanitizer) 是 C/C++开发者常用的内存错误检测工具,主要用于检测缓冲区溢出、访问已释放的内存等内存错误。 AArch64 上提供了 Top-Byte-Ingore 硬件特性,HWASan(HardWare-assisted AddressSanitizer) 就是利用 Top-Byte-Ignore 特性实现的增强版 ASan,与 ASAN 相比 HWASan 的内存开销更低,检测到的内存错误范围更大。因此在 AArch64 平台,建议使用 HWASAN。本篇文章将深入分析 HWASAN 检测内存错误的原理,帮助大家更好地理解和使用 HWASan 来排查程序中存在的疑难内存错误。

前言

在字节跳动,C++语言被广泛应用在各个业务中,由于C++语言的特性,导致 C++ 程序很容易出现内存问题。ASAN 等内存检测工具在字节跳动内部已经取得了可观的收益和效果(更多内容请查看:Sanitizer 在字节跳动 C C++ 业务中的实践),服务于60个业务线,近一年协助修复上百个内存缺陷。但是仍然有很大的提升空间,特别是在性能开销方面。随着 ARM 进入服务器芯片市场,ARM架构下的一些硬件特性可以用来缓解 ASAN 工具的性能问题,利用这些硬件特性研发的 HWASAN 检测工具在超大型 C++ 服务上的检测能力还有待确认。

为此,STE 团队对 HWASAN 进行了深入分析,并在字节跳动 C++ 核心服务上进行了落地验证。在落地 HWASAN 过程中,修复了 HWASAN 实现中的一些关键 bug,并对易用性进行了提升。相关 patch 已经贡献到LLVM开源社区(详情请查看文末链接)。本篇文章将深入分析 HWASAN 检测内存错误的原理,帮助大家更好地理解和使用 HWASan 来排查程序中存在的疑难内存错误。

概述

HWASAN: HardWare-assisted AddressSanitizer, a tool similar to AddressSanitizer, but based on partial hardware assistance and consumes much less memory.

这里所谓的 "partial hardware assistance" 就是指 AArch64 的 TBI (Top Byte Ignore) 特性。

TBI (Top Byte Ignore) feature of AArch64: bits [63:56] are ignored in address translation and can be used to store a tag.

以如下代码举例,Linux/AArch64 下将指针 x 的 top byte 设置为 0xfe,不影响程序执行:

// $ cat tbi.cpp
int main(int argc, char **argv) {
  int * volatile x = (int *)malloc(sizeof(int));
  *x = 666;
  printf("address: %p, value: %dn", x, *x);
  x = reinterpret_cast(reinterpret_cast(x) | (0xfeULL 0xec2bfffe0000: 69  69 [08] 00  00  00  00  00  00  00  00  00  00  00  00  00
  0xec2bfffe0100: 00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
  0xec2bfffe0200: 00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
  0xec2bfffe0300: 00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
  0xec2bfffe0400: 00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
  0xec2bfffe0500: 00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
  0xec2bfffe0600: 00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
  0xec2bfffe0700: 00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
  0xec2bfffe0800: 00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
Tags for short granules around the buggy address (one tag corresponds to 16 bytes):
  0xec2bfffdff00: ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..
=>0xec2bfffe0000: ..  .. [69] ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..
  0xec2bfffe0100: ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..
See https://clang.llvm.org/docs/HardwareAssistedAddressSanitizerDesign.html#short-granules for a description of short granule tags
Registers where the failure occurred (pc 0xaaad830db1a4):
    x0  a100ffffc4201580  x1  6900ec2bfffe0028  x2  0000000000000000  x3  0000000000000000
    x4  0000000000000020  x5  0000000000000000  x6  0000000000100000  x7  fffffffffff00005
    x8  6900ec2bfffe0000  x9  6900ec2bfffe0000  x10 0030f15d14c79f97  x11 00ffffffffffffff
    x12 00001f0d780b69d2  x13 0000000000000001  x14 0000ffffc4200b60  x15 0000000000000696
    x16 0000aaad830a3540  x17 000000000000000b  x18 0000000000000100  x19 0000aaad830db600
    x20 0200effd00000000  x21 0000aaad830907f0  x22 0000000000000000  x23 0000000000000000
    x24 0000000000000000  x25 0000000000000000  x26 0000000000000000  x27 0000000000000000
    x28 0000000000000000  x29 0000ffffc4201590  x30 0000aaad830db1a8   sp 0000ffffc4201550
SUMMARY: HWAddressSanitizer: tag-mismatch ./test.c:4:11 in main

如上所示,HWASAN 与 ASAN 相比不管是用法 (-fsanitize=hwaddress v.s. -fsanitize=address) 还是检测到错误后的报告都很相似。

下面对比分析 ASAN 与 HWASAN 检测内存错误的技术原理:

ASAN (AddressSanitizer):

  • 使用 shadow memory 技术,每 8-bytes 的 application memory 对应 1-byte 的 shadow memory。
  • 使用 redzone 来检测 buffer-overflow。不管是栈内存还是堆内存,在申请内存时都会在原本内存的两侧额外申请一定大小的内存作为 redzone,一旦访问到了 redzone 则说明发生了缓冲区溢出。
  • 使用 quarantine 检测 use-after-free。应用程序执行 delete 或 free 释放内存时,并不真正释放,而是放在一个暂存区 (quarantine) 中,一旦访问了位于 quarantine 中的内存则说明访问了已释放的内存。
  • 每 1-byte 的 shadow memory 编码表示对应的 8-byte application memory 的信息,每次访问 application memory 之前 ASAN 都会检查对应的 shadow memory 判断本次内存访问是否合法。例如:shadow memory 的值为 0xfd 表示对应的 8-bytes application memory 是 freed memory,所以当访问的 application memory 其 shadow memory byte 为 0xfd 就说明此时访问的是已经释放的内存了即 use-after-free;shadow memory 为 0xfa 表示相应的 application memory 是堆内存的 redzone,所以当访问到的 appllcation memory 其 shadow memory 为 0xfa 就说明此时访问的是堆内存附近的 redzone 发生了堆缓冲区溢出错误即 heap-buffer-overflow

HWASAN (HardWare-assisted AddressSanitizer)

  • 同样使用 shadow memory 技术,不过与 ASAN 不同的是:HWASAN 每 16-bytes 的 application memory 对应 1-byte 的 shadow memory。

  • 不依赖 redzone 检测 buffer-overflow,不依赖 quarantine 检测 use-after-free,仅基于 TBI 特性就能检测 buffer-overflow 和 use-after-free。

  • 举例说明 HWASAN 检测内存错误的原理

  • 因为 HWASAN 会将每 16-bytes 的 application memory 都对应 1-byte 的 shadow tag,所以 HWASAN 会将申请的内存都对齐到 16-bytes,因此下图中 new char[20] 实际申请的内存是 32-bytes。

  • HWASAN 会生成一个随机 tag 保存在 operator new 返回的指针 p 的 top byte 中,同时会将 tag 保存在 p 指向的内存对应 shadow memory 中。

  • 为了方便说明,下图中用不同的颜色表示不同的 tag,绿色表示 tag 0xa,蓝色表示 tag 0xb,紫色表示 tag 0xc。

    • 检测 heap-buffer-overflow

      •     假设 HWASAN 为 new char[20] 生成的 tag 为 0xa 即绿色,所以指针 p 的 top byte 为 0xa。在通过 p[32] 访问内存时,HWASAN 会检查保存在指针 p 的 tag 与 p[32] 指向的内存所对应的 shadow memory 中保存的 tag 是否一致。显然保存在指针 p 的 tag 是绿色 而p[32] 指向的内存所对应的 shadow memory 中保存的 tag 是蓝色,即 tag 是不匹配的,这说明访问 p[32] 时存在内存错误。
    • 检测 use-after-free

      •     假设 HWASAN 为 new char[20] 生成的 tag 为 0xa 即绿色,所以指针 p 的 top byte 为 0xa。执行 delete[] p 释放内存时,HWASAN 将这块释放的内存 retag 为紫色,即将这块释放的内存对应的 shadow memory 从绿色修改为紫色。在通过 p[0] 访问内存时,HWASAN 会检查保存在指针 p 的 tag 与 p[0] 指向的内存所对应的 shadow memory 中保存的 tag 是否一致。显然保存在指针 p 的 tag 是绿色 而p[0] 指向的内存所对应的 shadow memory 中保存的 tag 是紫色,即 tag 是不匹配的,这说明访问 p[0] 时存在内存错误。

算法

  • shadow memory:每 16-bytes 的 application memory 对应 1-byte 的 shadow memory。
  • 每个 heap/stack/global 内存对象都被对齐到 16-bytes,这样每个 heap/stack/global 内存对象至少对应的 1-byte shadow memory。
  • 为每一个 heap/stack/global 内存对象生成一个 1-byte 的随机 tag,将该随机 tag 保存到指向这些内存对象的指针的 top byte 中,同样将该随机 tag 保存到这些内存对象对应的 shadow memory 中。
  • 在每一处内存读写之前插桩:比较保存在指针 top byte 的 tag 和保存在 shadow memory 中的 tag 是否一致,如果不一致则报错。

实现

shadow mapping

HWASAN 与 ASAN 一样都使用了 shadow memory 技术。ASAN 默认使用 static shadow mapping,只有对 IOS 和 32-bit Android 平台才使用 dynamic shadow mapping。而 HWASAN 则总是使用 dynamic shadow mapping。

  • ASAN: static shadow mapping。在 llvm-project/compiler-rt/lib/asan/asan_mapping.h 中预定义了不同平台下 shadow memory 的布局:HighMem, HighShadow, ShadowGap, LowShadow, LowMem 的地址区间。

  • Linux/x86_64 下 ASAN 的 shadow mapping 如下所示:

       // Typical shadow mapping on Linux/x86_64 with SHADOW_OFFSET == 0x00007fff8000:
       || `[0x10007fff8000, 0x7fffffffffff]` || HighMem    ||
       || `[0x02008fff7000, 0x10007fff7fff]` || HighShadow ||
       || `[0x00008fff7000, 0x02008fff6fff]` || ShadowGap  ||
       || `[0x00007fff8000, 0x00008fff6fff]` || LowShadow  ||
       || `[0x000000000000, 0x00007fff7fff]` || LowMem     ||
       ```
    
    
  • 给定 application memory 地址 addr,计算其对应的 shadow memory 地址的公式如下:

     uptr MemToShadow(uptr addr) { return (addr >> 3) + 0x7fff8000; }
    
  • HWASAN: dynamic shadow mapping。根据 MaxUserVirtualAddress 计算 shadow memory 所需要的总大小 shadow_size,通过 mmap(shadow_size) 得到 shadow memory 区间,再具体划分 HighMem, HighShadow, ShadowGap, LowShadow, LowMem 的地址区间。

  • 伪算法如下(未考虑对齐):

       kHighMemEnd = GetMaxUserVirtualAddress();
    shadow_size = MemToShadowSize(kHighMemEnd);
    __hwasan_shadow_memory_dynamic_address = mmap(shadow_size);
    // Place the low memory first.
    kLowMemEnd = __hwasan_shadow_memory_dynamic_address - 1;
    kLowMemStart = 0;
    // Define the low shadow based on the already placed low memory.
    kLowShadowEnd = MemToShadow(kLowMemEnd);
    kLowShadowStart = __hwasan_shadow_memory_dynamic_address;
    // High shadow takes whatever memory is left up there.
    kHighShadowEnd = MemToShadow(kHighMemEnd);
    kHighShadowStart = Max(kLowMemEnd, MemToShadow(kHighShadowEnd)) + 1;
    // High memory starts where allocated shadow allows.
    kHighMemStart = ShadowToMem(kHighShadowStart);
    ```
    - ```
    uptr MemToShadow(uptr untagged_addr) {
    return (untagged_addr >> 4) + __hwasan_shadow_memory_dynamic_address;
    }
    uptr ShadowToMem(uptr shadow_addr) {
    return (shadow_addr - __hwasan_shadow_memory_dynamic_address)

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论