OH 标准系统启动结构梳理(四):appspawn sandbox 实现拆解
TL;DR 本文深入剖析 OpenHarmony
appspawn的 sandbox 环境构建系统。核心脉络可概括为:三段时序(服务预加载建模、父进程 pre-fork 准备、子进程 execute 构建)、四块配置(global定义根目录与命名空间、required提供基础视图、conditional按权限/标志/包名动态装配、name-groups复用挂载组)、关键映射(JSON 解析为内存结构、权限名编号为位索引、gid/flag 随命中结果动态追加)。理解这套设计,就能掌握 sandbox 从配置到执行的完整链路。
文档概述
说明:
1.文章由移远通信技术股份有限公司提供
2.以下内容包含了个人理解,仅供参考,如有不合理处,请联系笔者修改
1. 说明与范围
本文不再讨论 appspawn 的整体孵化链,而是把 sandbox 单独拆开讲。
这篇只回答下面几个问题:
appspawn的 sandbox JSON 到底长什么样;- JSON 里的
global/required/conditional/name-groups分别负责什么; - 权限位、
gid和消息flag是怎么关联起来的; mount/unshare/pivot_root/chdir是按什么顺序执行的;<PackageName>、<currentUserId>、<lib>这类变量是在哪里替换的;HspList/DataGroup/Overlay这类扩展配置是怎么接入的。如果只保留一句话,可以这样理解:
appspawn sandbox 不是“孵化后顺手做几个 bind mount”,而是一套在服务预加载阶段建模、在 parent pre-fork 阶段补齐权限和 staged mount、在 child execute 阶段完成 namespace 与根目录切换的完整环境构建系统。
2. 代码边界
当前标准系统的 sandbox 主实现集中在:
base/startup/appspawn/modules/sandbox/modern/sandbox_manager.cbase/startup/appspawn/modules/sandbox/modern/sandbox_load.cbase/startup/appspawn/modules/sandbox/modern/appspawn_sandbox.cbase/startup/appspawn/modules/sandbox/modern/sandbox_cfgvar.cbase/startup/appspawn/modules/sandbox/modern/sandbox_expand.cbase/startup/appspawn/modules/sandbox/appspawn_permission.c
从职责上可以这样看:
sandbox_load.c负责把 JSON 解析成内存结构;appspawn_permission.c负责权限项编号;sandbox_manager.c负责把消息、权限、gid、sandbox 类型接到孵化主链;appspawn_sandbox.c负责真实 mount、unshare、pivot_root 和根目录切换;sandbox_cfgvar.c负责变量替换;sandbox_expand.c负责扩展配置项的二次处理。
3. sandbox 在孵化链里的位置
如果从孵化时序看,sandbox 不是某一个单点函数,而是分布在三段链路上。
3.1 服务预加载阶段
在 StartSpawnService() 里,appspawn 会执行:
ServerStageHookExecute(STAGE_SERVER_PRELOAD, content)
sandbox 模块在这个阶段注册了多条 preload hook:
PreLoadAppSandboxCfgPreLoadIsoLatedSandboxCfgPreLoadNWebSandboxCfgPreLoadDebugSandboxCfg
这意味着 JSON 配置并不是每次收到请求时重新读取,而是在服务稳定运行前就已经预加载到内存。
3.2 父进程 pre-fork 阶段
在 ProcessSpawnReqMsg() 里,fork 之前会执行:
AppSpawnHookExecute(STAGE_PARENT_PRE_FORK, ...)
sandbox 模块在这里挂入:
SpawnPrepareSandboxCfgSpawnMountDirToShared
这一段负责:
- 根据请求选择 sandbox 类型;
- 根据权限补 gid;
- 根据权限补消息 flag;
- 做 staged mount 的前置准备。
3.3 子进程 execute 阶段
子进程在 AppSpawnChild() 中进入:
AppSpawnExecuteSpawningHook()
sandbox 模块在 STAGE_CHILD_EXECUTE 上挂了:
SpawnBuildSandboxEnv
真正的 namespace 构建、mount、pivot_root 和根目录切换都在这一段发生。
下面用一张时序图来直观展示 sandbox 在三段链路中的分布:
sequenceDiagram
participant Server as appspawn 服务
participant Parent as 父进程 (pre-fork)
participant Child as 子进程 (execute)
Note over Server: 服务启动阶段
Server->>Server: StartSpawnService()
Server->>Server: ServerStageHookExecute(STAGE_SERVER_PRELOAD)
Server->>Server: 预加载 sandbox JSON 到内存
Note over Parent: 收到孵化请求
Parent->>Parent: ProcessSpawnReqMsg()
Parent->>Parent: AppSpawnHookExecute(STAGE_PARENT_PRE_FORK)
Parent->>Parent: SpawnPrepareSandboxCfg()
Parent->>Parent: 选择 sandbox 类型、补 gid/flag
Parent->>Parent: StagedMountSystemConst()
Parent->>Child: fork/clone
Note over Child: 子进程环境构建
Child->>Child: AppSpawnExecuteSpawningHook()
Child->>Child: SpawnBuildSandboxEnv()
Child->>Child: MountSandboxConfigs()
Child->>Child: unshare() + pivot_root + mount
Child->>Child: 进入新 namespace 继续执行
4. sandbox JSON 结构
4.1 典型文件
标准系统下,至少会预加载下面几类 JSON:
appdata-sandbox-app.jsonappdata-sandbox-isolated-new.jsonappdata-sandbox-render.jsonappdata-sandbox-gpu.jsonappdata-sandbox-debug.json
每一类并不是“只是路径不同”,而是代表不同进程类型对应的一组完整 sandbox 规则。
4.2 顶层结构
从 appdata-sandbox-app.json 和 ParseAppSandboxConfig() 的实现看,顶层结构主要分四块:
globalrequiredconditionalname-groups
这四块不是并列堆数据,而是分别对应不同阶段和不同语义。
下面是一个简化的 appdata-sandbox-app.json 顶层结构示例,展示 global、required、conditional、name-groups 的实际字段和缩进关系:
{
"global": {
"sandbox-root": "/mnt/sandbox/<currentUserId>/app-root",
"sandbox-ns-flags": ["net"]
},
"required": {
"system-const": {
"mount-paths": [
{
"src-path": "/system",
"sandbox-path": "/system",
"sandbox-flags": ["bind", "ro", "nodev", "noexec", "nosuid"]
},
{
"src-path": "/vendor",
"sandbox-path": "/vendor",
"sandbox-flags": ["bind", "ro", "nodev", "noexec", "nosuid"]
}
],
"mount-files": [
{
"src-path": "/proc/version",
"sandbox-path": "/proc/version",
"sandbox-flags": ["bind", "ro"]
}
],
"symbol-links": [
{
"link-name": "/bin",
"link-target": "/system/bin"
}
]
},
"app-variable": {
"mount-paths": [
{
"src-path": "/data/app/el2/<currentUserId>/base/<PackageName>",
"sandbox-path": "/data/storage/el2/base",
"sandbox-flags": ["bind", "nodev", "noexec", "nosuid"]
}
]
}
},
"conditional": {
"permission": [
{
"name": "ohos.permission.FILE_ACCESS_MANAGER",
"sandbox-switch": "ON",
"gids": [3018, 3019],
"mount-paths": [
{
"src-path": "/data/app/el2/<currentUserId>/public",
"sandbox-path": "/data/storage/el2/share",
"sandbox-flags": ["bind", "nodev", "noexec", "nosuid"]
}
]
},
{
"name": "ohos.permission.GET_ALL_PROCESSES",
"sandbox-switch": "ON",
"gids": [3003],
"mount-paths": [
{
"src-path": "/proc/<currentUserId>/cmdline",
"sandbox-path": "/proc/<currentUserId>/cmdline",
"sandbox-flags": ["bind", "ro"]
}
]
}
],
"spawn-flag": [
{
"name": "APP_FLAGS_DEBUG",
"mount-paths": [
{
"src-path": "/data/local/tmp",
"sandbox-path": "/data/local/tmp",
"sandbox-flags": ["bind", "nodev", "noexec", "nosuid"]
}
]
}
],
"package-name": [
{
"name": "com.example.special",
"gids": [3020],
"mount-paths": [
{
"src-path": "/data/app/special",
"sandbox-path": "/data/storage/special",
"sandbox-flags": ["bind", "nodev", "noexec", "nosuid"]
}
]
}
]
},
"name-groups": {
"shared-libs": {
"mount-paths": [
{
"src-path": "/system/lib",
"sandbox-path": "/system/lib",
"sandbox-flags": ["bind", "ro", "nodev", "noexec", "nosuid"]
}
],
"depend-on": []
},
"ark-cache": {
"mount-paths": [
{
"src-path": "/data/app/el2/<currentUserId>/cache",
"sandbox-path": "/data/storage/el2/base/cache",
"sandbox-flags": ["bind", "nodev", "noexec", "nosuid"]
}
],
"depend-on": ["shared-libs"]
}
}
}
这个示例覆盖了四块顶层结构的关键字段:
- **
global**:定义sandbox-root和sandbox-ns-flags,是整个 sandbox 的根目录模板和 namespace 能力边界。 - **
required.system-const**:系统常量视图,包含mount-paths、mount-files、symbol-links,在 pre-fork 阶段由StagedMountSystemConst()处理。 - **
required.app-variable**:应用变量目录,路径中使用<currentUserId>、<PackageName>等变量,在 post-unshare 阶段处理。 - **
conditional.permission**:按权限名匹配的规则,包含gids和mount-paths,运行时根据消息中的权限位动态命中。 - **
conditional.spawn-flag**:按 spawn 标志位触发的规则,与权限无关但影响 sandbox 行为。 - **
conditional.package-name**:按包名覆写或追加的规则,用于业务级例外处理。 - **
name-groups**:可复用挂载组定义,支持depend-on依赖关系,避免在多个 permission 中重复展开相同路径。
5. global:定义沙箱根与命名空间
global 是 sandbox JSON 中最顶层的配置块,它不定义具体的挂载路径,而是定义整个沙箱的根目录模板和 namespace 能力边界。
5.1 字段含义
从第 4.2 节的 JSON 示例可以看到,global 包含两个核心字段:
- **
sandbox-root**:字符串类型,定义沙箱根目录的路径模板。例如/mnt/sandbox/<currentUserId>/app-root。这个路径会在运行时经过变量替换(<currentUserId>替换为实际用户 ID),作为后续所有 mount 操作的根目录锚点。ChangeCurrentDir()中的pivot_root最终也会切换到该路径。 - **
sandbox-ns-flags**:字符串数组,定义需要创建的 namespace 类型。例如["net"]表示需要创建新的网络命名空间。可选的 flag 包括net、pid、ipc等,对应unshare()系统调用的CLONE_NEWNET、CLONE_NEWPID、CLONE_NEWIPC等常量。
5.2 加载时机
global 在服务预加载阶段(STAGE_SERVER_PRELOAD)由 ParseAppSandboxConfig() 解析。解析结果存入 SandboxSection 结构体,后续在整个 sandbox 构建过程中作为上下文参数传递。
5.3 核心作用
- 为整个沙箱提供根目录锚点,所有
mount-paths中的sandbox-path都相对于此根目录展开; - 决定子进程运行在哪些独立的 namespace 中,直接影响进程的隔离边界;
- 在
unshare()调用时,sandbox-ns-flags直接决定系统调用参数。
6. required:无条件装配的基础视图
required 定义的是所有进程类型都必须装配的基础视图,不依赖任何运行时条件。它包含两个子块:system-const 和 app-variable。
6.1 system-const:系统常量视图
系统常量视图包含三类基础资源:
- **
mount-paths**:目录挂载项。每个条目包含src-path(宿主机源路径)、sandbox-path(沙箱内目标路径)和sandbox-flags(挂载标志,如bind、ro、nodev、noexec、nosuid)。典型例子是将/system、/vendor等系统目录以只读方式映射到沙箱内。 - **
mount-files**:文件挂载项。与mount-paths结构相同,但用于挂载单个文件而非目录,例如/proc/version。 - **
symbol-links**:符号链接项。包含link-name(链接名)和link-target(链接目标),用于在沙箱内创建快捷路径,例如/bin→/system/bin。
加载时机:system-const 在父进程 pre-fork 阶段由 StagedMountSystemConst() 处理。这一步发生在 fork 之前,以 sandbox-root 为根模板,提前把系统只读视图准备好。之所以在 pre-fork 阶段做,是因为这些 mount 不依赖子进程的 namespace 上下文,且宿主机上已有完整的 mount point。
6.2 app-variable:应用变量目录
应用变量目录定义的是与应用实例相关的路径,路径中通常包含 <currentUserId>、<PackageName> 等变量占位符。例如:
- 源路径:
/data/app/el2/<currentUserId>/base/<PackageName> - 沙箱路径:
/data/storage/el2/base
加载时机:app-variable 在子进程 post-unshare 阶段由 SetAppVariableConfig() 处理。因为路径中的变量需要运行时上下文(用户 ID、包名)才能展开,而这些信息在 fork 之后才完整可用。
6.3 核心作用
- 提供所有进程类型共享的基础文件系统视图,避免在每个
conditional规则中重复定义系统目录; - 通过
system-const和app-variable的分阶段处理,兼顾了 pre-fork 阶段的效率(系统目录提前挂载)和 post-unshare 阶段的灵活性(变量路径运行时展开)。
7. conditional:按条件动态装配规则
conditional 定义的是按运行时条件动态命中的规则。它包含三个子类型:permission、spawn-flag 和 package-name。
7.1 permission:按权限名匹配
permission 是 conditional 中最核心的子块。每个条目包含:
- **
name**:权限名,例如ohos.permission.FILE_ACCESS_MANAGER。 - **
sandbox-switch**:可选,值为"ON"或"OFF",控制该权限对应的 sandbox 功能是否启用。 - **
gids**:可选,整数数组,表示该权限命中后需要追加的 gid 列表。 - **
mount-paths**:可选,挂载路径列表,表示该权限命中后需要额外挂载的目录。
命中机制:运行时,SpawnPrepareSandboxCfg() 读取孵化请求消息中的权限位,通过 CheckAppPermissionFlagSet() 与 permissionQueue 中的 permissionIndex 做位匹配。命中后触发两个动作:
AppendPermissionGid()将gids追加到TLV_DAC_INFO;UpdateMsgFlagsWithPermission()根据权限名反向改写消息 flag(例如ohos.permission.GET_ALL_PROCESSES→APP_FLAGS_GET_ALL_PROCESSES)。
加载时机:permission 的 JSON 解析在服务预加载阶段完成,但命中判断和 gid/flag 追加在父进程 pre-fork 阶段执行,mount-paths 的挂载在子进程 post-unshare 阶段由 SetSandboxPermissionConfig() 执行。
7.2 spawn-flag:按 spawn 标志位触发
spawn-flag 与权限无关,而是根据孵化请求中的标志位触发。每个条目包含:
- **
name**:标志位名称,例如APP_FLAGS_DEBUG。 - **
mount-paths**:命中后需要挂载的路径。
典型场景:当应用以调试模式启动时(APP_FLAGS_DEBUG 置位),自动挂载 /data/local/tmp 到沙箱内,方便开发者推送调试文件。
加载时机:与 permission 类似,标志位判断在 pre-fork 阶段,mount 执行在 post-unshare 阶段。
7.3 package-name:按包名覆写或追加
package-name 用于对特定包名做例外处理。每个条目包含:
- **
name**:包名,例如com.example.special。 - **
gids**:可选,该包名特有的 gid。 - **
mount-paths**:可选,该包名特有的挂载路径。
命中机制:AppendPackageNameGids() 在 pre-fork 阶段根据消息中的包名匹配 package-name 条目,追加对应的 gid。mount 路径则在 post-unshare 阶段由 SetSandboxPackageNameConfig() 处理。
7.4 核心作用
- 实现权限驱动的动态环境构建:应用拥有什么权限,沙箱就自动装配对应的目录和 gid;
- 通过
spawn-flag和package-name提供灵活的例外处理机制,避免把业务特例写死在基础 JSON 中。
8. name-groups:可复用挂载组与依赖
name-groups 定义的是可复用的挂载组,支持组之间的依赖关系。它解决的核心问题是:当多个 permission 或 required 规则需要挂载同一组路径时,避免重复定义。
8.1 字段含义
每个挂载组是一个键值对,键为组名(如 shared-libs、ark-cache),值包含:
- **
mount-paths**:该组包含的挂载路径列表,结构与required中的mount-paths一致。 - **
depend-on**:字符串数组,声明该组依赖的其他组名。例如ark-cache依赖shared-libs,意味着挂载ark-cache前必须先挂载shared-libs。
8.2 依赖解析
在服务预加载阶段,ParseAppSandboxConfig() 解析 name-groups 后,会统计依赖组节点并构建依赖关系图。运行时,StagedMountPreUnShare() 按照拓扑顺序依次挂载:
- 先挂载没有依赖的组(如
shared-libs); - 再挂载依赖已满足的组(如
ark-cache依赖shared-libs已挂载)。
8.3 加载时机
name-groups 的挂载发生在子进程 pre-unshare 阶段,由 StagedMountPreUnShare() 处理。因为依赖组挂载需要在 unshare() 之前完成,以便后续的 namespace 切换能继承这些 mount point。
8.4 核心作用
- 消除重复配置:多个
permission规则可以引用同一个name-groups组,而不是各自重复定义相同的挂载路径; - 通过
depend-on表达挂载顺序约束,确保依赖组在依赖它的组之前完成挂载; - 降低 JSON 配置的维护成本:当基础路径发生变化时,只需修改
name-groups中的定义,所有引用该组的规则自动生效。6. required:无条件装配的基础视图
7. conditional:按条件动态装配规则
8. name-groups:可复用挂载组与依赖
9. JSON 到内存结构的映射
9.1 解析入口
JSON 总入口是:
LoadAppSandboxConfig()
它最终调用:
ParseJsonConfig("etc/sandbox", sandboxName, ParseAppSandboxConfig, &context)
随后:
- 解析
global - 解析
name-groups - 解析
required - 解析
conditional - 补充
pidNamespaceSupport - 补充
appFullMountEnable - 统计依赖组节点
9.2 基础 section 解析
大部分 section 最终都会进入:
ParseBaseConfig()
这一层统一解析的基础字段包括:
sandbox-switchsandbox-sharedgidsmount-pathsmount-filessymbol-linksmount-groups
因此,JSON 虽然按不同 section 组织,但底层载体是一套统一的 SandboxSection 结构。
9.3 权限编号
permission 项在加载完成后,不是继续按字符串查找,而是会被统一编号。
入口在:
PermissionRenumber()
它会遍历 permissionQueue,给每个 SandboxPermissionNode 分配 permissionIndex。
后续运行时就可以通过:
GetPermissionIndexInQueue()
把权限名映射到 index,再用消息里的权限位进行快速判断。
这一步非常关键,因为它把 JSON 字符串世界转换成了运行时的位索引世界。
下面用流程图来展示 JSON 配置到内存结构的完整映射过程:
flowchart TD
A["appdata-sandbox-app.json<br/>(磁盘文件)"] --> B["LoadAppSandboxConfig()"]
B --> C["ParseJsonConfig()"]
C --> D["ParseAppSandboxConfig()"]
D --> E["解析 global"]
D --> F["解析 name-groups"]
D --> G["解析 required"]
D --> H["解析 conditional"]
E --> I["SandboxSection<br/>(sandbox-root, ns-flags)"]
F --> J["nameGroupsQueue<br/>(含依赖关系)"]
G --> K["ParseBaseConfig()"]
H --> L["ParseBaseConfig()"]
K --> M["SandboxSection<br/>(mount-paths, gids, ...)"]
L --> M
M --> N["permissionQueue"]
N --> O["PermissionRenumber()"]
O --> P["SandboxPermissionNode<br/>(含 permissionIndex)"]
P --> Q["运行时通过<br/>GetPermissionIndexInQueue()<br/>快速匹配权限位"]
10. 权限位、gid 和消息 flag
10.1 权限位来源
权限位本身来自孵化请求消息。
sandbox 模块并不自己决定应用拥有哪些权限,而是读取 AppSpawningCtx 里的消息内容,再和本地的 permissionQueue 做匹配。
运行时判断核心用的是:
CheckAppPermissionFlagSet(property, permissionIndex)
10.2 gid 追加
当某个权限在消息里命中后,SpawnPrepareSandboxCfg() 会调用:
AppendPermissionGid()AppendPackageNameGids()
也就是说,gids 字段的作用不是静态声明,而是把匹配到的 permission / package-name 所关联的 gid 追加到 TLV_DAC_INFO 里。
因此,这里的 gid 是“随着 sandbox 命中结果动态长出来的”。
10.3 消息 flag 追加
权限命中后,除了 gid,某些权限还会反向改写消息 flag。
实现路径是:
UpdateMsgFlagsWithPermission()
当前比较典型的两个例子是:
ohos.permission.GET_ALL_PROCESSES->APP_FLAGS_GET_ALL_PROCESSESAPP_ALLOW_IOURING->APP_FLAGS_ALLOW_IOURING
这说明权限系统和孵化行为控制并不是完全分离的,sandbox 权限命中还会影响后续运行态标志。
10.4 权限默认补齐
UpdatePermissionFlags() 里还做了一个容易遗漏的逻辑:
- 如果
FILE_ACCESS_MANAGER_MODE和READ_WRITE_USER_FILE_MODE都没有命中; - 会根据
appFullMountEnable去补一个默认文件访问权限位。
这表示 sandbox 并不只是被动照单执行配置,它还会根据当前策略做一层默认修正。
11. sandbox 类型选择
并不是所有请求都走同一份 JSON。
GetSandboxType() 会根据当前模式和消息内容决定使用哪一类 sandbox:
- 普通 hap ->
EXT_DATA_APP_SANDBOX - 隔离沙箱 hap ->
EXT_DATA_ISOLATED_SANDBOX - NWeb render ->
EXT_DATA_RENDER_SANDBOX - NWeb gpu ->
EXT_DATA_GPU_SANDBOX
另外,如果是可调试 hap 且开发者模式打开,还会额外叠加 debug sandbox 规则。
因此,第四篇里最重要的一点是:
appspawn sandbox 不是一份配置,而是一组按进程类型分流的配置集合。
12. parent pre-fork 阶段做了什么
父进程 pre-fork 阶段的 sandbox 工作主要集中在:
SpawnPrepareSandboxCfg()SpawnMountDirToShared()
12.1 SpawnPrepareSandboxCfg()
这一步至少完成四件事:
- 选出当前 sandbox 类型;
- 更新权限默认值;
- 根据权限补 flag;
- 根据权限和包名补 gid;
- 执行
StagedMountSystemConst()。
要点在于:
- 这是 fork 前的逻辑;
- 所以它更偏“准备元数据”和“准备 staged mount”;
- 还没有真正进入最终 root。
12.2 StagedMountSystemConst()
这一步处理的是 required.system-const。
从源码注释看,它的逻辑是:
- 以
sandbox-root为根模板; - 遍历
system-const的mount-paths/mount-files/symbol-links; - 在真正
unshare前,把这部分基础只读视图准备好。
这就是为什么系统目录映射不放到最后,而是提前 staged mount。
13. child execute 阶段的执行链
真正的环境构建发生在:
SpawnBuildSandboxEnv()MountSandboxConfigs()
13.1 SpawnBuildSandboxEnv()
这一层先做几个判断:
- 如果消息带
APP_FLAGS_NO_SANDBOX,直接跳过; - 根据消息选择 sandbox 类型;
- 如果 namespace 里包含
CLONE_NEWPID,提前处理 pid 环境; - 再进入
MountSandboxConfigs()。
13.2 MountSandboxConfigs() 的主顺序
这是整套 sandbox 的关键执行链,大致顺序是:
InitSandboxContext()StagedMountPreUnShare()unshare(context->sandboxNsFlags)SandboxRootFolderCreate()StagedMountPostUnshare()ChangeCurrentDir()
这条顺序必须理解清楚,因为它决定了为什么某些 mount 必须在 unshare 前做,某些必须在后面做。
下面用流程图来展示 MountSandboxConfigs() 中 staged mount 的完整执行顺序:
flowchart TD
A["MountSandboxConfigs()"] --> B["InitSandboxContext()"]
B --> C["StagedMountPreUnShare()"]
C --> D["处理依赖组挂载<br/>(name-groups)"]
D --> E["处理通用配置的 staged mount"]
E --> F["unshare(sandboxNsFlags)"]
F --> G["SandboxRootFolderCreate()"]
G --> H["StagedMountPostUnshare()"]
H --> I["SetAppVariableConfig()<br/>(处理 app-variable)"]
I --> J["SetExpandSandboxConfig()<br/>(处理 HSP/DataGroup/Overlay)"]
J --> K["SetSandboxPackageNameConfig()<br/>(处理 package-name 特例)"]
K --> L["SetSandboxPermissionConfig()<br/>(处理 permission 增量规则)"]
L --> M["ChangeCurrentDir()"]
M --> N["chdir(rootPath)"]
N --> O["pivot_root 切换根目录"]
O --> P["子进程继续执行"]
style C fill:#e1f5fe,stroke:#01579b
style F fill:#fff3e0,stroke:#e65100
style H fill:#e8f5e9,stroke:#1b5e20
style M fill:#fce4ec,stroke:#b71c1c
14. StagedMountPreUnShare()
这一步发生在 unshare() 之前。
它主要处理:
- 依赖组挂载;
- 通用配置的 staged mount;
- 某些必须在 namespace 切换前完成的 mount。
源码里这一层强调的是:
- “pre-unshare 阶段依赖已经存在的 mount point”
换句话说,这一步解决的是“先把后面切 namespace 需要依赖的骨架搭起来”。
15. unshare()、根目录创建与 root 切换
15.1 unshare()
MountSandboxConfigs() 中真正切 namespace 的 syscall 是:
unshare(context->sandboxNsFlags)
这里的 namespace flag 来自:
- JSON
global.sandbox-ns-flags - 以及运行时模式判断,例如 NWeb 或 pid namespace 支持。
也就是说,namespace 不是在代码里硬编码固定开启,而是配置和模式共同决定。
15.2 SandboxRootFolderCreate()
unshare() 之后,才真正进入 sandbox root 的建立阶段。
SandboxRootFolderCreate() 会根据:
topSandboxSwitchsandboxSwitchsandboxShared
决定如何处理根目录以及基础 mount 语义。
这一步本质上是在新 namespace 里把根目录的骨架搭出来。
15.3 ChangeCurrentDir()
最后一步 ChangeCurrentDir() 的关键动作有两个:
chdir(context->rootPath)syscall(SYS_pivot_root, context->rootPath, context->rootPath)
然后再切换到新的根目录视图。
这说明 appspawn sandbox 不只是 bind mount 一堆目录,而是明确做了 root 切换。
16. StagedMountPostUnshare()
后置阶段主要做“依赖当前 sandbox 根目录”的那部分配置装配。
源码顺序大致是:
SetAppVariableConfig()SetExpandSandboxConfig()SetSandboxPackageNameConfig()SetSandboxPermissionConfig()
翻译成语义就是:
- 先处理按用户和包名展开的基础目录;
- 再处理扩展配置;
- 再处理 package-name 特例;
- 最后处理 permission 命中的增量规则。
为什么 permission 放在后面?
因为这部分最依赖当前请求上下文,也最适合在根目录已经切换完成后再做最终补充。
17. 变量替换
17.1 变量系统不是 JSON 特例
变量替换并不是 JSON 解析时一次性展开的字符串替换,而是独立的一套运行时处理机制。
核心入口在:
AddVariableReplaceHandler()AddDefaultVariable()
17.2 默认变量
默认注册的变量至少包括:
<PackageName><currentUserId><currentHostUserId><PackageNameIndex><arkWebPackageName><deps-sandbox-path><deps-src-path><deps-path><variablePackageName>
这些变量并不是全从同一份数据里来,而是分别取自:
- bundle info
- DAC info
- parent uid 扩展字段
- 系统参数
- 依赖 group 上下文
17.3 典型替换函数
变量替换核心函数包括:
VarPackageNameReplace()VarCurrentUserIdReplace()VarCurrentHostUserIdReplace()VarArkWebPackageNameReplace()ReplaceVariableForpackageName()这说明变量系统本质上是“变量名 -> handler”的注册表,而不是 if-else 字符串大拼接。
17.4 <lib> 的特殊性
sandbox_cfgvar.c 里对 <lib> 有专门分支处理。
这意味着 <lib> 不是普通变量替换,而更像架构相关路径的特殊占位符,用来在 /vendor/<lib>、/system/<lib> 一类路径里做二次转换。
18. 扩展配置
sandbox_expand.c 给这套 JSON 又补了一层扩展能力。
当前默认注册的扩展 handler 有:
HspListDataGroupOverlay
对应函数分别是:
ProcessHSPListConfig()ProcessDataGroupConfig()ProcessOverlayAppConfig()
注册入口是:
RegisterExpandSandboxCfgHandler(name, prio, handler)
这层机制的价值在于:
- 不必把所有业务特例都硬写进基础 JSON 语义;
- 可以把 HSP、DataGroup、Overlay 这类特殊对象的处理延后到扩展层。
因此,sandbox 的配置系统不是封闭的,它本身预留了可扩展面。
19. 文字版执行时序
下面用一张详细的时序图来展示从 appspawn 启动到子进程继续执行的完整17步流程,并标注三个阶段和关键函数调用:
sequenceDiagram
participant Server as appspawn 服务
participant Parent as 父进程 (pre-fork)
participant Child as 子进程 (execute)
Note over Server: 阶段一:服务预加载阶段
Server->>Server: 1. appspawn 启动
Server->>Server: 2. ServerStageHookExecute(STAGE_SERVER_PRELOAD)
Server->>Server: 3. 预加载 sandbox JSON 到内存<br/>PreLoadAppSandboxCfg / PreLoadIsoLatedSandboxCfg<br/>PreLoadNWebSandboxCfg / PreLoadDebugSandboxCfg
Note over Server,Parent: 收到孵化请求
Note over Parent: 阶段二:父进程 pre-fork 阶段
Parent->>Parent: 4. ProcessSpawnReqMsg()
Parent->>Parent: 5. AppSpawnHookExecute(STAGE_PARENT_PRE_FORK)
Parent->>Parent: 6. SpawnPrepareSandboxCfg()<br/>→ GetSandboxType() 选定 sandbox 类型
Parent->>Parent: 7. 权限位命中 → AppendPermissionGid() 补 gid<br/>UpdateMsgFlagsWithPermission() 补 flag
Parent->>Parent: 8. StagedMountSystemConst()<br/>处理 required.system-const 基础只读视图
Parent->>Child: 9. fork/clone
Note over Child: 阶段三:子进程 execute 阶段
Child->>Child: 10. AppSpawnExecuteSpawningHook()
Child->>Child: 11. SpawnBuildSandboxEnv()<br/>→ 检查 APP_FLAGS_NO_SANDBOX<br/>→ 选择 sandbox 类型<br/>→ 处理 CLONE_NEWPID
Child->>Child: 12. InitSandboxContext()
Child->>Child: 13. StagedMountPreUnShare()<br/>→ 处理 name-groups 依赖组挂载<br/>→ 处理通用配置 staged mount
Child->>Child: 14. unshare(sandboxNsFlags)<br/>根据 global.sandbox-ns-flags 切 namespace
Child->>Child: 15. SandboxRootFolderCreate()<br/>在新 namespace 中建立根目录骨架
Child->>Child: 16. StagedMountPostUnshare()<br/>→ SetAppVariableConfig()<br/>→ SetExpandSandboxConfig() (HSP/DataGroup/Overlay)<br/>→ SetSandboxPackageNameConfig()<br/>→ SetSandboxPermissionConfig()
Child->>Child: 17. ChangeCurrentDir()<br/>→ chdir(rootPath)<br/>→ pivot_root 切换根目录
Child->>Child: 子进程继续后续 child looper
21. 常见问题排查流程图
当 sandbox 环境构建失败时,可以按下面的流程图根据现象定位到具体的代码模块和配置项。
flowchart TD
A["sandbox 环境构建失败"] --> B{"现象是什么?"}
B --> C["权限不足<br/>(Permission denied)"]
B --> D["目录/文件缺失<br/>(No such file or directory)"]
B --> E["mount 失败<br/>(Mount error)"]
B --> F["gid 或 flag 不对"]
C --> C1["检查 permission 配置项<br/>→ 第 7.1 节"]
C --> C2["检查权限编号是否命中<br/>→ 第 9.3 节"]
C --> C3["检查权限默认补齐逻辑<br/>→ 第 10.4 节"]
D --> D1{"缺失的是系统目录<br/>还是应用目录?"}
D1 --> D2["系统目录缺失<br/>→ 检查 `required.system-const`<br/>→ 第 6.1 节 / 第 12.2 节"]
D1 --> D3["应用目录缺失<br/>→ 检查 `required.app-variable`<br/>→ 第 6.2 节"]
D1 --> D4["路径包含变量?<br/>→ 检查变量替换 handler<br/>→ 第 17 节"]
E --> E1{"mount 发生在哪个阶段?"}
E1 --> E2["pre-unshare 阶段<br/>→ 检查 `StagedMountPreUnShare()`<br/>→ 第 14 节"]
E1 --> E3["post-unshare 阶段<br/>→ 检查 `StagedMountPostUnshare()`<br/>→ 第 16 节"]
E1 --> E4["检查 `name-groups` 依赖组<br/>→ 第 8 节"]
F --> F1["gid 未追加<br/>→ 检查 `AppendPermissionGid()`<br/>→ 第 10.2 节"]
F --> F2["flag 未设置<br/>→ 检查 `UpdateMsgFlagsWithPermission()`<br/>→ 第 10.3 节"]
F --> F3["sandbox 类型选错?<br/>→ 检查 `GetSandboxType()`<br/>→ 第 11 节"]
C1 --> G["定位到具体 JSON 配置<br/>→ 第 4 节"]
D2 --> G
D3 --> G
E2 --> H["定位到具体代码模块<br/>→ 第 2 节"]
E3 --> H
F1 --> H
F2 --> H
流程图使用说明
- 权限不足:优先排查 JSON 中
conditional.permission的mount-paths和gids是否覆盖了目标路径,以及权限编号是否成功映射到位索引。 - 目录缺失:区分系统常量目录(
system-const)和应用变量目录(app-variable),前者在 pre-fork 阶段 staged mount,后者在 post-unshare 阶段处理。若路径含<PackageName>等变量,检查变量替换 handler 是否注册正确。 - mount 失败:根据执行时序判断 mount 发生在
unshare前还是后。pre-unshare 阶段依赖宿主机已有 mount point,post-unshare 阶段依赖新 namespace 内的根目录结构。 - gid/flag 异常:确认权限位在消息中是否命中,以及
SpawnPrepareSandboxCfg()中AppendPermissionGid()和UpdateMsgFlagsWithPermission()的执行路径。
把最核心的 sandbox 主线压成时序,可以写成下面这样:
appspawn启动STAGE_SERVER_PRELOAD- 预加载 app / isolated / render / gpu / debug sandbox JSON
- 收到孵化请求
STAGE_PARENT_PRE_FORK- 选定 sandbox 类型
- 权限位命中,补 gid 和 flag
StagedMountSystemConst()fork/clone- 子进程
STAGE_CHILD_EXECUTE InitSandboxContext()StagedMountPreUnShare()unshare(sandboxNsFlags)SandboxRootFolderCreate()StagedMountPostUnshare()ChangeCurrentDir()- 子进程继续后续 child looper
这条时序能帮助快速定位问题:
- JSON 没生效,先看 preload 和 parse;
- gid 不对,先看
SpawnPrepareSandboxCfg(); - mount 缺失,区分是 pre-unshare 还是 post-unshare;
- 根目录切换异常,直接看
pivot_root和ChangeCurrentDir(); - 路径展开错,优先看变量替换 handler。
20. 工程化理解
把实现细节抽掉后,这套 sandbox 设计体现了四个稳定思路。
20.1 配置先建模,再执行
JSON 在服务启动时就预加载成内存结构,运行时不再临时解析整份文件。这让孵化时延和执行路径更可控。
20.2 权限不是只做校验,还直接驱动环境构建
权限命中后不仅决定能否访问某路径,还会直接影响 gid 追加、flag 追加和最终 mount 结果。
20.3 mount 被拆成 staged 执行
不是所有挂载都在一个时间点完成,而是:
- system-const 提前做;
- 一部分依赖组在 unshare 前做;
- app-variable / permission / package-name 在 unshare 后做。
这种 staged 设计比“一次性全 mount”更稳定,也更贴近 namespace 切换的约束。
20.4 变量与扩展机制让 JSON 具备伸缩性
如果只有静态路径表,这套配置很快就会失控。变量替换和扩展 handler 让 JSON 可以表达用户、包名、依赖组和业务扩展,而不必把复杂度全压回 C 代码主链。
21. 建议的阅读顺序
如果后续还要继续下钻,建议按下面顺序看:
base/startup/appspawn/appdata-sandbox-app.jsonbase/startup/appspawn/modules/sandbox/modern/sandbox_load.cbase/startup/appspawn/modules/sandbox/appspawn_permission.cbase/startup/appspawn/modules/sandbox/modern/sandbox_manager.cbase/startup/appspawn/modules/sandbox/modern/appspawn_sandbox.cbase/startup/appspawn/modules/sandbox/modern/sandbox_cfgvar.cbase/startup/appspawn/modules/sandbox/modern/sandbox_expand.cbase/startup/appspawn/modules/sandbox/modern/sandbox_shared.c
按这个顺序,先看 JSON 结构,再看权限编号,再看执行链,最后看变量和扩展,会更容易形成完整脑图。
更多推荐

所有评论(0)