☰
XNU 通用内存分配器深度指南:Zone Allocator、kalloc Heaps 与安全模型
2026/10/7 7:43:15 网站建设 项目流程
  • 操作系统
  • 驱动开发

【免费下载链接】darwin-xnu

Legacy mirror of Darwin Kernel. Replaced by https://github.com/apple-oss-distributions/xnu

项目地址:https://gitcode.com/gh_mirrors/da/darwin-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 提供两种内存分配途径:

  1. VM 子系统:以页面(page)为粒度提供分配,接口如kernel_memory_allocate及其同类(文档中记为 KMA)。这类分配适合大块、页面对齐的内存需求。
  2. 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)。

InterfaceCore XNUPrivate ExportPublic ExportComments
Core primitives(核心原语)
zallocYesYesNo受实现限制,zone 的数量有限。在该限制解除前,向任意内核扩展开放原始 zalloc 是有问题的。
kheap_allocYesNoNo这才是kalloc的真正核心实现,详见 kalloc heaps 一节。
kallocYesYes, RedirectedNo在 XNU 内核中kalloc等价于kheap_alloc(KHEAP_DEFAULT);在内核扩展(kext)中等价于kheap_alloc(KHEAP_KEXT)。由于历史契约允许在 XNU/kext 边界两侧分别分配与释放,kfree允许释放到任一堆;新代码应改用对应的kheap_*变体。
Popular wrappers(常用封装)
IOMallocYesYes, RedirectedYes, RedirectedIOMalloc是kalloc的直接封装,行为与kalloc一致,并集成 IOKit 的调试特性,是驱动(Driver)应使用的分配器。只有提供核心基础设施(文件系统、sandbox 等)的树外核心内核组件,才应直接使用原语zalloc/kalloc。
C++ newYesYes, RedirectedYes, RedirectedXNU 实现了 C++ 各种new/delete操作符,重定向到KHEAP_KEXTkalloc 堆(Core Kernel 内部不使用 C++ 默认 operator new)。用 IOKit 宏创建OSObject子类时,会为该对象提供专属operator new/operator delete:类定义在 Core XNU 中锚定到KHEAP_DEFAULT堆,定义在内核扩展中则锚定到KHEAP_KEXT堆。
MALLOCYesObsolete, RedirectedNo遗留 BSD 接口,行为基本等同kalloc。对 kext,FREE()因历史原因允许释放到KHEAP_DEFAULT或KHEAP_KEXT。
Obsolete wrappers(废弃封装)
mcacheYesKindaKindamcache/mbuf 子系统主要由 BSD 网络栈使用;不与其接口交互的代码不应采用 mcache。
OSMallocNoObsolete, RedirectedObsolete, Redirected<libkern/OSMalloc.h>是遗留子系统,记账极慢且不可扩展,新代码一律不用,改用IOMalloc。
MALLOC_ZONENoObsolete, RedirectedNo曾是zalloc的怪异封装且安全保证更差,已从 XNU 彻底移除。为向后兼容仍导出符号,但行为与MALLOC完全一致。
kern_os_*NoObsolete, RedirectedObsolete, 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 提供两种技术:

  1. zone_create()传ZC_SEQUESTER(0x01)标志;
  2. 手动调用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_DEFAULTCore 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

项目地址:https://gitcode.com/gh_mirrors/da/darwin-xnu
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询