feat(nyai): M35 包身份以已签名 app.json 清单为准 + 暂存迁到 SD 并必清理

包名/版本/依赖不再从 .mpk 文件名推导(用户定案:不对安装包文件名强依赖,
一切信息从清单取),改为解包后读 app.json.package 作唯一权威:
  - 落地目录 MeowAPPs/<pkg>、meow_verify --package <pkg>、防降级基准全用清单包名;
  - 新增 footer.package == app.json.package 一致性检查,不一致拒装并同时报两个名字;
  - valid_pkg 拒绝 "." / ".."(此前 MeowAPPs/.. 可越出安装目录并被 rm_rf);
  - index 的 Package 字段同样取自清单(缺 package 的包跳过并告警);
  - scan/export 同步;--help 与错误文案去掉「需要 <pkg>.mpk」。

暂存:MeowAPPs/.stg_<pid>_<pkg>(/usr/data NAND,仅剩 4MB)
  → <MeowROOT>/MeowOS/tmp/pkgs/<pkg>-<UTC时间戳>-<pid>-<n>/(真机即 SD 卡):
  - mkdir 原子占位 + 失败重试 + pid/序号保证唯一,时间戳只给人看;
  - SD 未挂载/不可写直接拒装(不回退 /usr/data:目标本就在 SD,且跨 fs rename 会 EXDEV);
  - 解包前后各一道防护:tar -tv 只允许普通文件/目录(拒符号链接/硬链接/设备/FIFO),
    解包后复核暂存树(非普通文件、nlink>1 一律拒);
  - 中央化 cleanup + SIGINT/SIGTERM/SIGHUP handler(先收子进程组再清)+ atexit 兜底,
    成功/验签失败/清单非法/降级/解压失败/被中断全部删除暂存目录。

真机 time() 受 zig 0.17 mipsel time64 ABI 缺陷影响返回垃圾(实测 3381223399391169494、
localtime_r=NULL),故时间戳直取内核 o32 time32(syscall 4263)并自行换算 UTC,
不依赖 libc time()/localtime_r()。

upgrade 复用 install 同一实现(清单包名必须等于目标包名、目标必须已装)。

新增 host 回归 patch_v2/meowpkg/test_nyai.sh(49 断言:改名包/真实 0.1.7 反例/
footer≠清单/os.meow 无签名/第三方无签名/恶意包名/解压失败/SIGTERM/索引路径/upgrade)。

真机验证(固件 0.8.48,二进制只放 MeowOS/tmp,未覆盖 /usr/bin/nyai):
os.meow.reader-0.1.4.bak.mpk 现在「签名校验通过 (package=os.meow.reader)」后才被
降级门禁拒绝(旧版此处是包名不符);weird-name.bin 装成 dev.m35.probe;SIGTERM
中断后暂存目录为空;Music=199164544、/usr/bin/nyai md5 a7d24553… 均未变。
This commit is contained in:
AskaEth committed 2026-11-10 21:42:37 +08:00
1 parent 8f748b137d
commit 829d4475e8
4 files changed
+746 -148

No files matched your search

