已开启
feat: add appenv module for GUI app environment variable configuration #2941
feat: add appenv module for GUI app environment variable configuration #2941
已开启
wangfenging创建于 10 天前
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 = false45 appspawn_sandbox_root_tmpfs = false
46 appspawn_enable_spm = true46 appspawn_enable_spm = true
47 appspawn_controlled_app = false47 appspawn_controlled_app = false
48+ # FEAT-20260825-001: appenv(GUI 应用环境变量配置)总开关,异常时可整体裁剪回退
49+ appspawn_support_appenv = true
48}50}
49 51 
50if (!defined(global_parts_info) ||52if (!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+#include "appenv_cache.h"
17+ 
18+#include "appspawn_utils.h"
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+#ifndef APPENV_CACHE_H
17+#define APPENV_CACHE_H
18+ 
19+#include <stdint.h>
20+#include "appenv_module.h"
21+ 
22+#ifdef __cplusplus
23+extern "C" {
24+#endif
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+#ifdef __cplusplus
51+}
52+#endif
53+#endif // APPENV_CACHE_H
@@ -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+#include "appspawn_hook.h"
17+#include "appspawn_manager.h"
18+#include "appspawn_utils.h"
19+#include "appenv_module.h"
20+#include "appenv_loader.h"
21+#include "appenv_merger.h"
22+#include "appenv_policy.h"
23+#include "appenv_path.h"
24+#include "appenv_cache.h"
25+ 
26+/**
27+ * @brief appenv 注入 hook 入口(TASK-001 骨架)。
28+ *
29+ * 挂接于 STAGE_CHILD_PRE_COLDBOOT 阶段,优先级 HOOK_PRIO_COMMON + 32003),
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+#include "appenv_loader.h"
17+ 
18+#include <ctype.h>
19+#include <errno.h>
20+#include <stdio.h>
21+#include <string.h>
22+ 
23+#include "appspawn_utils.h"
24+#include "securec.h"
25+ 
26+/** system 作用域配置路径(只读分区,镜像预置)。 */
27+#define APPENV_SYSTEM_CONF_PATH "/system/etc/appenv.conf"
28+/** el1 配置根目录(session 与 user/app 层的公共前缀)。 */
29+#define APPENV_EL1_BASE_PATH "/data/service/el1"
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+#ifndef APPENV_LOADER_H
17+#define APPENV_LOADER_H
18+ 
19+#include <stdint.h>
20+#include "appenv_module.h"
21+ 
22+#ifdef __cplusplus
23+extern "C" {
24+#endif
25+ 
26+/** 单文件最大行数(与 /etc/environment 解析器约束一致)。 */
27+#define APPENV_MAX_FILE_LINES 1000
28+/** 单行最大长度(含 KEY、'=' 与 VALUE)。 */
29+#define APPENV_MAX_LINE_LEN 4096
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+#ifdef __cplusplus
83+}
84+#endif
85+#endif // APPENV_LOADER_H
@@ -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+#include "appenv_merger.h"
17+ 
18+#include <string.h>
19+ 
20+#include "appspawn_utils.h"
21+#include "securec.h"
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+#ifndef APPENV_MERGER_H
17+#define APPENV_MERGER_H
18+ 
19+#include <stdint.h>
20+#include "appenv_module.h"
21+ 
22+#ifdef __cplusplus
23+extern "C" {
24+#endif
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+#ifdef __cplusplus
44+}
45+#endif
46+#endif // APPENV_MERGER_H
@@ -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+#ifndef APPENV_MODULE_H
17+#define APPENV_MODULE_H
18+ 
19+#include <stdint.h>
20+ 
21+#ifdef __cplusplus
22+extern "C" {
23+#endif
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+#ifdef __cplusplus
65+}
66+#endif
67+#endif // APPENV_MODULE_H
@@ -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+#include "appenv_path.h"
17+ 
18+#include <string.h>
19+ 
20+#include "appspawn_utils.h"
21+#include "securec.h"
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+#ifndef APPENV_PATH_H
17+#define APPENV_PATH_H
18+ 
19+#include <stdint.h>
20+ 
21+#ifdef __cplusplus
22+extern "C" {
23+#endif
24+ 
25+/** 单个作用域允许的 PATH 追加目录上限(BR-10 数量受限约束)。 */
26+#define APPENV_PATH_APPEND_MAX 16
27+/** 单个追加目录路径的最大长度(含结尾符)。 */
28+#define APPENV_PATH_DIR_MAX 512
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+#ifdef __cplusplus
49+}
50+#endif
51+#endif // APPENV_PATH_H
@@ -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+#include "appenv_policy.h"
17+ 
18+#include <stdio.h>
19+#include <string.h>
20+ 
21+#include "appspawn_utils.h"
22+#include "securec.h"
23+ 
24+/** 白名单条目上限(镜像文件约束,超出部分忽略)。 */
25+#define APPENV_WHITELIST_MAX 256
26+/** 白名单单行长度上限(含注释行)。 */
27+#define APPENV_WHITELIST_LINE_MAX 256
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+#define CLASS_PREFIX_COUNT (sizeof(g_classPrefixes) / sizeof(g_classPrefixes[0]))
83+/** 精确表条目数。 */
84+#define CLASS_ENTRY_COUNT (sizeof(g_classTable) / sizeof(g_classTable[0]))
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+#ifndef APPENV_POLICY_H
17+#define APPENV_POLICY_H
18+ 
19+#include <stdint.h>
20+#include "appenv_module.h"
21+#include "appspawn_manager.h"
22+ 
23+#ifdef __cplusplus
24+extern "C" {
25+#endif
26+ 
27+/** restricted 白名单文件路径(ADR-8:镜像预置只读,仅构建/OTA 可变更)。 */
28+#define APPENV_WHITELIST_PATH "/system/etc/appenv/restricted-whitelist.conf"
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+#ifdef __cplusplus
118+}
119+#endif
120+#endif // APPENV_POLICY_H
@@ -35,6 +35,20 @@ extern "C" {
35#define ASAN_MODULE_PATH "/system/lib/appspawn/libappspawn_asan"35#define ASAN_MODULE_PATH "/system/lib/appspawn/libappspawn_asan"
36#endif36#endif
37 37 
38+// FEAT-20260825-001: appenv 环境变量注入模块(libappspawn_appenv.so)
39+#if defined(__aarch64__) || defined(__x86_64__)
40+#define APPENV_MODULE_PATH "/system/lib64/appspawn/libappspawn_appenv"
41+#else
42+#define APPENV_MODULE_PATH "/system/lib/appspawn/libappspawn_appenv"
43+#endif
44+ 
45+// FEAT-20260825-001: AppEnvManager IPC 服务(libappenv_manager.so,dlopen 加载)
46+#if defined(__aarch64__) || defined(__x86_64__)
47+#define APPENV_MANAGER_PATH "/system/lib64/appspawn/libappenv_manager"
48+#else
49+#define APPENV_MANAGER_PATH "/system/lib/appspawn/libappenv_manager"
50+#endif
51+ 
38typedef enum {52typedef 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+#include "appenv_client.h"
17+ 
18+#include <errno.h>
19+#include <poll.h>
20+#include <sys/socket.h>
21+#include <sys/un.h>
22+#include <unistd.h>
23+ 
24+#include "appspawn_utils.h"
25+#include "securec.h"
26+ 
27+/** IPC 消息魔数(与服务端 appenv_manager.c 一致)。 */
28+#define APPENV_IPC_MAGIC 0x41505045
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+#ifndef APPENV_CLIENT_H
17+#define APPENV_CLIENT_H
18+ 
19+#include <stdint.h>
20+#include "appenv_ipc.h"
21+ 
22+#ifdef __cplusplus
23+extern "C" {
24+#endif
25+ 
26+/** IPC 等待响应超时(毫秒),超时映射为退出码 5。 */
27+#define APPENV_RPC_TIMEOUT_MS 3000
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+#ifdef __cplusplus
48+}
49+#endif
50+#endif // APPENV_CLIENT_H
@@ -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+#include "appenv_cmd.h"
17+ 
18+#include <stdio.h>
19+#include <string.h>
20+ 
21+#include "appenv_client.h"
22+#include "appenv_ipc.h"
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+#ifndef APPENV_CMD_H
17+#define APPENV_CMD_H
18+ 
19+#include <stdint.h>
20+ 
21+#ifdef __cplusplus
22+extern "C" {
23+#endif
24+ 
25+/** CLI 子命令数(8 个:set/unset/get/list/path/check/effective/explain)。 */
26+#define APPENV_CMD_COUNT 8
27+ 
28+/** 单条命令帮助文本上限。 */
29+#define APPENV_HELP_LINE_MAX 128
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+#ifdef __cplusplus
60+}
61+#endif
62+#endif // APPENV_CMD_H
@@ -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+#ifndef APPENV_IPC_H
17+#define APPENV_IPC_H
18+ 
19+#include <stdint.h>
20+ 
21+#ifdef __cplusplus
22+extern "C" {
23+#endif
24+ 
25+/** AppEnvManager IPC server socket 路径(appspawn 进程内监听)。 */
26+#define APPENV_SOCKET_NAME "appenv"
27+ 
28+/** IPC 协议版本(协议不兼容变更时递增,客户端据此拒绝连接)。 */
29+#define APPENV_IPC_VERSION 1
30+ 
31+/** 单条 IPC 消息上限(含头部;key/value/bundle 载荷受 FR 约束远小于此)。 */
32+#define APPENV_IPC_MSG_MAX 8192
33+ 
34+/** key/bundleName 字段长度上限。 */
35+#define APPENV_IPC_KEY_MAX 256
36+#define APPENV_IPC_BUNDLE_MAX 256
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+#ifdef __cplusplus
91+}
92+#endif
93+#endif // APPENV_IPC_H
@@ -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+#include <string.h>
17+ 
18+#include "appenv_cmd.h"
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+#include "appenv_manager.h"
17+ 
18+#include <errno.h>
19+#include <fcntl.h>
20+#include <sys/socket.h>
21+ 
22+#include "appspawn_hook.h"
23+#include "appspawn_utils.h"
24+#include "appenv_ipc.h"
25+#include "init_socket.h"
26+#include "loop_event.h"
27+#include "securec.h"
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+#define APPENV_IPC_MAGIC 0x41505045
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+#ifndef APPENV_MANAGER_H
17+#define APPENV_MANAGER_H
18+ 
19+#include <stdint.h>
20+ 
21+#ifdef __cplusplus
22+extern "C" {
23+#endif
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+#ifdef __cplusplus
50+}
51+#endif
52+#endif // APPENV_MANAGER_H
@@ -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- 
266static void OnClose(const TaskHandle taskHandle)249static 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 app270 // 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 ID327 * @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 failed1497 // 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 dir1503 // mount el2 dir
1537 // getWrapBundleNameValue1504 // 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 result1518 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+#ifdef APPSPAWN_SUPPORT_APPENV
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+#endif
1968+ 
1974AppSpawnContent *AppSpawnCreateContent(const char *socketName, char *longProcName, uint32_t nameLen, int mode)1969AppSpawnContent *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+#ifdef APPSPAWN_SUPPORT_APPENV
2016+ // FEAT-20260825-001: appenv 模块(环境变量注入),与 asan 同级显式安装
2017+ AppSpawnModuleMgrInstall(APPENV_MODULE_PATH);
2018+#endif
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#endif2037#endif
2039 AddAppSpawnHook(STAGE_CHILD_PRE_RUN, HOOK_PRIO_LOWEST, AppSpawnClearEnv);2038 AddAppSpawnHook(STAGE_CHILD_PRE_RUN, HOOK_PRIO_LOWEST, AppSpawnClearEnv);
2039+#ifdef APPSPAWN_SUPPORT_APPENV
2040+ if (arg->mode == MODE_FOR_APP_SPAWN) {
2041+ AppEnvStartManagerByDlopen();
2042+ }
2043+#endif
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+#include <gtest/gtest.h>
17+#include "securec.h"
18+#include <cstring>
19+#include <string>
20+ 
21+#include "appenv_loader.h"
22+#include "appenv_merger.h"
23+#include "appenv_module.h"
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+}