fix(nyai): 导出 exec 绝对路径相对化 + gzip stderr 重定向 (agent-签名产签链路)

- cmd_export 原先对 app.json 的 exec(绝对路径,如 /usr/data/mnt/sd_0/MeowAPPs/<pkg>/<bin>)
  误用 sanitize_member 剥前导 '/' 拼到包目录下,导致 access(<src>/usr/data/...) 必然失败,
  所有真实 app 导出被拒(exec 文件不存在或不可执行),卡死「导出→产签」链路。
  新增 exec_rel_to_pkg(): 按 ①src/ 前缀 ②/MeowAPPs/<pkg>/ 段(根路径不一致的沙箱/CI)
  ③相对路径 ④绝对但定位不到包目录→拒, 正确相对化 exec。
- install/upgrade 解包命令把 2>/dev/null 只绑到 tar,gzip stderr(footer trailing garbage /
  损坏 crc error)会漏到用户输出;改 gzip 也重定向。
- 同链验证: 宿主 meow_sign/meow_verify_host/qemu-mipsel 验签矩阵通过; 真机(10.78.193.50)
  无签 os.meow.*拒装/已签安装OK+.mpk.src/篡改拒装/降级拒装 全过; 装后已卸并恢复原状。
  (devdocs 本地不入库; 产签实走记录+README 见 patch_v2/meow_sign/README.md §8)
This commit is contained in:
MeowOS committed 2026-08-31 19:30:32 +08:00
1 parent 92086129c5
commit 5fac5e380d
4 files changed
+47 -4

No files matched your search

