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
背景信息
| ASAN | HWASAN | MemDebug | GWP-ASan | |
|---|---|---|---|---|
| 全名 | Address Sanitizer | Hardware-Assisted Address Sanitizer | Memory Debug | Google-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电网 | |
KN故障模式:认为关键是谁申请的内存,谁拿着指针做非法访问
| C指针 | KN指针 | |
|---|---|---|
| C使用 | 正常san | 1. 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是不能访问的

- 插桩:分配栈内存时前后poison,读写内存前根据sm进行校验
运行时:分配堆内存时前后poison,释放的堆内存整个poison在FIFO队列中等一段时间
- 性能开销:sm操作
- 内存开销:8 byte对齐导致的空洞,前后的红区,释放后的隔离区,申请的内存8 byte向上取整和的1/8用于sm
HWASAN
Detecting-memory-safety-violations
- 利用arm硬件寻址时的Top Bit Ignore功能:128G = 2 ^ 7 * 2 ^ 10 (kb) * 2 ^ 10 (mb) * 2 ^ 10 (gb) = 2 ^ 37 bytes。37 bit就能表示128G的地址空间,64位系统上寻址时高位是用不到的
- 拿到的地址是带tag的,使用地址时检查地址中的tag和被访问地址的tag是不是一致

- 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不匹配:无效
- 如果shadow byte值 1 ~ 15,包含:
- 分配
GWP
GWP-ASan的实现通常被描述为电网。性能方面为了减小开销只随机sample部分内存分配进行防护,检查通过mmu硬件进行开销较小,分配和释放内存时会回栈有一些开销,总体性能开销~5%。内存方面开销来自分配guard页和空洞,开销是固定可调的。
- 上下溢:如检查下溢时将数据分配到gwp内存池中向slot低地址方向对齐,这样指针-1的位置会访问到上一个不可读不可写不可执行的guard page,触发mmu异常信号
uaf:释放内存时将这段内存所在的整个Slot设为不可读不可写不可执行,持续一段时间,uaf访问时触发mmu异常信号
- 一次申请超过1 page的内存不会防护
DevEco LLVM san编译参数
开启hvigor详细日志查看cmake参数
构建日志中搜 [cmake]
对比DevEco中是否勾选asan,cmake选项差一个 '-DOHOS_ENABLE_ASAN=ON'
对比 .cxx/default/default/debug/arm64-v8a/compile_commands.json 中具体cpp文件的编译命令,clang++多了几个选项
1
2
3
4
-shared-libasan
-fsanitize=address
-fno-omit-frame-pointer
-fsanitize-recover=address
比较加不加这四个选项的链接命令,编一个最简单的 int main() {}
1
2
3
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++ --target=aarch64-linux-ohos test.cpp -v
# vs
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++ --target=aarch64-linux-ohos test.cpp -v -shared-libasan -fsanitize=address -fno-omit-frame-pointer -fsanitize-recover=address
比较输出中的ld命令
KN 内存分配
分配过程
对象的地址可以赋给堆或者栈上的变量
内存布局
!!!本章节基本是大模型走读KN runtime代码总结,缺乏验证!!!
对象:
| Offset | Field | Size | Alignment | Bit-Level Details |
|---|---|---|---|---|
| 0x00 | gc::GC::ObjectData | 8 bytes | 8 bytes | GC Header |
| next_ (atomic pointer) | 8 bytes | 8 bytes | 8 bytes for all 3 gc types | |
| 0x08 | KObject (ObjHeader) | varies | 8 bytes | Kotlin Object Data |
| typeInfoOrMeta_ | 8 bytes | 8 bytes | Bits 0-63: Pointer to TypeInfo or MetaObjHeader | |
| Bits 0-1: Tag bits (OBJECT_TAG_MASK) | ||||
| 0x10+ | Object fields | N bytes | natural | ⚠️ 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 bytes | — | Pad 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
}
| Offset | Field | Size | Explanation |
|---|---|---|---|
| 0x00 | ObjectData.next_ | 8 bytes | GC header |
| 0x08 | typeInfoOrMeta_ | 8 bytes | Object header |
| 0x10 | ref | 8 bytes | Largest field first (8-byte aligned) |
| 0x18 | count | 4 bytes | Medium field (4-byte aligned, NOT 8!) |
| 0x1C | flag | 1 byte | Only 1 byte used (1-byte aligned) |
| 0x1D | [padding] | 3 bytes | Pad to 8-byte boundary |
| Total | 32 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_ ─┐ │
│ │ │
│ (...) ──┘ │
└────────────────┘
数组
| Offset | Field | Size | Alignment | Bit-Level Details |
|---|---|---|---|---|
| 0x00 | gc::GC::ObjectData | 8 bytes | 8 bytes | GC Header |
| next_ (atomic pointer) | 8 bytes | 8 bytes | Same as HeapObject | |
| 0x08 | KArray (ArrayHeader) | 16 bytes | 8 bytes | Array Header (fixed size) |
| typeInfoOrMeta_ | 8 bytes | 8 bytes | Bits 0-63: Pointer to array TypeInfo | |
| Bits 0-1: Tag bits | ||||
| 0x10 | count_ | 4 bytes | 4 bytes | Bits 0-31: Element count (uint32_t) |
| 0x14 | objcFlags_ / padding | 4 bytes | 4 bytes | Bits 0-31: ObjC flags if KONAN_OBJC_INTEROP, Otherwise: padding to 8-byte struct alignment |
| 0x18 | [Padding] | 0-7 bytes | E bytes | Align to element size boundary |
| AlignUp(sizeof(ArrayHeader), elementSize) | ||||
| 0x18+ | Array elements | E × count | E 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 bytes | — | Pad entire array to 8-byte boundary |
1
val flags = BooleanArray(5) // 5 booleans
| Offset | Field | Size | Explanation |
|---|---|---|---|
| 0x00 | ObjectData.next_ | 8 bytes | GC header |
| 0x08 | typeInfoOrMeta_ | 8 bytes | Array header |
| 0x10 | count_ = 5 | 4 bytes | |
| 0x14 | padding | 4 bytes | |
| 0x18 | element[0] | 1 byte | Only 1 byte per boolean! |
| 0x19 | element[1] | 1 byte | No padding between elements |
| 0x1A | element[2] | 1 byte | Tightly packed |
| 0x1B | element[3] | 1 byte | |
| 0x1C | element[4] | 1 byte | |
| 0x1D | [padding] | 3 bytes | Final 8-byte alignment |
| Total | 32 bytes | AlignUp(24 + 5, 8) = 32 |
page:
| Constant | OHOS Target | Standard Target |
|---|---|---|
| kPageAlignment | 8 bytes | 8 bytes |
| Cell size | 8 bytes | 8 bytes |
| FixedBlockCell size | 8 bytes | 8 bytes |
| NextFitPage::SIZE | 128 KiB | 256 KiB |
| FixedBlockPage::SIZE | 128 KiB | 256 KiB |
| ExtraObjectPage::SIZE | 64 KiB | 64 KiB |
| MAX_BLOCK_SIZE | 32 cells (256 bytes) | 128 cells (1024 bytes) |
SingleObjectPage
| Offset | Field | Size | Alignment | Description |
|---|---|---|---|---|
| 0x00 | **AnyPage | 16 bytes | 8 bytes | Base class |
| 0x00 | ├─ next_ | 8 bytes | 8 bytes | std::atomic<SingleObjectPage*> for page list |
| 0x08 | └─ allocatedSizeTracker_ | 8 bytes | 8 bytes | AllocatedSizeTracker::Page (size_t field) |
| └─ threadName_ | 24 bytes | 8 bytes | after libc++ small string optimization a string is 24 bytes | |
| 0x10 | isAllocated_ | 1 byte | 1 byte | Boolean: 0 = available, 1 = occupied |
| 0x11 | [padding] | 7 bytes | — | Pad to 8-byte boundary |
| 0x18 | size_ | 8 bytes | 8 bytes | Total page allocation size (size_t) |
| 0x20 | dataAddress | 8 bytes | 8 bytes | Tencent Code: cached uintptr_t of data_ address |
| 0x28 | alignas(8) struct | 0 bytes | 8 bytes | Alignment directive only |
| 0x28 | ├─ data_[] | N bytes | 8 bytes | Flexible array - object begins here ↓ |
| 0x28 | Object Layout | Within data_[] array: | ||
| 0x28 | ├─ gc::GC::ObjectData | 8 bytes | 8 bytes | GC header: std::atomic<ObjectData*> next_ |
| 0x30 | ├─ ObjHeader | 8 bytes | 8 bytes | TypeInfo* typeInfoOrMeta_ pointer |
| 0x38 | ├─ [Object fields…] | varies | natural | Field data (1/2/4/8-byte aligned) |
| ??? | └─ [padding] | 0-7 bytes | — | Pad total object to 8-byte boundary |












