diff --git a/patch_v2/meowpanel/README.md b/patch_v2/meowpanel/README.md index 4c592c7..2d08b8a 100644 --- a/patch_v2/meowpanel/README.md +++ b/patch_v2/meowpanel/README.md @@ -28,7 +28,7 @@ cd /home/Aska/Project/R3ProII/patch_v2/meowpanel - 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**(`-Werror` 亦通过)。 - 产出(本次构建实测): - `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`, - 体积约 **256 KB**(固件阈值内)。md5:`aed2ea17a34614acaa26dcbc8965ebb1`(批2:崩溃/日志中心 `/api/logs/crash` + 日志分组/`group`;`/api/system` 补 `reboot_reason/upgrade_status`;`/api/overview` net 块并入 DNS 统计;音频**写级** `/api/audio/route`(仅 0/2/3,拒 1/4/5)+ `/api/audio/volume`(clamp 到 vol_max)+ `/api/audio` 补 `vol_max`)。 + 体积约 **256 KB**(固件阈值内)。md5:`820b24541ed69f2e27e4a301aff10c98`(UTF-8 加固:`sA_str` 统一嵌 JSON 字符串做 UTF-8 消毒,非法/截断字节逐字节回退 `?`——amsd 曾把「流媒体客户端」截成 16B 腰斩「端」字导致浏览器拒收整页,面板侧防任一上游再产坏字节;`--selftest` 宿主回归;原始 IPC JSON(perf/power/net 含 ssid)先 `utf8_valid_n` 校验,坏字节降级不拖垮 /api/overview)。 **重防火墙注意**:运行中二进制先 `.new` 再 `mv`,切勿直接覆盖运行中的 `meowpanel`)。 - `meowpanel_host`:aarch64 动态链接(宿主编译,仅供调试),体积约 **148 KB**。 - 依赖:仅 libc + Linux 内核头(`linux/input.h`),零第三方库(设备无 python3/lua)。鉴权用**内联 SHA-256**(拷贝自 diff --git a/patch_v2/meowpanel/meowpanel b/patch_v2/meowpanel/meowpanel index c7bcd66..c42c919 100755 Binary files a/patch_v2/meowpanel/meowpanel and b/patch_v2/meowpanel/meowpanel differ diff --git a/patch_v2/meowpanel/meowpanel.c b/patch_v2/meowpanel/meowpanel.c index 2d4df50..0459168 100644 --- a/patch_v2/meowpanel/meowpanel.c +++ b/patch_v2/meowpanel/meowpanel.c @@ -208,18 +208,101 @@ static void sA(sbuf_t *s, const char *fmt, ...) s->b[s->n] = 0; } -/* JSON 字符串转义(version/hostname/文本值;UTF-8 按字节透传,控制字符转 \u00xx) */ +/* ---------------- UTF-8 消毒(JSON 字符串收口) ---------------- + * 面板把外部文本(amsd/svcd/audiod/net/app.json/字体/日志…)嵌进 JSON 字符串,任一上游产出非法 + * UTF-8(如 amsd 曾把「流媒体客户端」截成 16B,腰斩「端」字)都会让浏览器 JSON.parse 拒收整页。 + * 统一在 sA_str 内逐字节校验:合法序列透传,非法/截断序列逐字节替换为 '?'(保持字节长度)。 */ +static int utf8_seq_len(unsigned char lead) +{ + if (lead >= 0xC2 && lead <= 0xDF) return 2; /* C2+:规避 overlong C0/C1 */ + if (lead >= 0xE0 && lead <= 0xEF) return 3; /* 3 字节(含代理区,由 seq_ok 剔除) */ + if (lead >= 0xF0 && lead <= 0xF4) return 4; /* F5+ 超 U+10FFFF,剔除 */ + return 0; +} + +/* 校验从 p 开始的 len 字节序列是否构成合法 UTF-8 字符(len 由 utf8_seq_len 给出)。 + * 调用方须保证 p+len 在可读边界内。检查续字节/overlong/代理对/超 U+10FFFF。 */ +static int utf8_seq_ok(const char *p, int len) +{ + unsigned char c0 = (unsigned char)p[0]; + for (int k = 1; k < len; k++) { + unsigned char ck = (unsigned char)p[k]; + if (ck == 0 || (ck & 0xC0) != 0x80) return 0; /* 截断/非续字节 */ + } + unsigned long cp; + if (len == 2) cp = ((unsigned long)(c0 & 0x1F) << 6) | ((unsigned char)p[1] & 0x3F); + else if (len == 3) cp = ((unsigned long)(c0 & 0x0F) << 12) | (((unsigned char)p[1] & 0x3F) << 6) | ((unsigned char)p[2] & 0x3F); + else cp = ((unsigned long)(c0 & 0x07) << 18) | (((unsigned char)p[1] & 0x3F) << 12) | (((unsigned char)p[2] & 0x3F) << 6) | ((unsigned char)p[3] & 0x3F); + if (len == 2 && cp < 0x80) return 0; /* overlong(C0/C1 已由 seq_len 拒,双保险) */ + if (len == 3 && cp < 0x800) return 0; /* overlong E0 80.. */ + if (len == 4 && cp < 0x10000) return 0; /* overlong F0 80.. */ + if (cp >= 0xD800 && cp <= 0xDFFF) return 0; /* 代理对(JS 字符串不允许孤立) */ + if (cp > 0x10FFFF) return 0; + return 1; +} + +/* 扫描 [s, s+n) 是否为合法 UTF-8:返回 1 合法(纯 ASCII 亦属),0 存在非法/截断字节。 */ +static int utf8_valid_n(const char *s, size_t n) +{ + size_t i = 0; + while (i < n) { + unsigned char c = (unsigned char)s[i]; + if (c < 0x80) { i++; continue; } + int len = utf8_seq_len(c); + if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(s + i, len)) return 0; + i += (size_t)len; + } + return 1; +} + +/* UTF-8 消毒:将 src 前 n 字节拷贝到 dst(dst 容量 ≥ n+1,恒 NUL 结尾),非法/截断序列 + * 逐字节替换为 '?'(保持字节长度)。返回 1 = 发生过替换,0 = 原样合法。无依赖,纯指针运算。 */ +static int utf8_sanitize(char *dst, const char *src, size_t n) +{ + size_t i = 0, j = 0; + int changed = 0; + while (i < n) { + unsigned char c = (unsigned char)src[i]; + if (c < 0x80) { dst[j++] = (char)c; i++; continue; } + int len = utf8_seq_len(c); + if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(src + i, len)) { + dst[j++] = '?'; changed = 1; i++; continue; /* 逐字节替换,前进 1 重对齐 */ + } + memcpy(dst + j, src + i, (size_t)len); + j += (size_t)len; i += (size_t)len; + } + dst[j] = 0; + return changed; +} + +/* JSON 字符串转义 + UTF-8 消毒(非 NUL 终止、长度明确版;控制字符转 \u00xx,非法 UTF-8 逐字节转 '?')。 */ +static void sA_str_n(sbuf_t *s, const char *v, size_t n) +{ + sA(s, "\""); + size_t i = 0; + while (i < n) { + unsigned char c = (unsigned char)v[i]; + if (c == '"' || c == '\\') { sA(s, "\\%c", (int)c); i++; } + else if (c < 0x20) { sA(s, "\\u%04x", c); i++; } + else if (c < 0x80) { sA(s, "%c", (int)c); i++; } + else { + int len = utf8_seq_len(c); + if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(v + i, len)) { + sA(s, "?"); i++; /* 非法/截断序列 → 逐字节替换为 '?',前进 1 重对齐 */ + } else { + for (int k = 0; k < len; k++) sA(s, "%c", (int)(unsigned char)v[i + k]); + i += (size_t)len; + } + } + } + sA(s, "\""); +} + +/* NUL 结尾字符串的 JSON 嵌入(统一收口:所有外部文本嵌 JSON 字符串都走这里)。 */ static void sA_str(sbuf_t *s, const char *v) { if (!v) v = ""; - sA(s, "\""); - for (const char *p = v; *p; p++) { - unsigned char c = (unsigned char)*p; - if (c == '"' || c == '\\') { sA(s, "\\%c", (int)c); } - else if (c < 0x20) { sA(s, "\\u%04x", c); } - else sA(s, "%c", (int)c); - } - sA(s, "\""); + sA_str_n(s, v, strlen(v)); } /* 追加一段原始字节到 sbuf(长度明确,绕过 %s 的固定缓冲限制;用于大 JSON 数组复用)。 */ @@ -1128,7 +1211,7 @@ static int api_power(int sock) { char js[MEOW_IPC_MAX]; int rc = ipc_pm_query(0, js, sizeof js); - if (rc != MEOW_OK) + if (rc != MEOW_OK || !utf8_valid_n(js, strlen(js))) /* 上游坏字节 → 不返回,降级 */ return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用"); return send_json(sock, 200, js); } @@ -1189,6 +1272,8 @@ static int api_net_status(int sock) int n = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &t, &code, NULL, json, sizeof json); if (n <= 0 || t != MSG_NET_STATUS_RESULT || code != MEOW_OK || !json[0]) return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用"); + if (!utf8_valid_n(json, strlen(json))) /* 上游坏字节(含 ssid)→ 不返回,降级 */ + return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务应答包含非法字节"); return send_json(sock, 200, json); } @@ -1301,6 +1386,7 @@ static int net_block_fill(sbuf_t *s) size_t l = strlen(njson); while (l > 0 && (njson[l - 1] == ' ' || njson[l - 1] == '\t' || njson[l - 1] == '\n' || njson[l - 1] == '\r')) njson[--l] = 0; if (l < 2 || njson[0] != '{' || njson[l - 1] != '}') return -1; + if (!utf8_valid_n(njson, l)) return -1; /* 任一上游(netmgrd 含 ssid)产出非法 UTF-8 → 整块降级,不拖垮整页 */ njson[l - 1] = 0; /* 去尾部 } */ sbuf_t dns = {0}; int drc = net_dns_obj_fill(&dns); @@ -1393,17 +1479,8 @@ static int api_net_log(int sock) const char *line_end = nl ? nl : end; if (rowidx >= startrow) { if (gotlines) sA(&s, ","); - sbuf_t ln = {0}; - for (const char *c = p; c < line_end; c++) { - unsigned char cc = (unsigned char)*c; - if (cc == '"' || cc == '\\') sA(&ln, "\\%c", (int)cc); - else if (cc < 0x20 && cc != '\t') sA(&ln, "\\u%04x", cc); - else sA(&ln, "%c", (int)cc); - } - sA(&s, "\""); - for (const char *c = ln.b ? ln.b : ""; *c; c++) sA(&s, "%c", (int)(unsigned char)*c); - sA(&s, "\""); - sbuf_free(&ln); + /* sA_str_n:转义 + UTF-8 消毒(任意长度安全;勿 sA("%s")——单次超 511B 会截断坏 JSON) */ + sA_str_n(&s, p, (size_t)(line_end - p)); gotlines++; } rowidx++; @@ -1587,10 +1664,12 @@ static int api_overview(int sock) sbuf_t s = {0}; sA(&s, "{\"ts\":%lld,", (long long)time(NULL)); char pb[4096]; - if (ipc_perf_query(0, pb, sizeof pb) == MEOW_OK) sA(&s, "\"perf\":%s,", pb); + if (ipc_perf_query(0, pb, sizeof pb) == MEOW_OK && utf8_valid_n(pb, strlen(pb))) + sA(&s, "\"perf\":%s,", pb); else sA(&s, "\"perf\":{\"available\":false},"); char pw[MEOW_IPC_MAX]; - if (ipc_pm_query(0, pw, sizeof pw) == MEOW_OK) sA(&s, "\"power\":%s,", pw); + if (ipc_pm_query(0, pw, sizeof pw) == MEOW_OK && utf8_valid_n(pw, strlen(pw))) + sA(&s, "\"power\":%s,", pw); else sA(&s, "\"power\":{\"available\":false},"); sA(&s, "\"audio\":"); audio_fill(&s); @@ -3335,7 +3414,8 @@ static int api_perf(int sock, const char *path) { char json[4096]; int rc = ipc_perf_query(perf_subset_for_path(path), json, sizeof json); - if (rc != MEOW_OK) return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)"); + if (rc != MEOW_OK || !utf8_valid_n(json, strlen(json))) /* 上游坏字节 → 不返回,降级 */ + return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)"); return send_json(sock, 200, json); } @@ -3436,8 +3516,9 @@ static int api_system(int sock) sA_str(&s, bootmode); sA(&s, "},\"network\":"); char netbuf[1024]; - if (perf_extract_network(json, netbuf, sizeof netbuf) == 0) { - sA(&s, "%s", netbuf); /* 透传 perf 权威 network 段(wifi/bluetooth/hostname_local) */ + /* 透传 perf 权威 network 段(wifi/bluetooth/hostname_local);含非法 UTF-8(如坏 ssid)→ 回退,不嵌坏字节 */ + if (perf_extract_network(json, netbuf, sizeof netbuf) == 0 && utf8_valid_n(netbuf, strlen(netbuf))) { + sA(&s, "%s", netbuf); } else { /* 回退:perf 未含 network 段 → 仅 hostname_local(保持原语义,不崩) */ sA(&s, "{\"hostname_local\":"); @@ -3698,18 +3779,8 @@ static int api_logs_read(int sock, const req_t *r) const char *line_end = nl ? nl : end; if (rowidx >= startrow) { if (gotlines) sA(&s, ","); - sbuf_t ln = {0}; - for (const char *c = p; c < line_end; c++) { - unsigned char cc = (unsigned char)*c; - if (cc == '"' || cc == '\\') sA(&ln, "\\%c", (int)cc); - else if (cc < 0x20 && cc != '\t') sA(&ln, "\\u%04x", cc); - else sA(&ln, "%c", (int)cc); - } - /* 用 sA_str(任意长度安全);不要 sA("%s")——单次超 511B 会截断坏 JSON */ - sA(&s, "\""); - for (const char *c = ln.b ? ln.b : ""; *c; c++) sA(&s, "%c", (int)(unsigned char)*c); - sA(&s, "\""); - sbuf_free(&ln); + /* sA_str_n:转义 + UTF-8 消毒(任意长度安全;勿 sA("%s")——单次超 511B 会截断坏 JSON) */ + sA_str_n(&s, p, (size_t)(line_end - p)); gotlines++; } rowidx++; @@ -5523,11 +5594,75 @@ static void handle_conn(int sock) close(sock); } +/* ---------------- 宿主自测(`meowpanel --selftest`) ---------------- + * 复现 2026-09-02 事故:amsd 的 am list 把 App 显示名「流媒体客户端」(18B)截成 16B, + * 腰斩「端」字(U+7AEF = E7 AB AF → 只剩首字节 E7)。面板必须把这类非法 UTF-8 消毒成合法字符, + * JSON 才不会被浏览器拒收。直接喂截断串给 utf8_sanitize / sA_str / ams_fill 桩,断言: + * 1) utf8_sanitize 改写截断串、保持字节长度、产物为合法 UTF-8,且不误改完整合法名; + * 2) sA_str 嵌入 JSON 字符串后整串为合法 UTF-8、jv_parse 可完整解析; + * 3) ams_fill 桩(APP 行解析 + sA_str)产物同理可解析。 + * 仅宿主 `meowpanel --selftest` 进入;设备服务循环不触发。 */ +static int panel_selftest(void) +{ + const char *trunc = "流媒体客户\xE7"; /* 事故里被腰斩的显示名(16B,末字节为「端」首字节) */ + const char *oknat = "流媒体客户端"; /* 完整合法名(18B,回归确认不误伤) */ + size_t n = strlen(trunc); + + /* 1a) utf8_sanitize:截断串必被改写,长度保持,产物合法 UTF-8 */ + char san[64]; + if (!utf8_sanitize(san, trunc, n)) { printf("SELFTEST FAIL: utf8_sanitize 未改写截断串\n"); return 1; } + if (strlen(san) != n) { printf("SELFTEST FAIL: utf8_sanitize 长度变化 %zu->%zu\n", n, strlen(san)); return 1; } + if (!utf8_valid_n(san, strlen(san))) { printf("SELFTEST FAIL: utf8_sanitize 产物含非法字节\n"); return 1; } + + /* 1b) 完整合法名不得被改写(误伤检测) */ + char san2[64]; + if (utf8_sanitize(san2, oknat, strlen(oknat)) != 0 || strcmp(san2, oknat) != 0) { + printf("SELFTEST FAIL: utf8_sanitize 误改合法 UTF-8\n"); return 1; + } + + /* 2) sA_str 嵌入:产物合法 UTF-8 且 jv_parse 完整解析 */ + sbuf_t s = {0}; + sA(&s, "{\"pkg\":"); + sA_str(&s, "com.example.trunc"); + sA(&s, ",\"name\":"); + sA_str(&s, trunc); + sA(&s, ",\"state\":"); + sA_str(&s, "running"); + sA(&s, "}"); + if (!s.b) { printf("SELFTEST FAIL: sA_str 输出空\n"); return 1; } + if (!utf8_valid_n(s.b, s.n)) { printf("SELFTEST FAIL: sA_str 产物含非法字节\n"); sbuf_free(&s); return 1; } + jv_t *r = jv_parse(s.b, (int)s.n); + if (!r) { printf("SELFTEST FAIL: sA_str 产物 JSON 无法解析\n"); sbuf_free(&s); return 1; } + jv_t *nm = jv_obj_get(r, "name"); + if (!nm || nm->type != J_STR) { printf("SELFTEST FAIL: sA_str 产物缺 name\n"); jv_free(r); sbuf_free(&s); return 1; } + jv_free(r); + sbuf_free(&s); + + /* 3) ams_fill 桩:模拟 APP 行 parse 出的截断名(join_tokens 后)→ apps 数组可解析 */ + sbuf_t a = {0}; + sA(&a, "{\"available\":true,\"count\":1,\"apps\":[{\"pkg\":"); + sA_str(&a, "com.example.trunc"); + sA(&a, ",\"name\":"); + sA_str(&a, trunc); + sA(&a, ",\"state\":"); + sA_str(&a, "running"); + sA(&a, ",\"pid\":123}]}"); + if (!a.b || !utf8_valid_n(a.b, a.n)) { printf("SELFTEST FAIL: ams_fill 桩产物含非法字节/为空\n"); sbuf_free(&a); return 1; } + jv_t *ar = jv_parse(a.b, (int)a.n); + if (!ar) { printf("SELFTEST FAIL: ams_fill 桩产物 JSON 无法解析\n"); sbuf_free(&a); return 1; } + jv_free(ar); + sbuf_free(&a); + + printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」→ 合法 JSON + 无非法字节\n"); + return 0; +} + int main(int argc, char **argv) { /* 参数:-p PORT(可选)。默认端口 8080。 */ int port = DEFAULT_PORT; for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--selftest")) return panel_selftest(); if (!strcmp(argv[i], "-p") && i + 1 < argc) port = atoi(argv[++i]); else if (!strcmp(argv[i], "--sock-perf") && i + 1 < argc) g_sock_perf = argv[++i]; else if (!strcmp(argv[i], "--sock-power") && i + 1 < argc) g_sock_power = argv[++i]; diff --git a/patch_v2/meowpanel/meowpanel_host b/patch_v2/meowpanel/meowpanel_host index daeaaac..9234445 100755 Binary files a/patch_v2/meowpanel/meowpanel_host and b/patch_v2/meowpanel/meowpanel_host differ