diff --git a/patch_v2/meowpkg/nyai b/patch_v2/meowpkg/nyai index 3e7fccb..59b3e17 100755 Binary files a/patch_v2/meowpkg/nyai and b/patch_v2/meowpkg/nyai differ diff --git a/patch_v2/meowpkg/nyai.c b/patch_v2/meowpkg/nyai.c index d09b2d8..760af21 100644 --- a/patch_v2/meowpkg/nyai.c +++ b/patch_v2/meowpkg/nyai.c @@ -6,32 +6,61 @@ * - 源配置 MeowOS/opt/nyai/sources.list(每行一个源;local <路径> / http / * deb [trusted=yes] / file:// ; # 注释)。默认 local MeowOS/opt/repo。 * - 每源一个 Packages 索引(行式 Package/Version/Version-Code/Filename/Size/SHA256/Depends/Description), - * 由 `nyai index <目录>` 生成。 + * 由 `nyai index <目录>` 生成;Package 字段取自包内 app.json.package。 * - update 缓存索引到 MeowOS/opt/nyai/cache;install 按名称从索引取包(本地文件或 http 拉取), * 处理 Depends(递归)、校验 SHA256 + app.json;同 name 先备份;解压 MeowAPPs//; * 数据 init(MeowData//{data,cache})。exFAT 无符号链接 → 数据目录由环境变量注入,不建链接。 * + * 包身份(M35):**包名/版本/依赖全部来自包内 app.json**,文件名与后缀只作路径,不参与身份判定。 + * 安装流:解包到 SD 暂存目录 → 读 app.json(验签前,无循环依赖)→ 校验 footer.package 与 + * app.json.package 一致 → 用清单包名调 meow_verify → 防降级 → rename 部署。 + * 暂存目录 = /MeowOS/tmp/pkgs/---/,成功/失败/被信号中断都删。 + * * 路径前缀默认 /usr/data/mnt/sd_0;环境变量 MEOWROOT 可覆盖(仅本地 qemu/原生测试用,真机不设)。 */ #include #include +#include #include +#include #include #include #include +#include #include #include +#include +#include +#include #include +#include #include #include -#define NYAI_VERSION "0.2.0" +#define NYAI_VERSION "0.3.1" #define BUF 1024 #define MAX_SRC 16 #define MAX_PKG 256 #define MAX_DEPS 64 #define MAX_DEPTH 32 +/* 解包暂存目录根(相对 MeowROOT;真机 = /usr/data/mnt/sd_0/MeowOS/tmp/pkgs)。 + * 每次安装一个子目录 ---,无论成败都删。 */ +#define STG_SUBDIR "MeowOS/tmp/pkgs" + +/* 单包解压后体积上限(防解压炸弹:解包先于验签,恶意包可以在验签失败前把卡写满)。 + * 现网最大 App ≈ 6MB(ncm),512MB 留足余量;另加 statvfs 余量检查。 */ +#define MAX_UNPACK_BYTES (512LL * 1024 * 1024) +#define STG_SPACE_MARGIN (16LL * 1024 * 1024) + +/* .mpk 签名 footer 布局(与 patch_v2/meow_verify/meow_sig.h 同步;此处只读 package 用于 + * 「footer.package vs app.json.package」一致性检查——真正的验签仍由 meow_verify 执行)。 */ +#define SIG_MAGIC "MEOWSIG1" +#define SIG_PKG_MAX 64 +#define SIG_BLOCK_LEN_V1 180 +#define SIG_BLOCK_LEN_V2 184 +#define SIG_OFF_PKG 12 + /* ---------------- 路径 ---------------- */ static const char *meow_root(void) { @@ -46,6 +75,8 @@ static void path_cache(char *out, int n) { snprintf(out,n,"%s/MeowOS/opt/nyai/cache", meow_root()); } static void path_repo_default(char *out, int n) { snprintf(out,n,"%s/MeowOS/opt/repo", meow_root()); } +static void path_stg_base(char *out, int n) +{ snprintf(out,n,"%s/%s", meow_root(), STG_SUBDIR); } /* ---------------- 源结构 ---------------- */ typedef struct { @@ -87,12 +118,20 @@ static void strip_comment(char *s) if (p) *p = 0; } -/* 包名合法:仅 [A-Za-z0-9._-] */ +/* 包名合法:仅 [a-z0-9._-],且拒绝 "." / ".." / 前导或尾随 '.'。 + * 只收小写是安全要求:SD 卡是 exFAT,名字比较不区分大小写, + * 允许大写就能用 "OS.MEOW.READER" 落到已装的 MeowAPPs/os.meow.reader 上 + * 并绕过 is_system_pkg() 的签名门(现网包名全小写,收紧无兼容代价)。 */ static int valid_pkg(const char *s) { if (!s || !*s) return 0; - for (; *s; s++) - if (!(isalnum((unsigned char)*s) || *s=='.' || *s=='-' || *s=='_')) return 0; + if (s[0] == '.') return 0; /* "." / ".." / 隐藏名 */ + size_t n = strlen(s); + if (s[n-1] == '.') return 0; /* exFAT/Windows 尾点映射怪癖 */ + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + if (!(islower(c) || isdigit(c) || c=='.' || c=='-' || c=='_')) return 0; + } return 1; } static int unsafe_path(const char *s) @@ -100,6 +139,11 @@ static int unsafe_path(const char *s) return s && strpbrk(s, " ;`$'\"&|<>") != NULL; } +/* 前向声明:错误前缀(install/upgrade),定义见安装期安全设施一节。 */ +static void op_err(const char *fmt, ...); +/* 前向声明:独立 '..' 路径组件判定,定义见导出过滤一节(安装期成员名检查也用)。 */ +static int has_dotdot_comp(const char *p); + static int mkdir_p(const char *path) { char buf[BUF]; @@ -156,6 +200,92 @@ static void human_size(long long b, char *out, int n) else snprintf(out,n,"%.2f MB", b/(1024.0*1024)); } +/* ---------------- 时钟 / 暂存目录 ---------------- */ +/* 真机 time() 不可用:zig 0.17 自带 musl 在 mipsel 上把 time64 入口编成了 time32, + * 而 struct timespec 按 16 字节声明 → time() 返回「秒 + 纳秒<<32」的垃圾值 + * (实测 time()=3381223399391169494,见 devdocs/CLOCK_ABI.md)。这里直接走内核 o32 + * time32 ABI(SYS_clock_gettime=4263,缓冲 8 字节 {int32 sec,int32 nsec},真机实测正确)。 + * 该时间只用于给人看的目录名,唯一性由 mkdir(2) 失败重试 + pid 保证。 */ +static long long nyai_now(void) +{ +#if defined(__mips__) && !defined(__mips64) + struct { int32_t tv_sec, tv_nsec; } tv = { 0, 0 }; + if (syscall(4263, 0 /* CLOCK_REALTIME */, &tv) == 0 && tv.tv_sec > 0) + return (long long)tv.tv_sec; + return 0; +#else + return (long long)time(NULL); +#endif +} + +/* UTC "YYYYmmdd-HHMMSS"(localtime_r 在真机同样受 time64 缺陷影响返回 NULL,故自己算)。 */ +static void fmt_utc(long long sec, char *out, int n) +{ + if (sec <= 0) { snprintf(out, n, "t0"); return; } + long long z = sec / 86400 + 719468; /* civil_from_days */ + long long era = (z >= 0 ? z : z - 146096) / 146097; + unsigned doe = (unsigned)(z - era * 146097); + unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365; + long long y = (long long)yoe + era * 400; + unsigned doy = doe - (365*yoe + yoe/4 - yoe/100); + unsigned mp = (5*doy + 2)/153; + unsigned d = doy - (153*mp+2)/5 + 1; + int m = (int)mp + (mp < 10 ? 3 : -9); /* March-based → 1..12 */ + if (m <= 2) y++; + long long rem = sec % 86400; + snprintf(out, n, "%04lld%02d%02u-%02u%02u%02u", y, m, d, + (unsigned)(rem/3600), (unsigned)((rem%3600)/60), (unsigned)(rem%60)); +} + +/* 路径是否正好是某个挂载点(读 /proc/mounts 第二列)。1=是 0=否 -1=读不到。 */ +static int is_mount_point(const char *path) +{ + FILE *f = fopen("/proc/mounts", "r"); + if (!f) return -1; + char dev[256], mnt[512]; + int hit = 0; + while (fscanf(f, "%255s %511s %*s %*s %*s %*s", dev, mnt) == 2) { + if (!strcmp(mnt, path)) { hit = 1; break; } + } + fclose(f); + return hit; +} + +/* 准备暂存根:返回 0 成功;失败已打印原因并返回 -1。 + * SD 未挂载 / 不可写时**拒绝安装**(不回退 /usr/data):安装目标 MeowAPPs 本身就在 SD 上, + * 卡不在装也没意义;且 /usr/data 只剩 ~4MB,跨文件系统 rebase 还会让部署 rename 变 EXDEV。 */ +static int stg_prepare(char *base, int n) +{ + path_stg_base(base, n); + /* 真机根(/usr/data/...)必须是真实挂载点;宿主/CI 的 MEOWROOT 沙箱不查。 */ + const char *root = meow_root(); + int dev_root = !strcmp(root, "/usr/data") || !strncmp(root, "/usr/data/", 10); + if (dev_root && is_mount_point(root) != 1) { + op_err("暂存目录 %s 不可用:SD 卡未挂载(%s 不是挂载点),拒绝安装\n", base, root); + return -1; + } + if (mkdir_p(base) != 0 || access(base, W_OK) != 0) { + op_err("暂存目录 %s 不可用(SD 卡只读/写满?): %s\n", base, strerror(errno)); + return -1; + } + return 0; +} + +/* 建暂存目录(解包前包名未知,用 .prep 前缀):mkdir(2) 原子占位 + 失败递增重试; + * 唯一性由 mkdir 的 EEXIST + pid + 序号保证,时间戳只给人看。 */ +static int stg_mkdir(const char *base, char *out, int n) +{ + char ts[32]; + fmt_utc(nyai_now(), ts, sizeof ts); + for (int i = 0; i < 100; i++) { + if (snprintf(out, n, "%s/.prep-%s-%d-%d", base, ts, (int)getpid(), i) >= n) return -1; + if (mkdir(out, 0755) == 0) return 0; + if (errno != EEXIST) return -1; + } + return -1; +} + + /* ---------------- JSON 读取 ---------------- */ static int json_str(const char *j, const char *key, char *out, int n) { @@ -252,7 +382,7 @@ typedef struct { long long version_code; /* app.json "version_code"(正整数,安装/升级比较唯一依据;缺时 has_=0) */ int has_version_code; /* app.json 是否显式声明 version_code */ char exec[512]; - char package[64]; /* app.json "package"(权威包名,== 文件名茎;II 期绑定校验用) */ + char package[64]; /* app.json "package"(唯一权威包名;M35 后与文件名无关) */ char author[128]; char routes[512]; char services[512]; @@ -288,8 +418,21 @@ static int load_app_meta(const char *path, appmeta *m) return 0; } -/* 从 .mpk 里尽力读 app.json 字段(name/version/depends/description)。 - * 用 tar -t 定位成员,再 tar -xO 取内容;失败返回 -1。 */ +/* 从 .mpk 里尽力读 app.json 字段(package/name/version/depends/description)。 + * 用 tar -t 定位成员,再 tar -xO 取内容;失败返回 -1。 + * 成员名来自归档本身 → 保持原样('./app.json' 与 'app.json' 在 tar 里是不同名字), + * 但必须先做 shell/选项注入校验:禁前导 '-'(会被当成 tar 选项)、禁 shell 元字符、 + * 禁控制字符、禁 '..'(index/scan 会对任意文件调用)。 */ +static int member_safe_for_arg(const char *s) +{ + if (!s || !*s || *s == '-') return 0; + if (unsafe_path(s)) return 0; + if (strstr(s, "..")) return 0; + for (const char *p = s; *p; p++) + if ((unsigned char)*p < 0x20 || *p == 0x7f) return 0; + return 1; +} + static int mpk_appinfo(const char *file, appmeta *m) { memset(m, 0, sizeof *m); @@ -304,6 +447,7 @@ static int mpk_appinfo(const char *file, appmeta *m) size_t mlen=strlen(mm); while (mlen&&(mm[mlen-1]=='\n'||mm[mlen-1]=='\r'||mm[mlen-1]==' ')) mm[--mlen]=0; pclose(pf); if (!mlen) return -1; + if (!member_safe_for_arg(mm)) return -1; snprintf(cmd,sizeof cmd,"gzip -dc '%s' 2>/dev/null | tar -xO '%s' 2>/dev/null", file, mm); pf = popen(cmd, "r"); if (!pf) return -1; @@ -313,6 +457,7 @@ static int mpk_appinfo(const char *file, appmeta *m) if (!r) return -1; j[r]=0; if (json_str(j,"name",m->name,sizeof m->name)!=0 || !m->name[0]) return -1; + if (json_str(j,"package",m->package,sizeof m->package)!=0) m->package[0]=0; if (json_str(j,"version",m->version,sizeof m->version)!=0) m->version[0]=0; m->has_version_code = (json_int(j,"version_code",&m->version_code)==0); if (json_str(j,"depends",m->depends,sizeof m->depends)!=0) m->depends[0]=0; @@ -495,7 +640,7 @@ static int pkg_installed(const char *pkg) return access(p, R_OK) == 0; } -static int install_mpk_file(const char *mpk_path); +static int install_mpk_file(const char *mpk_path, const char *expect_pkg, int must_be_installed); /* 按记录安装一个索引包(含 sha256 校验 + 依赖),done 记录已装避免循环 */ static int install_rec(pkgrec *r, pkgrec *recs, int nrec, char (*done)[128], int ndone, int depth); @@ -586,7 +731,7 @@ static int install_rec(pkgrec *r, pkgrec *recs, int nrec, char (*done)[128], int return 1; } } - int rc = install_mpk_file(local); + int rc = install_mpk_file(local, r->pkg, 0); if (s->http) remove(local); return rc; } @@ -624,9 +769,11 @@ static int cmd_update(void) } /* ---------------- index ---------------- + * 索引的 Package 字段 = 包内 app.json.package(**不看文件名**;文件名只作 Filename 定位)。 + * 清单缺 package / 非法 → 跳过并告警,不入索引(否则 install 时验签与落盘目录会错)。 * 注意(MPK_SIGNATURE_DESIGN §5.5):索引记录的 SHA256 = 整包哈希(含尾部签名 footer 的 .mpk)。 * 必须先 `meow_sign sign .mpk` 完成签名、再 `nyai index `,否则索引与装后的整包不一致, - * install 的 sha256_file 校验会失败。本命令不改,仅标注此顺序约束(签名分级见 install_mpk_file)。 */ + * install 的 sha256_file 校验会失败。 */ static int cmd_index(const char *dir) { if (!dir || !*dir) { fprintf(stderr,"nyai: index: 缺目录\n"); return 2; } @@ -646,15 +793,16 @@ static int cmd_index(const char *dir) if (L<4 || strcmp(de->d_name+L-4, ".mpk")!=0) continue; char fpath[BUF]; snprintf(fpath,sizeof fpath,"%s/%s", r, de->d_name); struct stat st; if (stat(fpath,&st)!=0) continue; - char stem[128]; snprintf(stem,sizeof stem,"%.*s", (int)(L-4), de->d_name); appmeta m; char ver[64]="", dep[256]="", desc[256]=""; - if (mpk_appinfo(fpath,&m)==0) { - snprintf(ver,sizeof ver,"%s", m.version); snprintf(dep,sizeof dep,"%s", m.depends); - snprintf(desc,sizeof desc,"%s", m.description[0]?m.description:m.name); + if (mpk_appinfo(fpath,&m)!=0 || !m.package[0] || !valid_pkg(m.package)) { + fprintf(stderr,"nyai: index: 跳过 %s(app.json 缺合法 package 字段)\n", de->d_name); + continue; } + snprintf(ver,sizeof ver,"%s", m.version); snprintf(dep,sizeof dep,"%s", m.depends); + snprintf(desc,sizeof desc,"%s", m.description[0]?m.description:m.name); char sha[128]=""; sha256_file(fpath, sha, sizeof sha); - fprintf(f,"Package: %s\n", stem); + fprintf(f,"Package: %s\n", m.package); fprintf(f,"Version: %s\n", ver[0]?ver:"0.0.0"); if (m.has_version_code) fprintf(f,"Version-Code: %lld\n", m.version_code); fprintf(f,"Filename: %s\n", de->d_name); @@ -663,7 +811,7 @@ static int cmd_index(const char *dir) if (dep[0]) fprintf(f,"Depends: %s\n", dep); if (desc[0]) fprintf(f,"Description: %s\n", desc); fprintf(f,"\n"); - printf(" %s (%s)\n", stem, ver[0]?ver:"0.0.0"); + printf(" %s (%s)\n", m.package, ver[0]?ver:"0.0.0"); n++; } fclose(f); @@ -728,10 +876,14 @@ static int cmd_list_upgradable(pkgrec *recs, int nrec) } /* ---------------- II 期签名校验(MPK_SIGNATURE_DESIGN §5.1,shell-out meow_verify) ---------------- */ -/* 系统包前缀:os.meow.*(防伪造系统包名;独立于 valid_pkg,不改变现有合法包名序列)。 */ +/* 系统包前缀:os.meow.*(防伪造系统包名)。 + * **必须大小写不敏感**:安装落盘目录在 exFAT 上(名字比较不区分大小写), + * 若这里区分大小写,"OS.MEOW.READER" 会被当成第三方放行并落到已装的 + * MeowAPPs/os.meow.reader 上,直接顶掉系统 App(valid_pkg 也已收紧为只收小写, + * 这里是第二道防线:任一改动失效都不会打开这个口子)。 */ static int is_system_pkg(const char *pkg) { - return pkg && !strncmp(pkg, "os.meow.", 8); + return pkg && !strncasecmp(pkg, "os.meow.", 8); } /* meow_verify 二进制路径:环境变量 MEOW_VERIFY_BIN 覆盖(宿主测试用),缺省 /usr/bin/meow_verify。 */ @@ -855,117 +1007,515 @@ static void nyai_check_bin_depends(const appmeta *m) } } -/* ---------------- install(文件或名称) ---------------- */ -static int install_mpk_file(const char *mpk_path) +/* ---------------- 安装期安全设施:信号清理 / 子进程 / 成员检查 / footer 读取 ---------------- */ +static const char *g_op = "install"; /* 当前操作名(错误前缀:install / upgrade) */ +static char g_stg[BUF]; /* 当前暂存目录(空 = 无);所有退出路径都要清 */ +static pid_t g_child = -1; +static volatile sig_atomic_t g_sig = 0; + +static void op_err(const char *fmt, ...) { - if (!mpk_path || !*mpk_path) { fprintf(stderr,"nyai: install: 缺 .mpk 路径\n"); return 2; } - if (access(mpk_path, R_OK)!=0) { fprintf(stderr,"nyai: install: 打不开 %s\n", mpk_path); return 1; } - if (unsafe_path(mpk_path)) { fprintf(stderr,"nyai: install: 路径含非法字符\n"); return 1; } - const char *bn = strrchr(mpk_path,'/'); - bn = bn ? bn+1 : mpk_path; - const char *dot = strrchr(bn,'.'); - if (!dot || strcmp(dot,".mpk")!=0) { fprintf(stderr,"nyai: install: 需要 .mpk\n"); return 1; } - char pkg[64]; - int len=(int)(dot-bn); - if (len<=0 || len>=(int)sizeof pkg) { fprintf(stderr,"nyai: install: 包名无效\n"); return 1; } - memcpy(pkg,bn,len); pkg[len]=0; - if (!valid_pkg(pkg)) { fprintf(stderr,"nyai: install: 包名含非法字符: %s\n", pkg); return 1; } + va_list ap; + va_start(ap, fmt); + fprintf(stderr, "nyai: %s: ", g_op); + vfprintf(stderr, fmt, ap); + va_end(ap); +} + +/* 删掉本次的暂存目录(幂等;atexit / 信号 / 各失败分支共用)。 */ +static void stg_cleanup(void) +{ + if (!g_stg[0]) return; + rm_rf(g_stg); + g_stg[0] = 0; +} + +/* SIGINT/SIGTERM/SIGHUP:先收掉自己的子进程组(gzip|tar 还在写暂存目录时不能直接删), + * 主流程在下一个检查点清理并退出(stg_check_signal)。 */ +static void on_term(int sig) +{ + g_sig = sig; + if (g_child > 0) kill(-g_child, SIGTERM); +} + +/* 有挂起信号 → 清暂存目录并按惯例退出码退出。在所有阻塞步骤后调用。 */ +static void stg_check_signal(void) +{ + if (!g_sig) return; + if (g_stg[0]) op_err("收到信号 %d,已清理暂存目录 %s\n", (int)g_sig, g_stg); + stg_cleanup(); + exit(128 + (int)g_sig); +} + +/* 跑 shell 命令:子进程单独成组(setpgid),信号时整组收掉。 + * 返回 0=成功;非 0=子进程退出码;-1=派生失败或被信号中断。 */ +static int run_shell(const char *cmd) +{ + pid_t pid = fork(); + if (pid < 0) return -1; + if (pid == 0) { + setpgid(0, 0); + execl("/bin/sh", "sh", "-c", cmd, (char *)NULL); + _exit(127); + } + setpgid(pid, pid); + g_child = pid; + int st = 0; + for (;;) { + pid_t r = waitpid(pid, &st, 0); + if (r == pid) break; + if (r < 0 && errno == EINTR) { if (g_sig) kill(-pid, SIGTERM); continue; } + g_child = -1; + return -1; + } + g_child = -1; + if (g_sig) return -1; + if (!WIFEXITED(st)) return -1; + return WEXITSTATUS(st); +} + +/* 把不可打印字节换成 '?'(错误信息回显归档内容时防终端控制字符)。 */ +static void scrub(char *s) +{ + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + if (c < 0x20 || c == 0x7f) *s = '?'; + } +} + +/* 解析一行 `tar -tv`:`