Post

KN 地址消毒

Kotlin Native 中使用地址消毒工具检测内存安全问题。介绍 ASAN、HWASAN、MemDebug、GWP-ASan 的原理与开销,分析 KN 与 C 混合场景下的故障模式,涵盖堆栈溢出、use-after-free、double-free 等异常类型的检测方法。

KN 地址消毒

One of the first Internet-spread computer worms was the Internet Worm in 1988, which exploited a buffer overrun. More than thirty years later, we are still seeing attacks that exploit this type of programming bug. – Detecting memory safety violations, Arm Developer Manual

背景信息

 ASANHWASANMemDebugGWP-ASan
全名Address SanitizerHardware-Assisted Address SanitizerMemory DebugGoogle-Wide Profiling
GWP-ASan Will Provide Allocation SANity
部分检测依赖插桩
开销cpu,内存 200%cpu 200% 内存 10~35%cpu,内存 10~20%cpu,内存 < 5%,可调
基本原理Shadow Memory,红区Shadow Memory,指针Tag page电网
     

鸿蒙san文档

KN故障模式:认为关键是谁申请的内存,谁拿着指针做非法访问

 C指针KN指针
C使用正常san1. KN栈内存没发现获取指针方式,给不到C
2. KN堆内存溢出到内存池外
3. KN堆内存溢出到内存池内
KN使用konan接llvm san插桩pass同C使用

异常类型

  • 空间
    • heap buffer under/overflow
    • stack buffer under/overflow
  • 时间
    • heap use after free
    • stack use after scope (return?)
    • double free
    • free nonallocated memory

鸿蒙官网异常类型

  • asan: heap-buffer-overflow stack-buffer-overflow stack-buffer-underflow heap-use-after-free stack-use-after-scope attempt-free-nonallocated-memory double-free
  • hwasan:stack-buffer-overflow/underflow stack-use-after-return heap-buffer-overflow heap-buffer-underflow heap-use-after-free double-free
  • gwpasan:double free use_after_free invalid free left invalid free right buffer underflow

堆栈分配方法

    • calloc/malloc/free
    • jemalloc
    • new delete
    • alloc

san 原理

Asan

  • 被投毒(poison)的内存叫红区(redzone),哪里有毒的信息记在影子内存(shadow memory)里
  • 1 shadow memory byte对应8个normal memory byte,分配的正常内存8 byte对齐,如malloc 13 bytes,05代表sm这个byte对应的应用8byte内存中前5byte是可以访问的,后3byte是不能访问的 alt text alt text

  • 插桩:分配栈内存时前后poison,读写内存前根据sm进行校验
  • 运行时:分配堆内存时前后poison,释放的堆内存整个poison在FIFO队列中等一段时间

  • 性能开销:sm操作
  • 内存开销:8 byte对齐导致的空洞,前后的红区,释放后的隔离区,申请的内存8 byte向上取整和的1/8用于sm

HWASAN

  • Detecting-memory-safety-violations
  • Arm Memory Tagging Extension Whitepaper

  • 利用arm硬件寻址时的Top Byte Ignore功能:128G = 2 ^ 7 * 2 ^ 10 (kb) * 2 ^ 10 (mb) * 2 ^ 10 (gb) = 2 ^ 37 bytes。37 bit就能表示128G的内存地址空间,64位系统上寻址时高位是用不到的
  • 分配内存时拿到的地址是带tag的,使用地址时检查地址中的tag和被访问地址的tag是不是一致 alt text
  • LLVM的hwasan实现tag是8位,也有shadow memory,1 sm byte对 16 应用byte。应用内存分配16 byte对齐
    • 分配
      • 分配了 n * 16 + 1~15 bytes:sm中保存实际分配的byte数,应用内存最后一个byte保存tag
      • 分配了 n * 16 bytes:sm中保存tag
    • 检查
      • 如果shadow byte值 [1, 15]:
        • 指针tag == shadow byte:视为完整16字节粒度的正常tag,有效
        • 指针tag != shadow byte:视为短粒度tag,检查访问地址是否在16字节粒度的前N字节内(N为shadow byte值),且指针tag是否与应用内存16 byte中最后一个byte存的tag匹配
          • 如果都满足:短粒度访问有效
          • 否则:无效
      • 否则(shadow byte值 > 15或为0):
        • 如果指针tag与shadow byte匹配:有效
        • 如果指针tag与shadow byte不匹配:无效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌─────────────────────────┐
