已开启
feat: add appenv module for GUI app environment variable configuration #2941
wangfenging创建于 10 天前
feat: add appenv module for GUI app environment variable configuration #2941
已开启
共 35 个文件变更+2999-50
| @@ -91,7 +91,10 @@ group("appspawn_all") { | |||
| 91 | deps += [ ":appspawn.rc" ] | 91 | deps += [ ":appspawn.rc" ] |
| 92 | deps += [ "etc:etc_files" ] | 92 | deps += [ "etc:etc_files" ] |
| 93 | deps += [ "modules/ace_adapter:appspawn_ace" ] | 93 | deps += [ "modules/ace_adapter:appspawn_ace" ] |
| 94 | + deps += [ "modules/appenv:libappspawn_appenv" ] | ||
| 94 | deps += [ "modules/asan:appspawn_asan" ] | 95 | deps += [ "modules/asan:appspawn_asan" ] |
| 96 | + deps += [ "service/appenv:libappenv_manager" ] | ||
| 97 | + deps += [ "service/appenv:appenv" ] | ||
| 95 | deps += [ "modules/native_adapter:nativespawn" ] | 98 | deps += [ "modules/native_adapter:nativespawn" ] |
| 96 | deps += [ "modules/sandbox:appspawn_sandbox" ] | 99 | deps += [ "modules/sandbox:appspawn_sandbox" ] |
| 97 | deps += [ "modules/common:appspawn_common" ] | 100 | deps += [ "modules/common:appspawn_common" ] |
| @@ -51,6 +51,16 @@ | |||
| 51 | "gid" : "appspawn", | 51 | "gid" : "appspawn", |
| 52 | "option" : [ | 52 | "option" : [ |
| 53 | ] | 53 | ] |
| 54 | + }, { | ||
| 55 | + "name" : "appenv", | ||
| 56 | + "family" : "AF_LOCAL", | ||
| 57 | + "type" : "SOCK_STREAM", | ||
| 58 | + "protocol" : "default", | ||
| 59 | + "permissions" : "0666", | ||
| 60 | + "uid" : "root", | ||
| 61 | + "gid" : "root", | ||
| 62 | + "option" : [ | ||
| 63 | + ] | ||
| 54 | }], | 64 | }], |
| 55 | "sandbox" : 0, | 65 | "sandbox" : 0, |
| 56 | "start-mode" : "boot", | 66 | "start-mode" : "boot", |
| @@ -45,6 +45,8 @@ declare_args() { | |||
| 45 | appspawn_sandbox_root_tmpfs = false | 45 | appspawn_sandbox_root_tmpfs = false |
| 46 | appspawn_enable_spm = true | 46 | appspawn_enable_spm = true |
| 47 | appspawn_controlled_app = false | 47 | appspawn_controlled_app = false |
| 48 | + # FEAT-20260825-001: appenv(GUI 应用环境变量配置)总开关,异常时可整体裁剪回退 | ||
| 49 | + appspawn_support_appenv = true | ||
| 48 | } | 50 | } |
| 49 | 51 | ||
| 50 | if (!defined(global_parts_info) || | 52 | if (!defined(global_parts_info) || |
| @@ -0,0 +1,60 @@ | |||
| 1 | +# Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 2 | +# Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 3 | +# you may not use this file except in compliance with the License. | ||
| 4 | +# You may obtain a copy of the License at | ||
| 5 | +# | ||
| 6 | +# http://www.apache.org/licenses/LICENSE-2.0 | ||
| 7 | +# | ||
| 8 | +# Unless required by applicable law or agreed to in writing, software | ||
| 9 | +# distributed under the License is distributed on an "AS IS" BASIS, | ||
| 10 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 11 | +# See the License for the specific language governing permissions and | ||
| 12 | +# limitations under the License. | ||
| 13 | + | ||
| 14 | +import("//base/startup/appspawn/appspawn.gni") | ||
| 15 | +import("//build/ohos.gni") | ||
| 16 | + | ||
| 17 | +# FEAT-20260825-001 TASK-001: appenv 模块骨架 | ||
| 18 | +# 环境变量注入模块:loader/merger/policy/path/cache + hook 注册 | ||
| 19 | +# 挂接 STAGE_CHILD_PRE_COLDBOOT / HOOK_PRIO_COMMON + 3(2003) | ||
| 20 | +ohos_shared_library("libappspawn_appenv") { | ||
| 21 | + if (!use_clang_coverage) { | ||
| 22 | + version_script = "appenv.versionscript" | ||
| 23 | + } | ||
| 24 | + branch_protector_ret = "pac_ret" | ||
| 25 | + sanitize = { | ||
| 26 | + cfi = true | ||
| 27 | + cfi_cross_dso = true | ||
| 28 | + cfi_no_nvcall = true | ||
| 29 | + cfi_vcall_icall_only = true | ||
| 30 | + debug = false | ||
| 31 | + } | ||
| 32 | + sources = [ | ||
| 33 | + "appenv_cache.cpp", | ||
| 34 | + "appenv_hook.cpp", | ||
| 35 | + "appenv_loader.cpp", | ||
| 36 | + "appenv_merger.cpp", | ||
| 37 | + "appenv_path.cpp", | ||
| 38 | + "appenv_policy.cpp", | ||
| 39 | + ] | ||
| 40 | + include_dirs = [ | ||
| 41 | + ".", | ||
| 42 | + "${appspawn_path}/common", | ||
| 43 | + "${appspawn_path}/standard", | ||
| 44 | + ] | ||
| 45 | + deps = [ "${appspawn_path}/modules/module_engine:libappspawn_module_engine" ] | ||
| 46 | + external_deps = [ | ||
| 47 | + "bounds_checking_function:libsec_shared", | ||
| 48 | + "hilog:libhilog", | ||
| 49 | + "init:libbegetutil", | ||
| 50 | + ] | ||
| 51 | + install_enable = true | ||
| 52 | + subsystem_name = "${subsystem_name}" | ||
| 53 | + part_name = "${part_name}" | ||
| 54 | + if (target_cpu == "arm64" || target_cpu == "x86_64" || | ||
| 55 | + target_cpu == "riscv64") { | ||
| 56 | + module_install_dir = "lib64/appspawn" | ||
| 57 | + } else { | ||
| 58 | + module_install_dir = "lib/appspawn" | ||
| 59 | + } | ||
| 60 | +} | ||
| @@ -0,0 +1,79 @@ | |||
| 1 | +# appenv 环境变量注入模块 | ||
| 2 | + | ||
| 3 | +## 简介 | ||
| 4 | + | ||
| 5 | +`modules/appenv` 是 appspawn 的环境变量注入功能模块(FEAT-20260825-001)。它在应用孵化流程的 `STAGE_CHILD_PRE_COLDBOOT` 阶段挂接 hook(优先级 `HOOK_PRIO_COMMON + 3`,即 2003),读取 `appenv.conf` 四层作用域配置,经安全分级过滤后将环境变量注入目标应用进程。 | ||
| 6 | + | ||
| 7 | +模块定位:**生效阶段**(对应设计说明第 15 节两阶段策略)—— 即使配置写入侧(AppEnvManager)已完成过滤,本模块仍独立重复关键校验,作为最终裁决(纵深防御)。 | ||
| 8 | + | ||
| 9 | +```text | ||
| 10 | +appenv.conf(四层)→ loader(读取容错)→ merger(优先级合并) | ||
| 11 | + → policy(分级裁决)→ path(PATH 专项)→ hook(setenv 注入) | ||
| 12 | +``` | ||
| 13 | + | ||
| 14 | +## 目录 | ||
| 15 | + | ||
| 16 | +``` | ||
| 17 | +modules/appenv | ||
| 18 | +├─appenv_module.h # 公共类型:作用域/安全分级/快照条目 | ||
| 19 | +├─appenv_hook.cpp # hook 入口 SpawnSetAppEnvConf + MODULE_CONSTRUCTOR 注册 | ||
| 20 | +├─appenv_loader.cpp/h # 四层配置定位与解析(KEY=VALUE、黑名单语法、容错) | ||
| 21 | +├─appenv_merger.cpp/h # app>user>session>system 优先级合并(保留来源 scope) | ||
| 22 | +├─appenv_policy.cpp/h # 分级表 + 最终裁决链 + restricted 白名单 | ||
| 23 | +├─appenv_path.cpp/h # PATH 专项:七项校验 + 系统默认段拼接 | ||
| 24 | +├─appenv_cache.cpp/h # 快照缓存 + param watcher 版本失效(TASK-007) | ||
| 25 | +├─appenv.versionscript # 符号全隐藏(constructor 自注册,无导出 API) | ||
| 26 | +└─BUILD.gn # ohos_shared_library("libappspawn_appenv") | ||
| 27 | +``` | ||
| 28 | + | ||
| 29 | +## 模块说明 | ||
| 30 | + | ||
| 31 | +### hook 挂接次序 | ||
| 32 | + | ||
| 33 | +| 优先级 | hook | 职责 | | ||
| 34 | +|--------|------|------| | ||
| 35 | +| 1000 | SpawnInitSpawningEnv | 子进程环境清理(既有) | | ||
| 36 | +| 2001 | SpawnSetAppEnv | AMS AppEnv JSON 通道(既有,系统内部) | | ||
| 37 | +| 2002 | SpawnSetCustomSandboxEnv | /etc/environment(既有) | | ||
| 38 | +| **2003** | **SpawnSetAppEnvConf(本模块)** | **appenv 用户配置注入** | | ||
| 39 | + | ||
| 40 | +系统内部通道优先;本模块不覆盖其设置的受限变量。 | ||
| 41 | + | ||
| 42 | +### 四层配置(合并优先级 app > user > session > system) | ||
| 43 | + | ||
| 44 | +| 层 | 路径 | | ||
| 45 | +|----|------| | ||
| 46 | +| system | `/system/etc/appenv.conf` | | ||
| 47 | +| session | `/data/service/el1/public/appenv/session.conf` | | ||
| 48 | +| user | `/data/service/el1/<userId>/appenv/user.conf` | | ||
| 49 | +| app | `/data/service/el1/<userId>/appenv/apps/<bundleName>.conf` | | ||
| 50 | + | ||
| 51 | +### 安全分级(policy 最终裁决) | ||
| 52 | + | ||
| 53 | +| 级别 | 语义 | 初始集合 | | ||
| 54 | +|------|------|----------| | ||
| 55 | +| blocked | 恒拒(含 root 写入) | LD_PRELOAD、LD_AUDIT、DYLD_、MALLOC_、ASAN_、TSAN_ | | ||
| 56 | +| restricted | debuggable ∧ 开发者模式 ∧ (app 作用域 或 白名单) | LD_LIBRARY_PATH、HTTP(S)_PROXY、SSL_CERT_、HILOG_、ARK_、ACE_ | | ||
| 57 | +| system-only | 仅 system 来源 ∧ 系统应用 | HOME、USER、SHELL、TMPDIR、XDG_ | | ||
| 58 | +| public | 普通放行 | 未命中规则 | | ||
| 59 | + | ||
| 60 | +restricted 白名单:`/system/etc/appenv/restricted-whitelist.conf`(镜像预置只读,无命令行修改入口)。 | ||
| 61 | + | ||
| 62 | +### 容错语义(不阻断启动) | ||
| 63 | + | ||
| 64 | +- 单行非法(export/`$()`/反引号/`PATH=` 直写/超长):跳过该行记日志 | ||
| 65 | +- 文件级异常(不存在/不可读/超 1000 行):整文件按空处理 | ||
| 66 | + | ||
| 67 | +### 编译开关 | ||
| 68 | + | ||
| 69 | +`appspawn_support_appenv`(appspawn.gni,默认 true)—— 关闭时模块不编译、hook 不注册,零 footprint 回退。 | ||
| 70 | + | ||
| 71 | +## 单元测试 | ||
| 72 | + | ||
| 73 | +`test/unittest/appenv_test/`(目标 `AppSpawn_AppEnv_LoaderMerger_Test`):覆盖解析正反例、文件容错、合并优先级、分级裁决、白名单,累计 31 用例。 | ||
| 74 | + | ||
| 75 | +## 相关资料 | ||
| 76 | + | ||
| 77 | +- 设计说明:`../../../appspawn-appenv-design.md` | ||
| 78 | +- 需求规格:`.specs/features/FEAT-20260825-001-appenv/`(本仓 `.specs/` 目录) | ||
| 79 | +- 命令工具:`service/appenv/`(appenv CLI 与 AppEnvManager,见其 README) | ||
| @@ -0,0 +1,19 @@ | |||
| 1 | +# Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 2 | +# Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 3 | +# you may not use this file except in compliance with the License. | ||
| 4 | +# You may obtain a copy of the License at | ||
| 5 | +# | ||
| 6 | +# http://www.apache.org/licenses/LICENSE-2.0 | ||
| 7 | +# | ||
| 8 | +# Unless required by applicable law or agreed to in writing, software | ||
| 9 | +# distributed under the License is distributed on an "AS IS" BASIS, | ||
| 10 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 11 | +# See the License for the specific language governing permissions and | ||
| 12 | +# limitations under the License. | ||
| 13 | + | ||
| 14 | +# This library exports no symbols. Hook registration is done via | ||
| 15 | +# __attribute__((constructor)) in .init_array; hide all symbols to reduce ROM. | ||
| 16 | +{ | ||
| 17 | + local: | ||
| 18 | + *; | ||
| 19 | +}; | ||
| @@ -0,0 +1,50 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | +/** | ||
| 21 | + * @brief 将快照标记为失效(TASK-007 实现)。 | ||
| 22 | + * | ||
| 23 | + * 骨架阶段为空操作,watcher 接线随 TASK-007 落地。 | ||
| 24 | + */ | ||
| 25 | +void AppEnvCacheInvalidate(void) | ||
| 26 | +{ | ||
| 27 | + APPSPAWN_LOGV("appenv cache skeleton, invalidate lands with TASK-007"); | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +/** | ||
| 31 | + * @brief 获取合并快照,失效则在调用线程重建(TASK-007 实现)。 | ||
| 32 | + * | ||
| 33 | + * 骨架阶段恒返回 NULL,调用方按空配置处理(不注入)。 | ||
| 34 | + * | ||
| 35 | + * @param userId 目标用户 ID(多用户分桶键) | ||
| 36 | + * @param bundleName 应用 bundle 名 | ||
| 37 | + * @param count 输出快照条目数;骨架阶段置 0 | ||
| 38 | + * @return 快照指针;当前骨架恒为 NULL | ||
| 39 | + */ | ||
| 40 | +const AppEnvEntry *AppEnvCacheGetSnapshot(uint32_t userId, const char *bundleName, | ||
| 41 | + uint32_t *count) | ||
| 42 | +{ | ||
| 43 | + (void)userId; | ||
| 44 | + (void)bundleName; | ||
| 45 | + if (count != NULL) { | ||
| 46 | + *count = 0; | ||
| 47 | + } | ||
| 48 | + APPSPAWN_LOGV("appenv cache skeleton, snapshot lands with TASK-007"); | ||
| 49 | + return NULL; | ||
| 50 | +} | ||
| @@ -0,0 +1,53 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +extern "C" { | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * @brief 将快照标记为失效(TASK-007 实现)。 | ||
| 28 | + * | ||
| 29 | + * 由 param watcher 回调调用(persist.appspawn.appenv.version 变化), | ||
| 30 | + * 仅置失效标志不做 IO;实际重载发生在 appspawn 主循环下一次孵化时, | ||
| 31 | + * 避免 watcher 线程 IO(design.md 线程并发模型)。 | ||
| 32 | + * 骨架阶段为空操作。 | ||
| 33 | + */ | ||
| 34 | +void AppEnvCacheInvalidate(void); | ||
| 35 | + | ||
| 36 | +/** | ||
| 37 | + * @brief 获取合并快照,失效则在调用线程重建(TASK-007 实现)。 | ||
| 38 | + * | ||
| 39 | + * 父进程孵化流程内调用(与 fork 串行,无并发重载),子进程 hook 经 | ||
| 40 | + * fork 继承快照内存,单次孵化零文件 IO(NFR-01)。 | ||
| 41 | + * 骨架阶段恒返回 NULL,调用方应按空配置处理。 | ||
| 42 | + * | ||
| 43 | + * @param userId 目标用户 ID(多用户分桶键) | ||
| 44 | + * @param bundleName 应用 bundle 名 | ||
| 45 | + * @return 快照指针;骨架阶段恒为 NULL | ||
| 46 | + */ | ||
| 47 | +const AppEnvEntry *AppEnvCacheGetSnapshot(uint32_t userId, const char *bundleName, | ||
| 48 | + uint32_t *count); | ||
| 49 | + | ||
| 50 | + | ||
| 51 | +} | ||
| 52 | + | ||
| 53 | + | ||
| @@ -0,0 +1,57 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * @brief appenv 注入 hook 入口(TASK-001 骨架)。 | ||
| 28 | + * | ||
| 29 | + * 挂接于 STAGE_CHILD_PRE_COLDBOOT 阶段,优先级 HOOK_PRIO_COMMON + 3(2003), | ||
| 30 | + * 位于 SpawnInitSpawningEnv(1000)、SpawnSetAppEnv(2001)、 | ||
| 31 | + * SpawnSetCustomSandboxEnv(2002) 之后,保证系统内部通道优先。 | ||
| 32 | + * | ||
| 33 | + * 失败语义:任何内部失败仅记录日志并返回 0(不注入),绝不阻断应用孵化 | ||
| 34 | + * (FR-13/NFR-03,AC-1.4)。完整链路在后续 Task 中填充: | ||
| 35 | + * loader(TASK-004) -> merger(TASK-004) -> policy(TASK-005) | ||
| 36 | + * -> path(TASK-006) -> cache(TASK-007)。 | ||
| 37 | + * | ||
| 38 | + * @param content appspawn 管理数据 | ||
| 39 | + * @param property 业务孵化数据(含 bundleName/userId/debuggable 等元数据) | ||
| 40 | + * @return 恒为 0,配置异常降级为不注入 | ||
| 41 | + */ | ||
| 42 | +static int SpawnSetAppEnvConf(AppSpawnMgr *content, AppSpawningCtx *property) | ||
| 43 | +{ | ||
| 44 | + APPSPAWN_LOGI("appenv hook enter, process %{public}s", GetProcessName(property)); | ||
| 45 | + // TASK-004~007: 读缓存快照 -> 合并 -> 分级过滤 -> PATH 重建 -> setenv 注入 | ||
| 46 | + (void)content; | ||
| 47 | + (void)property; | ||
| 48 | + APPSPAWN_LOGV("appenv hook skeleton done, inject logic lands with TASK-004~007"); | ||
| 49 | + return 0; | ||
| 50 | +} | ||
| 51 | + | ||
| 52 | +MODULE_CONSTRUCTOR(void) | ||
| 53 | +{ | ||
| 54 | + APPSPAWN_LOGI("Load appenv module ..."); | ||
| 55 | + // 2003: 位于 SpawnSetCustomSandboxEnv(2002) 之后,系统内部通道优先 | ||
| 56 | + AddAppSpawnHook(STAGE_CHILD_PRE_COLDBOOT, HOOK_PRIO_COMMON + 3, SpawnSetAppEnvConf); | ||
| 57 | +} | ||
| @@ -0,0 +1,230 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +/** system 作用域配置路径(只读分区,镜像预置)。 */ | ||
| 27 | + | ||
| 28 | +/** el1 配置根目录(session 与 user/app 层的公共前缀)。 */ | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +/** 裁剪目标字符集:空格、水平制表、换行、回车。 */ | ||
| 32 | +static const char * const g_trimChars = " \t\n\r"; | ||
| 33 | + | ||
| 34 | +/** | ||
| 35 | + * @brief 就地裁剪首尾空白。 | ||
| 36 | + * | ||
| 37 | + * @param str 待裁剪字符串(非 NULL,以 '\0' 结尾) | ||
| 38 | + * @return 裁剪后内容的首地址(原缓冲区内) | ||
| 39 | + */ | ||
| 40 | +static char *AppEnvTrim(char *str) | ||
| 41 | +{ | ||
| 42 | + char *start = str + strspn(str, g_trimChars); | ||
| 43 | + size_t len = strlen(start); | ||
| 44 | + while (len > 0 && strchr(g_trimChars, start[len - 1]) != NULL) { | ||
| 45 | + start[--len] = '\0'; | ||
| 46 | + } | ||
| 47 | + return start; | ||
| 48 | +} | ||
| 49 | + | ||
| 50 | +/** | ||
| 51 | + * @brief 校验变量名合法性。 | ||
| 52 | + * | ||
| 53 | + * 规则:首字符字母或下划线,其余字母/数字/下划线,长度 (0, 256)。 | ||
| 54 | + * | ||
| 55 | + * @param key 待校验名 | ||
| 56 | + * @return 合法返回 true | ||
| 57 | + */ | ||
| 58 | +static bool AppEnvIsValidKey(const char *key) | ||
| 59 | +{ | ||
| 60 | + size_t len = strlen(key); | ||
| 61 | + if (len == 0 || len >= sizeof(((AppEnvEntry *)0)->key)) { | ||
| 62 | + return false; | ||
| 63 | + } | ||
| 64 | + if (!isalpha((unsigned char)key[0]) && key[0] != '_') { | ||
| 65 | + return false; | ||
| 66 | + } | ||
| 67 | + for (size_t i = 1; i < len; i++) { | ||
| 68 | + if (!isalnum((unsigned char)key[i]) && key[i] != '_') { | ||
| 69 | + return false; | ||
| 70 | + } | ||
| 71 | + } | ||
| 72 | + return true; | ||
| 73 | +} | ||
| 74 | + | ||
| 75 | +/** | ||
| 76 | + * @brief 校验 bundleName 可安全用作文件路径(防路径穿越)。 | ||
| 77 | + * | ||
| 78 | + * 仅允许 [A-za-z0-9._] 且不以 '.' 开头(拒绝相对路径组件)。 | ||
| 79 | + * | ||
| 80 | + * @param bundle 待校验名 | ||
| 81 | + * @return 合法返回 true | ||
| 82 | + */ | ||
| 83 | +static bool AppEnvIsValidBundle(const char *bundle) | ||
| 84 | +{ | ||
| 85 | + size_t len = (bundle == NULL) ? 0 : strlen(bundle); | ||
| 86 | + if (len == 0 || len > 200 || bundle[0] == '.') { | ||
| 87 | + return false; | ||
| 88 | + } | ||
| 89 | + for (size_t i = 0; i < len; i++) { | ||
| 90 | + char c = bundle[i]; | ||
| 91 | + if (!isalnum((unsigned char)c) && c != '.' && c != '_') { | ||
| 92 | + return false; | ||
| 93 | + } | ||
| 94 | + } | ||
| 95 | + return true; | ||
| 96 | +} | ||
| 97 | + | ||
| 98 | +int AppEnvParseLine(const char *line, AppEnvEntry *entry) | ||
| 99 | +{ | ||
| 100 | + if (line == NULL || entry == NULL) { | ||
| 101 | + return -1; | ||
| 102 | + } | ||
| 103 | + char buf[APPENV_MAX_LINE_LEN] = {0}; | ||
| 104 | + size_t lineLen = strlen(line); | ||
| 105 | + if (lineLen >= sizeof(buf)) { | ||
| 106 | + return -1; // 超长行非法(AC-1.3 记日志后由调用方跳过) | ||
| 107 | + } | ||
| 108 | + errno_t ret = memcpy_s(buf, sizeof(buf), line, lineLen + 1); | ||
| 109 | + APPSPAWN_CHECK(ret == EOK, return -1, "appenv: memcpy line failed"); | ||
| 110 | + | ||
| 111 | + char *trimmed = AppEnvTrim(buf); | ||
| 112 | + if (trimmed[0] == '\0' || trimmed[0] == '#') { | ||
| 113 | + return 1; // 空行或注释:跳过(非错误) | ||
| 114 | + } | ||
| 115 | + // 黑名单语法(BR-5):export 前缀 | ||
| 116 | + if (strncmp(trimmed, "export ", strlen("export ")) == 0) { | ||
| 117 | + return -1; | ||
| 118 | + } | ||
| 119 | + char *eq = strchr(trimmed, '='); | ||
| 120 | + if (eq == NULL) { | ||
| 121 | + return -1; // 缺 '=' | ||
| 122 | + } | ||
| 123 | + *eq = '\0'; | ||
| 124 | + char *key = AppEnvTrim(trimmed); | ||
| 125 | + char *value = AppEnvTrim(eq + 1); | ||
| 126 | + // 黑名单语法(BR-5):值含 '$'($VAR/$())或反引号(命令替换) | ||
| 127 | + if (strpbrk(value, "$`") != NULL) { | ||
| 128 | + return -1; | ||
| 129 | + } | ||
| 130 | + // PATH 不可经 KV 配置(BR-3:仅 path.list 结构化管理) | ||
| 131 | + if (strcmp(key, "PATH") == 0) { | ||
| 132 | + return -1; | ||
| 133 | + } | ||
| 134 | + if (!AppEnvIsValidKey(key) || strlen(value) >= sizeof(entry->value)) { | ||
| 135 | + return -1; | ||
| 136 | + } | ||
| 137 | + // 成对引号剥离(与 /etc/environment 解析语义一致) | ||
| 138 | + size_t valueLen = strlen(value); | ||
| 139 | + if (valueLen >= 2 && (value[0] == '"' || value[0] == '\'') && value[valueLen - 1] == value[0]) { | ||
| 140 | + value[valueLen - 1] = '\0'; | ||
| 141 | + value++; | ||
| 142 | + } | ||
| 143 | + ret = strcpy_s(entry->key, sizeof(entry->key), key); | ||
| 144 | + APPSPAWN_CHECK(ret == EOK, return -1, "appenv: copy key failed"); | ||
| 145 | + ret = strcpy_s(entry->value, sizeof(entry->value), value); | ||
| 146 | + APPSPAWN_CHECK(ret == EOK, return -1, "appenv: copy value failed"); | ||
| 147 | + return 0; | ||
| 148 | +} | ||
| 149 | + | ||
| 150 | +uint32_t AppEnvLoadFile(const char *path, uint8_t scope, | ||
| 151 | + AppEnvEntry *entries, uint32_t *count, uint32_t maxEntries) | ||
| 152 | +{ | ||
| 153 | + if (path == NULL || entries == NULL || count == NULL) { | ||
| 154 | + return 0; | ||
| 155 | + } | ||
| 156 | + FILE *fp = fopen(path, "r"); | ||
| 157 | + if (fp == NULL) { | ||
| 158 | + // ENOENT 为常态(未配置该层);其他不可读也整文件忽略(AC-1.4) | ||
| 159 | + if (errno != ENOENT) { | ||
| 160 | + APPSPAWN_LOGW("appenv: open %{public}s failed, errno %{public}d, ignore file", path, errno); | ||
| 161 | + } | ||
| 162 | + return 0; | ||
| 163 | + } | ||
| 164 | + char line[APPENV_MAX_LINE_LEN] = {0}; | ||
| 165 | + uint32_t added = 0; | ||
| 166 | + uint32_t lineNo = 0; | ||
| 167 | + while (fgets(line, sizeof(line), fp) != NULL) { | ||
| 168 | + lineNo++; | ||
| 169 | + if (lineNo > APPENV_MAX_FILE_LINES) { | ||
| 170 | + APPSPAWN_LOGW("appenv: %{public}s exceeds max lines, ignore rest", path); | ||
| 171 | + break; | ||
| 172 | + } | ||
| 173 | + AppEnvEntry entry = {}; | ||
| 174 | + entry.scope = scope; | ||
| 175 | + int ret = AppEnvParseLine(line, &entry); | ||
| 176 | + if (ret != 0) { | ||
| 177 | + if (ret < 0) { | ||
| 178 | + APPSPAWN_LOGW("appenv: %{public}s:%{public}u invalid line skipped", path, lineNo); | ||
| 179 | + } | ||
| 180 | + continue; | ||
| 181 | + } | ||
| 182 | + if (*count >= maxEntries) { | ||
| 183 | + APPSPAWN_LOGW("appenv: entry buffer full (%{public}u), drop rest of %{public}s", | ||
| 184 | + maxEntries, path); | ||
| 185 | + break; | ||
| 186 | + } | ||
| 187 | + entries[(*count)++] = entry; | ||
| 188 | + added++; | ||
| 189 | + } | ||
| 190 | + (void)fclose(fp); | ||
| 191 | + return added; | ||
| 192 | +} | ||
| 193 | + | ||
| 194 | +uint32_t AppEnvLoadConfigs(uint32_t userId, const char *bundleName, | ||
| 195 | + AppEnvEntry *entries, uint32_t maxEntries) | ||
| 196 | +{ | ||
| 197 | + if (entries == NULL || maxEntries == 0) { | ||
| 198 | + return 0; | ||
| 199 | + } | ||
| 200 | + uint32_t count = 0; | ||
| 201 | + char path[512] = {0}; | ||
| 202 | + | ||
| 203 | + // system 层(全用户共用) | ||
| 204 | + (void)AppEnvLoadFile(APPENV_SYSTEM_CONF_PATH, APP_ENV_SCOPE_SYSTEM, entries, &count, maxEntries); | ||
| 205 | + | ||
| 206 | + // session 层(公共 el1) | ||
| 207 | + (void)AppEnvLoadFile(APPENV_EL1_BASE_PATH "/public/appenv/session.conf", | ||
| 208 | + APP_ENV_SCOPE_SESSION, entries, &count, maxEntries); | ||
| 209 | + | ||
| 210 | + // user 层(用户 el1);userId 0 视为无用户层(系统侧启动场景) | ||
| 211 | + if (userId != 0) { | ||
| 212 | + int ret = snprintf_s(path, sizeof(path), sizeof(path) - 1, | ||
| 213 | + "%s/%u/appenv/user.conf", APPENV_EL1_BASE_PATH, userId); | ||
| 214 | + if (ret > 0) { | ||
| 215 | + (void)AppEnvLoadFile(path, APP_ENV_SCOPE_USER, entries, &count, maxEntries); | ||
| 216 | + } | ||
| 217 | + } | ||
| 218 | + | ||
| 219 | + // app 层(指定 bundle);bundle 名须过白名单(防路径穿越) | ||
| 220 | + if (userId != 0 && bundleName != NULL && AppEnvIsValidBundle(bundleName)) { | ||
| 221 | + int ret = snprintf_s(path, sizeof(path), sizeof(path) - 1, | ||
| 222 | + "%s/%u/appenv/apps/%s.conf", APPENV_EL1_BASE_PATH, userId, bundleName); | ||
| 223 | + if (ret > 0) { | ||
| 224 | + (void)AppEnvLoadFile(path, APP_ENV_SCOPE_APP, entries, &count, maxEntries); | ||
| 225 | + } | ||
| 226 | + } else if (bundleName != NULL && !AppEnvIsValidBundle(bundleName)) { | ||
| 227 | + APPSPAWN_LOGW("appenv: invalid bundle name, skip app scope"); | ||
| 228 | + } | ||
| 229 | + return count; | ||
| 230 | +} | ||
| @@ -0,0 +1,85 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +extern "C" { | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +/** 单文件最大行数(与 /etc/environment 解析器约束一致)。 */ | ||
| 27 | + | ||
| 28 | +/** 单行最大长度(含 KEY、'=' 与 VALUE)。 */ | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +/** | ||
| 32 | + * @brief 解析单行 KEY=VALUE(容错语义入口,独立可测)。 | ||
| 33 | + * | ||
| 34 | + * 解析规则(与 appspawn_custom_config.cpp 的 /etc/environment 语义 | ||
| 35 | + * 对齐):首尾空白裁剪;'#' 开头跳过;首个 '=' 分割;成对引号剥离。 | ||
| 36 | + * 黑名单语法拒绝(BR-5):'export' 前缀、值含 '$' 或反引号、 | ||
| 37 | + * KEY 为 "PATH"(PATH 仅允许 path.list 结构化管理,BR-3)。 | ||
| 38 | + * | ||
| 39 | + * @param line 原始行(不以换行符结尾也合法) | ||
| 40 | + * @param entry 输出解析结果(key/value/scope 由调用方按层预填保留) | ||
| 41 | + * @return 0 解析成功;1 跳过该行(空行/注释,非错误);-1 该行非法 | ||
| 42 | + */ | ||
| 43 | +int AppEnvParseLine(const char *line, AppEnvEntry *entry); | ||
| 44 | + | ||
| 45 | +/** | ||
| 46 | + * @brief 加载单个配置文件到条目数组(容错:单行跳过/整文件忽略)。 | ||
| 47 | + * | ||
| 48 | + * 文件级异常(不存在 ENOENT 视为空配置、不可读、行数超限)忽略整个 | ||
| 49 | + * 文件并返回 0,绝不视为错误(AC-1.4:配置异常不得阻断应用启动)。 | ||
| 50 | + * 单行非法仅跳过并记日志(AC-1.3)。 | ||
| 51 | + * | ||
| 52 | + * @param path 配置文件绝对路径 | ||
| 53 | + * @param scope 该文件对应的作用域(写入每条 entry.scope) | ||
| 54 | + * @param entries 输出条目数组(调用方分配,从 entries[*count] 起追加) | ||
| 55 | + * @param count 输入为已有条数,输出为追加后的总条数(容量上限 maxEntries) | ||
| 56 | + * @param maxEntries 数组容量 | ||
| 57 | + * @return 本次追加的条数;文件级异常返回 0 | ||
| 58 | + */ | ||
| 59 | +uint32_t AppEnvLoadFile(const char *path, uint8_t scope, | ||
| 60 | + AppEnvEntry *entries, uint32_t *count, uint32_t maxEntries); | ||
| 61 | + | ||
| 62 | +/** | ||
| 63 | + * @brief 加载四层作用域配置文件为条目数组。 | ||
| 64 | + * | ||
| 65 | + * 按应用身份定位最多 4 个文件(system/session/user/app),逐层调用 | ||
| 66 | + * AppEnvLoadFile。文件路径规则(design.md 6.8): | ||
| 67 | + * system: /system/etc/appenv.conf | ||
| 68 | + * session: /data/service/el1/public/appenv/session.conf | ||
| 69 | + * user: /data/service/el1/<userId>/appenv/user.conf | ||
| 70 | + * app: /data/service/el1/<userId>/appenv/apps/<bundleName>.conf | ||
| 71 | + * bundleName 需通过白名单字符校验([A-Za-z0-9._],防路径穿越)。 | ||
| 72 | + * | ||
| 73 | + * @param userId 目标用户 ID | ||
| 74 | + * @param bundleName 应用 bundle 名;为 NULL 时跳过 app 层 | ||
| 75 | + * @param entries 输出条目数组(调用方分配) | ||
| 76 | + * @param maxEntries 数组容量 | ||
| 77 | + * @return 成功读取的总条数(各文件级异常按 0 条计) | ||
| 78 | + */ | ||
| 79 | +uint32_t AppEnvLoadConfigs(uint32_t userId, const char *bundleName, | ||
| 80 | + AppEnvEntry *entries, uint32_t maxEntries); | ||
| 81 | + | ||
| 82 | + | ||
| 83 | +} | ||
| 84 | + | ||
| 85 | + | ||
| @@ -0,0 +1,94 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +/** | ||
| 24 | + * @brief 作用域优先级数值(越大优先级越高,BR-4)。 | ||
| 25 | + * | ||
| 26 | + * app(3) > user(2) > session(1) > system(0)。 | ||
| 27 | + * | ||
| 28 | + * @param scope AppEnvScope 作用域 | ||
| 29 | + * @return 优先级数值;非法作用域返回 -1(条目被丢弃) | ||
| 30 | + */ | ||
| 31 | +static int AppEnvScopePriority(uint8_t scope) | ||
| 32 | +{ | ||
| 33 | + switch (scope) { | ||
| 34 | + case APP_ENV_SCOPE_SYSTEM: | ||
| 35 | + return 0; | ||
| 36 | + case APP_ENV_SCOPE_SESSION: | ||
| 37 | + return 1; | ||
| 38 | + case APP_ENV_SCOPE_USER: | ||
| 39 | + return 2; | ||
| 40 | + case APP_ENV_SCOPE_APP: | ||
| 41 | + return 3; | ||
| 42 | + default: | ||
| 43 | + return -1; | ||
| 44 | + } | ||
| 45 | +} | ||
| 46 | + | ||
| 47 | +/** | ||
| 48 | + * @brief 在合并结果中查找已存在的 KEY。 | ||
| 49 | + * | ||
| 50 | + * @param merged 合并结果数组 | ||
| 51 | + * @param mergedCount 当前条数 | ||
| 52 | + * @param key 待查 KEY | ||
| 53 | + * @return 命中返回下标;未命中返回 -1 | ||
| 54 | + */ | ||
| 55 | +static int AppEnvFindKey(const AppEnvEntry *merged, uint32_t mergedCount, const char *key) | ||
| 56 | +{ | ||
| 57 | + for (uint32_t i = 0; i < mergedCount; i++) { | ||
| 58 | + if (strcmp(merged[i].key, key) == 0) { | ||
| 59 | + return (int)i; | ||
| 60 | + } | ||
| 61 | + } | ||
| 62 | + return -1; | ||
| 63 | +} | ||
| 64 | + | ||
| 65 | +uint32_t AppEnvMergeEntries(const AppEnvEntry *entries, uint32_t count, | ||
| 66 | + AppEnvEntry *merged, uint32_t maxMerged) | ||
| 67 | +{ | ||
| 68 | + if (entries == NULL || merged == NULL || maxMerged == 0) { | ||
| 69 | + return 0; | ||
| 70 | + } | ||
| 71 | + uint32_t mergedCount = 0; | ||
| 72 | + for (uint32_t i = 0; i < count; i++) { | ||
| 73 | + int newPrio = AppEnvScopePriority(entries[i].scope); | ||
| 74 | + if (newPrio < 0) { | ||
| 75 | + APPSPAWN_LOGW("appenv: drop entry with invalid scope %{public}u", entries[i].scope); | ||
| 76 | + continue; | ||
| 77 | + } | ||
| 78 | + int idx = AppEnvFindKey(merged, mergedCount, entries[i].key); | ||
| 79 | + if (idx < 0) { | ||
| 80 | + if (mergedCount >= maxMerged) { | ||
| 81 | + APPSPAWN_LOGW("appenv: merged buffer full, drop %{public}s", entries[i].key); | ||
| 82 | + continue; | ||
| 83 | + } | ||
| 84 | + merged[mergedCount++] = entries[i]; | ||
| 85 | + continue; | ||
| 86 | + } | ||
| 87 | + int oldPrio = AppEnvScopePriority(merged[idx].scope); | ||
| 88 | + // 高优先级覆盖;同优先级后者覆盖(文件内重复 KEY 取后出现者) | ||
| 89 | + if (newPrio >= oldPrio) { | ||
| 90 | + merged[idx] = entries[i]; | ||
| 91 | + } | ||
| 92 | + } | ||
| 93 | + return mergedCount; | ||
| 94 | +} | ||
| @@ -0,0 +1,46 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +extern "C" { | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * @brief 按 app > user > session > system 优先级合并条目(AC-1.6)。 | ||
| 28 | + * | ||
| 29 | + * 同一 KEY 多层配置时取最高优先级层的值;每条结果保留来源 scope | ||
| 30 | + * (policy 阶段 checkSource 判定输入,如 system-only 仅系统来源放行)。 | ||
| 31 | + * 同层内同 KEY 重复:取后出现者(文件内后者覆盖前者,语义与 | ||
| 32 | + * setenv overwrite 一致)。 | ||
| 33 | + * | ||
| 34 | + * @param entries loader 输出的原始条目数组(输入,不修改) | ||
| 35 | + * @param count 条目数 | ||
| 36 | + * @param merged 输出合并后条目数组(调用方分配) | ||
| 37 | + * @param maxMerged 输出数组容量 | ||
| 38 | + * @return 合并后条目数;参数非法或容量不足返回 0 | ||
| 39 | + */ | ||
| 40 | +uint32_t AppEnvMergeEntries(const AppEnvEntry *entries, uint32_t count, | ||
| 41 | + AppEnvEntry *merged, uint32_t maxMerged); | ||
| 42 | + | ||
| 43 | + | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | + | ||
| @@ -0,0 +1,67 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +extern "C" { | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +/** | ||
| 26 | + * @brief appenv 配置作用域类型。 | ||
| 27 | + * | ||
| 28 | + * 对应 FEAT-20260825-001 设计的四层配置作用域,合并优先级为 | ||
| 29 | + * app > user > session > system。 | ||
| 30 | + */ | ||
| 31 | +typedef enum TagAppEnvScope { | ||
| 32 | + APP_ENV_SCOPE_SYSTEM = 0, // 系统作用域 /system/etc/appenv.conf | ||
| 33 | + APP_ENV_SCOPE_SESSION = 1, // 会话作用域 session.conf | ||
| 34 | + APP_ENV_SCOPE_USER = 2, // 用户作用域 user.conf | ||
| 35 | + APP_ENV_SCOPE_APP = 3, // 应用作用域 apps/<bundleName>.conf | ||
| 36 | + APP_ENV_SCOPE_MAX = 4, | ||
| 37 | +} AppEnvScope; | ||
| 38 | + | ||
| 39 | +/** | ||
| 40 | + * @brief 环境变量安全分级。 | ||
| 41 | + * | ||
| 42 | + * 分级判定顺序:精确匹配分级表 -> 前缀匹配 -> 缺省 public。 | ||
| 43 | + */ | ||
| 44 | +typedef enum TagAppEnvClassification { | ||
| 45 | + APP_ENV_CLASS_PUBLIC = 0, // 普通变量,允许注入 | ||
| 46 | + APP_ENV_CLASS_RESTRICTED = 1, // 受限变量,需满足放行条件 | ||
| 47 | + APP_ENV_CLASS_SYSTEM_ONLY = 2, // 仅系统来源、系统应用可见 | ||
| 48 | + APP_ENV_CLASS_BLOCKED = 3, // 永不允许经 appenv 注入 | ||
| 49 | +} AppEnvClassification; | ||
| 50 | + | ||
| 51 | +/** | ||
| 52 | + * @brief 单条环境变量条目(快照内存储单元)。 | ||
| 53 | + * | ||
| 54 | + * 快照由 appspawn 父进程构建,fork 后子进程只读, | ||
| 55 | + * 单次孵化内不发生修改,因此无需并发保护。 | ||
| 56 | + */ | ||
| 57 | +typedef struct TagAppEnvEntry { | ||
| 58 | + char key[256]; // 变量名,上报与过滤判定使用 | ||
| 59 | + char value[4096]; // 变量值,与配置文件单行上限一致 | ||
| 60 | + uint8_t scope; // AppEnvScope,来源作用域 | ||
| 61 | + uint8_t klass; // AppEnvClassification,安全分级 | ||
| 62 | +} AppEnvEntry; | ||
| 63 | + | ||
| 64 | + | ||
| 65 | +} | ||
| 66 | + | ||
| 67 | + | ||
| @@ -0,0 +1,50 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +/** 系统默认 PATH 基线,最终 PATH 的恒定前缀段(BR-3)。 */ | ||
| 24 | +static const char * const g_systemDefaultPath = "/system/bin:/system/xbin:/vendor/bin"; | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * @brief 构建目标应用最终 PATH(TASK-006 实现)。 | ||
| 28 | + * | ||
| 29 | + * 骨架阶段仅拷贝系统默认 PATH(无追加段,等价于无配置行为), | ||
| 30 | + * 七项校验与 path.list 读取随 TASK-006 落地。 | ||
| 31 | + * | ||
| 32 | + * @param userId 目标用户 ID | ||
| 33 | + * @param bundleName 应用 bundle 名;为 NULL 时不含 app 追加段 | ||
| 34 | + * @param outPath 输出缓冲区(调用方分配) | ||
| 35 | + * @param outLen 缓冲区容量 | ||
| 36 | + * @return 成功返回 0;参数非法返回 -1 | ||
| 37 | + */ | ||
| 38 | +int AppEnvBuildFinalPath(uint32_t userId, const char *bundleName, | ||
| 39 | + char *outPath, uint32_t outLen) | ||
| 40 | +{ | ||
| 41 | + if (outPath == NULL || outLen == 0) { | ||
| 42 | + return -1; | ||
| 43 | + } | ||
| 44 | + (void)userId; | ||
| 45 | + (void)bundleName; | ||
| 46 | + errno_t ret = strcpy_s(outPath, outLen, g_systemDefaultPath); | ||
| 47 | + APPSPAWN_CHECK(ret == EOK, return -1, "appenv path: copy default path failed"); | ||
| 48 | + APPSPAWN_LOGV("appenv path skeleton, append validation lands with TASK-006"); | ||
| 49 | + return 0; | ||
| 50 | +} | ||
| @@ -0,0 +1,51 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +extern "C" { | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +/** 单个作用域允许的 PATH 追加目录上限(BR-10 数量受限约束)。 */ | ||
| 26 | + | ||
| 27 | +/** 单个追加目录路径的最大长度(含结尾符)。 */ | ||
| 28 | + | ||
| 29 | + | ||
| 30 | +/** | ||
| 31 | + * @brief 构建目标应用最终 PATH(TASK-006 实现)。 | ||
| 32 | + * | ||
| 33 | + * 拼接规则:final_PATH = system_default + system_append + mdm_append + | ||
| 34 | + * app_append(BR-3),系统默认段恒在最前;追加项逐条通过七项安全校验 | ||
| 35 | + * (绝对路径/存在/无 ../非未受信 symlink/非 world-writable/属主合法/ | ||
| 36 | + * label 合法),校验失败跳过该项不影响默认段(AC-4.4/4.5)。 | ||
| 37 | + * 骨架阶段仅输出系统默认 PATH。 | ||
| 38 | + * | ||
| 39 | + * @param userId 目标用户 ID | ||
| 40 | + * @param bundleName 应用 bundle 名;为 NULL 时不含 app 追加段 | ||
| 41 | + * @param outPath 输出缓冲区(调用方分配) | ||
| 42 | + * @param outLen 缓冲区容量 | ||
| 43 | + * @return 成功返回 0;参数非法或截断风险返回 -1 | ||
| 44 | + */ | ||
| 45 | +int AppEnvBuildFinalPath(uint32_t userId, const char *bundleName, | ||
| 46 | + char *outPath, uint32_t outLen); | ||
| 47 | + | ||
| 48 | + | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | + | ||
| @@ -0,0 +1,268 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | +/** 白名单条目上限(镜像文件约束,超出部分忽略)。 */ | ||
| 25 | + | ||
| 26 | +/** 白名单单行长度上限(含注释行)。 */ | ||
| 27 | + | ||
| 28 | + | ||
| 29 | +/** 上次裁决的过滤原因(静态存储;注入在子进程单线程执行,无竞争)。 */ | ||
| 30 | +static const char *g_lastReason = ""; | ||
| 31 | + | ||
| 32 | +/** | ||
| 33 | + * @brief 分级表条目(编译期静态,精确匹配优先于前缀匹配)。 | ||
| 34 | + */ | ||
| 35 | +typedef struct { | ||
| 36 | + const char *key; // 精确变量名 | ||
| 37 | + AppEnvClassification klass; // 分级 | ||
| 38 | +} AppEnvClassEntry; | ||
| 39 | + | ||
| 40 | +/** | ||
| 41 | + * @brief 前缀分级表(按序匹配,命中即止)。 | ||
| 42 | + */ | ||
| 43 | +typedef struct { | ||
| 44 | + const char *prefix; // 变量名前缀 | ||
| 45 | + AppEnvClassification klass; // 分级 | ||
| 46 | +} AppEnvClassPrefix; | ||
| 47 | + | ||
| 48 | +/** 精确匹配分级表(设计说明第 7 载体,BR-6)。 */ | ||
| 49 | +static const AppEnvClassEntry g_classTable[] = { | ||
| 50 | + // blocked:动态库/内存/线程 sanitizer 注入面 | ||
| 51 | + {"LD_PRELOAD", APP_ENV_CLASS_BLOCKED}, | ||
| 52 | + {"LD_AUDIT", APP_ENV_CLASS_BLOCKED}, | ||
| 53 | + // restricted:库路径/代理/证书/日志/运行时开关(条件放行) | ||
| 54 | + {"LD_LIBRARY_PATH", APP_ENV_CLASS_RESTRICTED}, | ||
| 55 | + {"PATH", APP_ENV_CLASS_RESTRICTED}, // 正常不出现(loader 已拒 PATH=),防御性归 restricted | ||
| 56 | + {"HTTP_PROXY", APP_ENV_CLASS_RESTRICTED}, | ||
| 57 | + {"HTTPS_PROXY", APP_ENV_CLASS_RESTRICTED}, | ||
| 58 | + {"ALL_PROXY", APP_ENV_CLASS_RESTRICTED}, | ||
| 59 | + {"NO_PROXY", APP_ENV_CLASS_RESTRICTED}, | ||
| 60 | + {"SSL_CERT_FILE", APP_ENV_CLASS_RESTRICTED}, | ||
| 61 | + {"SSL_CERT_DIR", APP_ENV_CLASS_RESTRICTED}, | ||
| 62 | + // system-only:进程身份类变量(系统来源才可信) | ||
| 63 | + {"HOME", APP_ENV_CLASS_SYSTEM_ONLY}, | ||
| 64 | + {"USER", APP_ENV_CLASS_SYSTEM_ONLY}, | ||
| 65 | + {"SHELL", APP_ENV_CLASS_SYSTEM_ONLY}, | ||
| 66 | + {"TMPDIR", APP_ENV_CLASS_SYSTEM_ONLY}, | ||
| 67 | +}; | ||
| 68 | + | ||
| 69 | +/** 前缀匹配分级表(DYLD_/MALLOC_/ASAN_/TSAN_/HILOG_/ARK_/ACE_/XDG_ 前缀族)。 */ | ||
| 70 | +static const AppEnvClassPrefix g_classPrefixes[] = { | ||
| 71 | + {"DYLD_", APP_ENV_CLASS_BLOCKED}, | ||
| 72 | + {"MALLOC_", APP_ENV_CLASS_BLOCKED}, | ||
| 73 | + {"ASAN_", APP_ENV_CLASS_BLOCKED}, | ||
| 74 | + {"TSAN_", APP_ENV_CLASS_BLOCKED}, | ||
| 75 | + {"HILOG_", APP_ENV_CLASS_RESTRICTED}, | ||
| 76 | + {"ARK_", APP_ENV_CLASS_RESTRICTED}, | ||
| 77 | + {"ACE_", APP_ENV_CLASS_RESTRICTED}, | ||
| 78 | + {"XDG_", APP_ENV_CLASS_SYSTEM_ONLY}, | ||
| 79 | +}; | ||
| 80 | + | ||
| 81 | +/** 前缀表条目数(编译期推导)。 */ | ||
| 82 | + | ||
| 83 | +/** 精确表条目数。 */ | ||
| 84 | + | ||
| 85 | + | ||
| 86 | +/** 白名单缓存(只读镜像文件加载一次)。 */ | ||
| 87 | +static char g_whitelist[APPENV_WHITELIST_MAX][APPENV_WHITELIST_LINE_MAX]; | ||
| 88 | +/** 白名单已加载标志(0 未加载,1 已加载)。 */ | ||
| 89 | +static int g_whitelistLoaded = 0; | ||
| 90 | + | ||
| 91 | +AppEnvClassification AppEnvClassifyKey(const char *key) | ||
| 92 | +{ | ||
| 93 | + if (key == NULL || key[0] == '\0') { | ||
| 94 | + return APP_ENV_CLASS_BLOCKED; // 非法名保守按 blocked | ||
| 95 | + } | ||
| 96 | + for (uint32_t i = 0; i < CLASS_ENTRY_COUNT; i++) { | ||
| 97 | + if (strcmp(key, g_classTable[i].key) == 0) { | ||
| 98 | + return g_classTable[i].klass; | ||
| 99 | + } | ||
| 100 | + } | ||
| 101 | + for (uint32_t i = 0; i < CLASS_PREFIX_COUNT; i++) { | ||
| 102 | + if (strncmp(key, g_classPrefixes[i].prefix, strlen(g_classPrefixes[i].prefix)) == 0) { | ||
| 103 | + return g_classPrefixes[i].klass; | ||
| 104 | + } | ||
| 105 | + } | ||
| 106 | + return APP_ENV_CLASS_PUBLIC; | ||
| 107 | +} | ||
| 108 | + | ||
| 109 | +/** | ||
| 110 | + * @brief 加载 restricted 白名单文件到缓存(一次性)。 | ||
| 111 | + * | ||
| 112 | + * 每行一个 bundleName('#' 注释与空行跳过);文件不存在/不可读 | ||
| 113 | + * 视为空白名单(AC-1.4 容错语义,不阻断)。 | ||
| 114 | + */ | ||
| 115 | +static void AppEnvLoadWhitelist(void) | ||
| 116 | +{ | ||
| 117 | + if (g_whitelistLoaded) { | ||
| 118 | + return; | ||
| 119 | + } | ||
| 120 | + g_whitelistLoaded = 1; | ||
| 121 | + FILE *fp = fopen(APPENV_WHITELIST_PATH, "r"); | ||
| 122 | + if (fp == NULL) { | ||
| 123 | + return; // 不存在为常态(未预置白名单) | ||
| 124 | + } | ||
| 125 | + char line[APPENV_WHITELIST_LINE_MAX] = {0}; | ||
| 126 | + uint32_t count = 0; | ||
| 127 | + while (count < APPENV_WHITELIST_MAX && fgets(line, sizeof(line), fp) != NULL) { | ||
| 128 | + // 裁剪空白 | ||
| 129 | + char *start = line + strspn(line, " \t\n\r"); | ||
| 130 | + size_t len = strlen(start); | ||
| 131 | + while (len > 0 && strchr(" \t\n\r", start[len - 1]) != NULL) { | ||
| 132 | + start[--len] = '\0'; | ||
| 133 | + } | ||
| 134 | + if (len == 0 || start[0] == '#') { | ||
| 135 | + continue; | ||
| 136 | + } | ||
| 137 | + errno_t ret = strcpy_s(g_whitelist[count], sizeof(g_whitelist[count]), start); | ||
| 138 | + if (ret == EOK) { | ||
| 139 | + count++; | ||
| 140 | + } | ||
| 141 | + } | ||
| 142 | + (void)fclose(fp); | ||
| 143 | +} | ||
| 144 | + | ||
| 145 | +bool AppEnvIsWhitelisted(const char *bundleName) | ||
| 146 | +{ | ||
| 147 | + if (bundleName == NULL || bundleName[0] == '\0') { | ||
| 148 | + return false; | ||
| 149 | + } | ||
| 150 | + AppEnvLoadWhitelist(); | ||
| 151 | + for (uint32_t i = 0; i < APPENV_WHITELIST_MAX; i++) { | ||
| 152 | + if (g_whitelist[i][0] == '\0') { | ||
| 153 | + break; | ||
| 154 | + } | ||
| 155 | + if (strcmp(g_whitelist[i], bundleName) == 0) { | ||
| 156 | + return true; | ||
| 157 | + } | ||
| 158 | + } | ||
| 159 | + return false; | ||
| 160 | +} | ||
| 161 | + | ||
| 162 | +bool AppEnvIsSystemApp(const AppSpawningCtx *property) | ||
| 163 | +{ | ||
| 164 | + if (property == NULL) { | ||
| 165 | + return false; | ||
| 166 | + } | ||
| 167 | + AppSpawnMsgDomainInfo *domain = | ||
| 168 | + (AppSpawnMsgDomainInfo *)GetAppProperty(property, TLV_DOMAIN_INFO); | ||
| 169 | + if (domain == NULL) { | ||
| 170 | + return false; | ||
| 171 | + } | ||
| 172 | + return strcmp(domain->apl, "system_basic") == 0 || strcmp(domain->apl, "system_core") == 0; | ||
| 173 | +} | ||
| 174 | + | ||
| 175 | +bool AppEnvIsDebuggableApp(const AppSpawningCtx *property) | ||
| 176 | +{ | ||
| 177 | + return property != NULL && CheckAppMsgFlagsSet(property, APP_FLAGS_DEBUGGABLE); | ||
| 178 | +} | ||
| 179 | + | ||
| 180 | +bool AppEnvIsDeveloperMode(const AppSpawningCtx *property) | ||
| 181 | +{ | ||
| 182 | + if (property != NULL && CheckAppMsgFlagsSet(property, APP_FLAGS_DEVELOPER_MODE)) { | ||
| 183 | + return true; | ||
| 184 | + } | ||
| 185 | + return IsDeveloperModeOpen() > 0; // 消息缺失时回退系统参数 | ||
| 186 | +} | ||
| 187 | + | ||
| 188 | +/** | ||
| 189 | + * @brief checkSource + checkTarget + checkRuntimeContext:restricted 放行判定。 | ||
| 190 | + * | ||
| 191 | + * 生效矩阵(AC-1.5/4.6/9.1): | ||
| 192 | + * app 作用域:debuggable + 开发者模式 | ||
| 193 | + * user/system 作用域:默认拒;白名单命中 + debuggable + 开发者模式 | ||
| 194 | + * 才放行(ADR-8) | ||
| 195 | + * 系统应用:按系统策略放行(当前实现为放行,MDM 层三期细化) | ||
| 196 | + * | ||
| 197 | + * @param entry 待判定条目 | ||
| 198 | + * @param property 业务孵化数据 | ||
| 199 | + * @return true 放行 | ||
| 200 | + */ | ||
| 201 | +static bool AppEnvAllowRestricted(const AppEnvEntry *entry, const AppSpawningCtx *property) | ||
| 202 | +{ | ||
| 203 | + if (!AppEnvIsDebuggableApp(property) || !AppEnvIsDeveloperMode(property)) { | ||
| 204 | + return false; // 目标非 debuggable 或非开发者模式:恒拒(AC-4.6) | ||
| 205 | + } | ||
| 206 | + if (entry->scope == APP_ENV_SCOPE_APP) { | ||
| 207 | + return true; // app 作用域 + 双条件(AC-1.5) | ||
| 208 | + } | ||
| 209 | + if (entry->scope == APP_ENV_SCOPE_USER || entry->scope == APP_ENV_SCOPE_SYSTEM) { | ||
| 210 | + // user/system 作用域:白名单例外(AC-9.1) | ||
| 211 | + const char *bundle = GetBundleName(property); | ||
| 212 | + return AppEnvIsWhitelisted(bundle); | ||
| 213 | + } | ||
| 214 | + return false; // session 作用域 restricted:默认拒 | ||
| 215 | +} | ||
| 216 | + | ||
| 217 | +bool AppEnvAllowInject(AppEnvEntry *entry, const AppSpawningCtx *property) | ||
| 218 | +{ | ||
| 219 | + if (entry == NULL || entry->key[0] == '\0') { | ||
| 220 | + g_lastReason = "invalid-entry"; | ||
| 221 | + return false; | ||
| 222 | + } | ||
| 223 | + // validateValue:loader 侧已保证非空/长度/无 $ 与反引号;此处防御 | ||
| 224 | + // 性复查值不含命令替换(FR-17 纵深防御) | ||
| 225 | + if (strpbrk(entry->value, "$`") != NULL) { | ||
| 226 | + g_lastReason = "value-syntax"; | ||
| 227 | + return false; | ||
| 228 | + } | ||
| 229 | + AppEnvClassification klass = AppEnvClassifyKey(entry->key); | ||
| 230 | + entry->klass = klass; // 回写分级,供注入记录/observability 使用 | ||
| 231 | + switch (klass) { | ||
| 232 | + case APP_ENV_CLASS_BLOCKED: | ||
| 233 | + // blocked 恒拒:含 root 写入、含系统应用(AC-4.1/4.2) | ||
| 234 | + g_lastReason = "blocked-class"; | ||
| 235 | + return false; | ||
| 236 | + case APP_ENV_CLASS_SYSTEM_ONLY: | ||
| 237 | + // 仅 system 来源 + 系统应用可见 | ||
| 238 | + if (entry->scope != APP_ENV_SCOPE_SYSTEM || !AppEnvIsSystemApp(property)) { | ||
| 239 | + g_lastReason = "system-only-source"; | ||
| 240 | + return false; | ||
| 241 | + } | ||
| 242 | + g_lastReason = ""; | ||
| 243 | + return true; | ||
| 244 | + case APP_ENV_CLASS_RESTRICTED: | ||
| 245 | + if (!AppEnvAllowRestricted(entry, property)) { | ||
| 246 | + g_lastReason = (entry->scope == APP_ENV_SCOPE_APP) | ||
| 247 | + ? "restricted-not-debuggable" : "restricted-scope-or-whitelist"; | ||
| 248 | + return false; | ||
| 249 | + } | ||
| 250 | + g_lastReason = ""; | ||
| 251 | + return true; | ||
| 252 | + case APP_ENV_CLASS_PUBLIC: | ||
| 253 | + default: | ||
| 254 | + // public 恒放;系统应用默认只接收 system 作用域用户配置 | ||
| 255 | + // (高安全应用语义的当前实现,design.md 6.5) | ||
| 256 | + if (AppEnvIsSystemApp(property) && entry->scope != APP_ENV_SCOPE_SYSTEM) { | ||
| 257 | + g_lastReason = "system-app-user-scope"; | ||
| 258 | + return false; | ||
| 259 | + } | ||
| 260 | + g_lastReason = ""; | ||
| 261 | + return true; | ||
| 262 | + } | ||
| 263 | +} | ||
| 264 | + | ||
| 265 | +const char *AppEnvLastFilterReason(void) | ||
| 266 | +{ | ||
| 267 | + return g_lastReason; | ||
| 268 | +} | ||
| @@ -0,0 +1,120 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | +extern "C" { | ||
| 25 | + | ||
| 26 | + | ||
| 27 | +/** restricted 白名单文件路径(ADR-8:镜像预置只读,仅构建/OTA 可变更)。 */ | ||
| 28 | + | ||
| 29 | + | ||
| 30 | +/** | ||
| 31 | + * @brief 判定变量的安全分级(BR-6)。 | ||
| 32 | + * | ||
| 33 | + * 判定顺序:分级表精确匹配 -> 前缀匹配(LD_/DYLD_/MALLOC_/ASAN_/TSAN_/ | ||
| 34 | + * HILOG_/ARK_/ACE_/XDG_ 前缀族)-> 缺省 public。 | ||
| 35 | + * 判定表为编译期静态数据(不可被运行时篡改),策略文件覆盖能力 | ||
| 36 | + * 随版本演进评估(当前不做)。 | ||
| 37 | + * | ||
| 38 | + * @param key 变量名 | ||
| 39 | + * @return AppEnvClassification 分级结果;参数非法返回 BLOCKED(保守) | ||
| 40 | + */ | ||
| 41 | +AppEnvClassification AppEnvClassifyKey(const char *key); | ||
| 42 | + | ||
| 43 | +/** | ||
| 44 | + * @brief 查询 bundle 是否在 restricted 白名单中(ADR-8,AC-9.x)。 | ||
| 45 | + * | ||
| 46 | + * 白名单文件每行一个 bundleName('#' 注释);文件不存在视为空 | ||
| 47 | + * (容错语义同配置加载,AC-1.4)。结果在进程内缓存(白名单为 | ||
| 48 | + * 只读镜像文件,生命周期内不变)。 | ||
| 49 | + * | ||
| 50 | + * @param bundleName 应用 bundle 名 | ||
| 51 | + * @return true 在白名单中;false 不在或文件异常 | ||
| 52 | + */ | ||
| 53 | +bool AppEnvIsWhitelisted(const char *bundleName); | ||
| 54 | + | ||
| 55 | +/** | ||
| 56 | + * @brief 判定目标应用是否为系统/系统签名应用。 | ||
| 57 | + * | ||
| 58 | + * 依据 TLV_DOMAIN_INFO 的 apl 字段(system_core / system_basic)。 | ||
| 59 | + * | ||
| 60 | + * @param property 业务孵化数据 | ||
| 61 | + * @return true 系统 APL 应用 | ||
| 62 | + */ | ||
| 63 | +bool AppEnvIsSystemApp(const AppSpawningCtx *property); | ||
| 64 | + | ||
| 65 | +/** | ||
| 66 | + * @brief 判定目标应用是否 debuggable(开发者模式视角)。 | ||
| 67 | + * | ||
| 68 | + * 依据消息 flags 的 APP_FLAGS_DEBUGGABLE 位。 | ||
| 69 | + * | ||
| 70 | + * @param property 业务孵化数据 | ||
| 71 | + * @return true debuggable 应用 | ||
| 72 | + */ | ||
| 73 | +bool AppEnvIsDebuggableApp(const AppSpawningCtx *property); | ||
| 74 | + | ||
| 75 | +/** | ||
| 76 | + * @brief 判定系统开发者模式是否开启。 | ||
| 77 | + * | ||
| 78 | + * 依据消息 flags 的 APP_FLAGS_DEVELOPER_MODE 位(AMS 按系统状态 | ||
| 79 | + * 填充);消息缺失时回退 IsDeveloperModeOpen() 系统参数。 | ||
| 80 | + * | ||
| 81 | + * @param property 业务孵化数据;可为 NULL(直接走系统参数) | ||
| 82 | + * @return true 开发者模式 | ||
| 83 | + */ | ||
| 84 | +bool AppEnvIsDeveloperMode(const AppSpawningCtx *property); | ||
| 85 | + | ||
| 86 | +/** | ||
| 87 | + * @brief 最终裁决:判定单条变量能否注入目标应用(FR-17,AC-4.x)。 | ||
| 88 | + * | ||
| 89 | + * 执行设计说明 15.7 裁决链: | ||
| 90 | + * classify(key) -> validateValue(value) -> checkSource(scope) | ||
| 91 | + * -> checkTarget(targetApp) -> checkRuntimeContext -> applySpecialRule(PATH) | ||
| 92 | + * 即使 AppEnvManager 设置阶段已过滤,此处仍重复关键校验 —— | ||
| 93 | + * 防止配置文件被绕过修改后直接污染进程环境(纵深防御,AC-4.2)。 | ||
| 94 | + * 生效矩阵(AC-1.5/4.6/9.1): | ||
| 95 | + * blocked 恒拒(含 root 写入、含系统应用) | ||
| 96 | + * public 恒放(高安全应用例外逻辑随签名识别能力演进,当前按 | ||
| 97 | + * 系统应用默认仅收 system 作用域实现) | ||
| 98 | + * restricted:debuggable + 开发者模式 + (app 作用域 或 白名单命中) | ||
| 99 | + * system-only:仅 system 来源 + 系统应用 | ||
| 100 | + * | ||
| 101 | + * @param entry 待判定条目(key/value/scope;判定后回写 klass 分级) | ||
| 102 | + * @param property 业务孵化数据;NULL 时仅允许 public(保守) | ||
| 103 | + * @return true 允许注入;false 拒绝(调用方记录过滤原因) | ||
| 104 | + */ | ||
| 105 | +bool AppEnvAllowInject(AppEnvEntry *entry, const AppSpawningCtx *property); | ||
| 106 | + | ||
| 107 | +/** | ||
| 108 | + * @brief 裁决结果的过滤原因(可观测,配合 appenv explain 输出)。 | ||
| 109 | + * | ||
| 110 | + * 必须在 AppEnvAllowInject 返回 false 后调用(复用上次判定的原因); | ||
| 111 | + * 线程安全性:appspawn 注入在子进程 fork 后单线程执行,无竞争。 | ||
| 112 | + * | ||
| 113 | + * @return 原因字符串(静态存储,如 "blocked-class") | ||
| 114 | + */ | ||
| 115 | +const char *AppEnvLastFilterReason(void); | ||
| 116 | + | ||
| 117 | + | ||
| 118 | +} | ||
| 119 | + | ||
| 120 | + | ||
| @@ -35,6 +35,20 @@ extern "C" { | |||
| 35 | 35 | ||
| 36 | 36 | ||
| 37 | 37 | ||
| 38 | +// FEAT-20260825-001: appenv 环境变量注入模块(libappspawn_appenv.so) | ||
| 39 | + | ||
| 40 | + | ||
| 41 | + | ||
| 42 | + | ||
| 43 | + | ||
| 44 | + | ||
| 45 | +// FEAT-20260825-001: AppEnvManager IPC 服务(libappenv_manager.so,dlopen 加载) | ||
| 46 | + | ||
| 47 | + | ||
| 48 | + | ||
| 49 | + | ||
| 50 | + | ||
| 51 | + | ||
| 38 | typedef enum { | 52 | typedef enum { |
| 39 | MODULE_DEFAULT, | 53 | MODULE_DEFAULT, |
| 40 | MODULE_APPSPAWN, | 54 | MODULE_APPSPAWN, |
| @@ -0,0 +1,80 @@ | |||
| 1 | +# Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 2 | +# Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 3 | +# you may not use this file except in compliance with the License. | ||
| 4 | +# You may obtain a copy of the License at | ||
| 5 | +# | ||
| 6 | +# http://www.apache.org/licenses/LICENSE-2.0 | ||
| 7 | +# | ||
| 8 | +# Unless required by applicable law or agreed to in writing, software | ||
| 9 | +# distributed under the License is distributed on an "AS IS" BASIS, | ||
| 10 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 11 | +# See the License for the specific language governing permissions and | ||
| 12 | +# limitations under the License. | ||
| 13 | + | ||
| 14 | +import("//base/startup/appspawn/appspawn.gni") | ||
| 15 | +import("//build/ohos.gni") | ||
| 16 | + | ||
| 17 | +# FEAT-20260825-001 TASK-002/003: appenv 服务与命令(v1.2 单仓收敛,仿 | ||
| 18 | +# service/hnp、service/devicedebug 多 bin 先例) | ||
| 19 | +# - libappenv_manager: AppEnvManager IPC 服务(appspawn 进程内加载) | ||
| 20 | +# - appenv: CLI bin(命令分发 + 内嵌 IPC 客户端,/system/bin) | ||
| 21 | +ohos_shared_library("libappenv_manager") { | ||
| 22 | + branch_protector_ret = "pac_ret" | ||
| 23 | + sanitize = { | ||
| 24 | + cfi = true | ||
| 25 | + cfi_cross_dso = true | ||
| 26 | + cfi_no_nvcall = true | ||
| 27 | + cfi_vcall_icall_only = true | ||
| 28 | + debug = false | ||
| 29 | + } | ||
| 30 | + sources = [ "appenv_manager.c" ] | ||
| 31 | + include_dirs = [ | ||
| 32 | + ".", | ||
| 33 | + "${appspawn_path}/common", | ||
| 34 | + "${appspawn_path}/standard", | ||
| 35 | + "${appspawn_path}/util/include", | ||
| 36 | + ] | ||
| 37 | + deps = [ "${appspawn_path}/modules/module_engine:libappspawn_module_engine" ] | ||
| 38 | + external_deps = [ | ||
| 39 | + "bounds_checking_function:libsec_shared", | ||
| 40 | + "hilog:libhilog", | ||
| 41 | + "init:libbegetutil", | ||
| 42 | + ] | ||
| 43 | + install_enable = true | ||
| 44 | + subsystem_name = "${subsystem_name}" | ||
| 45 | + part_name = "${part_name}" | ||
| 46 | + if (target_cpu == "arm64" || target_cpu == "x86_64" || | ||
| 47 | + target_cpu == "riscv64") { | ||
| 48 | + module_install_dir = "lib64/appspawn" | ||
| 49 | + } else { | ||
| 50 | + module_install_dir = "lib/appspawn" | ||
| 51 | + } | ||
| 52 | +} | ||
| 53 | + | ||
| 54 | +ohos_executable("appenv") { | ||
| 55 | + branch_protector_ret = "pac_ret" | ||
| 56 | + sanitize = { | ||
| 57 | + cfi = true | ||
| 58 | + cfi_cross_dso = true | ||
| 59 | + cfi_no_nvcall = true | ||
| 60 | + cfi_vcall_icall_only = true | ||
| 61 | + debug = false | ||
| 62 | + } | ||
| 63 | + sources = [ | ||
| 64 | + "appenv_client.c", | ||
| 65 | + "appenv_cmd.c", | ||
| 66 | + "appenv_main.c", | ||
| 67 | + ] | ||
| 68 | + include_dirs = [ | ||
| 69 | + ".", | ||
| 70 | + "${appspawn_path}/util/include", | ||
| 71 | + ] | ||
| 72 | + deps = [] | ||
| 73 | + external_deps = [ | ||
| 74 | + "bounds_checking_function:libsec_shared", | ||
| 75 | + "hilog:libhilog", | ||
| 76 | + ] | ||
| 77 | + install_enable = true | ||
| 78 | + subsystem_name = "${subsystem_name}" | ||
| 79 | + part_name = "${part_name}" | ||
| 80 | +} | ||
| @@ -0,0 +1,96 @@ | |||
| 1 | +# appenv 命令与服务(AppEnvManager) | ||
| 2 | + | ||
| 3 | +## 简介 | ||
| 4 | + | ||
| 5 | +`service/appenv` 提供 GUI 应用环境变量配置能力的**命令入口与权威管理服务**(FEAT-20260825-001),与 hnp/devicedebug 同级(appspawn 仓 service 目录多 bin 先例)。 | ||
| 6 | + | ||
| 7 | +两个构建产物: | ||
| 8 | + | ||
| 9 | +- **`appenv`**(`/system/bin/appenv`):标准 POSIX 命令行工具,8 个子命令,内嵌 IPC 客户端 | ||
| 10 | +- **`libappenv_manager.so`**:AppEnvManager 权威服务(appspawn 进程内加载,唯一配置写者) | ||
| 11 | + | ||
| 12 | +```text | ||
| 13 | +hishell / hdc shell / 脚本 / IDE 终端 | ||
| 14 | + │ PATH 直接执行(零集成) | ||
| 15 | + ▼ | ||
| 16 | +/system/bin/appenv ──(unix socket + SO_PEERCRED)──> AppEnvManager(appspawn 进程内) | ||
| 17 | + │ 设置阶段:权限决策矩阵 | ||
| 18 | + │ → 参数校验 → 原子写 appenv.conf | ||
| 19 | + ▼ | ||
| 20 | + modules/appenv(生效阶段注入) | ||
| 21 | +``` | ||
| 22 | + | ||
| 23 | +## 目录 | ||
| 24 | + | ||
| 25 | +``` | ||
| 26 | +service/appenv | ||
| 27 | +├─appenv_main.c # CLI 入口:子命令分发 | ||
| 28 | +├─appenv_cmd.cpp/h # 8 子命令静态表 + usage | ||
| 29 | +├─appenv_client.c/h # 内嵌 IPC 客户端(AppEnvRpc:连接/超时/响应校验) | ||
| 30 | +├─appenv_ipc.h # IPC 协议(操作码/结果码/消息头,与 CLI 退出码语义对齐) | ||
| 31 | +├─appenv_manager.c/h # AppEnvManager:loopevent server + SO_PEERCRED + 请求处理 | ||
| 32 | +└─BUILD.gn # ohos_executable("appenv") + ohos_shared_library("libappenv_manager") | ||
| 33 | +``` | ||
| 34 | + | ||
| 35 | +## 命令说明 | ||
| 36 | + | ||
| 37 | +```sh | ||
| 38 | +# KV 变量(四层作用域;缺省 --user) | ||
| 39 | +appenv set KEY VALUE [--system | --session | --user | --bundle BN] | ||
| 40 | +appenv unset KEY [--scope…] | ||
| 41 | +appenv get KEY [--scope…] | ||
| 42 | +appenv list [--scope…] | ||
| 43 | + | ||
| 44 | +# PATH 专项(唯一合法入口;appenv set PATH 一律拒绝) | ||
| 45 | +appenv path append DIR [--bundle BN] # 仅追加,放末尾 | ||
| 46 | +appenv path remove DIR [--bundle BN] | ||
| 47 | +appenv path list [--bundle BN] | ||
| 48 | + | ||
| 49 | +# 可观测 | ||
| 50 | +appenv check # 配置体检(格式/权限/超限) | ||
| 51 | +appenv effective --bundle X # 生效预览(下次启动的合并视图) | ||
| 52 | +appenv explain KEY --bundle X # 变量来源/状态/过滤原因 | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +**退出码**:0 成功|1 参数非法|2 权限拒绝|3 restricted 条件不满足|4 黑名单/PATH 直写拒绝|5 服务不可达|6 超限 | ||
| 56 | + | ||
| 57 | +### 生效语义(重要) | ||
| 58 | + | ||
| 59 | +- 只影响**新启动**的应用进程;已运行应用的 environ 不变 | ||
| 60 | +- 不等价于 shell `export` | ||
| 61 | +- 系统过滤敏感变量(四级分级);`PATH` 不能被覆盖,仅授权场景结构化追加 | ||
| 62 | +- release 应用默认不接收 restricted 变量;debuggable 应用在开发者模式下可接收部分受限变量(app 作用域) | ||
| 63 | + | ||
| 64 | +## IPC 协议 | ||
| 65 | + | ||
| 66 | +| 项 | 值 | | ||
| 67 | +|----|-----| | ||
| 68 | +| socket 路径 | `/dev/unix/socket/appenv` | | ||
| 69 | +| 身份判定 | 内核 SO_PEERCRED(connect 时填充,不可伪造) | | ||
| 70 | +| 消息格式 | 定长头(magic/version/opcode/result/payloadLen)+ 载荷 | | ||
| 71 | +| 超时 | 3000ms(客户端 poll) | | ||
| 72 | + | ||
| 73 | +**权限要点**:连接层不设 uid 白名单 —— 权限差异由设置阶段决策矩阵逐请求判定(shell 可写 user/app public、三方全拒、系统应用需声明权限)。协议面向外部工具保持稳定(二期文档化直连接入方式)。 | ||
| 74 | + | ||
| 75 | +## 编译与安装 | ||
| 76 | + | ||
| 77 | +```text | ||
| 78 | +# 依赖(standard/BUILD.gn,appspawn 目标) | ||
| 79 | +appspawn_support_appenv = true # appspawn.gni 总开关(默认 true) | ||
| 80 | + | ||
| 81 | +# 产物 | ||
| 82 | +/system/bin/appenv | ||
| 83 | +/system/lib[64]/appspawn/libappenv_manager.so | ||
| 84 | +``` | ||
| 85 | + | ||
| 86 | +模块安装路径与 `libappspawn_appenv.so`(注入模块)同级;appspawn 启动时经 `AppSpawnModuleMgrInstall(APPENV_MODULE_PATH)` 显式安装注入模块,并调用 `AppEnvManagerStart()` 启动 IPC server(失败仅记录日志,不阻断孵化服务)。 | ||
| 87 | + | ||
| 88 | +## 单元测试 | ||
| 89 | + | ||
| 90 | +`test/unittest/appenv_test/`:loader/merger/policy 纯函数用例(31 个)。 | ||
| 91 | + | ||
| 92 | +## 相关资料 | ||
| 93 | + | ||
| 94 | +- 设计说明:`../../../appspawn-appenv-design.md` | ||
| 95 | +- 需求规格:`.specs/features/FEAT-20260825-001-appenv/` | ||
| 96 | +- 注入模块:`modules/appenv/`(见其 README) | ||
| @@ -0,0 +1,108 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + | ||
| 27 | +/** IPC 消息魔数(与服务端 appenv_manager.c 一致)。 */ | ||
| 28 | + | ||
| 29 | + | ||
| 30 | +/** | ||
| 31 | + * @brief 连接 AppEnvManager server socket。 | ||
| 32 | + * | ||
| 33 | + * @return 成功返回 fd;失败返回 -1 | ||
| 34 | + */ | ||
| 35 | +static int AppEnvConnectServer(void) | ||
| 36 | +{ | ||
| 37 | + int fd = socket(AF_UNIX, SOCK_STREAM, 0); | ||
| 38 | + if (fd < 0) { | ||
| 39 | + return -1; | ||
| 40 | + } | ||
| 41 | + struct sockaddr_un addr = {0}; | ||
| 42 | + addr.sun_family = AF_UNIX; | ||
| 43 | + errno_t ret = strcpy_s(addr.sun_path, sizeof(addr.sun_path), | ||
| 44 | + APPSPAWN_SOCKET_DIR APPENV_SOCKET_NAME); | ||
| 45 | + if (ret != EOK || connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { | ||
| 46 | + close(fd); | ||
| 47 | + return -1; | ||
| 48 | + } | ||
| 49 | + return fd; | ||
| 50 | +} | ||
| 51 | + | ||
| 52 | +/** | ||
| 53 | + * @brief 发起一次 IPC 请求并同步等待响应。 | ||
| 54 | + * | ||
| 55 | + * 骨架阶段协议面完整(连接/收发/超时/校验),服务端业务 deny-all | ||
| 56 | + * (APPENV_ERR_UNKNOWN_OP),客户端拿到确定性响应即验证通路。 | ||
| 57 | + * | ||
| 58 | + * @param opcode AppEnvIpcOp 操作码 | ||
| 59 | + * @param payload 请求载荷(可为 NULL) | ||
| 60 | + * @param payloadLen 载荷长度 | ||
| 61 | + * @param respOut 响应头输出(可为 NULL) | ||
| 62 | + * @return 结果码(APPENV_ERR_INTERNAL 表示本地连接/超时/协议失败) | ||
| 63 | + */ | ||
| 64 | +uint32_t AppEnvRpc(uint32_t opcode, const AppEnvIpcPayload *payload, uint32_t payloadLen, | ||
| 65 | + AppEnvIpcMsg *respOut) | ||
| 66 | +{ | ||
| 67 | + if ((payload == NULL && payloadLen != 0) || payloadLen > APPENV_IPC_MSG_MAX) { | ||
| 68 | + return APPENV_ERR_ARG; | ||
| 69 | + } | ||
| 70 | + int fd = AppEnvConnectServer(); | ||
| 71 | + APPSPAWN_CHECK(fd >= 0, return APPENV_ERR_INTERNAL, | ||
| 72 | + "appenv: connect manager failed, errno %{public}d", errno); | ||
| 73 | + | ||
| 74 | + AppEnvIpcMsg req = {0}; | ||
| 75 | + req.magic = APPENV_IPC_MAGIC; | ||
| 76 | + req.version = APPENV_IPC_VERSION; | ||
| 77 | + req.opcode = opcode; | ||
| 78 | + req.payloadLen = payloadLen; | ||
| 79 | + uint32_t result = APPENV_ERR_INTERNAL; | ||
| 80 | + do { | ||
| 81 | + if (write(fd, &req, sizeof(req)) != (ssize_t)sizeof(req)) { | ||
| 82 | + APPSPAWN_LOGE("appenv: send request failed, errno %{public}d", errno); | ||
| 83 | + break; | ||
| 84 | + } | ||
| 85 | + if (payloadLen > 0 && write(fd, payload, payloadLen) != (ssize_t)payloadLen) { | ||
| 86 | + APPSPAWN_LOGE("appenv: send payload failed, errno %{public}d", errno); | ||
| 87 | + break; | ||
| 88 | + } | ||
| 89 | + struct pollfd pfd = {.fd = fd, .events = POLLIN}; | ||
| 90 | + int pollRet = poll(&pfd, 1, APPENV_RPC_TIMEOUT_MS); | ||
| 91 | + if (pollRet <= 0) { | ||
| 92 | + APPSPAWN_LOGE("appenv: wait response timeout or error, ret %{public}d", pollRet); | ||
| 93 | + break; | ||
| 94 | + } | ||
| 95 | + AppEnvIpcMsg resp = {0}; | ||
| 96 | + if (read(fd, &resp, sizeof(resp)) != (ssize_t)sizeof(resp) || | ||
| 97 | + resp.magic != APPENV_IPC_MAGIC || resp.version != APPENV_IPC_VERSION) { | ||
| 98 | + APPSPAWN_LOGE("appenv: invalid response from manager"); | ||
| 99 | + break; | ||
| 100 | + } | ||
| 101 | + result = resp.result; | ||
| 102 | + if (respOut != NULL) { | ||
| 103 | + *respOut = resp; | ||
| 104 | + } | ||
| 105 | + } while (0); | ||
| 106 | + close(fd); | ||
| 107 | + return result; | ||
| 108 | +} | ||
| @@ -0,0 +1,50 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +extern "C" { | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +/** IPC 等待响应超时(毫秒),超时映射为退出码 5。 */ | ||
| 27 | + | ||
| 28 | + | ||
| 29 | +/** | ||
| 30 | + * @brief 发起一次 IPC 请求并同步等待响应(TASK-003 骨架)。 | ||
| 31 | + * | ||
| 32 | + * bin 内嵌客户端:connect AppEnvManager socket -> 发送头+载荷 -> | ||
| 33 | + * poll 等待响应(APPENV_RPC_TIMEOUT_MS)-> 返回结果码。 | ||
| 34 | + * 调用方身份无需显式传递 —— 服务端经 SO_PEERCRED 从内核获取, | ||
| 35 | + * 本进程 uid 即判定输入(BR-8)。 | ||
| 36 | + * | ||
| 37 | + * @param opcode AppEnvIpcOp 操作码 | ||
| 38 | + * @param payload 请求载荷(可为 NULL,payloadLen 需为 0) | ||
| 39 | + * @param payloadLen 载荷长度 | ||
| 40 | + * @param respOut 响应头输出(可为 NULL,调用方不关心响应细节时) | ||
| 41 | + * @return APPENV_ERR_OK 成功;APPENV_ERR_INTERNAL 连接/超时/协议错误; | ||
| 42 | + * 其余为服务端返回的结果码 | ||
| 43 | + */ | ||
| 44 | +uint32_t AppEnvRpc(uint32_t opcode, const AppEnvIpcPayload *payload, uint32_t payloadLen, | ||
| 45 | + AppEnvIpcMsg *respOut); | ||
| 46 | + | ||
| 47 | + | ||
| 48 | +} | ||
| 49 | + | ||
| 50 | + | ||
| @@ -0,0 +1,149 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | +/** | ||
| 25 | + * @brief 子命令通用 stub:发起 IPC 并透传结果码。 | ||
| 26 | + * | ||
| 27 | + * 骨架阶段所有子命令共用 —— 验证"CLI -> IPC -> 服务端"全链路; | ||
| 28 | + * 参数解析与载荷组装在 TASK-009 完善。 | ||
| 29 | + * | ||
| 30 | + * @param opcode 该子命令对应的 IPC 操作码 | ||
| 31 | + * @return 进程退出码(IPC 结果码语义一致,直接透传) | ||
| 32 | + */ | ||
| 33 | +static int AppEnvCmdStub(uint32_t opcode) | ||
| 34 | +{ | ||
| 35 | + return (int)AppEnvRpc(opcode, NULL, 0, NULL); | ||
| 36 | +} | ||
| 37 | + | ||
| 38 | +/** | ||
| 39 | + * @brief `appenv set KEY VALUE [scope]` 入口(载荷组装随 TASK-009)。 | ||
| 40 | + */ | ||
| 41 | +static int AppEnvCmdSet(int argc, char *argv[]) | ||
| 42 | +{ | ||
| 43 | + (void)argc; | ||
| 44 | + (void)argv; | ||
| 45 | + return AppEnvCmdStub(APPENV_OP_SET); | ||
| 46 | +} | ||
| 47 | + | ||
| 48 | +/** | ||
| 49 | + * @brief `appenv unset KEY [scope]` 入口(载荷组装随 TASK-009)。 | ||
| 50 | + */ | ||
| 51 | +static int AppEnvCmdUnset(int argc, char *argv[]) | ||
| 52 | +{ | ||
| 53 | + (void)argc; | ||
| 54 | + (void)argv; | ||
| 55 | + return AppEnvCmdStub(APPENV_OP_UNSET); | ||
| 56 | +} | ||
| 57 | + | ||
| 58 | +/** | ||
| 59 | + * @brief `appenv get KEY [scope]` 入口(载荷组装随 TASK-009)。 | ||
| 60 | + */ | ||
| 61 | +static int AppEnvCmdGet(int argc, char *argv[]) | ||
| 62 | +{ | ||
| 63 | + (void)argc; | ||
| 64 | + (void)argv; | ||
| 65 | + return AppEnvCmdStub(APPENV_OP_GET); | ||
| 66 | +} | ||
| 67 | + | ||
| 68 | +/** | ||
| 69 | + * @brief `appenv list [scope]` 入口(载荷组装随 TASK-009)。 | ||
| 70 | + */ | ||
| 71 | +static int AppEnvCmdList(int argc, char *argv[]) | ||
| 72 | +{ | ||
| 73 | + (void)argc; | ||
| 74 | + (void)argv; | ||
| 75 | + return AppEnvCmdStub(APPENV_OP_LIST); | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +/** | ||
| 79 | + * @brief `appenv path append|remove|list DIR/none` 入口(载荷组装随 TASK-009)。 | ||
| 80 | + */ | ||
| 81 | +static int AppEnvCmdPath(int argc, char *argv[]) | ||
| 82 | +{ | ||
| 83 | + (void)argc; | ||
| 84 | + (void)argv; | ||
| 85 | + return AppEnvCmdStub(APPENV_OP_PATH_LIST); | ||
| 86 | +} | ||
| 87 | + | ||
| 88 | +/** | ||
| 89 | + * @brief `appenv check` 入口(本地体检逻辑随 TASK-010,走 QUERY 通道)。 | ||
| 90 | + */ | ||
| 91 | +static int AppEnvCmdCheck(int argc, char *argv[]) | ||
| 92 | +{ | ||
| 93 | + (void)argc; | ||
| 94 | + (void)argv; | ||
| 95 | + return AppEnvCmdStub(APPENV_OP_QUERY); | ||
| 96 | +} | ||
| 97 | + | ||
| 98 | +/** | ||
| 99 | + * @brief `appenv effective --bundle X` 入口(载荷组装随 TASK-009/010)。 | ||
| 100 | + */ | ||
| 101 | +static int AppEnvCmdEffective(int argc, char *argv[]) | ||
| 102 | +{ | ||
| 103 | + (void)argc; | ||
| 104 | + (void)argv; | ||
| 105 | + return AppEnvCmdStub(APPENV_OP_QUERY); | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +/** | ||
| 109 | + * @brief `appenv explain KEY --bundle X` 入口(载荷组装随 TASK-009/010)。 | ||
| 110 | + */ | ||
| 111 | +static int AppEnvCmdExplain(int argc, char *argv[]) | ||
| 112 | +{ | ||
| 113 | + (void)argc; | ||
| 114 | + (void)argv; | ||
| 115 | + return AppEnvCmdStub(APPENV_OP_QUERY); | ||
| 116 | +} | ||
| 117 | + | ||
| 118 | +/** 子命令表(顺序即 --help 输出顺序)。 */ | ||
| 119 | +static const AppEnvCmd g_cmdTable[APPENV_CMD_COUNT] = { | ||
| 120 | + {"set", "set KEY VALUE [--system|--session|--user|--bundle BN]", AppEnvCmdSet}, | ||
| 121 | + {"unset", "unset KEY [--system|--session|--user|--bundle BN]", AppEnvCmdUnset}, | ||
| 122 | + {"get", "get KEY [--system|--session|--user|--bundle BN]", AppEnvCmdGet}, | ||
| 123 | + {"list", "list [--system|--session|--user|--bundle BN]", AppEnvCmdList}, | ||
| 124 | + {"path", "path append|remove|list DIR [--bundle BN]", AppEnvCmdPath}, | ||
| 125 | + {"check", "check", AppEnvCmdCheck}, | ||
| 126 | + {"effective", "effective --bundle BN", AppEnvCmdEffective}, | ||
| 127 | + {"explain", "explain KEY --bundle BN", AppEnvCmdExplain}, | ||
| 128 | +}; | ||
| 129 | + | ||
| 130 | +/** | ||
| 131 | + * @brief 获取子命令表。 | ||
| 132 | + * | ||
| 133 | + * @return 子命令表首地址(APPENV_CMD_COUNT 项) | ||
| 134 | + */ | ||
| 135 | +const AppEnvCmd *AppEnvGetCmdTable(void) | ||
| 136 | +{ | ||
| 137 | + return g_cmdTable; | ||
| 138 | +} | ||
| 139 | + | ||
| 140 | +/** | ||
| 141 | + * @brief 输出 usage。 | ||
| 142 | + */ | ||
| 143 | +void AppEnvPrintUsage(void) | ||
| 144 | +{ | ||
| 145 | + printf("usage: appenv <command> [options]\n"); | ||
| 146 | + for (int i = 0; i < APPENV_CMD_COUNT; i++) { | ||
| 147 | + printf(" %-10s %s\n", g_cmdTable[i].name, g_cmdTable[i].help); | ||
| 148 | + } | ||
| 149 | +} | ||
| @@ -0,0 +1,62 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +extern "C" { | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +/** CLI 子命令数(8 个:set/unset/get/list/path/check/effective/explain)。 */ | ||
| 26 | + | ||
| 27 | + | ||
| 28 | +/** 单条命令帮助文本上限。 */ | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +/** | ||
| 32 | + * @brief CLI 子命令入口函数类型。 | ||
| 33 | + * | ||
| 34 | + * @param argc 参数个数(不含子命令名) | ||
| 35 | + * @param argv 参数数组(argv[0] 为子命令名) | ||
| 36 | + * @return 进程退出码(语义见 spec.md 退出码总表:0-6) | ||
| 37 | + */ | ||
| 38 | +typedef int (*AppEnvCmdEntry)(int argc, char *argv[]); | ||
| 39 | + | ||
| 40 | +/** 子命令描述表项(驱动命令分发与 --help 输出)。 */ | ||
| 41 | +typedef struct TagAppEnvCmd { | ||
| 42 | + const char *name; // 子命令名 | ||
| 43 | + const char *help; // 一行帮助(<= APPENV_HELP_LINE_MAX) | ||
| 44 | + AppEnvCmdEntry entry; // 入口函数 | ||
| 45 | +} AppEnvCmd; | ||
| 46 | + | ||
| 47 | +/** | ||
| 48 | + * @brief 获取子命令表(编译期静态表,APPENV_CMD_COUNT 项)。 | ||
| 49 | + * | ||
| 50 | + * @return 子命令表首地址 | ||
| 51 | + */ | ||
| 52 | +const AppEnvCmd *AppEnvGetCmdTable(void); | ||
| 53 | + | ||
| 54 | +/** | ||
| 55 | + * @brief 输出 usage(appenv --help / 无参数 / 未知子命令时调用)。 | ||
| 56 | + */ | ||
| 57 | +void AppEnvPrintUsage(void); | ||
| 58 | + | ||
| 59 | + | ||
| 60 | +} | ||
| 61 | + | ||
| 62 | + | ||
| @@ -0,0 +1,93 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +extern "C" { | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +/** AppEnvManager IPC server socket 路径(appspawn 进程内监听)。 */ | ||
| 26 | + | ||
| 27 | + | ||
| 28 | +/** IPC 协议版本(协议不兼容变更时递增,客户端据此拒绝连接)。 */ | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +/** 单条 IPC 消息上限(含头部;key/value/bundle 载荷受 FR 约束远小于此)。 */ | ||
| 32 | + | ||
| 33 | + | ||
| 34 | +/** key/bundleName 字段长度上限。 */ | ||
| 35 | + | ||
| 36 | + | ||
| 37 | + | ||
| 38 | +/** | ||
| 39 | + * @brief IPC 操作码(v1 协议面,对应 appenv CLI 8 个子命令)。 | ||
| 40 | + * | ||
| 41 | + * opcode 决定载荷字段的有效性;未知 opcode 一律回 ERR_UNKNOWN_OP, | ||
| 42 | + * 保证协议前向兼容(旧服务端遇到新客户端命令时明确报错而非误执行)。 | ||
| 43 | + */ | ||
| 44 | +typedef enum TagAppEnvIpcOp { | ||
| 45 | + APPENV_OP_SET = 1, // 设置 KEY=VALUE(载荷: scope/key/value/bundle) | ||
| 46 | + APPENV_OP_UNSET = 2, // 删除 KEY(载荷: scope/key/bundle) | ||
| 47 | + APPENV_OP_GET = 3, // 查询单个 KEY(载荷: scope/key/bundle) | ||
| 48 | + APPENV_OP_LIST = 4, // 列出作用域全部 KEY(载荷: scope/bundle) | ||
| 49 | + APPENV_OP_PATH_APPEND = 5, // PATH 追加目录(载荷: scope/bundle/dir) | ||
| 50 | + APPENV_OP_PATH_REMOVE = 6, // PATH 移除目录(载荷: scope/bundle/dir) | ||
| 51 | + APPENV_OP_PATH_LIST = 7, // 列出 PATH 追加项(载荷: scope/bundle) | ||
| 52 | + APPENV_OP_QUERY = 8, // effective/explain 查询(载荷: bundle/key) | ||
| 53 | + APPENV_OP_MAX = 9, | ||
| 54 | +} AppEnvIpcOp; | ||
| 55 | + | ||
| 56 | +/** | ||
| 57 | + * @brief IPC 结果码(与 CLI 退出码语义对齐,spec.md 退出码总表)。 | ||
| 58 | + * | ||
| 59 | + * 0 成功;2 权限拒绝;3 restricted 放行条件不满足;4 黑名单/PATH 直写 | ||
| 60 | + * 拒绝;5 服务内部错误;6 超出总量限制。 | ||
| 61 | + */ | ||
| 62 | +typedef enum TagAppEnvIpcErr { | ||
| 63 | + APPENV_ERR_OK = 0, | ||
| 64 | + APPENV_ERR_PERM = 2, | ||
| 65 | + APPENV_ERR_RESTRICTED = 3, | ||
| 66 | + APPENV_ERR_BLOCKED = 4, | ||
| 67 | + APPENV_ERR_INTERNAL = 5, | ||
| 68 | + APPENV_ERR_LIMIT = 6, | ||
| 69 | + APPENV_ERR_UNKNOWN_OP = 7, | ||
| 70 | + APPENV_ERR_ARG = 1, | ||
| 71 | +} AppEnvIpcErr; | ||
| 72 | + | ||
| 73 | +/** IPC 消息头(请求与响应共用;多字节字段小端,本机字节序直接传输)。 */ | ||
| 74 | +typedef struct TagAppEnvIpcMsg { | ||
| 75 | + uint32_t magic; // 0xAPPE4E56 ('APPENV'),校验合法连接 | ||
| 76 | + uint32_t version; // APPENV_IPC_VERSION | ||
| 77 | + uint32_t opcode; // AppEnvIpcOp(请求)/ 0(响应) | ||
| 78 | + uint32_t result; // 请求时为 0;响应时为 AppEnvIpcErr | ||
| 79 | + uint32_t payloadLen; // 载荷字节数(不含头部,<= APPENV_IPC_MSG_MAX - sizeof(header)) | ||
| 80 | +} AppEnvIpcMsg; | ||
| 81 | + | ||
| 82 | +/** 请求载荷:按 opcode 取用的扁平字段,以 '\0' 分隔顺序排列。 */ | ||
| 83 | +typedef struct TagAppEnvIpcPayload { | ||
| 84 | + char scope[16]; // "system"/"session"/"user"/"app" | ||
| 85 | + char key[APPENV_IPC_KEY_MAX]; | ||
| 86 | + char value[APPENV_IPC_MSG_MAX]; // set 时使用 | ||
| 87 | + char bundle[APPENV_IPC_BUNDLE_MAX]; | ||
| 88 | +} AppEnvIpcPayload; | ||
| 89 | + | ||
| 90 | + | ||
| 91 | +} | ||
| 92 | + | ||
| 93 | + | ||
| @@ -0,0 +1,45 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | +/** | ||
| 21 | + * @brief appenv CLI 入口:子命令分发。 | ||
| 22 | + * | ||
| 23 | + * 标准 POSIX 命令形态(argv[1] 为子命令),无参数/未知子命令输出 | ||
| 24 | + * usage 并返回 1。身份语义:本进程 uid 即权限判定输入(服务端 | ||
| 25 | + * SO_PEERCRED 获取,BR-8)。 | ||
| 26 | + * | ||
| 27 | + * @param argc 参数个数 | ||
| 28 | + * @param argv 参数数组 | ||
| 29 | + * @return 子命令退出码(0-6,语义见 spec.md 退出码总表) | ||
| 30 | + */ | ||
| 31 | +int main(int argc, char *argv[]) | ||
| 32 | +{ | ||
| 33 | + if (argc < 2) { | ||
| 34 | + AppEnvPrintUsage(); | ||
| 35 | + return 1; | ||
| 36 | + } | ||
| 37 | + const AppEnvCmd *table = AppEnvGetCmdTable(); | ||
| 38 | + for (int i = 0; i < APPENV_CMD_COUNT; i++) { | ||
| 39 | + if (strcmp(argv[1], table[i].name) == 0) { | ||
| 40 | + return table[i].entry(argc - 1, argv + 1); | ||
| 41 | + } | ||
| 42 | + } | ||
| 43 | + AppEnvPrintUsage(); | ||
| 44 | + return 1; | ||
| 45 | +} | ||
| @@ -0,0 +1,210 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + | ||
| 28 | + | ||
| 29 | +/** | ||
| 30 | + * @brief 设置 fd 的 FD_CLOEXEC 标志。 | ||
| 31 | + * | ||
| 32 | + * appenv server/stream fd 不得泄漏进孵化出的应用进程 | ||
| 33 | + * (appspawn_service.c:85 SetFdCtrl 同款语义,该函数为文件内 | ||
| 34 | + * static inline 不可跨文件复用,故此处内联实现)。 | ||
| 35 | + * | ||
| 36 | + * @param fd 目标 fd | ||
| 37 | + */ | ||
| 38 | +static void AppEnvSetFdCloexec(int fd) | ||
| 39 | +{ | ||
| 40 | + int flags = fcntl(fd, F_GETFD, 0); | ||
| 41 | + if (flags >= 0) { | ||
| 42 | + (void)fcntl(fd, F_SETFD, flags | FD_CLOEXEC); | ||
| 43 | + } | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | +/** server task 句柄(appspawn 进程内单例)。 */ | ||
| 47 | +static TaskHandle g_appEnvServer = NULL; | ||
| 48 | + | ||
| 49 | +/** IPC 消息魔数('APPE' 打包,防非法连接与误连其他 socket)。 */ | ||
| 50 | + | ||
| 51 | + | ||
| 52 | +/** 连接上下文(userDataSize 随 LE_AcceptStreamClient 分配):存对端 uid。 */ | ||
| 53 | +typedef struct { | ||
| 54 | + uint32_t peerUid; // SO_PEERCRED 取得,设置阶段决策矩阵的 caller 输入 | ||
| 55 | +} AppEnvConnection; | ||
| 56 | + | ||
| 57 | +/** | ||
| 58 | + * @brief 处理单条 IPC 请求(TASK-008 填充业务)。 | ||
| 59 | + * | ||
| 60 | + * 骨架阶段校验消息头(magic/version/opcode/长度),非法返回 ERR_ARG, | ||
| 61 | + * 合法但未实现的 opcode 返回 ERR_UNKNOWN_OP —— 协议面已通、业务面 | ||
| 62 | + * deny-all,与 policy 骨架的保守策略一致。 | ||
| 63 | + * | ||
| 64 | + * @param uid 对端凭证 uid(决策矩阵 caller 输入,骨架阶段未消费) | ||
| 65 | + * @param msg 已收到的请求头 | ||
| 66 | + * @return 响应码(AppEnvIpcErr) | ||
| 67 | + */ | ||
| 68 | +static uint32_t AppEnvHandleRequest(uint32_t uid, const AppEnvIpcMsg *msg) | ||
| 69 | +{ | ||
| 70 | + if (msg->magic != APPENV_IPC_MAGIC || msg->version != APPENV_IPC_VERSION || | ||
| 71 | + msg->opcode == 0 || msg->opcode >= APPENV_OP_MAX || | ||
| 72 | + msg->payloadLen > APPENV_IPC_MSG_MAX) { | ||
| 73 | + return APPENV_ERR_ARG; | ||
| 74 | + } | ||
| 75 | + // TASK-008: 按 opcode 分发到设置阶段决策矩阵 -> 原子写 -> 版本递增 -> 审计 | ||
| 76 | + APPSPAWN_LOGV("appenv: opcode %{public}u from uid %{public}u lands with TASK-008", | ||
| 77 | + msg->opcode, uid); | ||
| 78 | + return APPENV_ERR_UNKNOWN_OP; | ||
| 79 | +} | ||
| 80 | + | ||
| 81 | +/** | ||
| 82 | + * @brief 连接数据到达回调:读请求 -> 处理 -> 回响应。 | ||
| 83 | + * | ||
| 84 | + * 单请求单响应模型(CLI 一次性命令形态),响应写入后由对端关闭连接。 | ||
| 85 | + * 响应发送沿用 appspawn SendResponse 同款机制(LE_CreateBuffer + LE_Send, | ||
| 86 | + * appspawn_service.c:306 先例)。 | ||
| 87 | + * | ||
| 88 | + * @param taskHandle 连接 stream task | ||
| 89 | + * @param buffer 收到的数据 | ||
| 90 | + * @param buffLen 数据长度 | ||
| 91 | + */ | ||
| 92 | +static void AppEnvOnMessage(const TaskHandle taskHandle, const uint8_t *buffer, uint32_t buffLen) | ||
| 93 | +{ | ||
| 94 | + AppEnvConnection *conn = (AppEnvConnection *)LE_GetUserData(taskHandle); | ||
| 95 | + if (taskHandle == NULL || buffer == NULL || buffLen < sizeof(AppEnvIpcMsg)) { | ||
| 96 | + return; | ||
| 97 | + } | ||
| 98 | + const AppEnvIpcMsg *msg = (const AppEnvIpcMsg *)buffer; | ||
| 99 | + uint32_t bufferSize = sizeof(AppEnvIpcMsg); | ||
| 100 | + BufferHandle handle = LE_CreateBuffer(LE_GetDefaultLoop(), bufferSize); | ||
| 101 | + APPSPAWN_CHECK(handle != NULL, return, "appenv: alloc response buffer failed"); | ||
| 102 | + AppEnvIpcMsg *resp = (AppEnvIpcMsg *)LE_GetBufferInfo(handle, NULL, &bufferSize); | ||
| 103 | + if (resp == NULL) { | ||
| 104 | + LE_FreeBuffer(LE_GetDefaultLoop(), NULL, handle); | ||
| 105 | + return; | ||
| 106 | + } | ||
| 107 | + resp->magic = APPENV_IPC_MAGIC; | ||
| 108 | + resp->version = APPENV_IPC_VERSION; | ||
| 109 | + resp->result = AppEnvHandleRequest(conn != NULL ? conn->peerUid : (uint32_t)-1, msg); | ||
| 110 | + (void)LE_Send(LE_GetDefaultLoop(), taskHandle, handle, bufferSize); | ||
| 111 | +} | ||
| 112 | + | ||
| 113 | +/** | ||
| 114 | + * @brief 连接关闭回调:stream 资源由 loop 框架回收,无自定义资源。 | ||
| 115 | + * | ||
| 116 | + * @param client 关闭的连接 task | ||
| 117 | + */ | ||
| 118 | +static void AppEnvOnDisconnect(const TaskHandle client) | ||
| 119 | +{ | ||
| 120 | + (void)client; | ||
| 121 | + APPSPAWN_LOGV("appenv: connection closed"); | ||
| 122 | +} | ||
| 123 | + | ||
| 124 | +/** | ||
| 125 | + * @brief 新连接回调:accept stream -> SO_PEERCRED 取对端 uid -> 挂上下文。 | ||
| 126 | + * | ||
| 127 | + * 对端凭证经内核获取(connect 时填充,不可伪造,appspawn_service.c:449 | ||
| 128 | + * 同款机制)。与主 socket 不同:此处不设 uid 白名单 —— 所有 uid 都允许 | ||
| 129 | + * 连接,权限差异由设置阶段决策矩阵逐请求判定(shell 可写 user/app public | ||
| 130 | + * 而三方全拒,spec.md BR-8),连接层拒绝反而会掩盖权限语义。 | ||
| 131 | + * | ||
| 132 | + * @param loopHandle loop 句柄 | ||
| 133 | + * @param server server task | ||
| 134 | + * @return 0 接受连接;-1 accept 或取凭证失败(连接被关闭) | ||
| 135 | + */ | ||
| 136 | +static int AppEnvOnConnection(const LoopHandle loopHandle, const TaskHandle server) | ||
| 137 | +{ | ||
| 138 | + APPSPAWN_CHECK(server != NULL && loopHandle != NULL, return -1, "Error server"); | ||
| 139 | + TaskHandle stream; | ||
| 140 | + LE_StreamInfo info = {}; | ||
| 141 | + info.baseInfo.flags = TASK_STREAM | TASK_PIPE | TASK_CONNECT; | ||
| 142 | + info.baseInfo.userDataSize = sizeof(AppEnvConnection); | ||
| 143 | + info.disConnectComplete = AppEnvOnDisconnect; | ||
| 144 | + info.sendMessageComplete = NULL; | ||
| 145 | + info.recvMessage = AppEnvOnMessage; | ||
| 146 | + info.handleRecvMsg = NULL; | ||
| 147 | + LE_STATUS ret = LE_AcceptStreamClient(loopHandle, server, &stream, &info); | ||
| 148 | + APPSPAWN_CHECK(ret == 0, return -1, "Failed to alloc stream"); | ||
| 149 | + | ||
| 150 | + AppEnvConnection *connection = (AppEnvConnection *)LE_GetUserData(stream); | ||
| 151 | + APPSPAWN_CHECK(connection != NULL, return -1, "Failed to alloc stream"); | ||
| 152 | + struct ucred cred = {-1, -1, -1}; | ||
| 153 | + socklen_t credSize = sizeof(struct ucred); | ||
| 154 | + if (getsockopt(LE_GetSocketFd(stream), SOL_SOCKET, SO_PEERCRED, &cred, &credSize) < 0) { | ||
| 155 | + APPSPAWN_LOGE("appenv: get peer cred failed, errno %{public}d", errno); | ||
| 156 | + LE_CloseStreamTask(LE_GetDefaultLoop(), stream); | ||
| 157 | + return -1; | ||
| 158 | + } | ||
| 159 | + connection->peerUid = (uint32_t)cred.uid; | ||
| 160 | + AppEnvSetFdCloexec(LE_GetSocketFd(stream)); | ||
| 161 | + APPSPAWN_LOGI("appenv: connection from uid %{public}d pid %{public}d", cred.uid, cred.pid); | ||
| 162 | + return 0; | ||
| 163 | +} | ||
| 164 | + | ||
| 165 | +/** | ||
| 166 | + * @brief 启动 AppEnvManager IPC server。 | ||
| 167 | + * | ||
| 168 | + * 复用 appspawn 主 socket 建立机制(CreateAppSpawnServer 同款, | ||
| 169 | + * appspawn_service.c:1718 先例):优先 GetControlSocket 从 init 接管 | ||
| 170 | + * rc 声明的 fd(正式形态,TASK-002 后续随 sepolicy 联调加入 rc); | ||
| 171 | + * 返回 -1 时由 LE_CreateStreamServer 按 info.server 路径自建 unix | ||
| 172 | + * socket(联调形态)。失败仅记录日志,不阻断 appspawn 启动。 | ||
| 173 | + * | ||
| 174 | + * @return 0 成功;-1 socket 创建失败 | ||
| 175 | + */ | ||
| 176 | +int AppEnvManagerStart(void) | ||
| 177 | +{ | ||
| 178 | + if (g_appEnvServer != NULL) { | ||
| 179 | + return 0; | ||
| 180 | + } | ||
| 181 | + static char serverPath[] = APPSPAWN_SOCKET_DIR APPENV_SOCKET_NAME; | ||
| 182 | + LE_StreamServerInfo info = {}; | ||
| 183 | + info.baseInfo.flags = TASK_STREAM | TASK_PIPE | TASK_SERVER; | ||
| 184 | + info.socketId = GetControlSocket(APPENV_SOCKET_NAME); | ||
| 185 | + info.server = serverPath; | ||
| 186 | + info.disConnectComplete = NULL; | ||
| 187 | + info.incommingConnect = AppEnvOnConnection; | ||
| 188 | + info.sendMessageComplete = NULL; | ||
| 189 | + info.recvMessage = NULL; // stream 级回调挂在 AppEnvOnConnection 的 info 中 | ||
| 190 | + info.baseInfo.close = NULL; | ||
| 191 | + | ||
| 192 | + MakeDirRec(APPSPAWN_SOCKET_DIR, 0711, 0); // 0711 default mask | ||
| 193 | + LE_STATUS ret = LE_CreateStreamServer(LE_GetDefaultLoop(), &g_appEnvServer, &info); | ||
| 194 | + APPSPAWN_CHECK(ret == 0, g_appEnvServer = NULL; return -1, | ||
| 195 | + "appenv: create server failed, errno %{public}d", errno); | ||
| 196 | + AppEnvSetFdCloexec(LE_GetSocketFd(g_appEnvServer)); | ||
| 197 | + APPSPAWN_LOGI("appenv: manager started, fd %{public}d", LE_GetSocketFd(g_appEnvServer)); | ||
| 198 | + return 0; | ||
| 199 | +} | ||
| 200 | + | ||
| 201 | +/** | ||
| 202 | + * @brief 停止 AppEnvManager 并释放资源。 | ||
| 203 | + */ | ||
| 204 | +void AppEnvManagerStop(void) | ||
| 205 | +{ | ||
| 206 | + if (g_appEnvServer != NULL) { | ||
| 207 | + LE_CloseStreamTask(LE_GetDefaultLoop(), g_appEnvServer); | ||
| 208 | + g_appEnvServer = NULL; | ||
| 209 | + } | ||
| 210 | +} | ||
| @@ -0,0 +1,52 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +extern "C" { | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +/** | ||
| 26 | + * @brief 启动 AppEnvManager IPC server(TASK-002 骨架)。 | ||
| 27 | + * | ||
| 28 | + * 在 appspawn 父进程的 loopevent 循环内创建第二个 stream server | ||
| 29 | + * (socket: /dev/unix/socket/appenv),复用 appspawn 主 socket 的 | ||
| 30 | + * 建立机制(CreateAppSpawnServer 同款,appspawn_service.c:1718 先例)。 | ||
| 31 | + * 服务逻辑宿主为 appspawn 进程(不新增守护进程:生命周期一致、 | ||
| 32 | + * sepolicy 面最小),本模块编译为独立 so 便于裁剪。 | ||
| 33 | + * | ||
| 34 | + * 每个连接 accept 后立即经内核 SO_PEERCRED 取对端 uid/pid 作为 | ||
| 35 | + * 权限判定唯一依据(不可伪造;appspawn_service.c:449 同款机制), | ||
| 36 | + * uid 记录到连接上下文供请求处理使用。 | ||
| 37 | + * | ||
| 38 | + * @return 0 成功;-1 socket 创建失败(不阻断 appspawn 启动,仅记录日志) | ||
| 39 | + */ | ||
| 40 | +int AppEnvManagerStart(void); | ||
| 41 | + | ||
| 42 | +/** | ||
| 43 | + * @brief 停止 AppEnvManager 并释放资源。 | ||
| 44 | + * | ||
| 45 | + * appspawn 服务退出路径调用;骨架阶段仅关闭 server fd。 | ||
| 46 | + */ | ||
| 47 | +void AppEnvManagerStop(void); | ||
| 48 | + | ||
| 49 | + | ||
| 50 | +} | ||
| 51 | + | ||
| 52 | + | ||
| @@ -22,6 +22,7 @@ config("appspawn_server_config") { | |||
| 22 | "${appspawn_path}/modules/common", | 22 | "${appspawn_path}/modules/common", |
| 23 | "${appspawn_path}/modules/modulemgr", | 23 | "${appspawn_path}/modules/modulemgr", |
| 24 | "${appspawn_path}/modules/sysevent", | 24 | "${appspawn_path}/modules/sysevent", |
| 25 | + "${appspawn_path}/service/appenv", | ||
| 25 | ] | 26 | ] |
| 26 | 27 | ||
| 27 | cflags = [] | 28 | cflags = [] |
| @@ -78,6 +79,11 @@ ohos_executable("appspawn") { | |||
| 78 | if (appspawn_support_prefork) { | 79 | if (appspawn_support_prefork) { |
| 79 | defines += [ "APPSPAWN_SUPPORT_PREFORK" ] | 80 | defines += [ "APPSPAWN_SUPPORT_PREFORK" ] |
| 80 | } | 81 | } |
| 82 | + if (appspawn_support_appenv) { | ||
| 83 | + # FEAT-20260825-001: appenv manager 走 dlopen(修复不开机:链接期依赖 | ||
| 84 | + # 会因 so 装在 lib64/appspawn/ 非默认搜索路径导致 appspawn 启动即失败) | ||
| 85 | + defines += [ "APPSPAWN_SUPPORT_APPENV" ] | ||
| 86 | + } | ||
| 81 | 87 | ||
| 82 | external_deps = [ | 88 | external_deps = [ |
| 83 | "bounds_checking_function:libsec_shared", | 89 | "bounds_checking_function:libsec_shared", |
| @@ -246,23 +246,6 @@ static void AppSpawningCtxOnClose(const AppSpawnMgr *mgr, AppSpawningCtx *ctx, v | |||
| 246 | DeleteAppSpawningCtx(ctx); | 246 | DeleteAppSpawningCtx(ctx); |
| 247 | } | 247 | } |
| 248 | 248 | ||
| 249 | -APPSPAWN_STATIC void CloseFdArgsFromConnection(AppSpawnConnection *connection) | ||
| 250 | -{ | ||
| 251 | - APPSPAWN_CHECK(connection != NULL, return, "Invalid connection"); | ||
| 252 | - int fdCount = connection->receiverCtx.fdCount; | ||
| 253 | - int *fds = connection->receiverCtx.fds; | ||
| 254 | - APPSPAWN_CHECK_LOGW(fds != NULL && fdCount > 0, return, | ||
| 255 | - "Invalid fd info %{public}d %{public}d", fds == NULL, fdCount); | ||
| 256 | - | ||
| 257 | - APPSPAWN_LOGI("CloseFdArgsFromConnection: close %{public}d fds", fdCount); | ||
| 258 | - for (int i = 0; i < fdCount; i++) { | ||
| 259 | - APPSPAWN_CHECK_ONLY_EXPER(fds[i] > 0, continue); | ||
| 260 | - close(fds[i]); | ||
| 261 | - fds[i] = -1; | ||
| 262 | - } | ||
| 263 | - connection->receiverCtx.fdCount = 0; | ||
| 264 | -} | ||
| 265 | - | ||
| 266 | static void OnClose(const TaskHandle taskHandle) | 249 | static void OnClose(const TaskHandle taskHandle) |
| 267 | { | 250 | { |
| 268 | int fd = LE_GetSocketFd(taskHandle); | 251 | int fd = LE_GetSocketFd(taskHandle); |
| @@ -284,7 +267,6 @@ static void OnClose(const TaskHandle taskHandle) | |||
| 284 | APPSPAWN_LOGI("OnClose connectionId: %{public}u socket %{public}d", connection->connectionId, fd); | 267 | APPSPAWN_LOGI("OnClose connectionId: %{public}u socket %{public}d", connection->connectionId, fd); |
| 285 | DeleteAppSpawnMsg(&connection->receiverCtx.incompleteMsg); | 268 | DeleteAppSpawnMsg(&connection->receiverCtx.incompleteMsg); |
| 286 | connection->receiverCtx.incompleteMsg = NULL; | 269 | connection->receiverCtx.incompleteMsg = NULL; |
| 287 | - CloseFdArgsFromConnection(connection); | ||
| 288 | // connect close, to close spawning app | 270 | // connect close, to close spawning app |
| 289 | AppSpawningCtxTraversal(AppSpawningCtxOnClose, connection); | 271 | AppSpawningCtxTraversal(AppSpawningCtxOnClose, connection); |
| 290 | } | 272 | } |
| @@ -318,11 +300,10 @@ static void SendMessageComplete(const TaskHandle taskHandle, BufferHandle handle | |||
| 318 | } | 300 | } |
| 319 | } | 301 | } |
| 320 | 302 | ||
| 321 | -static int SendResponse(AppSpawnConnection *connection, const AppSpawnMsg *msg, int result, pid_t pid) | 303 | +static int SendResponse(const AppSpawnConnection *connection, const AppSpawnMsg *msg, int result, pid_t pid) |
| 322 | { | 304 | { |
| 323 | APPSPAWN_LOGV("SendResponse connectionId: %{public}u result: 0x%{public}x pid: %{public}d", | 305 | APPSPAWN_LOGV("SendResponse connectionId: %{public}u result: 0x%{public}x pid: %{public}d", |
| 324 | connection->connectionId, result, pid); | 306 | connection->connectionId, result, pid); |
| 325 | - CloseFdArgsFromConnection(connection); | ||
| 326 | uint32_t bufferSize = sizeof(AppSpawnResponseMsg); | 307 | uint32_t bufferSize = sizeof(AppSpawnResponseMsg); |
| 327 | BufferHandle handle = LE_CreateBuffer(LE_GetDefaultLoop(), bufferSize); | 308 | BufferHandle handle = LE_CreateBuffer(LE_GetDefaultLoop(), bufferSize); |
| 328 | AppSpawnResponseMsg *buffer = (AppSpawnResponseMsg *)LE_GetBufferInfo(handle, NULL, &bufferSize); | 309 | AppSpawnResponseMsg *buffer = (AppSpawnResponseMsg *)LE_GetBufferInfo(handle, NULL, &bufferSize); |
| @@ -346,12 +327,11 @@ static int SendResponse(AppSpawnConnection *connection, const AppSpawnMsg *msg, | |||
| 346 | * @param checkPointId checkpoint ID | 327 | * @param checkPointId checkpoint ID |
| 347 | * @return 成功返回0,失败返回错误码 | 328 | * @return 成功返回0,失败返回错误码 |
| 348 | */ | 329 | */ |
| 349 | -static int SendResponseEx(AppSpawnConnection *connection, const AppSpawnMsg *msg, | 330 | +static int SendResponseEx(const AppSpawnConnection *connection, const AppSpawnMsg *msg, |
| 350 | int result, pid_t pid, uint64_t checkPointId) | 331 | int result, pid_t pid, uint64_t checkPointId) |
| 351 | { | 332 | { |
| 352 | APPSPAWN_LOGI("SendResponseEx connectionId: %{public}u result: 0x%{public}x pid: %{public}d " | 333 | APPSPAWN_LOGI("SendResponseEx connectionId: %{public}u result: 0x%{public}x pid: %{public}d " |
| 353 | "checkPointId: %{public}" PRId64"", connection->connectionId, result, pid, checkPointId); | 334 | "checkPointId: %{public}" PRId64"", connection->connectionId, result, pid, checkPointId); |
| 354 | - CloseFdArgsFromConnection(connection); | ||
| 355 | uint32_t bufferSize = sizeof(AppSpawnResponseMsg); | 335 | uint32_t bufferSize = sizeof(AppSpawnResponseMsg); |
| 356 | BufferHandle handle = LE_CreateBuffer(LE_GetDefaultLoop(), bufferSize); | 336 | BufferHandle handle = LE_CreateBuffer(LE_GetDefaultLoop(), bufferSize); |
| 357 | AppSpawnResponseMsg *buffer = (AppSpawnResponseMsg *)LE_GetBufferInfo(handle, NULL, &bufferSize); | 337 | AppSpawnResponseMsg *buffer = (AppSpawnResponseMsg *)LE_GetBufferInfo(handle, NULL, &bufferSize); |
| @@ -408,30 +388,16 @@ static int HandleRecvMessage(const TaskHandle taskHandle, uint8_t * buffer, int | |||
| 408 | APPSPAWN_CHECK_ONLY_LOG(errno == 0, "recvmsg with errno %{public}d", errno); | 388 | APPSPAWN_CHECK_ONLY_LOG(errno == 0, "recvmsg with errno %{public}d", errno); |
| 409 | struct cmsghdr *cmsg = NULL; | 389 | struct cmsghdr *cmsg = NULL; |
| 410 | for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) { | 390 | for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) { |
| 411 | - if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) { | 391 | + if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) { |
| 412 | - continue; | 392 | + int fdCount = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int); |
| 393 | + int *fd = (int *) CMSG_DATA(cmsg); | ||
| 394 | + APPSPAWN_CHECK(fdCount <= APP_MAX_FD_COUNT, return -1, | ||
| 395 | + "failed to recv fd %{public}d %{public}d", connection->receiverCtx.fdCount, fdCount); | ||
| 396 | + int ret = memcpy_s(connection->receiverCtx.fds, | ||
| 397 | + fdCount * sizeof(int), fd, fdCount * sizeof(int)); | ||
| 398 | + APPSPAWN_CHECK(ret == 0, return -1, "memcpy_s fd ret %{public}d", ret); | ||
| 399 | + connection->receiverCtx.fdCount = fdCount; | ||
| 413 | } | 400 | } |
| 414 | - CloseFdArgsFromConnection(connection); | ||
| 415 | - int fdCount = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int); | ||
| 416 | - int *fds = (int *) CMSG_DATA(cmsg); | ||
| 417 | - if (fdCount > APP_MAX_FD_COUNT) { | ||
| 418 | - APPSPAWN_LOGE("failed to recv fd %{public}d %{public}d", connection->receiverCtx.fdCount, fdCount); | ||
| 419 | - for (int i = 0; i < fdCount; i++) { | ||
| 420 | - APPSPAWN_CHECK_ONLY_EXPER(fds[i] > 0, continue); | ||
| 421 | - close(fds[i]); | ||
| 422 | - } | ||
| 423 | - return -1; | ||
| 424 | - } | ||
| 425 | - int ret = memcpy_s(connection->receiverCtx.fds, fdCount * sizeof(int), fds, fdCount * sizeof(int)); | ||
| 426 | - if (ret != 0) { | ||
| 427 | - APPSPAWN_LOGE("failed to memcpy_s fd %{public}d ret %{public}d errno %{public}d", fdCount, ret, errno); | ||
| 428 | - for (int i = 0; i < fdCount; i++) { | ||
| 429 | - APPSPAWN_CHECK_ONLY_EXPER(fds[i] > 0, continue); | ||
| 430 | - close(fds[i]); | ||
| 431 | - } | ||
| 432 | - return -1; | ||
| 433 | - } | ||
| 434 | - connection->receiverCtx.fdCount = fdCount; | ||
| 435 | } | 401 | } |
| 436 | 402 | ||
| 437 | return recvLen; | 403 | return recvLen; |
| @@ -1529,9 +1495,10 @@ static void ProcessSpawnReqMsg(AppSpawnConnection *connection, AppSpawnMsgNode * | |||
| 1529 | ret = AppSpawnHookExecute(STAGE_PARENT_MSG_DECODE, HOOK_STOP_WHEN_ERROR, GetAppSpawnContent(), &property->client); | 1495 | ret = AppSpawnHookExecute(STAGE_PARENT_MSG_DECODE, HOOK_STOP_WHEN_ERROR, GetAppSpawnContent(), &property->client); |
| 1530 | FinishAppspawnTrace(); | 1496 | FinishAppspawnTrace(); |
| 1531 | // Check if SPM message rebuild hook failed | 1497 | // Check if SPM message rebuild hook failed |
| 1532 | - APPSPAWN_CHECK(ret == 0, SendResponse(connection, &message->msgHeader, ret, 0); | 1498 | + APPSPAWN_ONLY_EXPER(ret != 0, APPSPAWN_LOGE("rebuild hook failed: %{public}d, aborting spawn", ret); |
| 1499 | + SendResponse(connection, &message->msgHeader, ret, 0); | ||
| 1533 | DeleteAppSpawningCtx(property); | 1500 | DeleteAppSpawningCtx(property); |
| 1534 | - return, "MSG_DECODE hook failed: %{public}d, aborting spawn", ret); | 1501 | + return); |
| 1535 | 1502 | ||
| 1536 | // mount el2 dir | 1503 | // mount el2 dir |
| 1537 | // getWrapBundleNameValue | 1504 | // getWrapBundleNameValue |
| @@ -1544,9 +1511,10 @@ static void ProcessSpawnReqMsg(AppSpawnConnection *connection, AppSpawnMsgNode * | |||
| 1544 | clock_gettime(CLOCK_MONOTONIC, &property->spawnStart); | 1511 | clock_gettime(CLOCK_MONOTONIC, &property->spawnStart); |
| 1545 | ret = RunAppSpawnProcessMsg(GetAppSpawnContent(), &property->client, &property->pid); | 1512 | ret = RunAppSpawnProcessMsg(GetAppSpawnContent(), &property->client, &property->pid); |
| 1546 | AppSpawnHookExecute(STAGE_PARENT_POST_FORK, 0, GetAppSpawnContent(), &property->client); | 1513 | AppSpawnHookExecute(STAGE_PARENT_POST_FORK, 0, GetAppSpawnContent(), &property->client); |
| 1547 | - APPSPAWN_CHECK(ret == 0, AbortSpawnAndCleanup(ret, connection, message, property); | 1514 | + if (ret != 0) { // wait child process result |
| 1548 | - return, "POST_FORK hook failed: %{public}d", ret); | 1515 | + AbortSpawnAndCleanup(ret, connection, message, property); |
| 1549 | - | 1516 | + return; |
| 1517 | + } | ||
| 1550 | if (AddChildWatcher(property) != 0) { // wait child process result | 1518 | if (AddChildWatcher(property) != 0) { // wait child process result |
| 1551 | kill(property->pid, SIGKILL); | 1519 | kill(property->pid, SIGKILL); |
| 1552 | AbortSpawnAndCleanup(ret, connection, message, property); | 1520 | AbortSpawnAndCleanup(ret, connection, message, property); |
| @@ -1971,6 +1939,33 @@ static int IsEnablePrefork(void) | |||
| 1971 | return strcmp(buffer, "true") == 0; | 1939 | return strcmp(buffer, "true") == 0; |
| 1972 | } | 1940 | } |
| 1973 | 1941 | ||
| 1942 | + | ||
| 1943 | +/** | ||
| 1944 | + * @brief dlopen 加载 AppEnvManager 并启动其 IPC server。 | ||
| 1945 | + * | ||
| 1946 | + * 修复不开机问题(FEAT-20260825-001):manager so 安装于 | ||
| 1947 | + * /system/lib[64]/appspawn/ 子目录,不在动态链接器默认搜索路径; | ||
| 1948 | + * 若采用链接期依赖(NEEDED),appspawn 进程在 init 拉起时即因 | ||
| 1949 | + * 找不到 so 而启动失败,导致系统无法开机。故改为 dlopen 绝对路径 | ||
| 1950 | + * 加载(与 asan/appenv 注入模块的 AppSpawnModuleMgrInstall 同模式)。 | ||
| 1951 | + * | ||
| 1952 | + * 失败语义:dlopen/dlsym 任一失败仅记录日志并返回 —— manager 不 | ||
| 1953 | + * 启动,appenv 命令不可用,但孵化服务完全不受影响(不阻断开机)。 | ||
| 1954 | + */ | ||
| 1955 | +static void AppEnvStartManagerByDlopen(void) | ||
| 1956 | +{ | ||
| 1957 | + void *handle = dlopen(APPENV_MANAGER_PATH, RTLD_NOW | RTLD_LOCAL); | ||
| 1958 | + APPSPAWN_CHECK(handle != NULL, return, | ||
| 1959 | + "appenv: dlopen manager failed, %{public}s", dlerror()); | ||
| 1960 | + int (*start)(void) = (int (*)(void))dlsym(handle, "AppEnvManagerStart"); | ||
| 1961 | + APPSPAWN_CHECK(start != NULL, dlclose(handle); return, | ||
| 1962 | + "appenv: dlsym AppEnvManagerStart failed, %{public}s", dlerror()); | ||
| 1963 | + int ret = start(); | ||
| 1964 | + APPSPAWN_CHECK(ret == 0, return, "appenv: manager start failed, ret %{public}d", ret); | ||
| 1965 | + // handle 故意不 dlclose:server 生命周期与 appspawn 进程一致 | ||
| 1966 | +} | ||
| 1967 | + | ||
| 1968 | + | ||
| 1974 | AppSpawnContent *AppSpawnCreateContent(const char *socketName, char *longProcName, uint32_t nameLen, int mode) | 1969 | AppSpawnContent *AppSpawnCreateContent(const char *socketName, char *longProcName, uint32_t nameLen, int mode) |
| 1975 | { | 1970 | { |
| 1976 | APPSPAWN_CHECK(socketName != NULL && longProcName != NULL, return NULL, "Invalid name"); | 1971 | APPSPAWN_CHECK(socketName != NULL && longProcName != NULL, return NULL, "Invalid name"); |
| @@ -2017,6 +2012,10 @@ AppSpawnContent *StartSpawnService(const AppSpawnStartArg *startArg, uint32_t ar | |||
| 2017 | AppSpawnLoadAutoRunModules(MODULE_COMMON); | 2012 | AppSpawnLoadAutoRunModules(MODULE_COMMON); |
| 2018 | FinishAppspawnTrace(); | 2013 | FinishAppspawnTrace(); |
| 2019 | AppSpawnModuleMgrInstall(ASAN_MODULE_PATH); | 2014 | AppSpawnModuleMgrInstall(ASAN_MODULE_PATH); |
| 2015 | + | ||
| 2016 | + // FEAT-20260825-001: appenv 模块(环境变量注入),与 asan 同级显式安装 | ||
| 2017 | + AppSpawnModuleMgrInstall(APPENV_MODULE_PATH); | ||
| 2018 | + | ||
| 2020 | 2019 | ||
| 2021 | APPSPAWN_CHECK(LE_GetDefaultLoop() != NULL, return NULL, "Invalid default loop"); | 2020 | APPSPAWN_CHECK(LE_GetDefaultLoop() != NULL, return NULL, "Invalid default loop"); |
| 2022 | AppSpawnContent *content = AppSpawnCreateContent(arg->socketName, argv[0], argvSize, arg->mode); | 2021 | AppSpawnContent *content = AppSpawnCreateContent(arg->socketName, argv[0], argvSize, arg->mode); |
| @@ -2037,6 +2036,11 @@ AppSpawnContent *StartSpawnService(const AppSpawnStartArg *startArg, uint32_t ar | |||
| 2037 | } | 2036 | } |
| 2038 | 2037 | ||
| 2039 | AddAppSpawnHook(STAGE_CHILD_PRE_RUN, HOOK_PRIO_LOWEST, AppSpawnClearEnv); | 2038 | AddAppSpawnHook(STAGE_CHILD_PRE_RUN, HOOK_PRIO_LOWEST, AppSpawnClearEnv); |
| 2039 | + | ||
| 2040 | + if (arg->mode == MODE_FOR_APP_SPAWN) { | ||
| 2041 | + AppEnvStartManagerByDlopen(); | ||
| 2042 | + } | ||
| 2043 | + | ||
| 2040 | if (arg->mode == MODE_FOR_APP_SPAWN) { | 2044 | if (arg->mode == MODE_FOR_APP_SPAWN) { |
| 2041 | SetParameter("bootevent.appspawn.started", "true"); | 2045 | SetParameter("bootevent.appspawn.started", "true"); |
| 2042 | } | 2046 | } |
| @@ -41,6 +41,9 @@ group("unittest") { | |||
| 41 | deps += [ "unittest/app_spawn_standard_test/app_spawn_spm_test:AppSpawn_SPM_ut" ] | 41 | deps += [ "unittest/app_spawn_standard_test/app_spawn_spm_test:AppSpawn_SPM_ut" ] |
| 42 | } | 42 | } |
| 43 | deps += [ "unittest/hnp_test:HnpTest" ] | 43 | deps += [ "unittest/hnp_test:HnpTest" ] |
| 44 | + if (appspawn_support_appenv) { | ||
| 45 | + deps += [ "unittest/appenv_test:AppSpawn_AppEnv_LoaderMerger_Test" ] | ||
| 46 | + } | ||
| 44 | deps += [ "unittest/hnp_test/private_hnp_test:HnpPrivateTest" ] | 47 | deps += [ "unittest/hnp_test/private_hnp_test:HnpPrivateTest" ] |
| 45 | deps += [ "unittest/hnp_test/hnp_rebuild_test:HnpRebuildCfgTest" ] | 48 | deps += [ "unittest/hnp_test/hnp_rebuild_test:HnpRebuildCfgTest" ] |
| 46 | deps += [ "unittest/single_test/hnp_installer:hnp_installer_test" ] | 49 | deps += [ "unittest/single_test/hnp_installer:hnp_installer_test" ] |
| @@ -0,0 +1,64 @@ | |||
| 1 | +# Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 2 | +# Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 3 | +# you may not use this file except in compliance with the License. | ||
| 4 | +# You may obtain a copy of the License at | ||
| 5 | +# | ||
| 6 | +# http://www.apache.org/licenses/LICENSE-2.0 | ||
| 7 | +# | ||
| 8 | +# Unless required by applicable law or agreed to in writing, software | ||
| 9 | +# distributed under the License is distributed on an "AS IS" BASIS, | ||
| 10 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 11 | +# See the License for the specific language governing permissions and | ||
| 12 | +# limitations under the License. | ||
| 13 | + | ||
| 14 | +import("//base/startup/appspawn/appspawn.gni") | ||
| 15 | +import("//build/test.gni") | ||
| 16 | + | ||
| 17 | +if (!defined(ohos_lite)) { | ||
| 18 | + ohos_unittest("AppSpawn_AppEnv_LoaderMerger_Test") { | ||
| 19 | + module_out_path = "appspawn/appspawn" | ||
| 20 | + if (appspawn_unittest_coverage) { | ||
| 21 | + cflags = [ "--coverage" ] | ||
| 22 | + ldflags = [ "--coverage" ] | ||
| 23 | + cflags_cc = "--coverage" | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + defines = [ "APPSPAWN_TEST" ] | ||
| 27 | + | ||
| 28 | + include_dirs = [ | ||
| 29 | + "${appspawn_path}/modules/appenv", | ||
| 30 | + "${appspawn_path}/modules/module_engine/include", | ||
| 31 | + "${appspawn_path}/modules/modulemgr", | ||
| 32 | + "${appspawn_path}/modules/common", | ||
| 33 | + "${appspawn_path}/modules/sysevent", | ||
| 34 | + "${appspawn_path}/util/include", | ||
| 35 | + "${appspawn_path}/common", | ||
| 36 | + "${appspawn_path}/standard", | ||
| 37 | + ] | ||
| 38 | + | ||
| 39 | + # Target code under test(loader/merger 纯函数;policy 依赖 msg 访问器) | ||
| 40 | + sources = [ | ||
| 41 | + "${appspawn_path}/modules/appenv/appenv_loader.cpp", | ||
| 42 | + "${appspawn_path}/modules/appenv/appenv_merger.cpp", | ||
| 43 | + "${appspawn_path}/modules/appenv/appenv_policy.cpp", | ||
| 44 | + "${appspawn_path}/standard/appspawn_msgmgr.c", | ||
| 45 | + ] | ||
| 46 | + | ||
| 47 | + # Test sources | ||
| 48 | + sources += [ | ||
| 49 | + "appenv_loader_merger_test.cpp", | ||
| 50 | + "appenv_policy_test.cpp", | ||
| 51 | + ] | ||
| 52 | + | ||
| 53 | + configs = [ "${appspawn_path}:appspawn_config" ] | ||
| 54 | + external_deps = [ | ||
| 55 | + "bounds_checking_function:libsec_shared", | ||
| 56 | + "googletest:gmock_main", | ||
| 57 | + "hilog:libhilog", | ||
| 58 | + "init:libbegetutil", | ||
| 59 | + ] | ||
| 60 | + | ||
| 61 | + # IsDeveloperModeOpen 走 wrap(避免拖入 utils.c 的重依赖链) | ||
| 62 | + ldflags = [ "-Wl,--wrap=IsDeveloperModeOpen" ] | ||
| 63 | + } | ||
| 64 | +} | ||
| @@ -0,0 +1,233 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +using namespace testing::ext; | ||
| 26 | + | ||
| 27 | +namespace { | ||
| 28 | +/** | ||
| 29 | + * @brief 构造指定层级的测试条目。 | ||
| 30 | + * | ||
| 31 | + * @param key 变量名 | ||
| 32 | + * @param value 变量值 | ||
| 33 | + * @param scope 作用域 | ||
| 34 | + * @return 填充好的条目 | ||
| 35 | + */ | ||
| 36 | +AppEnvEntry MakeEntry(const char *key, const char *value, uint8_t scope) | ||
| 37 | +{ | ||
| 38 | + AppEnvEntry e = {}; | ||
| 39 | + (void)strcpy_s(e.key, sizeof(e.key), key); | ||
| 40 | + (void)strcpy_s(e.value, sizeof(e.value), value); | ||
| 41 | + e.scope = scope; | ||
| 42 | + e.klass = APP_ENV_CLASS_PUBLIC; | ||
| 43 | + return e; | ||
| 44 | +} | ||
| 45 | +} // namespace | ||
| 46 | + | ||
| 47 | +// ============ AppEnvParseLine:合法行(AC-1.3 正例) ============ | ||
| 48 | + | ||
| 49 | +HWTEST(AppEnvLoaderTest, ParseLineBasic, TestSize.Level0) | ||
| 50 | +{ | ||
| 51 | + AppEnvEntry e = {}; | ||
| 52 | + EXPECT_EQ(AppEnvParseLine("FOO=bar", &e), 0); | ||
| 53 | + EXPECT_STREQ(e.key, "FOO"); | ||
| 54 | + EXPECT_STREQ(e.value, "bar"); | ||
| 55 | +} | ||
| 56 | + | ||
| 57 | +HWTEST(AppEnvLoaderTest, ParseLineTrimAndQuote, TestSize.Level0) | ||
| 58 | +{ | ||
| 59 | + AppEnvEntry e = {}; | ||
| 60 | + // 首尾空白裁剪 + 成对双引号剥离 | ||
| 61 | + EXPECT_EQ(AppEnvParseLine(" MY_VAR = \"hello world\" \n", &e), 0); | ||
| 62 | + EXPECT_STREQ(e.key, "MY_VAR"); | ||
| 63 | + EXPECT_STREQ(e.value, "hello world"); | ||
| 64 | +} | ||
| 65 | + | ||
| 66 | +HWTEST(AppEnvLoaderTest, ParseLineUnderscoreKey, TestSize.Level0) | ||
| 67 | +{ | ||
| 68 | + AppEnvEntry e = {}; | ||
| 69 | + EXPECT_EQ(AppEnvParseLine("_PRIVATE_KEY=v1", &e), 0); | ||
| 70 | + EXPECT_STREQ(e.key, "_PRIVATE_KEY"); | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +// ============ AppEnvParseLine:跳过行(非错误) ============ | ||
| 74 | + | ||
| 75 | +HWTEST(AppEnvLoaderTest, ParseLineSkipCommentAndBlank, TestSize.Level0) | ||
| 76 | +{ | ||
| 77 | + AppEnvEntry e = {}; | ||
| 78 | + EXPECT_EQ(AppEnvParseLine("# comment", &e), 1); | ||
| 79 | + EXPECT_EQ(AppEnvParseLine(" ", &e), 1); | ||
| 80 | + EXPECT_EQ(AppEnvParseLine("", &e), 1); | ||
| 81 | +} | ||
| 82 | + | ||
| 83 | +// ============ AppEnvParseLine:黑名单语法拒绝(BR-5,AC-1.3 反例) ============ | ||
| 84 | + | ||
| 85 | +HWTEST(AppEnvLoaderTest, ParseLineRejectExport, TestSize.Level0) | ||
| 86 | +{ | ||
| 87 | + AppEnvEntry e = {}; | ||
| 88 | + EXPECT_EQ(AppEnvParseLine("export FOO=bar", &e), -1); | ||
| 89 | +} | ||
| 90 | + | ||
| 91 | +HWTEST(AppEnvLoaderTest, ParseLineRejectCommandSubstitution, TestSize.Level0) | ||
| 92 | +{ | ||
| 93 | + AppEnvEntry e = {}; | ||
| 94 | + EXPECT_EQ(AppEnvParseLine("FOO=$(cmd)", &e), -1); | ||
| 95 | + EXPECT_EQ(AppEnvParseLine("FOO=`cmd`", &e), -1); | ||
| 96 | +} | ||
| 97 | + | ||
| 98 | +HWTEST(AppEnvLoaderTest, ParseLineRejectVarReference, TestSize.Level0) | ||
| 99 | +{ | ||
| 100 | + AppEnvEntry e = {}; | ||
| 101 | + EXPECT_EQ(AppEnvParseLine("FOO=$PATH:/xxx", &e), -1); | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +HWTEST(AppEnvLoaderTest, ParseLineRejectPathDirectWrite, TestSize.Level0) | ||
| 105 | +{ | ||
| 106 | + AppEnvEntry e = {}; | ||
| 107 | + // PATH 不可经 KV 配置(BR-3) | ||
| 108 | + EXPECT_EQ(AppEnvParseLine("PATH=/bin", &e), -1); | ||
| 109 | +} | ||
| 110 | + | ||
| 111 | +HWTEST(AppEnvLoaderTest, ParseLineRejectMalformed, TestSize.Level0) | ||
| 112 | +{ | ||
| 113 | + AppEnvEntry e = {}; | ||
| 114 | + EXPECT_EQ(AppEnvParseLine("no_equals_sign", &e), -1); // 缺 '=' | ||
| 115 | + EXPECT_EQ(AppEnvParseLine("=value", &e), -1); // 空 KEY | ||
| 116 | + EXPECT_EQ(AppEnvParseLine("9START=num", &e), -1); // 首字符数字 | ||
| 117 | + EXPECT_EQ(AppEnvParseLine("BAD-KEY=v", &e), -1); // 非法字符 | ||
| 118 | +} | ||
| 119 | + | ||
| 120 | +// ============ AppEnvLoadFile:文件级容错(AC-1.4) ============ | ||
| 121 | + | ||
| 122 | +HWTEST(AppEnvLoaderTest, LoadFileNotExistTreatedAsEmpty, TestSize.Level0) | ||
| 123 | +{ | ||
| 124 | + AppEnvEntry entries[4] = {}; | ||
| 125 | + uint32_t count = 0; | ||
| 126 | + // 不存在的文件按 0 条计,不报错(ENOENT 为常态) | ||
| 127 | + uint32_t added = AppEnvLoadFile("/data/local/tmp/appenv_ut_not_exist.conf", | ||
| 128 | + APP_ENV_SCOPE_USER, entries, &count, 4); | ||
| 129 | + EXPECT_EQ(added, 0u); | ||
| 130 | + EXPECT_EQ(count, 0u); | ||
| 131 | +} | ||
| 132 | + | ||
| 133 | +HWTEST(AppEnvLoaderTest, LoadFileValidAndInvalidLinesMixed, TestSize.Level0) | ||
| 134 | +{ | ||
| 135 | + const char *path = "/data/local/tmp/appenv_ut_mixed.conf"; | ||
| 136 | + FILE *fp = fopen(path, "w"); | ||
| 137 | + ASSERT_NE(fp, nullptr); | ||
| 138 | + (void)fprintf(fp, "# comment\n"); | ||
| 139 | + (void)fprintf(fp, "GOOD_ONE=v1\n"); | ||
| 140 | + (void)fprintf(fp, "export BAD=x\n"); // 非法:跳过 | ||
| 141 | + (void)fprintf(fp, "GOOD_TWO=v2\n"); | ||
| 142 | + (void)fprintf(fp, "PATH=/bin\n"); // 非法:跳过 | ||
| 143 | + (void)fclose(fp); | ||
| 144 | + | ||
| 145 | + AppEnvEntry entries[8] = {}; | ||
| 146 | + uint32_t count = 0; | ||
| 147 | + uint32_t added = AppEnvLoadFile(path, APP_ENV_SCOPE_USER, entries, &count, 8); | ||
| 148 | + EXPECT_EQ(added, 2u); // 仅 2 条合法 | ||
| 149 | + EXPECT_STREQ(entries[0].key, "GOOD_ONE"); | ||
| 150 | + EXPECT_STREQ(entries[1].key, "GOOD_TWO"); | ||
| 151 | + EXPECT_EQ(entries[0].scope, (uint8_t)APP_ENV_SCOPE_USER); | ||
| 152 | + (void)remove(path); | ||
| 153 | +} | ||
| 154 | + | ||
| 155 | +// ============ AppEnvMergeEntries:优先级合并(AC-1.6,BR-4) ============ | ||
| 156 | + | ||
| 157 | +HWTEST(AppEnvMergerTest, MergePriorityAppWins, TestSize.Level0) | ||
| 158 | +{ | ||
| 159 | + AppEnvEntry in[3] = { | ||
| 160 | + MakeEntry("K", "sys", APP_ENV_SCOPE_SYSTEM), | ||
| 161 | + MakeEntry("K", "usr", APP_ENV_SCOPE_USER), | ||
| 162 | + MakeEntry("K", "app", APP_ENV_SCOPE_APP), | ||
| 163 | + }; | ||
| 164 | + AppEnvEntry out[4] = {}; | ||
| 165 | + uint32_t n = AppEnvMergeEntries(in, 3, out, 4); | ||
| 166 | + EXPECT_EQ(n, 1u); | ||
| 167 | + EXPECT_STREQ(out[0].value, "app"); // app 最高优先级 | ||
| 168 | + EXPECT_EQ(out[0].scope, (uint8_t)APP_ENV_SCOPE_APP); // 来源随值 | ||
| 169 | +} | ||
| 170 | + | ||
| 171 | +HWTEST(AppEnvMergerTest, MergeUserOverSessionOverSystem, TestSize.Level0) | ||
| 172 | +{ | ||
| 173 | + AppEnvEntry in[3] = { | ||
| 174 | + MakeEntry("A", "sys", APP_ENV_SCOPE_SYSTEM), | ||
| 175 | + MakeEntry("A", "ses", APP_ENV_SCOPE_SESSION), | ||
| 176 | + MakeEntry("B", "sysOnly", APP_ENV_SCOPE_SYSTEM), | ||
| 177 | + }; | ||
| 178 | + AppEnvEntry out[4] = {}; | ||
| 179 | + uint32_t n = AppEnvMergeEntries(in, 3, out, 4); | ||
| 180 | + EXPECT_EQ(n, 2u); | ||
| 181 | + // A: session > system | ||
| 182 | + EXPECT_STREQ(out[0].value, "ses"); | ||
| 183 | + // B: 仅 system 层有,保留 | ||
| 184 | + EXPECT_STREQ(out[1].value, "sysOnly"); | ||
| 185 | + EXPECT_EQ(out[1].scope, (uint8_t)APP_ENV_SCOPE_SYSTEM); | ||
| 186 | +} | ||
| 187 | + | ||
| 188 | +HWTEST(AppEnvMergerTest, MergeSameScopeLastWins, TestSize.Level0) | ||
| 189 | +{ | ||
| 190 | + AppEnvEntry in[2] = { | ||
| 191 | + MakeEntry("K", "first", APP_ENV_SCOPE_USER), | ||
| 192 | + MakeEntry("K", "second", APP_ENV_SCOPE_USER), | ||
| 193 | + }; | ||
| 194 | + AppEnvEntry out[4] = {}; | ||
| 195 | + uint32_t n = AppEnvMergeEntries(in, 2, out, 4); | ||
| 196 | + EXPECT_EQ(n, 1u); | ||
| 197 | + EXPECT_STREQ(out[0].value, "second"); // 同层后者覆盖 | ||
| 198 | +} | ||
| 199 | + | ||
| 200 | +HWTEST(AppEnvMergerTest, MergeLowerScopeDoesNotOverride, TestSize.Level0) | ||
| 201 | +{ | ||
| 202 | + // loader 输出顺序若 system 在后,也不得覆盖先到的 user 值 | ||
| 203 | + AppEnvEntry in[2] = { | ||
| 204 | + MakeEntry("K", "usr", APP_ENV_SCOPE_USER), | ||
| 205 | + MakeEntry("K", "sys", APP_ENV_SCOPE_SYSTEM), | ||
| 206 | + }; | ||
| 207 | + AppEnvEntry out[4] = {}; | ||
| 208 | + uint32_t n = AppEnvMergeEntries(in, 2, out, 4); | ||
| 209 | + EXPECT_EQ(n, 1u); | ||
| 210 | + EXPECT_STREQ(out[0].value, "usr"); | ||
| 211 | + EXPECT_EQ(out[0].scope, (uint8_t)APP_ENV_SCOPE_USER); | ||
| 212 | +} | ||
| 213 | + | ||
| 214 | +HWTEST(AppEnvMergerTest, MergeInvalidArgs, TestSize.Level0) | ||
| 215 | +{ | ||
| 216 | + AppEnvEntry out[4] = {}; | ||
| 217 | + EXPECT_EQ(AppEnvMergeEntries(nullptr, 1, out, 4), 0u); | ||
| 218 | + AppEnvEntry in[1] = {MakeEntry("K", "v", APP_ENV_SCOPE_SYSTEM)}; | ||
| 219 | + EXPECT_EQ(AppEnvMergeEntries(in, 1, nullptr, 4), 0u); | ||
| 220 | + EXPECT_EQ(AppEnvMergeEntries(in, 1, out, 0), 0u); | ||
| 221 | +} | ||
| 222 | + | ||
| 223 | +HWTEST(AppEnvMergerTest, MergeBufferFull, TestSize.Level0) | ||
| 224 | +{ | ||
| 225 | + AppEnvEntry in[3] = { | ||
| 226 | + MakeEntry("A", "1", APP_ENV_SCOPE_SYSTEM), | ||
| 227 | + MakeEntry("B", "2", APP_ENV_SCOPE_SYSTEM), | ||
| 228 | + MakeEntry("C", "3", APP_ENV_SCOPE_SYSTEM), | ||
| 229 | + }; | ||
| 230 | + AppEnvEntry out[2] = {}; | ||
| 231 | + uint32_t n = AppEnvMergeEntries(in, 3, out, 2); | ||
| 232 | + EXPECT_EQ(n, 2u); // 容量满丢弃余量,不越界 | ||
| 233 | +} | ||