+244
View File
@@ -0,0 +1,244 @@
#!/bin/bash
# test_nyai.sh — nyai 宿主回归(M35:app.json 清单权威 + SD 暂存 + 无条件清理)
#
# 覆盖:
# 1 文件名/后缀与包名完全不同(weird-name.bin)仍按清单装进 MeowAPPs/<app.json.package>
# 2 真实反例:签名 os.meow.reader 包命名成 os.meow.reader-0.1.7.mpk 仍装进 MeowAPPs/os.meow.reader
# 3 footer.package ≠ app.json.package → 拒装,错误文案同时给出两个名字
# 4 os.meow.* 无签名 → 拒装
# 5 第三方无签名 → 放行(行为不变)
# 6 恶意 app.json.package(../x、a/b、.、..)→ 拒装且不落盘到目录外
# 7 成功 / 各种失败 / 解压失败 / SIGTERM 中断 → MeowOS/tmp/pkgs 无残留
# 8 索引路径:Packages 的 Package 字段取自清单(文件改名后按包名仍可装)
# 9 upgrade 复用 install 引擎:清单包名/目标包名不一致、降级都拒
#
# 依赖:宿主 cc(编 nyai_host / meow_verify_host / meow_sign)。
# 签名用例需要维护者私钥:默认 ~/.keys/meowos-signing/signing.key,
# 可用 NYAI_TEST_SIGN_KEY 覆盖;无私钥时签名用例 SKIP(不判 FAIL)。
# 真实反例包默认 ~/.cache/os.meow.reader-0.1.7-signed.mpk,可用 NYAI_TEST_REAL_MPK 覆盖。
#
# 用法:./test_nyai.sh [-v]
set -u
HERE=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$HERE/../.." && pwd)
VERBOSE=0
[ "${1:-}" = "-v" ] && VERBOSE=1
WORK=${NYAI_TEST_WORK:-$(mktemp -d "${TMPDIR:-/tmp}/nyai-m35.XXXXXX")}
mkdir -p "$WORK"
CLEANUP_WORK=1
[ -n "${NYAI_TEST_WORK:-}" ] && CLEANUP_WORK=0
trap '[ "$CLEANUP_WORK" = 1 ] && rm -rf "$WORK"' EXIT
PASS=0; FAIL=0; SKIP=0
ok() { PASS=$((PASS+1)); printf 'PASS %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf 'FAIL %s\n' "$1"; [ -n "${2:-}" ] && printf ' %s\n' "$2"; }
skip() { SKIP=$((SKIP+1)); printf 'SKIP %s (%s)\n' "$1" "${2:-}"; }
KEY=${NYAI_TEST_SIGN_KEY:-$HOME/.keys/meowos-signing/signing.key}
[ -r "$KEY" ] || KEY=""
REAL_MPK=${NYAI_TEST_REAL_MPK:-$HOME/.cache/os.meow.reader-0.1.7-signed.mpk}
# ---------------- 构建 ----------------
echo "== build =="
( cd "$HERE" && ./build_nyai.sh host >/dev/null ) || { echo "build nyai_host failed"; exit 1; }
NYAI="$HERE/nyai_host"
CC=${CC_HOST:-cc}
V="$ROOT/patch_v2/meow_verify"
mkdir -p "$WORK/tools"
"$CC" -Os -Wno-sign-compare -Wno-unterminated-string-initialization -Wno-unused-function \
-c "$V/tweetnacl.c" -o "$WORK/tools/tn.o" || { echo "build tweetnacl failed"; exit 1; }
"$CC" -Os -c "$V/sha256.c" -o "$WORK/tools/sha.o" || exit 1
"$CC" -Os -I"$V" -c "$V/meow_verify.c" -o "$WORK/tools/mv.o" || exit 1
"$CC" "$WORK/tools/mv.o" "$WORK/tools/sha.o" "$WORK/tools/tn.o" -o "$WORK/tools/meow_verify_host" || exit 1
if [ -n "$KEY" ]; then
"$CC" -Os -I"$V" -I"$ROOT/patch_v2/meow_sign" "$ROOT/patch_v2/meow_sign/meow_sign.c" \
"$WORK/tools/tn.o" "$WORK/tools/sha.o" -o "$WORK/tools/meow_sign" || exit 1
fi
VERIFY="$WORK/tools/meow_verify_host"
# ---------------- 辅助 ----------------
sb() { # sb <name> → 打印沙箱 MeowROOT
local d="$WORK/case-$1"
rm -rf "$d"
mkdir -p "$d/root/MeowAPPs" "$d/root/MeowOS/opt/nyai"
printf '%s\n' "$d/root"
}
mkpkg() { # mkpkg <dir> <package> <version_code>
rm -rf "$1"; mkdir -p "$1"
printf '{"name":"Test","package":"%s","exec":"bin","version":"0.1.%s","version_code":%s}\n' \
"$2" "$3" "$3" > "$1/app.json"
printf '#!/bin/sh\nexit 0\n' > "$1/bin"; chmod +x "$1/bin"
}
pack() { tar czf "$2" -C "$1" .; }
sign() { # sign <pkg> <mpk>:文件名茎必须等于包名(meow_sign 约定),签完覆盖原文件
[ -n "$KEY" ] || return 2
mkdir -p "$WORK/sign"
cp "$2" "$WORK/sign/$1.mpk"
"$WORK/tools/meow_sign" sign "$WORK/sign/$1.mpk" "$KEY" >/dev/null 2>&1 || return 1
cp "$WORK/sign/$1.mpk" "$2"
}
nyai_run() { # nyai_run <root> <args...>
local r=$1; shift
MEOWROOT="$r" MEOW_VERIFY_BIN="$VERIFY" "$NYAI" "$@"
}
tmp_clean() { # tmp_clean <root>:MeowOS/tmp/pkgs 下无任何条目
local n
n=$(find "$1/MeowOS/tmp/pkgs" -mindepth 1 2>/dev/null | wc -l | tr -d ' ')
[ "$n" = "0" ]
}
show() { [ "$VERBOSE" = 1 ] && printf '%s\n' "$1"; }
expect() { # expect <label> <cond...>
local l=$1; shift
if "$@"; then ok "$l"; else bad "$l"; fi
}
echo "== tests =="
# ---- 1 文件名与包名完全不同 ----
r=$(sb 1); mkpkg "$WORK/b1" os.meow.demo 1; pack "$WORK/b1" "$WORK/p1.mpk"
if sign os.meow.demo "$WORK/p1.mpk"; then
cp "$WORK/p1.mpk" "$WORK/weird-name.bin"
out=$(nyai_run "$r" install "$WORK/weird-name.bin" 2>&1); rc=$?
show "$out"
expect "1a weird-name.bin(无 .mpk 后缀)按清单装入 MeowAPPs/os.meow.demo" \
test $rc -eq 0 -a -f "$r/MeowAPPs/os.meow.demo/app.json"
expect "1b 未按文件名生成目录" test ! -e "$r/MeowAPPs/weird-name"
expect "1c 成功后 MeowOS/tmp/pkgs 无残留" tmp_clean "$r"
else
skip "1 文件名解绑(签名)" "无签名私钥"
fi
# ---- 2 真实反例:reader 包改名 ----
if [ -r "$REAL_MPK" ]; then
r=$(sb 2)
cp "$REAL_MPK" "$WORK/os.meow.reader-0.1.7.mpk"
out=$(nyai_run "$r" install "$WORK/os.meow.reader-0.1.7.mpk" 2>&1); rc=$?
show "$out"
expect "2a os.meow.reader-0.1.7.mpk 仍装进 MeowAPPs/os.meow.reader(不再按文件名取包名)" \
test $rc -eq 0 -a -f "$r/MeowAPPs/os.meow.reader/app.json"
expect "2b 没有生成 MeowAPPs/os.meow.reader-0.1.7" test ! -e "$r/MeowAPPs/os.meow.reader-0.1.7"
expect "2c 成功后无暂存残留" tmp_clean "$r"
else
skip "2 真实 reader 包改名安装" "缺 $REAL_MPK"
fi
# ---- 3 footer.package ≠ app.json.package ----
if [ -n "$KEY" ]; then
r=$(sb 3); mkpkg "$WORK/b3" os.meow.mismatch-b 1; pack "$WORK/b3" "$WORK/mis-a.mpk"
# 签名时文件名茎 = 包名 → footer 写 os.meow.mismatch-a,清单里却是 os.meow.mismatch-b
sign os.meow.mismatch-a "$WORK/mis-a.mpk"
out=$(nyai_run "$r" install "$WORK/mis-a.mpk" 2>&1); rc=$?
show "$out"
expect "3a footer.package ≠ app.json.package → 拒装" test $rc -ne 0
expect "3b 错误文案含两个包名与「不一致」" \
sh -c 'printf "%s" "$1" | grep -q "签名包名(os.meow.mismatch-a) 与 app.json(os.meow.mismatch-b) 不一致"' _ "$out"
expect "3c 拒装后未落盘任何 App" test ! -e "$r/MeowAPPs/os.meow.mismatch-b" -a ! -e "$r/MeowAPPs/os.meow.mismatch-a"
expect "3d 拒装后无暂存残留" tmp_clean "$r"
else
skip "3 footer/清单不一致" "无签名私钥"
fi
# ---- 4 os.meow.* 无签名 ----
r=$(sb 4); mkpkg "$WORK/b4" os.meow.unsigned 1; pack "$WORK/b4" "$WORK/unsigned-sys.mpk"
out=$(nyai_run "$r" install "$WORK/unsigned-sys.mpk" 2>&1); rc=$?
show "$out"
expect "4a os.meow.* 无签名 → 拒装" test $rc -ne 0
expect "4b 错误文案含「必须签名」" sh -c 'printf "%s" "$1" | grep -q "必须签名"' _ "$out"
expect "4c 未落盘 + 无残留" test ! -e "$r/MeowAPPs/os.meow.unsigned"
expect "4d 拒装后无暂存残留" tmp_clean "$r"
# ---- 5 第三方无签名放行 ----
r=$(sb 5); mkpkg "$WORK/b5" dev.local.thirdparty 1; pack "$WORK/b5" "$WORK/thirdparty.mpk"
out=$(nyai_run "$r" install "$WORK/thirdparty.mpk" 2>&1); rc=$?
show "$out"
expect "5a 第三方无签名 → 放行安装(行为不变)" \
test $rc -eq 0 -a -f "$r/MeowAPPs/dev.local.thirdparty/app.json"
expect "5b 成功后无残留" tmp_clean "$r"
# ---- 6 恶意 package 名 ----
for badname in "../escape" "a/b" "." ".."; do
r=$(sb 6)
mkpkg "$WORK/b6" "$badname" 1; pack "$WORK/b6" "$WORK/evil.mpk"
out=$(nyai_run "$r" install "$WORK/evil.mpk" 2>&1); rc=$?
show "$out"
expect "6 [$badname] 拒装" test $rc -ne 0
expect "6 [$badname] 错误文案指向 app.json.package" \
sh -c 'printf "%s" "$1" | grep -q "app.json.package 非法"' _ "$out"
expect "6 [$badname] 未落盘到目录外(root/escape、root/a、沙箱父目录)" \
test ! -e "$r/escape" -a ! -e "$r/a" -a ! -e "$WORK/case-6/escape" -a ! -e "$WORK/case-6/a"
expect "6 [$badname] MeowAPPs/MeowOS 目录未被破坏" \
test -d "$r/MeowAPPs" -a -d "$r/MeowOS"
expect "6 [$badname] 无暂存残留" tmp_clean "$r"
done
# ---- 7 解压失败 / SIGTERM 中断 ----
r=$(sb 7)
printf 'this is not a gzip stream\n' > "$WORK/broken.mpk"
out=$(nyai_run "$r" install "$WORK/broken.mpk" 2>&1); rc=$?
show "$out"
expect "7a 非法包(非 gzip/tar)→ 拒装" test $rc -ne 0
expect "7b 解压失败后无暂存残留" tmp_clean "$r"
mkpkg "$WORK/b7" dev.test.slow 1
head -c 134217728 /dev/zero > "$WORK/b7/big.bin" # 128MB,拉长解包时间以便打信号
pack "$WORK/b7" "$WORK/slow.mpk"
r=$(sb 7s)
MEOWROOT="$r" MEOW_VERIFY_BIN="$VERIFY" "$NYAI" install "$WORK/slow.mpk" >"$WORK/sig.log" 2>&1 &
pid=$!
sleep 0.30
kill -TERM "$pid" 2>/dev/null
wait "$pid"; rc=$?
show "$(cat "$WORK/sig.log")"
if grep -q "收到信号" "$WORK/sig.log"; then
expect "7c SIGTERM 中断:退出码 143" test "$rc" -eq 143
expect "7d SIGTERM 中断后无暂存残留" tmp_clean "$r"
expect "7e SIGTERM 中断后未落盘 App" test ! -e "$r/MeowAPPs/dev.test.slow"
else
skip "7c-e SIGTERM 中断清理" "解包在信号前已完成(rc=$rc)"
fi
rm -f "$WORK/b7/big.bin"
# ---- 8 索引路径:Package 取自清单 ----
r=$(sb 8)
mkdir -p "$WORK/repo"
mkpkg "$WORK/b8" os.meow.idx 3; pack "$WORK/b8" "$WORK/idx.mpk"
if sign os.meow.idx "$WORK/idx.mpk"; then
cp "$WORK/idx.mpk" "$WORK/repo/zzz-renamed-file.mpk" # 文件名与包名完全不同
printf 'local %s\n' "$WORK/repo" > "$r/MeowOS/opt/nyai/sources.list"
"$NYAI" index "$WORK/repo" >"$WORK/idx.log" 2>&1
expect "8a 索引 Package 字段来自 app.json(不是文件名)" \
grep -q '^Package: os.meow.idx$' "$WORK/repo/Packages"
out=$(nyai_run "$r" install os.meow.idx 2>&1); rc=$?
show "$out"
expect "8b 按索引包名安装成功" test $rc -eq 0 -a -f "$r/MeowAPPs/os.meow.idx/app.json"
expect "8c 索引安装后无残留" tmp_clean "$r"
else
skip "8 索引路径" "无签名私钥"
fi
# ---- 9 upgrade 复用 install 引擎 ----
r=$(sb 9)
mkpkg "$WORK/b9" dev.local.up 1; pack "$WORK/b9" "$WORK/up1.mpk"
nyai_run "$r" install "$WORK/up1.mpk" >/dev/null 2>&1
mkpkg "$WORK/b9" dev.local.up 2; pack "$WORK/b9" "$WORK/up2-any-name.bin"
out=$(nyai_run "$r" upgrade dev.local.up "$WORK/up2-any-name.bin" 2>&1); rc=$?
show "$out"
expect "9a upgrade 接受任意文件名(清单包名 == 目标包名)" \
test $rc -eq 0 -a "$(grep -o '"version_code":[0-9]*' "$r/MeowAPPs/dev.local.up/app.json")" = '"version_code":2'
expect "9b upgrade 后无残留" tmp_clean "$r"
mkpkg "$WORK/b9" dev.local.up 0; pack "$WORK/b9" "$WORK/up0.mpk"
out=$(nyai_run "$r" upgrade dev.local.up "$WORK/up0.mpk" 2>&1); rc=$?
show "$out"
expect "9c 降级被拒" sh -c 'test $1 -ne 0 && printf "%s" "$2" | grep -q "不允许降级"' _ "$rc" "$out"
mkpkg "$WORK/b9" dev.local.other 3; pack "$WORK/b9" "$WORK/upother.mpk"
out=$(nyai_run "$r" upgrade dev.local.up "$WORK/upother.mpk" 2>&1); rc=$?
show "$out"
expect "9d 清单包名 ≠ upgrade 目标 → 拒" \
sh -c 'test $1 -ne 0 && printf "%s" "$2" | grep -q "与目标包名(dev.local.up) 不一致"' _ "$rc" "$out"
expect "9e 以上失败均无残留" tmp_clean "$r"
# ---------------- 汇总 ----------------
printf '\n== summary: %d passed, %d failed, %d skipped ==\n' "$PASS" "$FAIL" "$SKIP"
[ "$FAIL" -eq 0 ] || exit 1
exit 0