Android 系统属性
系统属性
Android Property 实现解析与黑魔法 - 残页的小博客
bionic
属性系统概述
bionic/libc/bionic/system_property_api.cpp
bionic/libc/include/sys/_system_properties.h
属性系统实际上就是一个整个系统共享的键值对存储器,通过共享文件内存映射实现。
存放属性的文件位于 /dev/__properties__
,这下面的多个文件,实际上是将属性按照前缀划分为不同的 SELinux 上下文,存放在对应的文件中。
划分的规则可以在 /{system,vendor,...}/etc/selinux/{plat,...}_property_contexts
等文件找到。
init 进程负责初始化属性存储区域,且是系统中唯一能够修改属性的进程(当然,只要某个进程能 rw 映射存放属性的文件,也是可以修改的),其他进程通过属性服务(一个名为 property_service 的 socket)修改属性。其他进程会以 ro 形式共享映射属性存储,用于读取。
初始化
一般进程:
bionic/libc/bionic/libc_init_common.cpp __libc_init_common
__system_properties_init
只能映射 ro 的属性区域共享内存
init:
system/core/init/property_service.cpp PropertyInit
__system_property_area_init
创建属性区域,并映射 rw 共享内存
结构
bionic/libc/system_properties/include/system_properties/prop_area.h
bionic/libc/system_properties/prop_area.cpp
prop_area 结构体对应了整个存储区域的结构。大小 128K ,前 32*4 字节包含已分配字节、用于同步的原子 serial、魔数和版本
1 | struct prop_trie_node { |
prop_trie_node(prop_bt) 是二叉搜索树。属性名按 .
分割为数个层级,每个层级都由二叉搜索树用于按名字索引。prop_area 数据区的第一个对象(偏移 0)就是根结点,其 children 是第一层级的根树。可以看到,每个结点还要记录自己的名字(在最末尾)。
这一点在源码的注释已经说得比较清楚了,总的来说,这就是一个二叉搜索树 + 字典树的混合体。
1 | // Properties are stored in a hybrid trie/binary tree structure. |
bionic/libc/system_properties/include/system_properties/prop_info.h
prop_info 就是属性对象,记录了属性的值。这个值可以是最长 92 字节的短值,也可以是更长的长值。长值会作为独立的对象进行分配。
1 | struct prop_info { |
对象分配
用于分配 prop_trie_node 、 prop_info 和 long value 。所有对象都在内存区域上依次分配。
1 | // bionic/libc/system_properties/prop_area.cpp |
prop_area 没有实现值的释放,因为 prop_info 是一个固定大小的结构体,只要已经分配过 prop 的结点,今后改写都在这个结点进行。而 long value 是 ro 属性专属的特性(众所周知 ro 不需要修改,且 bionic 也没有实现修改 long value 属性)。
Magisk resetprop
Magisk resetprop 并没有调用系统实现,而是根据 bionic 自行实现了 property 的操作。
在这里, __system_property_xxx
不是系统 API ,而是自己实现的,原始的系统 API 通过 dlsym 获得。
就在今年五月的一个提交后,该模块从 magisk 独立出来
https://github.com/topjohnwu/system_properties
实际上大部分源码都照搬 bionic ,属性的初始化也是从 __system_properties_init
-> SystemProperties::Init
,即一般进程的调用,映射的是 ro 属性区域。
不同之处在于给 prop_area::map_fd_ro
加了一个 rw 映射的实现,并且这个 rw 是可选的,如果不能 rw 才映射 ro 。
此外该库还实现了属性的 delete
1 | bool prop_area::remove(const char *name, bool prune) { |
方法是将 prop info 及 long value 填 0 ,并从 prop_bt 删除 prop ,还可能执行 prune ,会把没有 prop info 的树结点也全部填 0 。
为了实现修改 ro 属性,需要删除原先的 ro 属性,再用 add 重新分配结点(因为 update 不允许增加 long value)
权限检查
曾经尝试提权某 tv os ,由于其 selinux permissive ,利用 magica 拿到受限的 root ,尝试修改属性的 ro.debuggable 进一步提权,但是没有文件系统的 cap ,因此直接修改属性文件权限为 777 ,这样确实可以直接写文件了,但是 resetprop 报错初始化失败,启动其他新程序也无法正确初始化属性,将权限恢复后才正常,可能原因来自于 prop_area map_fd_ro 的一段检查:
1 | prop_area* prop_area::map_fd_ro(const int fd) { |
属性更新的三种通知方式
property wait
property area 和 property info 都有一个 atomic 的 serial ,可以对其使用 futex wait 监听变化。
system_properties 的 Add 和 Update 会负责更新这个 serial 。
LSPosed 中利用了 property wait ,监视 persist.log.tag
并立即重置。
P.S. 看代码的时候发现似乎前几个月 mainline 又增加了 appcompat override contexts 这种申必玩意
init trigger
通过 init 的 property service 设置 property 会触发 init 的 on property trigger ,如果通过 resetprop 等手段直接修改 property area 内存,不经过服务,则不会触发(?)。
binder transaction
transaction code = _SPR
(1599295570)
这个 transact 可以通过任意 binder触发其所属进程的属性更新回调。
用于 java 层的 SystemProperties.addChangeCallback
,显示布局边界(debug.layout
)的属性就是用这种方法派发更新的,以此为例分析整个过程。
由于属性在设置中修改,因此也是设置负责分发这个 transact 。
settings 分发给所有可访问的 binder service ,送达 activity 服务后又会转发给所有 app 进程的 IApplicationThread 。
通知给进程本地的 WindowManagerGlobal 的 callback ,最终派发到 ViewRootImpl ,更新 attachInfo 并 invalidate 。
WMG callback -> VRI.loadSystemProperties
View: isShowingLayoutBounds
debugDrawFocus
负责绘制