│ Extract PtrTag (>>56)   │
│ Compute Shadow = Addr>>4│
│ Load MemTag from Shadow │
└────────────┬────────────┘
             ↓
      ┌─────▼──────┐
      │ PtrTag ==  │
      │  MemTag?   │
      └──┬─────┬───┘
    Yes  │     │ No
         │     ↓
         │  ┌──▼──────────┐
         │  │MemTag > 15? │ (Not short granule)
         │  └──┬─────┬────┘
         │ Yes │     │ No
         │     │     ↓
         │     │  ┌──▼────────────────────┐
         │     │  │ (Addr&15)+Size > Tag? │
         │     │  └──┬─────┬──────────────┘
         │     │ Yes │     │ No
         │     │     │     ↓
         │     │     │  ┌──▼──────────────┐
         │     │     │  │ Load Tag @Addr|15│
         │     │     │  │ PtrTag == Tag?  │
         │     │     │  └──┬─────┬────────┘
         │     │     │ Yes │     │ No
         ↓     ↓     ↓     ↓     ↓
      [OK]    [ ERROR ]  [OK]  [ERROR]

GWP

GWP-ASan的实现通常被描述为电网。性能方面为了减小开销只随机sample部分内存分配进行防护,检查通过mmu硬件进行开销较小,分配和释放内存时会回栈有一些开销,总体性能开销~5%。内存方面开销来自分配guard页和空洞,开销是固定可调的。

alt text

  • 上下溢:如检查下溢时将数据分配到gwp内存池中向slot低地址方向对齐,这样指针-1的位置会访问到上一个不可读不可写不可执行的guard page,触发mmu异常信号
  • uaf:释放内存时将这段内存所在的整个Slot设为不可读不可写不可执行,持续一段时间,uaf访问时触发mmu异常信号

  • 一次申请超过1 page的内存不会防护

DevEco LLVM san编译参数

开启hvigor详细日志查看cmake参数

alt text

构建日志中搜 [cmake]

alt text

对比DevEco中是否勾选asan,cmake选项差一个 '-DOHOS_ENABLE_ASAN=ON'

alt text

对比 .cxx/default/default/debug/arm64-v8a/compile_commands.json 中具体cpp文件的编译命令,clang++多了几个选项

  • asan:
    1
    2
    3
    4
    
    -shared-libasan
    -fsanitize=address
    -fno-omit-frame-pointer
    -fsanitize-recover=address
    
  • hwasan:
    1
    2
    3
    4
    5
    
    -shared-libasan
    -fsanitize=hwaddress
    -mllvm -hwasan-globals=0
    -fno-emulated-tls
    -fno-omit-frame-pointer
    

    alt text

KN接入 LLVM 已有能力通常是两步:1. 调相关pass处理IR,2. 链接运行时库。编一个最简单的 int main() {} 比较差异

1
2
3
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++ --target=aarch64-linux-ohos test.cpp -v -mllvm -debug-pass=Structure
# vs
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++ --target=aarch64-linux-ohos test.cpp -v -mllvm -debug-pass=Structure -shared-libasan -fsanitize=address -fno-omit-frame-pointer -fsanitize-recover=address

比较输出中的ld命令

alt text

KN接入san时应当参考KN使用的 LLVM 版本中的链接参数,其他版本的 LLVM 参数和 DevEco中的 LLVM 15可能不同

KN 内存分配

分配过程

对象的地址可以赋给堆或者栈上的变量

alt text

alt text

alt text

alt text

alt text

alt text

alt text

内存布局

!!!本章节基本是大模型走读KN runtime代码总结,缺乏验证!!!

对象:

OffsetFieldSizeAlignmentBit-Level Details
0x00gc::GC::ObjectData8 bytes8 bytesGC Header
 next_ (atomic pointer)8 bytes8 bytes8 bytes for all 3 gc types
0x08KObject (ObjHeader)varies8 bytesKotlin Object Data
 typeInfoOrMeta_8 bytes8 bytesBits 0-63: Pointer to TypeInfo or MetaObjHeader
Bits 0-1: Tag bits (OBJECT_TAG_MASK)    
0x10+Object fieldsN bytesnatural⚠️ Fields use NATURAL alignment, NOT 8-byte!: Boolean: 1 byte (1-byte aligned),Byte: 1 byte (1-byte aligned),Short: 2 bytes (2-byte aligned),Int/Float: 4 bytes (4-byte aligned),Long/Double/Reference: 8 bytes (8-byte aligned)
Compiler reorders by size (large→small) to minimize padding    
END[Padding]0-7 bytesPad entire object to 8-byte boundary
1
2
3
4
5
class Example {
    val flag: Boolean    // 1 byte
    val count: Int       // 4 bytes  
    val ref: String?     // 8 bytes
}
OffsetFieldSizeExplanation
0x00ObjectData.next_8 bytesGC header
0x08typeInfoOrMeta_8 bytesObject header
0x10ref8 bytesLargest field first (8-byte aligned)
0x18count4 bytesMedium field (4-byte aligned, NOT 8!)
0x1Cflag1 byteOnly 1 byte used (1-byte aligned)
0x1D[padding]3 bytesPad to 8-byte boundary
    
Total32 bytes 8 + 24 where instanceSize_ = 24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
┌─────────────────────────────────────────────┐
  HEAP (per-instance data)                   
├─────────────────────────────────────────────┤
  0x00: gc::GC::ObjectData                   
        - next_ (8 bytes)                    
├─────────────────────────────────────────────┤
  0x08: ObjHeader (KObject)                  
        - typeInfoOrMeta_ (8 bytes) ────┐    
├─────────────────────────────────────────│────┤
  0x10: [Object fields]                      
        - field1                             
        - field2                             
        - ...                                
└─────────────────────────────────────────│────┘
                                          
                    Points to             
                                         
┌─────────────────────────────────────────┴────┐
  STATIC MEMORY (per-class metadata)          
├──────────────────────────────────────────────┤
  TypeInfo (shared by all instances):         
    - typeInfo_ (self-reference)              
    - extendedInfo_                           
    - unused_                                 
    - instanceSize_        Size of fields    
    - superType_                              
    - objOffsets_          GC scan info      
    - objOffsetsCount_                        
    - implementedInterfaces_                  
    - flags_                                  
    - classId_                                
    - associatedObjects                       
    - processObjectInMark                     
    - instanceAlignment_                      
    - vtable_[]            Virtual methods   
└──────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
TypeInfo
┌─────────────────────────────────────┐
 gc::GC::ObjectData (8 bytes)        
├─────────────────────────────────────┤
 ObjHeader                            
   typeInfoOrMeta_ ────────┐         
                                     
├────────────────────────────┼─────────┤
 [Object fields...]                  
└────────────────────────────┼─────────┘
                             
                             
                    ┌────────────────┐
                     TypeInfo       
                       typeInfo_ ─┐   Self-reference
                                   
                       (ptr == ───┘   ptr->typeInfo_)
                       ...)         
                    └────────────────┘

Meta
┌─────────────────────────────────────┐
 gc::GC::ObjectData (8 bytes)        
├─────────────────────────────────────┤
 ObjHeader                            
   typeInfoOrMeta_ ─────┐            
                                    
├────────────────────────┼────────────┤
 [Object fields...]                 
