这是一个以安全为核心的通用内存分配器,提供 malloc API 以及各种扩展。它针对堆破坏漏洞提供了强有力的加固。以安全为核心的设计还使得元数据开销和碎片化导致的内存浪费远低于更传统的分配器设计。它旨在提供良好的整体性能,重点关注长期性能和内存使用,而非分配器微基准测试。它通过可配置数量的完全独立 arena 提供可扩展性,arena 内部的锁进一步按大小类划分。
本项目目前支持 Bionic(Android)、musl 和 glibc。未来可能会支持其他非 Linux 操作系统。对于 Android,有自定义集成和其他加固特性,这些也计划在未来用于 musl。对 glibc 的支持将仅限于替换 malloc 实现,因为 musl 是更健壮、更干净的构建基础,并且可以覆盖相同的用例。
此分配器旨在作为先前实现的继任者,该先前实现基于扩展 OpenBSD malloc 并加入各种额外安全特性。它仍然大量基于 OpenBSD malloc 的设计,尽管除了复用哈希表实现之外,并不基于现有代码。设计上的主要区别在于:它只专注于加固而非查找 bug;使用更细粒度的大小类以及超过 4k 的 slab 大小以减少内部碎片;不依赖内核提供细粒度的 mmap 随机化;并且仅面向 64 位以积极利用大地址空间。实现方法上还有许多较小的差异。它整合了先前对 OpenBSD malloc 所做的扩展,包括为 canary 在分配中添加填充(不同于当前 OpenBSD malloc 的 canary)、与现有释放时清零相关联的写后释放检测、在现有随机化数组之外增加队列以隔离分配,以及对被隔离分配进行适当的双重释放检测。每个大小类拥有各自随机基址的内存区域,其灵感大致来自 PartitionAlloc 中基于大小和类型的分区。对 OpenBSD malloc 的计划变更最终变得过于广泛和侵入性,因此本项目作为全新实现启动,以更好地实现目标。对于 32 位,可以改用带有小型扩展的 OpenBSD malloc 移植版,因为此分配器从根本上不支持该环境。
Debian stable(当前为 Debian 13)决定了最古老的受支持依赖集:
对于 Android,支持 Linux GKI 6.1、6.6 和 6.12 分支。
但是,强烈建议使用更新的版本。较旧版本的依赖项目前可能兼容,但未经测试,并且明确不受支持。
对于使用 musl 进行外部 malloc 替换,需要 musl 1.1.20。不过,未来将提供性能更好的自定义集成,以及对 C 标准库实现的其他加固。
对于 Android,仅支持当前一代、正在积极开发的 Android 开源项目维护分支,目前即 android17-release。
preload.sh 脚本可用于测试使用 glibc 或 musl 的动态链接可执行文件:
./preload.sh krita --new-image RGBA,U8,500,500
可能需要大幅提高 vm.max_map_count sysctl 值,以容纳由保护 slab 和大型分配保护区域造成的大量映射。通过大幅增加 CONFIG_GUARD_SLABS_INTERVAL 也可以显著减少映射数量,但该特性具有较低的性能和内存使用成本,因此不建议这样做。
将其集成到 C 标准库中时可以提供略好的性能,并且在 C 标准库和动态链接器实现中还有其他类似加固的机会。例如,可以实现一个库区域,为动态库提供与此分配器在不同大小类之间提供的类似隔离。其意图是将其作为 Bionic 和 musl C 标准库的加固变体的一部分提供。
提供了一组简单的自动化测试,可以使用 make 命令运行如下:
make test
需要 OpenSSH 8.1 或更高版本,以允许在 seccomp-bpf 过滤器中执行 mprotect PROT_READ|PROT_WRITE 系统调用,而不是终止进程。
在 GrapheneOS 上,hardened_malloc 作为标准 malloc 实现集成到标准 C 库中。其他基于 Android 的操作系统可以复用集成代码来提供它。如果需要,可以通过仅有条件地使用 hardened_malloc 将 jemalloc 保留为运行时配置选项,让用户在性能和安全性之间进行选择。然而,这会降低持久状态不受信任的威胁模型的安全性,即验证启动和证明(参见证明姊妹项目)。
确保也大幅提高 vm.max_map_count,以容纳 hardened_malloc 创建的大量保护页。这可以在 init.rc(system/core/rootdir/init.rc)中靠近其他虚拟内存配置的位置完成:
write /proc/sys/vm/max_map_count 1048576
如果在构建配置中将 CONFIG_GUARD_SLABS_INTERVAL 设置为非常大的值,则无需这样做。
在传统基于 Linux 的操作系统上,hardened_malloc 既可以作为标准 malloc 实现的替代品集成到 libc 实现中,也可以作为动态库加载。无需重新构建每个可执行文件以链接到它,而是可以将其作为预加载库添加到 /etc/ld.so.preload。例如,将 libhardened_malloc.so 安装到 /usr/local/lib/libhardened_malloc.so,然后将该完整路径作为一行添加到 /etc/ld.so.preload 配置文件中:
/usr/local/lib/libhardened_malloc.so
此配置文件的格式是以空白分隔的列表,因此将每个库放在单独的行上是一种好做法。
为了获得最大兼容性,可以将 libhardened_malloc.so 安装到 /usr/lib/,以避免由 AppArmor 配置文件或 systemd ExecPaths= 限制导致的预加载失败。检查以下格式的日志:
ERROR: ld.so: object '/usr/local/lib/libhardened_malloc.so' from /etc/ld.so.preload cannot be preloaded (failed to map segment from shared object): ignored.
使用 LD_PRELOAD 环境变量按需加载它,在设置了 AT_SECURE 的情况下(例如 setuid 二进制文件)将无法工作。这通常也不是生产使用的推荐做法。推荐的做法是全局启用它,并对性能关键的情况做例外处理,即在未启用它的容器/命名空间中运行应用程序。
还要确保大幅提高 vm.max_map_count,以容纳 hardened_malloc 创建的大量保护页。例如,在 /etc/sysctl.d/hardened_malloc.conf 中:
vm.max_map_count = 1048576
如果你在构建配置中将 CONFIG_GUARD_SLABS_INTERVAL 设置为一个非常大的值,则无需这样做。
在 arm64 上,请确保你的内核配置为使用 4k 页,因为我们尚未添加对 16k 和 64k 页的支持。内核还必须配置为使用 4 级页表以支持完整的 48 位地址空间,而不是在默认 hardened_malloc 配置下仅有 39 位地址空间。可以大幅减小 class 区域大小以使 39 位地址空间可用,但默认配置将无法工作。
你可以在编译时通过向 make 命令传递参数来设置一些配置选项,如下所示:
make CONFIG_EXAMPLE=false
配置选项是在可移植性、性能、内存使用或安全性之间存在重大权衡时提供的。核心设计选择不可配置,即使禁用所有可选功能,分配器仍然非常注重安全性。
配置系统支持配置模板系统,包含两个标准预设:默认配置(config/default.mk)和轻量配置(config/light.mk)。强烈建议打包者同时提供标准的 default 和 light 配置。你可以使用 make VARIANT=light 选择要构建的配置,其中 make VARIANT=default 与 make 相同。非默认配置模板将构建带有 -variant 后缀的库,例如 libhardened_malloc-light.so,并将使用 out-variant 目录而不是 out 进行构建。
default 配置模板启用了所有正常的可选安全功能(只是不包括小众的 CONFIG_SEAL_METADATA),并且在为安全性牺牲性能和内存使用方面相当激进。light 配置模板禁用了 slab 隔离、释放后写入检查、槽位随机化,并将保护 slab 间隔从 1 提高到 8,但保留了释放时清零和 slab canary。light 配置具有稳定的性能和内存使用,同时仍然比主流分配器安全得多,具有更好的安全属性。禁用释放时清零会获得更多性能,但在不同时禁用 slab canary 的情况下,对小分配没有太大区别。Slab canary 会略微增加内存使用并略微降低性能,但对于缓解小溢出和 C 字符串溢出非常重要。在大多数情况下不建议禁用 slab canary,因为那样它就不再是对传统分配器(在分配上带有头部并对其进行基本一致性检查)的严格升级。
为了在牺牲性能的情况下减少内存使用(这也会减小空 slab 缓存和隔离区的大小,节省大量内存,因为它们目前基于最大 size class 的大小):
make \
CONFIG_N_ARENA=1 \
CONFIG_EXTENDED_SIZE_CLASSES=false
以下布尔配置选项可用:
CONFIG_WERROR:true(默认)或 false,用于控制是否将编译器警告视为错误。强烈建议启用,但如果使用的编译器版本未经项目测试且存在警告,可以禁用该选项以避免修改 Makefile。仍然建议调查这些警告,目标是始终保持无任何警告。CONFIG_NATIVE:true(默认)或 false,用于控制是否针对主机上检测到的 CPU 优化代码。如果禁用此选项,强烈建议设置高于基线架构的自定义 -march,因为此代码可获得显著的性能提升。CONFIG_CXX_ALLOCATOR:true(默认)或 false,用于控制是否替换 C++ 分配器,以略微提升性能并检测大小不匹配的有尺寸释放(通常是类型混淆漏洞)。这将导致链接到 C++ 标准库。CONFIG_ZERO_ON_FREE:true(默认)或 false,用于控制是否在释放时将小分配清零,以缓解释放后使用和未初始化使用漏洞,并尽快从进程中清除大量潜在敏感数据。这会产生随分配大小而扩展的性能开销,通常可以接受。这与大分配无关,因为页面会归还给内核。CONFIG_WRITE_AFTER_FREE_CHECK:true(默认)或 false,用于控制是否对新小分配包含零化内存进行健全性检查。这可以检测由释放后写入漏洞引起的写入,并与使内存重用随机化/延迟的功能很好地配合。这会产生随分配大小而扩展的性能开销,通常可以接受。这与大分配无关,因为它们始终是来自内核的新内存映射。CONFIG_SLOT_RANDOMIZE:true(默认)或 false,用于随机化 slab 内空闲槽的选择。这会产生可测量的性能开销,且并非重要的安全功能之一,但该开销被认为完全可以接受,因此默认启用。CONFIG_SLAB_CANARY:true(默认)或 false,用于启用向内存分配末尾添加 8 字节 canary 的支持。canary 的主要目的是通过吸收小型固定大小缓冲区溢出来使其无害。canary 的第一个字节始终为零,用于包含由缺少 C 字符串 NUL 终止符引起的溢出。其余 7 个字节是每个 slab 的随机值。释放时会检查 canary 的完整性,以检测线性溢出或由不精确利用原语引起的其他形式的堆损坏等攻击。然而,释放时检查往往太晚,无法阻止利用,因此这不是 canary 的主要目的。CONFIG_SEAL_METADATA:true 或 false(默认),用于控制是否使用内存保护密钥来禁止在内存分配器代码之外访问所有可写的分配器状态。由于在当前一代硬件上此用例存在显著的性能开销,目前默认禁用,未来该开销可能会大幅降低。无论是否启用此功能,元数据都包含在一个隔离的内存区域中,周围环绕着高熵随机保护区域。以下整数配置选项可用: