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.c
  • base/startup/appspawn/modules/sandbox/modern/sandbox_load.c
  • base/startup/appspawn/modules/sandbox/modern/appspawn_sandbox.c
  • base/startup/appspawn/modules/sandbox/modern/sandbox_cfgvar.c
  • base/startup/appspawn/modules/sandbox/modern/sandbox_expand.c
  • base/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:

  • PreLoadAppSandboxCfg
  • PreLoadIsoLatedSandboxCfg
  • PreLoadNWebSandboxCfg
  • PreLoadDebugSandboxCfg

这意味着 JSON 配置并不是每次收到请求时重新读取,而是在服务稳定运行前就已经预加载到内存。

3.2 父进程 pre-fork 阶段

ProcessSpawnReqMsg() 里,fork 之前会执行:

  • AppSpawnHookExecute(STAGE_PARENT_PRE_FORK, ...)

sandbox 模块在这里挂入:

  • SpawnPrepareSandboxCfg
  • SpawnMountDirToShared

这一段负责:

  • 根据请求选择 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.json
  • appdata-sandbox-isolated-new.json
  • appdata-sandbox-render.json
  • appdata-sandbox-gpu.json
  • appdata-sandbox-debug.json

每一类并不是“只是路径不同”,而是代表不同进程类型对应的一组完整 sandbox 规则。

4.2 顶层结构

appdata-sandbox-app.jsonParseAppSandboxConfig() 的实现看,顶层结构主要分四块:

  • global
  • required
  • conditional
  • name-groups

这四块不是并列堆数据,而是分别对应不同阶段和不同语义。

下面是一个简化的 appdata-sandbox-app.json 顶层结构示例,展示 globalrequiredconditionalname-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-rootsandbox-ns-flags,是整个 sandbox 的根目录模板和 namespace 能力边界。
  • **required.system-const**:系统常量视图,包含 mount-pathsmount-filessymbol-links,在 pre-fork 阶段由 StagedMountSystemConst() 处理。
  • **required.app-variable**:应用变量目录,路径中使用 <currentUserId><PackageName> 等变量,在 post-unshare 阶段处理。
  • **conditional.permission**:按权限名匹配的规则,包含 gidsmount-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 包括 netpidipc 等,对应 unshare() 系统调用的 CLONE_NEWNETCLONE_NEWPIDCLONE_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-constapp-variable

6.1 system-const:系统常量视图

系统常量视图包含三类基础资源:

  • **mount-paths**:目录挂载项。每个条目包含 src-path(宿主机源路径)、sandbox-path(沙箱内目标路径)和 sandbox-flags(挂载标志,如 bindronodevnoexecnosuid)。典型例子是将 /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-constapp-variable 的分阶段处理,兼顾了 pre-fork 阶段的效率(系统目录提前挂载)和 post-unshare 阶段的灵活性(变量路径运行时展开)。

7. conditional:按条件动态装配规则

conditional 定义的是按运行时条件动态命中的规则。它包含三个子类型:permissionspawn-flagpackage-name

7.1 permission:按权限名匹配

permissionconditional 中最核心的子块。每个条目包含:

  • **name**:权限名,例如 ohos.permission.FILE_ACCESS_MANAGER
  • **sandbox-switch**:可选,值为 "ON""OFF",控制该权限对应的 sandbox 功能是否启用。
  • **gids**:可选,整数数组,表示该权限命中后需要追加的 gid 列表。
  • **mount-paths**:可选,挂载路径列表,表示该权限命中后需要额外挂载的目录。

命中机制:运行时,SpawnPrepareSandboxCfg() 读取孵化请求消息中的权限位,通过 CheckAppPermissionFlagSet()permissionQueue 中的 permissionIndex 做位匹配。命中后触发两个动作:

  1. AppendPermissionGid()gids 追加到 TLV_DAC_INFO
  2. UpdateMsgFlagsWithPermission() 根据权限名反向改写消息 flag(例如 ohos.permission.GET_ALL_PROCESSESAPP_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-flagpackage-name 提供灵活的例外处理机制,避免把业务特例写死在基础 JSON 中。

8. name-groups:可复用挂载组与依赖

name-groups 定义的是可复用的挂载组,支持组之间的依赖关系。它解决的核心问题是:当多个 permissionrequired 规则需要挂载同一组路径时,避免重复定义。

8.1 字段含义

每个挂载组是一个键值对,键为组名(如 shared-libsark-cache),值包含:

  • **mount-paths**:该组包含的挂载路径列表,结构与 required 中的 mount-paths 一致。
  • **depend-on**:字符串数组,声明该组依赖的其他组名。例如 ark-cache 依赖 shared-libs,意味着挂载 ark-cache 前必须先挂载 shared-libs

8.2 依赖解析

在服务预加载阶段,ParseAppSandboxConfig() 解析 name-groups 后,会统计依赖组节点并构建依赖关系图。运行时,StagedMountPreUnShare() 按照拓扑顺序依次挂载:

  1. 先挂载没有依赖的组(如 shared-libs);
  2. 再挂载依赖已满足的组(如 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-switch
  • sandbox-shared
  • gids
  • mount-paths
  • mount-files
  • symbol-links
  • mount-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_PROCESSES
  • APP_ALLOW_IOURING -> APP_FLAGS_ALLOW_IOURING

这说明权限系统和孵化行为控制并不是完全分离的,sandbox 权限命中还会影响后续运行态标志。

10.4 权限默认补齐

UpdatePermissionFlags() 里还做了一个容易遗漏的逻辑:

  • 如果 FILE_ACCESS_MANAGER_MODEREAD_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()

这一步至少完成四件事:

  1. 选出当前 sandbox 类型;
  2. 更新权限默认值;
  3. 根据权限补 flag;
  4. 根据权限和包名补 gid;
  5. 执行 StagedMountSystemConst()

要点在于:

  • 这是 fork 前的逻辑;
  • 所以它更偏“准备元数据”和“准备 staged mount”;
  • 还没有真正进入最终 root。

12.2 StagedMountSystemConst()

这一步处理的是 required.system-const

从源码注释看,它的逻辑是:

  • sandbox-root 为根模板;
  • 遍历 system-constmount-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 的关键执行链,大致顺序是:

  1. InitSandboxContext()
  2. StagedMountPreUnShare()
  3. unshare(context->sandboxNsFlags)
  4. SandboxRootFolderCreate()
  5. StagedMountPostUnshare()
  6. 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() 会根据:

  • topSandboxSwitch
  • sandboxSwitch
  • sandboxShared

决定如何处理根目录以及基础 mount 语义。

这一步本质上是在新 namespace 里把根目录的骨架搭出来。

15.3 ChangeCurrentDir()

最后一步 ChangeCurrentDir() 的关键动作有两个:

  1. chdir(context->rootPath)
  2. syscall(SYS_pivot_root, context->rootPath, context->rootPath)

然后再切换到新的根目录视图。

这说明 appspawn sandbox 不只是 bind mount 一堆目录,而是明确做了 root 切换。

16. StagedMountPostUnshare()

后置阶段主要做“依赖当前 sandbox 根目录”的那部分配置装配。

源码顺序大致是:

  1. SetAppVariableConfig()
  2. SetExpandSandboxConfig()
  3. SetSandboxPackageNameConfig()
  4. 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 有:

  • HspList
  • DataGroup
  • Overlay

对应函数分别是:

  • 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.permissionmount-pathsgids 是否覆盖了目标路径,以及权限编号是否成功映射到位索引。
  • 目录缺失:区分系统常量目录(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 主线压成时序,可以写成下面这样:

  1. appspawn 启动
  2. STAGE_SERVER_PRELOAD
  3. 预加载 app / isolated / render / gpu / debug sandbox JSON
  4. 收到孵化请求
  5. STAGE_PARENT_PRE_FORK
  6. 选定 sandbox 类型
  7. 权限位命中,补 gid 和 flag
  8. StagedMountSystemConst()
  9. fork/clone
  10. 子进程 STAGE_CHILD_EXECUTE
  11. InitSandboxContext()
  12. StagedMountPreUnShare()
  13. unshare(sandboxNsFlags)
  14. SandboxRootFolderCreate()
  15. StagedMountPostUnshare()
  16. ChangeCurrentDir()
  17. 子进程继续后续 child looper

这条时序能帮助快速定位问题:

  • JSON 没生效,先看 preload 和 parse;
  • gid 不对,先看 SpawnPrepareSandboxCfg()
  • mount 缺失,区分是 pre-unshare 还是 post-unshare;
  • 根目录切换异常,直接看 pivot_rootChangeCurrentDir()
  • 路径展开错,优先看变量替换 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. 建议的阅读顺序

如果后续还要继续下钻,建议按下面顺序看:

  1. base/startup/appspawn/appdata-sandbox-app.json
  2. base/startup/appspawn/modules/sandbox/modern/sandbox_load.c
  3. base/startup/appspawn/modules/sandbox/appspawn_permission.c
  4. base/startup/appspawn/modules/sandbox/modern/sandbox_manager.c
  5. base/startup/appspawn/modules/sandbox/modern/appspawn_sandbox.c
  6. base/startup/appspawn/modules/sandbox/modern/sandbox_cfgvar.c
  7. base/startup/appspawn/modules/sandbox/modern/sandbox_expand.c
  8. base/startup/appspawn/modules/sandbox/modern/sandbox_shared.c

按这个顺序,先看 JSON 结构,再看权限编号,再看执行链,最后看变量和扩展,会更容易形成完整脑图。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