└────────────────────────┼────────────┘
                         
                         
              ┌──────────────────────────┐
               ExtraObjectData          
                 typeInfo_ ──────────┐  
                 flags_                  ptr != ptr->typeInfo_
                 associatedObject_       (so it's detected as meta)
                 weakReference...      
              └───────────────────────┼──┘
                                      
                                      
                             ┌────────────────┐
                              TypeInfo       
                                typeInfo_ ─┐ 
                                            
                                (...)    ──┘ 
                             └────────────────┘

数组

OffsetFieldSizeAlignmentBit-Level Details
0x00gc::GC::ObjectData8 bytes8 bytesGC Header
 next_ (atomic pointer)8 bytes8 bytesSame as HeapObject
0x08KArray (ArrayHeader)16 bytes8 bytesArray Header (fixed size)
 typeInfoOrMeta_8 bytes8 bytesBits 0-63: Pointer to array TypeInfo
Bits 0-1: Tag bits    
0x10count_4 bytes4 bytesBits 0-31: Element count (uint32_t)
0x14objcFlags_ / padding4 bytes4 bytesBits 0-31: ObjC flags if KONAN_OBJC_INTEROP, Otherwise: padding to 8-byte struct alignment
0x18[Padding]0-7 bytesE bytesAlign to element size boundary
AlignUp(sizeof(ArrayHeader), elementSize)    
0x18+Array elementsE × countE bytes⚠️ Elements use ELEMENT SIZE alignment, NOT 8-byte!
Each element size E = -typeInfo->instanceSize_
Elements are tightly packed with no extra padding
END[Padding]0-7 bytesPad entire array to 8-byte boundary
1
val flags = BooleanArray(5)  // 5 booleans
OffsetFieldSizeExplanation
0x00ObjectData.next_8 bytesGC header
0x08typeInfoOrMeta_8 bytesArray header
0x10count_ = 54 bytes 
0x14padding4 bytes 
0x18element[0]1 byteOnly 1 byte per boolean!
0x19element[1]1 byteNo padding between elements
0x1Aelement[2]1 byteTightly packed
0x1Belement[3]1 byte 
0x1Celement[4]1 byte 
0x1D[padding]3 bytesFinal 8-byte alignment
Total32 bytes AlignUp(24 + 5, 8) = 32

page:

ConstantOHOS TargetStandard Target
kPageAlignment8 bytes8 bytes
Cell size8 bytes8 bytes
FixedBlockCell size8 bytes8 bytes
NextFitPage::SIZE128 KiB256 KiB
FixedBlockPage::SIZE128 KiB256 KiB
ExtraObjectPage::SIZE64 KiB64 KiB
MAX_BLOCK_SIZE32 cells (256 bytes)128 cells (1024 bytes)

SingleObjectPage

OffsetFieldSizeAlignmentDescription
0x00**AnyPage**16 bytes8 bytesBase class
0x00├─ next_8 bytes8 bytesstd::atomic<SingleObjectPage*> for page list
0x08└─ allocatedSizeTracker_8 bytes8 bytesAllocatedSizeTracker::Page (size_t field)
 └─ threadName_24 bytes8 bytesafter libc++ small string optimization a string is 24 bytes
0x10isAllocated_1 byte1 byteBoolean: 0 = available, 1 = occupied
0x11[padding]7 bytesPad to 8-byte boundary
0x18size_8 bytes8 bytesTotal page allocation size (size_t)
0x20dataAddress8 bytes8 bytesTencent Code: cached uintptr_t of data_ address
0x28alignas(8) struct0 bytes8 bytesAlignment directive only
0x28├─ data_[]N bytes8 bytesFlexible array - object begins here ↓
     
0x28Object Layout  Within data_[] array:
0x28├─ gc::GC::ObjectData8 bytes8 bytesGC header: std::atomic<ObjectData*> next_
0x30├─ ObjHeader8 bytes8 bytesTypeInfo* typeInfoOrMeta_ pointer
0x38├─ [Object fields…]variesnaturalField data (1/2/4/8-byte aligned)
???└─ [padding]0-7 bytesPad total object to 8-byte boundary
All Rights Reserved.