本文档说明 LiCore 在 有 Root 的 Android 设备上的支持范围、运行时适配细节、 已知限制与排查方法。
无 Root 的 Android 官方不支持:缺少 namespace / cgroup / setns 等内核隔离 能力,任何用户态方案(含 proot)都无法提供真正的隔离。LiCore 不检测 proot、 不集成 Termux、不引导提权。详见 AGENTS.md 的《Android 支持策略》。
Android 有 Root 走与 Linux 服务器相同的 native_linux 后端:namespace + cgroup,
功能与 Linux 服务器一致。差异由运行时在探测到 Android 时自动适配,用户无需
额外参数。
| 能力 | Android(有 Root) | 说明 |
|---|---|---|
| namespace 隔离 | ✅ | CLONE_NEWPID/NEWNS/NEWUTS/NEWIPC,bridge 模式另加 NEWNET,见第 3 节矩阵 |
| cgroup 资源限制 | ✅ | 优先 cgroup v2,设备只有 v1(或 v1/v2 混合)时自动走 v1 |
| 网络(veth + NAT) | ✅ | 同 Linux |
卷 / :ro / 匿名卷 |
✅ | 同 Linux |
licore exec(含 -it) |
✅ | 依赖设备的 nsenter;Android 10+ 由 Toybox 自带,见 3.2 |
| 开机自启 | ❌ 未实现 | 设计为 Magisk service.d,internal/service 的 Android 后端尚未落地(见第 8 节) |
| SELinux | enforcing 设备上存在策略限制,无法完全消除 |
前提:设备已 Root(Magisk / KernelSU),内核开启 CONFIG_NAMESPACES(出厂 Android 10+ 的 GKI 内核普遍满足;uname -r 确认内核版本,licore doctor 的 kernel.namespaces 项给出最终判定)。
① 交叉编译二进制(开发机执行):
cd licore
CGO_ENABLED=0 GOOS=android GOARCH=arm64 go build -o licore-android-arm64 .
# 无需 NDK、无需 C 工具链:产物是纯 Go(无 cgo / glibc 依赖),exec 能力完整。
# 注意 Android 产物本身是动态链接的(bionic 平台运行时要求),这与 cgo 无关。
# `licore exec` 依赖设备上的 nsenter(见 3.2)。② 推送到设备:
adb push licore-android-arm64 /data/local/tmp/licore
adb shell su -c 'cp /data/local/tmp/licore /data/local/bin/licore; chmod 0755 /data/local/bin/licore'SELinux 提示:
/data/local/tmp与/data/local/bin的执行域受设备策略约束, 被拦时先按第 4.5 节查avc拒绝记录再调整安装位置/标签,LiCore 不会(也不能) 替你改策略。
③ 环境自检:
adb shell su -c '/data/local/bin/licore doctor'
# 关注 kernel.version / kernel.namespaces / cgroups.mount / cgroups.controllers 四项,
# 以及末尾的 android.env 专项(Android 版本/型号、root、cgroup 形态、SELinux、
# namespace 矩阵、userns),缺失能力会给出排查建议。逐项含义见 android-verify.md 第 2 节。④ 数据目录:root 默认为 /root/.licore,Android 上建议显式指定(/data/local/tmp 带 nosuid 且可能被清理):
adb shell su -c 'LICORE_HOME=/data/licore /data/local/bin/licore pull ./demo_v1.licore'⑤ 跑第一个容器:
adb shell su -c 'LICORE_HOME=/data/licore /data/local/bin/licore run -d --name demo --memory 64m demo:v1'
adb shell su -c 'LICORE_HOME=/data/licore /data/local/bin/licore ps'逐项验收(每条命令带期望输出)见 android-verify.md。
开机自启:当前版本
licore boot enable只生成 Linux systemd unit, Magiskservice.d后端未实现(第 8 节)。临时替代——自行创建/data/adb/service.d/licore.sh(chmod 0755):#!/system/bin/sh sleep 20 # 等 data 分区与网络就绪 export LICORE_HOME=/data/licore /data/local/bin/licore boot
每个命名空间是否需要 root、Android 上的可用性、缺失时 LiCore 的行为:
| 命名空间 | 需要 CAP_SYS_ADMIN | Android 典型可用性 | 缺失时 LiCore 行为 |
|---|---|---|---|
pid |
是(或 userns 内) | ✅ 内核标配 | 硬失败 ErrNoNamespaces:没有 pid ns 谈不上容器 |
mnt |
是(或 userns 内) | ✅ 内核标配 | 同上,硬失败 |
uts |
是(或 userns 内) | ✅ 内核标配 | 同上,硬失败 |
ipc |
是(或 userns 内) | ✅ 内核标配 | 同上,硬失败 |
net |
是 | ✅ 通常可用 | 软降级:容器共享宿主网络栈,Degraded + Warn(端口映射/独立网络不可用,其余照常) |
user |
内核开关(max_user_namespaces>0) |
root 路径根本不需要它(见 3.3);仅非 root 场景缺它才硬失败 ErrNotRoot |
|
cgroup |
是(或 userns 内) | 非必需项,当前实现不使用 cgroup ns |
探测方式是只读的(/proc/self/ns 目录项 + /proc/sys/user/max_user_namespaces),
任何情况下都不会为了"看看行不行"去真的创建命名空间。
二进制始终是纯 Go(CGO_ENABLED=0,无 cgo / glibc 依赖、无需 NDK;Android 产物
因 bionic 平台运行时是动态链接的,与 cgo 无关)。exec 需要进入容器的
挂载命名空间,即 setns(CLONE_NEWNS);纯 Go 无法安全调用(线程会在 syscall 间迁移,
Go issue #9091),因此这一步交给系统的 nsenter:
| 功能 | Android 10+ | Android 9 及以下 |
|---|---|---|
| 容器创建/运行/停止/资源限制 | ✅ | ✅ |
licore exec 进入容器 |
✅ Toybox 自带 nsenter |
internal/execns 的探测顺序:nsenter → busybox nsenter。都没有时返回
ErrNoNsenter,并给出安装指引(Magisk 或 busybox);其余功能完全不受影响。
装 busybox 后即自动走 busybox nsenter 后备,无需额外配置:
# Magisk 环境通常已内置 busybox
which busybox || magisk --install-module busybox # 或用 Magisk 模块商店安装
busybox nsenter --help | head -1 # 确认可用这不是 Android 特有的限制,Linux 服务器同理(只是 util-linux 标配了 nsenter)。
常见误解:"root 都不加 userns,是不是降级了?"——不是。规划逻辑是:
| euid | userns 可用性 | 决策 | 语义 |
|---|---|---|---|
| 0 | 任意 | 必需 ns(+可选 net),不加 CLONE_NEWUSER |
正常完整路径,日志 Debug 级(Android 常见形态,不是警告) |
| ≠0 | 可用 | 追加 CLONE_NEWUSER(rootless) |
用 uid 映射换隔离 |
| ≠0 | 不可用 | 硬失败 ErrNotRoot |
无隔离可用,绝不假装 |
root 下加 CLONE_NEWUSER 反而是负优化:容器 root 会被映射成宿主普通 uid,
凭空失去挂载、改网络、写 cgroup 的能力——为了解决"没有 root"的问题而制造
"root 不够用"的问题。Android 设备上多数 ROM 干脆禁用了非特权 userns,这不影响
root 路线,LiCore 因此把它处理为 Debug 而不是警告(回归测试
TestPlanNamespacesRootNoUserNSIsNotDegraded 锁定该语义:root+无 userns 组合
必须既不报错也不标记 Degraded)。
"降级(Degraded)"在 LiCore 里只有一个触发条件:隔离确实变弱了(bridge
网络下 net 命名空间缺失)。它与"能力探测结果不同"是两回事。
绝大多数 Android 设备默认 enforcing,这是与 Linux 服务器最主要的差异来源。
容器 init 进程在 execve 用户命令之前,继承引擎自身的 exec 过渡上下文:
- 读引擎进程的
/proc/self/attr/exec(当前继承到的上下文); - 写回容器 init 自己的同一路径,使
execve后过渡到该上下文; - 失败时只降级告警,容器继续启动。
设计原则:
- 容器上下文与引擎保持一致。这样"引擎能访问的东西容器也能访问",不会因为 放宽标签而扩大攻击面,也不需要在设备上预置任何策略。
- 绝不调用
setenforce,绝不修改/sys/fs/selinux/*,绝不动全局策略或 SELinux 运行状态。LiCore 只读写自身进程的attr/exec。这条约束由单元测试
TestSELinuxNoGlobalStateChange静态守护:源码中一旦出现setenforce//sys/fs/selinux等字样,测试立即失败。 - 不做"假装成功":写不进去就明确告警,不会静默吞掉。
/proc/self/attr/exec 是下一次 execve 的过渡目标(可写);
/proc/self/attr/current 是当前运行上下文(多数策略下不可写)。
容器 init 需要的是"exec 用户命令时进入哪个上下文",因此只能用前者。
另外,该属性只作用于调用者自己的下一次 execve,所以必须在容器 init 进程内、
紧邻 execve 写入——在父进程里写只会影响父进程自己。
| 设备 SELinux 状态 | LiCore 行为 | 日志 |
|---|---|---|
disabled(selinuxfs 未挂载) |
直接跳过 | Debug 级,无噪音 |
| permissive(只记录不拦截) | 尝试继承上下文;失败仅告警 | 失败时 Warn |
| enforcing(强制拦截) | 尝试继承上下文;失败仅告警 | 失败时 Warn(见下) |
状态不可读(unknown) |
同 enforcing 处理 | — |
非 SELinux 宿主上 /proc/self/attr/exec 读取返回 EINVAL(内核只在 SELinux 为
活跃 LSM 时才提供该属性的读写;若宿主用 AppArmor,该文件存在但读写返回 EINVAL)。
这被当作正常情况跳过而不是错误——否则所有非 SELinux 设备都会打出误导性告警。
该分支有真实宿主上的测试覆盖(本项目开发机即 AppArmor,attr/exec 恒返回 EINVAL)。
语义依据:proc(5) 说明
/proc/[pid]/attr/exec表示"后续 execve(2) 时要赋予 进程的属性",且"SELinux 下该属性在execve(2)时被重置"。这正是"必须在 execve 之前、在将要 exec 的那个进程里写"的原因。
LiCore 不修改策略、不放宽标签,因此下面这些情况必然存在:
-
licore二进制自身的域受限。若licore运行在受限域(如从/data/local/tmp执行、被 Magisk 域约束),引擎本身可能无法挂载、建 cgroup 或setns。 表现为创建容器时EPERM。 -
容器内进程沿用引擎的标签,可能被策略拦截。典型症状:
- 读取某些宿主路径返回
EACCES/Permission denied; exec容器内可执行文件报Permission denied(标签不允许execute);- 网络配置失败(标签无
net_admin等)。
- 读取某些宿主路径返回
-
把宿主目录 bind 进容器时标签不匹配。宿主目录自带标签,容器内进程若 无权访问该标签,即使挂载成功也读不到。设备文件同理。
第一步:确认 SELinux 状态与标签
getenforce # Enforcing / Permissive / Disabled
cat /sys/fs/selinux/enforce # 1=enforcing, 0=permissive
ls -Z /data/local/tmp/licore # 看 licore 自身的标签
id -Z # 当前 shell 的上下文第二步:用 LiCore 自检
licore doctor # 含 Android 环境探测:SELinux 状态、cgroup 形态、
# namespace 可用性、user namespace 支持第三步:读容器日志里的降级说明
若日志出现:
WARN 设置 SELinux exec 上下文失败,容器将继续启动;若系统处于 enforcing,
容器内进程可能被策略拦截 context=... err=...
说明上下文继承没成功,容器已用内核默认标签启动。此时容器内遇到的
EACCES/Permission denied 基本都可以归因到标签不匹配,而不是 LiCore 的缺陷。
第四步:定位是哪一步被拦
# 先看内核审计日志(最直接)
dmesg | grep -i avc | tail -20
logcat | grep -i avc | tail -20
# avc 记录会给出 scontext(谁)、tcontext(访问谁)、tclass 与被拒的权限第五步:可选缓解(由用户自行决定,LiCore 不代劳)
- 把
licore放到标签更宽松的位置执行; - 由用户自行编写并加载针对性的策略模块(需要设备端 SELinux 工具链);
- 仅在测试设备上临时
setenforce 0验证"是否为 SELinux 导致"。⚠️ LiCore 自身永远不会执行此操作,也不会建议在生产设备上这样做。 关闭 enforcing 会显著降低设备安全性。
SELinux 的上下文继承路径有完整单元测试覆盖(读取、写入、跳过、降级、 安全约束),但:
- enforcing 真机行为未经本项目实测——所有开发/验证主机均为非 SELinux
(
/sys/fs/selinux不存在,attr/exec读取返回 EINVAL)。因此 enforcing 设备 上的实际策略交互属于已知未验证区域,第 2.4 节的限制基于 SELinux 机制推导, 而非实测结论。 - 验证脚本 verify-root.sh 在本机(非 SELinux)全部通过, 确认非 SELinux 路径零回归。
Android 设备的 cgroup 形态比 Linux 服务器更分散:
| 形态 | 探测方式 | LiCore 行为 |
|---|---|---|
| cgroup v2 | /sys/fs/cgroup/cgroup.controllers 存在 |
优先使用 |
| cgroup v1 | 各控制器分别挂载(memory/、cpu/、pids/…) |
v2 不可用时自动回退 |
| v1/v2 混合 | 部分控制器在 v1、部分在 v2 | 按 v2 优先,v1 补齐 |
| 无 cgroup | 两者都无 | 容器可运行,但无资源限制(明确告警) |
优先 v2:保证支持 v2 的设备(含桌面 Linux 与新 Android)行为不回归。
v1 的控制器子集可以是不完整的:设备的 v1 常常只挂了部分控制器。此时 缺失的控制器不是错误——能设的限制照设,设不了的跳过并继续(与 v2 统一层级 的全有全无语义不同)。
两个 v1 与 v2 的语义陷阱(已在实现中显式处理):
memory.memsw.limit_in_bytes(v1)是内存 + swap 的总和,而 v2 的memory.swap.max是 swap 单独的上限。搞反会把容器总内存限得比--memory还小,导致容器被立刻 OOM。- v1 的
cpu.shares就是--cpu-shares的原始语义[2,262144],与 v2 的cpu.weight量纲不同,不能做换算。 - CPU 用量的位置也不同:v1 在 cpuacct 控制器(
cpuacct.usage,单位纳秒), v2 在cpu.stat的usage_usec(微秒)。licore stats已各自适配。
组布局(licore rm / 排查时按此找):
v2: /sys/fs/cgroup/licore/<容器ID>/ # 统一层级一个目录
v1: /sys/fs/cgroup/memory/licore/<容器ID>/ # 每个已挂载控制器一个
/sys/fs/cgroup/cpu/licore/<容器ID>/ …
LiCore 会在 v2 的父组 /sys/fs/cgroup/licore 上开启
cgroup.subtree_control(cpu memory pids)——不开启时子组的限额文件
根本不可写,这是 Android 定制内核上最容易踩的一处。
自查设备形态:
ls /sys/fs/cgroup/cgroup.controllers && echo v2 可用 || echo 无 v2
grep cgroup /proc/mounts # v1 时各控制器分别挂载容器拿到一个最小可用的 /dev,而不是整体 bind 宿主 /dev——后者会把
Android 的 binder / ashmem / kgsl 等平台专有节点暴露给容器,既无意义也
可能带来越权面。
| 内容 | 实现 |
|---|---|
null zero full random urandom tty ptmx |
按需从宿主 bind 单个节点 |
/dev/shm |
独立 tmpfs,默认 64 MiB;LICORE_SHM_SIZE 可调整(支持 16m/512k/1g 后缀,非法值告警后回退默认) |
/dev/pts |
devpts 实例(exec -it 依赖) |
/dev/fd stdin stdout stderr |
指向 /proc/self/fd 的符号链接 |
宿主缺少某设备节点时跳过(宿主自身的问题,不该阻断容器)。
部分 Android 设备默认 hidepid=2,会让容器内 ps 看不到自己的进程。
LiCore 先解析宿主 /proc/self/mountinfo 判断现状:
宿主 hidepid |
容器挂载参数 |
|---|---|
| 未设置 | 不传(用内核默认) |
0 |
不传(已最宽松) |
1 或 2 |
显式 hidepid=0 |
| 状态不可解析 | 不传(保守),记 Debug 日志 |
若内核拒绝 hidepid 参数,回退为不带参数重试;两次都失败才报错——
参数不被支持不应阻断容器启动。
用
mountinfo而非/proc/mounts:hidepid是 per-mount 选项,mounts只反映 superblock 级选项,读不到 hidepid。
| 限制 | 状态 | 说明 |
|---|---|---|
| 无 Root 的 Android | 不支持 | 官方策略,见开头 |
| enforcing 下的策略拦截 | 无法消除 | LiCore 不改策略;见第 4.4 节 |
| enforcing 真机实测 | 未验证 | 无 SELinux 测试机;见第 4.6 节 |
exec 报「需要 nsenter」 |
环境缺 nsenter | Android 10+ 由 Toybox 自带;更早版本装 busybox;见 3.2 |
| 开机自启(Magisk) | 未实现 | 手动 service.d 脚本可替代;见第 2 节与第 8 节 |
| 无 cgroup 的设备 | 可运行但无限制 | 明确告警,不假装成功 |
| user namespace 不可用 | 对 root 路线无影响 | root 本就不加 userns(3.3),非 root 才受影响 |
| 项 | 现状 | 计划 |
|---|---|---|
licore boot enable 的 Magisk 后端 |
未实现(internal/service 仅有 systemd 后端;AGENTS.md 已登记设计) |
生成 /data/adb/service.d/licore.sh |
licore version 的 Android 平台标识 |
显示 linux/arm64 等原始 GOOS/GOARCH |
识别 Android 身份后追加 android/arm64, root |
以上均为已识别、未接线状态;第 1 节矩阵中的 ✅ 不包含它们。
已接线(曾在本节列出,保留对照):licore doctor 已渲染 android.env 专项检查项
——Android 身份/版本/API/型号、root 状态、cgroup 形态、SELinux 状态、namespace 能力矩阵、
userns 可用性与降级提示;非 Android 平台折叠为一行 [跳过],不影响既有输出。
等级判定与检索建议见 android-verify.md 第 2 节。
- AGENTS.md — Android 支持策略与项目约定
- android-verify.md — Android 真机逐项验证手册(命令 + 期望输出 + 排查)
- verify-root.sh — Linux root 真机验证脚本(A–J,licore 全功能基线)
- test-report-v0.6.0.md — 历史真机验收报告