
1. 项目概述为什么我们需要为Android新增服务定义SELinux标签如果你在Android系统开发或设备定制比如ROM移植、内核适配中尝试添加一个由init启动的守护进程或服务十有八九会遇到服务启动失败或者在日志中看到一堆“avc: denied”的SELinux拒绝信息。这通常不是你代码写错了而是系统安全沙箱在“尽职尽责”地阻止你的服务执行未经授权的操作。今天要聊的就是如何为这个新服务“上户口”——即定义其SELinux域domain和文件标签让它能在Android的安全框架内合法、合规地运行。简单来说Android SELinuxSecurity-Enhanced Linux是一套强制访问控制MAC系统。它不像传统的Linux自主访问控制DAC靠文件权限rwx那样由进程所有者决定能访问什么。SELinux为每个进程称为主体subject和每个系统资源称为客体object如文件、套接字、设备节点都打上了一个“安全上下文”security context标签。所有的访问请求比如进程A想读文件B都会经过SELinux策略库的检查“进程A的标签是否有权限对标签为B的文件执行读操作”如果策略说“可以”则放行说“不行”则拒绝并记录日志。当你新增一个系统服务时它默认可能运行在一个非常受限的域比如init域或者根本没有明确定义的域导致它几乎什么都做不了。你的任务就是定义一个新的域例如my_custom_service明确这个服务是谁。为服务的可执行文件打上正确的标签例如my_custom_service_exec告诉SELinux“执行这个文件时请切换到my_custom_service域”。为这个新域编写策略规则精确授予它完成本职工作所必需的权限遵循“最小权限原则”。这个过程看似繁琐但它是构建安全、稳定的Android系统的基石。下面我将以一个虚构的“Foo”服务为例手把手带你走完从零开始定义服务标签、编写策略到调试的完整流程并分享一些官方文档不会明说但在实际项目中反复踩坑才总结出的经验。2. 核心概念与准备工作理解SELinux策略的组成在动手之前我们需要快速理清Android SELinux策略的几个核心文件和概念这能帮你理解我们每一步在做什么而不是机械地复制粘贴。2.1 关键策略文件类型在AOSP或设备厂商的sepolicy目录下你会看到一堆以特定后缀结尾的文件它们各司其职.te(Type Enforcement) 文件这是策略的核心。我们在这里定义类型Type、属性Attribute并编写访问向量规则AV Rules也就是allow、neverallow等语句。我们为服务创建的新域其规则就写在这里。file_contexts文件文件标签的“地图”。它定义了在文件系统创建时或通过restorecon命令哪些路径下的文件应该被赋予什么样的安全上下文标签。为我们服务的可执行文件打标签就是在这个文件里添加一条记录。property_contexts文件定义Android系统属性getprop/setprop看到的那些的安全上下文。如果你的服务需要读取或设置某些系统属性可能需要在这里配置。service_contexts文件定义Binder服务名称的安全上下文。如果你的服务通过Binder对外提供接口需要在这里注册。genfs_contexts文件为内核虚拟文件系统如proc、sysfs中的文件或目录定义安全上下文。mac_permissions.xml文件与应用签名和权限组相关的MAC策略通常与应用沙箱相关与系统服务关系不大。对于新增一个简单的init服务我们最常打交道的就是.te文件和file_contexts文件。2.2 安全上下文标签的格式一个完整的安全上下文通常长这样u:object_r:foo_exec:s0它由四部分组成用冒号分隔u(User) SELinux用户。在Android中几乎所有进程和文件都使用u代表unconfined用户这是一个历史遗留的简化设计我们可以暂时忽略它。object_r(Role) SELinux角色。对于客体文件、设备等角色几乎总是object_r。对于主体进程角色通常是r如u:r:mediaserver:s0。foo_exec(Type)类型这是SELinux策略中最重要的部分。它标识了客体或主体域的具体类别。我们为可执行文件定义的类型通常是xxx_exec而为进程域定义的类型就是xxx。s0(Level) MLS/MCS级别。在Android中这通常用于多级安全或类别大多数情况下都是s0表示单一安全级别。一个关键理解当init进程执行一个标签为u:object_r:foo_exec:s0的可执行文件时SELinux会检查策略。如果策略中定义了foo域并且有规则通常是type_transition或init_daemon_domain宏允许从init域转换到foo域那么新创建的进程就会运行在u:r:foo:s0这个域下而不是init域。2.3 工作环境与调试工具准备在开始修改策略前确保你的环境就绪获取设备root权限或使用userdebug/eng版本你需要能访问系统分区和查看完整内核日志。通常刷入一个userdebug版本的ROM是最方便的。准备好adb和串口调试adb logcat和adb shell是基础。对于早期启动阶段的SELinux拒绝串口日志可能更可靠。熟悉关键命令adb shell getenforce 查看SELinux当前是强制Enforcing模式还是宽容Permissive模式。adb shell setenforce 0 临时切换到宽容模式重启失效。这是初期调试的救命稻草可以让你先忽略拒绝使服务运行起来同时收集完整的拒绝日志。adb shell dmesg | grep avc或adb logcat -b all | grep avc 抓取SELinux的拒绝日志avc: denied。adb shell ls -Z /system/bin/foo 查看文件/system/bin/foo当前的安全上下文标签。adb shell ps -Z | grep foo 查看foo进程运行在哪个安全上下文中。3. 实战为“Foo”服务新增SELinux策略全流程假设我们有一个名为foo的守护进程源码编译后生成可执行文件/system/bin/foo由init.rc启动。现在我们要让它拥有自己的SELinux域foo。3.1 第一步在宽容模式下运行并收集拒绝日志永远不要一开始就在强制模式下修改策略。你可能会被第一个拒绝卡住而后面隐藏的十几个拒绝你根本看不到。将设备全局设为宽容模式在BoardConfig.mk中修改内核命令行添加androidboot.selinuxpermissive然后重新编译并刷写boot镜像。或者在设备启动后通过adb执行setenforce 0。建议采用前者以确保从启动伊始就是宽容模式。添加你的服务到init.rc在device/vendor/device/init.device.rc或类似文件中添加。service foo /system/bin/foo class core user system group system seclabel u:r:foo:s0 # 可选但建议加上明确指定期望的域注意seclabel行它直接告诉init“请以foo域启动这个服务”。如果foo域尚未定义或策略不允许服务会启动失败。初期调试时可以先注释掉这行让服务以默认域可能是init运行我们先收集权限需求。编译并刷写系统镜像启动设备。触发你的服务确保服务被启动可以通过start foo命令。收集avc拒绝日志adb shell “cat /proc/kmsg | grep avc” avc_denials.txt # 或者 adb logcat -b all -d | grep “avc:” avc_denials.txt你会得到一堆类似下面的记录avc: denied { open } for pid1234 comm“foo” path/dev/mydevice dev“tmpfs” ino123 scontextu:r:init:s0 tcontextu:object_r:device:s0 tclasschr_file permissive1日志解读denied { open } 被拒绝的操作是open。pid1234 comm“foo” 发起操作的进程是foo。scontextu:r:init:s0主体上下文。进程当前运行在init域。这正是因为我们还没定义foo域。tcontextu:object_r:device:s0客体上下文。目标文件/dev/mydevice的标签是device一个非常通用的设备类型。tclasschr_file 客体类别是字符设备文件。3.2 第二步创建SELinux策略文件现在我们在设备的策略目录通常是device/vendor/device/sepolicy/下创建必要的文件。创建域定义文件foo.te# 文件路径device/vendor/device/sepolicy/foo.te # foo服务策略 type foo, domain; # 声明foo是一个进程域类型 type foo_exec, exec_type, file_type, vendor_file_type; # 声明foo_exec是可执行文件类型并继承一些属性 # 关键宏定义从init域到foo域的自动转换规则。 # 当init执行标签为foo_exec的文件时新进程将进入foo域。 init_daemon_domain(foo)type foo, domain; 这行定义了foo是一个域类型domain用于标识进程。type foo_exec, exec_type, file_type, vendor_file_type; 定义了foo_exec类型用于标识可执行文件。exec_type和file_type是预定义的属性赋予该类型作为可执行文件和普通文件的基本权限。vendor_file_type是Android 8.0以后推荐添加的表明这是厂商文件。init_daemon_domain(foo)这是一个极其重要的宏。它展开后包含了一系列规则核心是允许init进程执行标签为foo_exec的文件并自动进行类型转换type_transition使新进程进入foo域。它还设置了foo域的一些基本权限如向init发送信号。在file_contexts中为可执行文件打标签# 文件路径device/vendor/device/sepolicy/file_contexts # 为foo可执行文件设置标签 /system/bin/foo u:object_r:foo_exec:s0这行告诉文件系统当在/system/bin/路径下创建或恢复foo文件的安全上下文时将其标签设置为u:object_r:foo_exec:s0。3.3 第三步根据拒绝日志编写权限规则回到第一步收集的日志。假设我们收集到这样一条avc: denied { read write } for pid1234 comm“foo” name“mydevice” dev“tmpfs” ino123 scontextu:r:foo:s0 tcontextu:object_r:device:s0 tclasschr_file permissive1注意现在scontext变成了u:r:foo:s0说明我们的域定义和文件标签生效了进程已经在foo域中运行但缺少访问/dev/mydevice的权限。错误的做法新手常犯直接使用audit2allow工具生成allow foo device:chr_file { read write };。这虽然能解决问题但违反了“最小权限原则”且可能触发neverallow规则。device是一个通用标签授予foo访问所有device标签的设备的权限是过度授权。正确的做法为特定的设备节点定义更精确的标签。创建或修改设备节点标签 查看设备现有的file_contexts看是否有类似设备的标签模式。例如GPU设备可能叫gpu_device音频设备叫audio_device。如果没有我们需要为/dev/mydevice创建一个新的类型。 在foo.te的同目录下可能有一个device.te或vendor_device.te文件用于声明设备类型。我们添加# 文件路径device/vendor/device/sepolicy/vendor_device.te (举例) type my_custom_device, dev_type;然后在file_contexts中为其打标签# 文件路径device/vendor/device/sepolicy/file_contexts /dev/mydevice u:object_r:my_custom_device:s0授予foo域访问权限 现在我们可以在foo.te中授予精确的权限# 文件路径device/vendor/device/sepolicy/foo.te # 允许foo域对my_custom_device字符设备进行读写 allow foo my_custom_device:chr_file { open read write ioctl };权限粒度{ open read write ioctl }是一个常见的字符设备操作集合。你应该根据实际需要的操作来授权比如可能不需要ioctl。查看其他类似服务的.te文件是很好的学习方式。处理其他拒绝重复上述过程。常见的权限需求包括文件操作allow foo system_data_file:dir search;允许搜索目录Binder调用binder_call(foo, servicemanager)允许向servicemanager注册或调用网络访问allow foo netd:udp_socket { create ioctl };等。属性读写需要在property_contexts中定义并在.te中使用get_prop(foo, some_property)或set_prop(foo, some_property)。访问sysfs 需要在genfs_contexts中为sysfs路径打标签然后授予sysfs文件类型的权限。3.4 第四步编译、刷写与测试编译策略在AOSP根目录执行m或mma来编译你的策略。策略文件会被编译进boot.img对于平台策略或vendor.img对于厂商策略的ramdisk中。刷写镜像刷写更新后的boot.img和system.img或vendor.img。重启并检查adb shell getenforce确认模式。adb shell ls -Z /system/bin/foo确认文件标签是否正确。adb shell ps -Z | grep foo确认进程是否运行在foo域。启动服务查看新的avc拒绝日志。重复第三步直到没有与服务功能相关的拒绝出现。3.5 第五步切回强制模式并最终验证当在宽容模式下你的服务能正常运行且没有新的、功能相关的avc拒绝时就可以考虑切回强制模式了。移除宽容模式设置从BoardConfig.mk中移除androidboot.selinuxpermissive。重新编译并刷写boot镜像。重启设备此时getenforce应返回Enforcing。进行全面功能测试在强制模式下测试服务的所有功能。因为宽容模式下一些边缘路径的访问可能没被触发。监控日志持续关注dmesg和logcat确保没有新的、未预期的拒绝。如果出现回到第三步处理。4. 高级技巧与避坑指南来自实战的经验官方流程看似清晰但实际项目中陷阱重重。下面这些经验能帮你节省大量排查时间。4.1 善用宏和属性而不是重复造轮子Android SELinux预定义了大量的宏macro和属性attribute。使用它们能让策略更简洁、更安全。属性Attribute 如file_type,dev_type,domain等。当你将类型声明为某个属性时它就自动拥有了该属性关联的一组基本权限或规则。例如type my_device, dev_type;意味着my_device自动继承了所有针对dev_type的通用规则。宏Macro 如init_daemon_domain,binder_call,r_dir_file等。它们是一组常用规则的集合。例如r_dir_file(foo, bar_data_file)宏展开后会允许foo域对bar_data_file类型的文件和目录执行读、打开、获取属性等操作这比手动写一堆allow规则要安全、方便得多。操作心得在添加一条allow规则前先搜索AOSP的te_macros文件如system/sepolicy/public/te_macros看看是否有现成的宏可用。使用宏能减少错误并保持策略与AOSP最佳实践的一致性。4.2 理解并处理“neverallow”规则冲突有时你添加的allow规则在编译时会报错提示违反了neverallow规则。例如libsepol.report_failure: neverallow on line xxx of system/sepolicy/public/domain.te violated by allow foo unlabeled:file { open };这意味着你试图授予的权限被系统全局的neverallow规则明确禁止了。永远不要试图去修改AOSP核心的neverallow规则。正确的做法是检查客体标签是否正确 如上例中的unlabeled这通常表示文件没有被正确标记。你应该确保你的文件如/data/foo/bar在file_contexts中有正确的标签而不是试图去访问unlabeled。检查是否使用了错误的客体类型 你可能需要为你的资源定义一个更具体的类型而不是使用通用的、被禁止的类型。检查操作是否真的必要 也许你的服务设计有问题需要访问一个本不该访问的资源。考虑调整架构。4.3 调试“隐式拒绝”与服务启动顺序有时候服务启动失败但日志里没有明显的avc: denied。这可能是因为SELinux布尔值boolean 某些权限被布尔值控制。使用adb shell getsebool -a查看并使用adb shell setsebool boolean_name on临时开启。如果有效可以在foo.te中用set_prop(foo, boolean_name)或在*.rc文件中用setprop来设置。服务启动顺序 如果你的服务在init阶段很早启动如class core但其所依赖的某个资源如某个设备节点、挂载点的SELinux标签是在稍后的阶段如late-init才由restorecon或ueventd设置的那么你的服务在启动时访问的可能是默认的、错误的标签。确保文件标签在服务访问前就已正确应用。权限问题与DAC冲突 记住SELinux是MAC在DAC之后检查。如果文件的Unix权限rwx不允许访问SELinux检查根本不会触发。先用ls -l检查文件权限和所有者。4.4 策略的存放位置平台、系统扩展还是供应商从Android 8.0Treble开始SELinux策略被清晰地分层平台策略 (system/sepolicy/) AOSP通用策略强烈不建议修改。系统扩展策略 (system/extensions/sepolicy/) 用于系统映像system.img中系统扩展的策略。供应商策略 (vendor/vendor/sepolicy/或device/vendor/device/sepolicy/) 设备制造商OEM/Vendor的专用策略。我们新增的服务策略绝大多数情况都应该放在这里。将策略放在正确的位置有助于通过VTSVendor Test Suite测试并确保系统OTA更新时策略能正确保留或合并。4.5 使用audit2allow的正确姿势audit2allow是一个从avc拒绝日志自动生成allow规则的工具但它是一把双刃剑。adb shell “cat /proc/kmsg | grep avc” | audit2allow -p vendor_sepolicy.cil它只是一个“提示生成器”而不是“策略生成器”。直接使用其输出会导致策略臃肿和过度授权。你应该归纳合并 将多条相似的拒绝合并成一条规则。具体化客体类型 将通用的客体类型如device,default_prop替换为更具体的、你自己定义的类型。检查必要性 逐条审视生成的规则问自己我的服务真的需要这个权限吗有没有更安全的方式5. 常见问题排查实录那些年我们踩过的坑这里记录几个真实项目中高频出现的问题和解决方案。问题一服务启动失败日志显示“Could not set SELinux context: ... Invalid argument”现象 在init.rc中设置了seclabel u:r:foo:s0但服务无法启动。排查检查foo.te中是否正确定义了type foo, domain;。检查init_daemon_domain(foo)宏是否被调用。没有这个宏init没有权限将进程转换到foo域。检查是否有neverallow规则阻止了这种域转换。编译整个系统时关注sepolicy编译错误。解决 确保.te文件语法正确且foo域已被正确定义并允许从init转换。问题二文件标签在重启后恢复默认现象 在file_contexts中添加了标签刷机后第一次启动生效但重启后又变回了默认标签如vendor_file。排查 这通常是因为你的文件路径在/vendor分区而该分区的file_contexts在启动时被多次应用。可能存在优先级更高的file_contexts文件覆盖了你的设置。解决确认文件所在的分区/system,/vendor,/product。检查对应分区的file_contexts文件如/vendor/etc/selinux/vendor_file_contexts中是否有通配符规则如/vendor/bin/(.*)覆盖了你的特定路径。你可能需要将你的规则放在更靠前的位置或者使用更精确的路径。问题三权限都加了但服务在强制模式下仍有功能异常现象 宽容模式一切正常切到强制模式后服务部分功能如网络通信、访问某个特定文件失效但日志没有新的avc拒绝。排查检查布尔值getsebool -a看看是否有相关的布尔值处于off状态。检查DAC权限 确保文件、套接字的Unix权限owner/group/mode是正确的。检查进程能力Capabilities 某些操作需要Linux Capabilities如CAP_NET_ADMIN。SELinux允许了但Capability没给也会失败。在init.rc中通过capabilities选项授予。开启全部拒绝日志 有时SELinux的安静拒绝规则会抑制某些日志。可以尝试在内核命令行添加androidboot.selinuxpermissive的同时加上androidboot.selinux.audit1来获取更详细的日志。问题四编译时出现“Duplicate declaration of type/attribute”错误现象 添加策略后编译报错类型或属性重复声明。排查 你定义的类型名如my_custom_device可能与系统或其他模块中已有的类型冲突。解决在整个代码库中搜索该类型名是否已被使用。为你的类型名添加公司或项目前缀降低冲突概率例如vendor_foo_device。为Android新增服务编写SELinux策略是一个从“为什么我的服务跑不起来”到“如何让我的服务在最小权限下安全地跑起来”的思维转变过程。它要求开发者不仅理解自己服务的功能还要理解它如何与Android庞大的安全体系互动。初期会觉得繁琐但一旦掌握它将成为你开发稳定、安全系统级应用和服务的强大工具。记住核心口诀宽容模式收集最小权限授予具体化标签善用宏属性强制模式验证。多参考AOSP中现有服务的策略写法那是最好的学习资料。