- 操作系统
- 驱动开发
【免费下载链接】darwin-xnu
Legacy mirror of Darwin Kernel. Replaced by https://github.com/apple-oss-distributions/xnu
导读
本文以 XNU(Darwin 内核)的官方文档 doc/allocators.md 为核心骨架,系统讲解 XNU 内核中围绕 zone allocator(slab 分配器)构建的全部通用内存分配变体。你将掌握如何在真实内核代码中选对分配接口(zalloc、kalloc、kheap_alloc、zalloc_permanent等)、理解各分配标志(Z_WAITOK、Z_NOFAIL、Z_ZERO……)的语义与适用场景,并深入理解 XNU 为对抗类型混淆、Use-after-Free 等漏洞而设计的 sequestering、zone_require()、zone caching、zone view 等安全与记账机制。全文所有结论均以仓库源码(osfmk/kern/zalloc.h、osfmk/kern/kalloc.h 等)为证据支撑。
双轨架构:页面粒度 VM 分配与固定大小 Zone 分配
XNU 提供两种内存分配途径:
- VM 子系统:以页面(page)为粒度提供分配,接口如
kernel_memory_allocate及其同类(文档中记为 KMA)。这类分配适合大块、页面对齐的内存需求。 - Zone 分配器子系统(
<kern/zalloc.h>):一个针对固定大小对象的 slab 分配器,也是本文的主角。
在这两者之间,<kern/kalloc.h>提供了一个可变大小的通用分配器:它实现为一组固定大小 zone 的集合(kalloc heap),当请求超过几页(撰写本文档时约为 32KB,未来可能调整)时,溢出到kernel_memory_allocate。
头文件分层
<kern/zalloc.h>与<kern/kalloc.h>:公开 API 面,绝大多数调用方只需要这两个;<kern/zalloc_internal.h>与<kern/zcache_internal.h>:为内省(introspection)与实现细节导出的接口,不面向一般消费。
TL;DR:分配器快速决策树
用哪个分配器?
| 场景 | 推荐接口 |
|---|---|
| 内存永不释放(且大小随机器配置变化、编译期无法确定) | zalloc_permanent*,超过一页会自动以KMA_PERMANENT走kernel_memory_allocate |
| 临时内存、不逃逸出所在 syscall 作用域 | kheap_alloc/kheap_free+KHEAP_TEMP;临时路径缓冲区应使用zalloc(ZV_NAMEI) |
| 内存不含指针,且内容可能受用户态直接影响 | kheap_alloc/kheap_free+KHEAP_DATA_BUFFERS |
| 一般情况 | 优先zalloc/kalloc接口;遗留的MALLOC/FREE接口计划逐步弃用 |
zalloc_permanent*的分配被假定永远成功(多用于早期启动阶段、需要随机器配置伸缩且无法在编译期决定大小的分配),返回的内存保证清零。
对kalloc/kheap_alloc全系变体,两条通用建议:
- 若分配大小固定、小于一页、且采用
Z_WAITOK(可阻塞)语义,考虑追加Z_NOFAIL; - 若你分配后会对内存做
bzero,改为传Z_ZERO——zone 分配器在多数情况下能把它优化掉。
Zones 的取舍考量
性能维度:如果某个 zone 在全内核生命周期内平均持有不足几页的元素(经验值 20 万+ 对象),为其单独建 zone 是问题——低利用率会带来碎片化。高分配/释放流量的 zone 可考虑 zone caching,但需评估其内存占用代价。
安全维度,创建 zone 前回答三个问题:
- 该类型是否"容易被混淆为另一类型"?若是,独立 zone 可启用
zone_require(),并默认获得虚拟地址空间 sequestering; - 该类型是否持有用户可控的 "bytes"?若是,可考虑用 zone view(如存放路径的
ZV_NAMEI)替代; - 该类型是否总是分配时清零?若是,开启
ZC_ZFREE_CLEARMEM的成本边际极低,却能发现 write-after-free 类 bug。
分配接口全景:Variants 一览
zalloc与kalloc是原语级分配接口,其余接口(IOKit 的IONew追加记账、C++ 各种new操作符为满足语言要求、以及历史遗留原因)都建立在它们之上。下表完整列出各接口及属性(来源:doc/allocators.md)。
| Interface | Core XNU | Private Export | Public Export | Comments |
|---|---|---|---|---|
| Core primitives(核心原语) | ||||
| zalloc | Yes | Yes | No | 受实现限制,zone 的数量有限。在该限制解除前,向任意内核扩展开放原始 zalloc 是有问题的。 |
| kheap_alloc | Yes | No | No | 这才是kalloc的真正核心实现,详见 kalloc heaps 一节。 |
| kalloc | Yes | Yes, Redirected | No | 在 XNU 内核中kalloc等价于kheap_alloc(KHEAP_DEFAULT);在内核扩展(kext)中等价于kheap_alloc(KHEAP_KEXT)。由于历史契约允许在 XNU/kext 边界两侧分别分配与释放,kfree允许释放到任一堆;新代码应改用对应的kheap_*变体。 |
| Popular wrappers(常用封装) | ||||
| IOMalloc | Yes | Yes, Redirected | Yes, Redirected | IOMalloc是kalloc的直接封装,行为与kalloc一致,并集成 IOKit 的调试特性,是驱动(Driver)应使用的分配器。只有提供核心基础设施(文件系统、sandbox 等)的树外核心内核组件,才应直接使用原语zalloc/kalloc。 |
| C++ new | Yes | Yes, Redirected | Yes, Redirected | XNU 实现了 C++ 各种new/delete操作符,重定向到KHEAP_KEXTkalloc 堆(Core Kernel 内部不使用 C++ 默认 operator new)。用 IOKit 宏创建OSObject子类时,会为该对象提供专属operator new/operator delete:类定义在 Core XNU 中锚定到KHEAP_DEFAULT堆,定义在内核扩展中则锚定到KHEAP_KEXT堆。 |
| MALLOC | Yes | Obsolete, Redirected | No | 遗留 BSD 接口,行为基本等同kalloc。对 kext,FREE()因历史原因允许释放到KHEAP_DEFAULT或KHEAP_KEXT。 |
| Obsolete wrappers(废弃封装) | ||||
| mcache | Yes | Kinda | Kinda | mcache/mbuf 子系统主要由 BSD 网络栈使用;不与其接口交互的代码不应采用 mcache。 |
| OSMalloc | No | Obsolete, Redirected | Obsolete, Redirected | <libkern/OSMalloc.h>是遗留子系统,记账极慢且不可扩展,新代码一律不用,改用IOMalloc。 |
| MALLOC_ZONE | No | Obsolete, Redirected | No | 曾是zalloc的怪异封装且安全保证更差,已从 XNU 彻底移除。为向后兼容仍导出符号,但行为与MALLOC完全一致。 |
| kern_os_* | No | Obsolete, Redirected | Obsolete, Redirected | 这些符号曾支撑 C++operator new的实现,仅因向后兼容保留,任何人都不应直接使用。 |
Zone 分配器:概念、性能与安全
Zone 通过zone_create()创建,设计上几乎从不销毁。可销毁 zone(需传ZC_DESTRUCTIBLE标志,见 osfmk/kern/zalloc.h)仅因历史遗留存在,且并非所有特性都对它们可用——源码中ZONE_DECLARE宏甚至用static_assert禁止对普通声明 zone 传入ZC_DESTRUCTIBLE。
Zone 从一个名为Zone Map的固定大小映射中分配对象,该映射细分为若干提供不同安全属性的子映射(submap):
- VA Restricted map:仅供 VM 子系统使用,允许对 VM 子系统指针做极其紧凑的打包;不使用 sequestering。对应源码 zalloc.h 中的
Z_SUBMAP_IDX_VA_RESTRICTED; - general map:zone 的默认选择;在嵌入式设备上默认启用完整 VA sequestering。对应
Z_SUBMAP_IDX_GENERAL; - "bag of bytes" map:用于存放内容受用户态控制的各种缓冲区。把这些分配与其他子映射隔离,可阻断攻击者利用此类分配对 general map 中的内核对象进行 heap spraying。对应
Z_SUBMAP_IDX_BAG_OF_BYTES。
重要约束:XNU 的任何分配函数都不允许在中断上下文调用——所有分配器均不可重入、不具备中断安全性。
基础特性:阻塞语义标志
<kern/zalloc.h>定义了可传给zalloc/kalloc的标志,用于改变阻塞行为:
Z_WAITOK(默认值0x0000):允许分配器等待并阻塞;Z_NOWAIT(0x0001):要求完全非阻塞,适用于自旋锁持有及其他抢占被禁的上下文;Z_NOPAGEWAIT(0x0002):允许分配器阻塞(典型如等待互斥锁),但内存压力下不等待可用页面。
语义要点:
- 除非 zone 是可耗尽的(exhaustible)或"特殊"(主要是 VM zone),否则
zalloc永不失败,但在 zone map 压力大时可能任意长时间阻塞。而由 VM 服务的kalloc分配不保证这点; - 若
Z_WAITOK与任一Z_NO*WAIT标志同时传入,Z_WAITOK被忽略; Z_NOFAIL(0x8000)意味着调用方假定分配必然成功,违反该假设将 panic;它与Z_NOWAIT/Z_NOPAGEWAIT不兼容,也不能用于可耗尽 zone(详见 zalloc.h);Z_ZERO(0x0004)保证返回的内存已清零。应优先使用它而非手动bzero:当某些已启用的安全特性已保证清零时,zone 分配器能把多余的清零优化掉。另外Z_VM_TAG_MASK位域可供 kalloc 传递vm_tag_t(zone tagging 调试特性)。
Zone Caching(zone 缓存)
分配/释放模式相对较快的 zone 可以在zone_create()时传ZC_CACHING(0x10)启用每 CPU 缓存,每个 CPU 持有若干份分配。这不该轻率开启,尤其对持有大元素的 zone——缓存会额外占用内存。对应地,ZC_NOCACHING(0x20)可强制关闭。源码中 zone caching 实际上构建在 per-cpu zone 之上。
类型混淆防护:Zone Sequestering 与zone_require()
为增强对 Use-after-Free(UaF)类 bug 的韧性,XNU 提供两种技术:
zone_create()传ZC_SEQUESTER(0x01)标志;- 手动调用
zone_require()或zone_id_require()。
Sequestering:某 zone 用过的虚拟地址范围永不再归还系统,即该地址范围被永久钉住、只放这一特定 zone 的分配。当 zone 是强类型时,意味着该地址上只可能存在这一种类型的对象。文档用 task zone 举例:task zone 同时使用 sequestering 和关键路径上的zone_require(),使得伪造一个task_t去混淆内核变得极其困难。
zone_require()(zalloc.h):在使用内存前断言该内存属于给定 zone,检查失败直接 panic(表明内核内部已被攻破)。其前提是:zone 不允许 foreign memory、且位于 general submap 中。zone_id_require()是其更快变体(按 zone ID 与元素大小校验,少一次加载和一次乘法,且不受zone_t::elem_size被破坏的影响,见 zalloc.h)。
当zone_require()能在所有咽喉点被穷尽使用时,sequestering 对该类型便不再必要。文档以ipc_port_t为例:它每次被使用前都会走ip_lock()或ip_reference(),这两个原语已扩展加入zone_id_require(),形成穷尽式保护,因此 ports zone 无需 sequestering——这对系统很重要:用户态可制造端口分配尖峰,sequestering 会放大 zone map 耗尽风险或显著增加描述该 zone 地址空间的开销。
IOKit 中的 Zone 使用
IOKit 是攻击者常攻击的子系统,XNU 因此暴露了"创建独立 zone 而非从 kalloc heap 分配"的能力。使用OSDefineMetaClassAndStructorsWithZone或其他OSDefineMetaClass.*WithZone变体,会使对象的operator new/operator delete用 zone 支撑存储。仓库中大量 IOKit 集合类即如此,例如 libkern/c++/OSData.cpp(OSDefineMetaClassAndStructorsWithZone(OSData, OSObject, ZC_ZFREE_CLEARMEM))、OSArray.cpp、OSDictionary.cpp 等。该能力只开放给第一方 kext,且应保留给那些"容易被用户态大量分配"、诱导碎片化可接受的类型。
自动清零(Auto-zeroing)
大量 bug 源自部分初始化的数据或 write-after-free。Zone 提供两级保护:
- 页面清理(page clearing):zone 新增页面时清除内容。zone 分配器的原始版本会把页面硬塞进 zone 而不改变内容;现在的版本会清除塞入 zone 的内存,缓解未初始化数据的泄漏/使用;
- 释放时元素清理(
ZC_ZFREE_CLEARMEM,0x02000000):更强的保护,zfree()把元素归还 zone 时擦除其内容;再次分配时会检查元素是否被篡改。当分配路径总是清零返回元素时(如zalloc/kalloc用Z_ZERO、MALLOC用M_ZERO),zone 分配器会省去这次多余清零。
文档写作时ZC_ZFREE_CLEARMEM默认对所有**元素小于 2 条缓存线(cachelines)**的 zone 生效。该技术尤其有效的原因:锁、引用计数、指针的合法状态不可能全为零,使 UaF 利用更困难。
毒化(Poisoning)
Zone 分配器还执行统计式毒化(细节见源码)。即使未启用ZC_ZFREE_CLEARMEM,释放时也总是清零任意分配的前 2 条缓存线,有时能缓解特定种类的线性缓冲区溢出;对把 refcount 或锁放在类型定义"靠前"位置的类型,零值对这类概念非法,故毒化可被此类类型利用(零即非法值)。
Per-CPU 分配
ZC_PERCPU(0x01000000)声明 per-cpu zone:从该 zone 分配会返回带已知步长(stride)的 NCPU 个元素。源码在 zalloc.h 提供配套访问接口:zalloc_percpu()/zfree_percpu()及zpercpu_get()、zpercpu_get_cpu()、zpercpu_foreach()宏——per-cpu 指针不能直接解引用,必须经这些接口按 CPU 槽位访问(指针会被 mangle/demangle,见ZPCPU_MANGLE_BIT)。
使用注意:
- 这类分配不应是快速分配模式,且不提供 zone caching(zone caching 实际构建在 per-cpu zone 之上);
- 因多核系统上巨大的放大系数(每 CPU 一份),per-cpu zone 应仅限极致性能敏感路径或全局计数器。
永久分配(Permanent Allocations)
内核有时需要持久分配:大小取决于非编译期常量、但不会随时间变化(NCPU 是典型例子)。zone 子系统提供zalloc_permanent*系列,以极紧凑的方式满足此类需求(zalloc.h):
- 允许任意大小,行为类似
kalloc; - 永不失败(分配失败会 panic);
- 总是返回清零内存;
- 尝试释放这些分配会触发 panic。
对应宏zalloc_permanent_type(type_t)会按类型的自然对齐分配;另有 per-cpu 版本zalloc_percpu_permanent*。ZONE_ID_PERMANENT、ZONE_ID_PERCPU_PERMANENT等预注册 zone ID(见 zalloc.h)即服务于这类分配。
kalloc:一个由 zone 组成的堆
Kalloc 是 malloc 风格的通用分配器:子页大小(文档写作时实际小于 32K,KASAN 等内存调试技术下可用负载上限可能更低)的分配由 zone 支撑,更大分配走kernel_memory_allocate(KMA)。
内核把支撑 kalloc 的 zone 集合称为 "kalloc heap",内置 3 个:
| 堆 | 服务对象 | 安全属性 |
|---|---|---|
KHEAP_DEFAULT | Core Kernel(XNU 本体)中的kalloc | 按 size-class 完全隔离(sequestered) |
KHEAP_KEXT | 内核扩展中的kalloc(见上表 "redirected" 符号) | 各 zone 自身 sequestered |
KHEAP_DATA_BUFFERS | 不含指针、倾向于受用户态控制的内容(路径、管道缓冲区、OSData 后备存储……) | 位于 "User Data" 子映射,与系统内任何其他分配都不可能地址混叠 |
KHEAP_DATA_BUFFERS的动机在 kalloc.h 有详细注释:这些对象(数据内容、分配/释放时机、大小均可控)长期被攻击者用于 heap spraying 以提升 UaF/溢出利用的可预测性,故整体隔离到独立子映射。
此外还有一个"魔法"堆KHEAP_TEMP:本质上是KHEAP_DEFAULT的别名,但强制额外语义——分配与释放必须在作用域内完成:分配该堆的线程必须是释放线程,且在返回用户态前必须释放完毕(kalloc.h)。KHEAP_TEMP用于支撑 syscall 的临时分配,会在返回用户态等检查点确保无未了结分配,一旦违反即 panic 系统。开发内核可用启动参数kheap_temp_debug=1调试此类问题——该参数在 osfmk/kern/kalloc.c 中以TUNABLE声明,panic 时若开启会打印分配调用栈(kheap_temp_leak_panic,kalloc.c)。仓库中KHEAP_TEMP的典型用法见 osfmk/kern/task.c(收集线程数组后于同一路径释放)与 osfmk/kern/stack.c。
KHEAP_ANY警告:源码为跨默认堆与 kext 堆释放提供了KHEAP_ANY(NULL值)——当内存来源不明时可用它随意释放。但注释明确指出:使用该常量的代码很可能成为释放任意内存的 gadget,强烈不建议使用(kalloc.h)。
实现层面,kalloc在 Core Kernel 中就是kheap_alloc(KHEAP_DEFAULT, size, Z_WAITOK)(kalloc.h),并有一系列带 allocation-site / tag 的扩展(kalloc_ext、kheap_alloc_tag_bt等)服务于 VM allocation-site 追踪调试;kfree/kheap_free宏会在释放前把指针置 NULL,防止悬挂指针残留。
记账:Zone Views 与 Kalloc Heap Aliases
zone 子系统提供多种记账属性,由zprint(1)命令报告。历史上,一些 zone 纯粹为记账而生,代价是碎片化上升(同一 zone 发出的分配越多,碎片化越低)。如今可以用zone views和kalloc heap aliases两个相似概念分别覆盖 zone 与 kalloc heap 的记账需求:
- Zone views:在头文件用
ZONE_VIEW_DECLARE声明、在模块用ZONE_VIEW_DEFINE定义(zalloc.h)。可以别名为另一普通 zone,或 kalloc heap 中的某个特定 size 区 zone。典型例子是ZV_NAMEI:临时路径从它分配,它是KHEAP_DATA_BUFFERS1024 字节 zone 的别名。仓库中zalloc(ZV_NAMEI)/zfree(ZV_NAMEI, ...)的实际调用遍布 BSD 层,如 bsd/kern/bsd_init.c、bsd/kern/decmpfs.c、bsd/kern/chunklist.c。zone view 的记账同样被zprint(1)报告; - Kalloc heap aliases:
KALLOC_HEAP_DECLARE/KALLOC_HEAP_DEFINE声明一个拥有独立记账的 kalloc heap 别名,尤其适合追踪泄漏等场景(kalloc.h、kalloc.h)。
实现细节:zone view 的记账在 view 层、分配后备存储复用目标 zone(struct zone_view含zv_zone、zv_stats、zv_name);zone view 与 kalloc heap view 均在STARTUP_SUB_ZALLOC阶段、STARTUP_RANK_LAST排名初始化,受内核 lockdown 保护、不可动态创建。
注意权衡:zone / heap view 的记账并非免费(有每 CPU 成本),应克制使用。但如果替代方案是新建一个完全独立的 zone,那么记账的内存成本大概率远小于新 zone 的碎片化成本。且目前 view 只能由 Core Kernel 创建(文档写作时的限制)。
源码地图:快速定位
- doc/allocators.md:本文对应的官方设计文档;
- osfmk/kern/zalloc.h:
zone_create/zalloc/zfree/zone_require/zalloc_permanent*/ per-cpu 接口 / zone views 的完整 API 与全部标志定义(ZC_*、Z_*); - osfmk/kern/kalloc.h:kalloc heaps 声明、
kalloc/kheap_alloc/kfree宏与KHEAP_*常量; - osfmk/kern/kalloc.c:kalloc 实现、
KHEAP_TEMP定义与kheap_temp_debug调试逻辑; - osfmk/kern/zalloc.c:zone 分配器实现、子映射管理(
zone_submaps、Z_SUBMAP_IDX_*)、zone_require与 sequestering 逻辑; - bsd/kern/bsd_init.c、bsd/kern/decmpfs.c:
ZV_NAMEIzone view 的落地使用示例; - libkern/c++/OSData.cpp:IOKit 对象以
OSDefineMetaClassAndStructorsWithZone使用 zone 支撑的示例。
结语
XNU 的内存分配体系呈现出清晰的层次:VM 页面分配为底座,zone 分配器提供高速固定大小对象 slab,kalloc 在其上堆叠出变长通用分配,再以 IOKitIOMalloc、C++new、BSDMALLOC等封装满足不同调用方契约。贯穿始终的是明确的安全设计:子映射隔离(bag of bytes)、sequestering 与zone_require()/zone_id_require()对抗类型混淆、ZC_ZFREE_CLEARMEM与毒化对抗 UaF、zone view 与 heap alias 实现无碎片化代价的精细记账。理解这张决策树与标志语义,是在 XNU 内核中写出既高效又安全的内存管理代码的前提。
- 操作系统
- 驱动开发
【免费下载链接】darwin-xnu
Legacy mirror of Darwin Kernel. Replaced by https://github.com/apple-oss-distributions/xnu
相关推荐
V8 Zone Allocator 深度解析:区域内存管理、bump pointer 分配与编译器中的应用
V8 Zone Allocator 深度解析:区域内存管理、bump pointer 分配与编译器中的应用 导读 Zone Allocator 是 V8 中一套
语言运行时编译器JIT编译解释器内存管理Pillow 内存分配器深度解析:Block Allocator 设计、内存池与调优环境变量
Pillow 内存分配器深度解析:Block Allocator 设计、内存池与调优环境变量 导读 :本文基于 block_allocator.rst http
图像处理计算机视觉Karabiner-Elements 中 Duktape 池分配器(Pool Allocator)低内存分配方案深度解析
Karabiner Elements 中 Duktape 池分配器(Pool Allocator)低内存分配方案深度解析 导读 本文基于 vendor/dukt
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考