+13
View File
@@ -130,3 +130,16 @@ static const uint8_t g_meow_pub[MEOW_PUB_KEY_COUNT][32] = {
| `../meow_verify/meow_signing_pub.h` | **固件内嵌公钥数组**(仅公钥,可入库) |
| `../meow_verify/meow_sig.h` | 签名块格式 + 被签消息构造(verifier/signer 共用) |
| `.keys/` | 私钥目录(**已 gitignore,绝不入库**) |
## 8. 端到端实走(真机已验证,2026-08-31)
> 完整「产签→验签→真机安装→导出保留签名」的可复现步骤、逐命令预期输出、6 个失败矩阵项、常见错误速查与本次修复,见
> `devdocs/plan/MPK_SIGNATURE_DESIGN.md` 的「产签实走记录 / 操作步骤(端到端)」节。此处只记三个**最容易踩的坑**:
1. **`.mpk` 文件内文件须在 tar 根**:`app.json`/`meow`/资源须并列于 tar 根(**无** `<pkg>/` 顶层包裹目录)。
打包用 `tar -C <pkg> .`(导出生成本就是 `tar -C src .`),否则设备安装报 `解压/校验 app.json 失败`。
2. **包名 = 文件名茎**:`meow_sign sign` 从 `<pkg>.mpk` 文件名茎推导包名并写入 footer;`<pkg>.mpk` 必须 == `app.json.package`。
改文件名/包名 → 验签 `package mismatch`。
3. **`exec` 可为绝对路径,导出须相对化**:真实 app 的 `app.json.exec` 是绝对路径(`launcher` 直接 `execve`)。
`nyai export` 须把 `exec` 相对化到包目录(已修 `nyai.c` 的 `exec_rel_to_pkg`)再存在性检查/白名单打包;
否则所有真实 app 导出报「exec 文件 … 不存在或不可执行」。
Binary file not shown.
+34 -4
View File
@@ -840,7 +840,7 @@ static int install_mpk_file(const char *mpk_path)
rm_rf(stg);
if (mkdir_p(stg)!=0) { fprintf(stderr,"nyai: install: 无法建临时目录 %s\n", stg); return 1; }
char cmd[1024];
snprintf(cmd,sizeof cmd,"gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && [ -f '%s/app.json' ]", mpk_path, stg, stg);
snprintf(cmd,sizeof cmd,"gzip -dc '%s' 2>/dev/null | tar -x -C '%s' 2>/dev/null && [ -f '%s/app.json' ]", mpk_path, stg, stg);
printf("Preparing to unpack %s ...\n", pkg);
if (system(cmd)!=0) {
fprintf(stderr,"nyai: install: 解压/校验 app.json 失败(%s 可能损坏或非 .mpk)\n", mpk_path);
@@ -1065,6 +1065,36 @@ static int sanitize_member(const char *in, char *out, int n)
snprintf(out,n,"%s", p);
return 0;
}
/* 把 app.json 的 exec 路径相对化到包目录下(导出前存在性检查 + 白名单打包用)。
* 约定(MEOW_APP_SDK 现有惯例,launcher 以绝对路径直接 execve):
* - exec 以本包目录 src/ 为前缀 → 取其后相对部分(同一 meow_root,如真机 src=.../MeowAPPs/os.meow.hello → hello);
* - exec 含 "/MeowAPPs/<pkg>/" 段 → 取该段后的相对路径(根路径与 src 不一致时仍可定位,如沙箱/CI 测试);
* - exec 为相对路径(无前导 /)→ 沿用 sanitize_member 语义(剥 ./、禁 ../、禁 shell 元字符);
* - 绝对路径但定位不到包目录段 → 非本包程序,返回 -1(拒绝导出)。
* 返回 0 成功,-1 非法。 */
static int exec_rel_to_pkg(const char *src, const char *exec, const char *pkg, char *out, int n)
{
if (!exec || !*exec) return -1;
const char *rel = NULL;
size_t sl = strlen(src);
if (strncmp(exec, src, sl) == 0 && exec[sl] == '/') {
rel = exec + sl + 1;
} else if (pkg && *pkg) {
char marker[272]; snprintf(marker, sizeof marker, "/MeowAPPs/%s/", pkg);
const char *m = strstr(exec, marker);
if (m) rel = m + strlen(marker);
}
if (rel) {
size_t rl = strlen(rel);
if (rl == 0 || rl >= (size_t)n) return -1;
if (has_dotdot_comp(rel) || unsafe_path(rel)) return -1;
memcpy(out, rel, rl); out[rl] = 0;
return 0;
}
if (exec[0] != '/') return sanitize_member(exec, out, n);
return -1;
}
/* 从 JSON 数组串(含 '[' ']')提取字符串成员;返回项数,负=格式错 */
static int json_array_strings(const char *arr, char items[][256], int max)
{
@@ -1147,9 +1177,9 @@ static int cmd_export(const char *pkg, const char *out)
char appj[BUF]; snprintf(appj,sizeof appj,"%s/app.json", src);
appmeta m;
if (load_app_meta(appj,&m)!=0) { fprintf(stderr,"nyai: export: %s/app.json 不合法(缺 name 或 exec)\n", pkg); return 1; }
/* 导出前体检:exec 文件存在且可执行 */
/* 导出前体检:exec 文件存在且可执行。exec 可为绝对(包目录下,相对化)或相对路径。 */
char exec_rel[BUF];
if (sanitize_member(m.exec, exec_rel, sizeof exec_rel)!=0) {
if (exec_rel_to_pkg(src, m.exec, pkg, exec_rel, sizeof exec_rel)!=0) {
fprintf(stderr,"nyai: export: exec 字段非法: %s\n", m.exec); return 1;
}
char exepath[BUF]; snprintf(exepath,sizeof exepath,"%s/%s", src, exec_rel);
@@ -1246,7 +1276,7 @@ static int cmd_upgrade(const char *pkg, const char *file)
if (rename(dst,bak)!=0) { fprintf(stderr,"nyai: upgrade: 备份失败: %s\n", strerror(errno)); return 1; }
if (mkdir_p(dst)!=0) { rename(bak,dst); return 1; }
char cmd[1024];
snprintf(cmd,sizeof cmd,"gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && [ -f '%s/app.json' ]", mpk,dst,dst);
snprintf(cmd,sizeof cmd,"gzip -dc '%s' 2>/dev/null | tar -x -C '%s' 2>/dev/null && [ -f '%s/app.json' ]", mpk,dst,dst);
printf("Upgrading %s ...\n", pkg);
if (system(cmd)!=0) { fprintf(stderr,"nyai: upgrade: 解压失败,回滚\n"); rm_rf(dst); rename(bak,dst); return 1; }
char appj[BUF]; snprintf(appj,sizeof appj,"%s/app.json", dst);
Binary file not shown.